KSSpectrumView.m 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. //
  2. // KSSpectrumView.m
  3. // KulexiuSchoolStudent
  4. //
  5. // Created by 王智 on 2024/7/29.
  6. //
  7. #import "KSSpectrumView.h"
  8. #import <Accelerate/Accelerate.h>
  9. @interface KSSpectrumView ()
  10. @property (nonatomic, strong) CAGradientLayer *combinedGradientLayer;
  11. @property (nonatomic, strong) CAShapeLayer *combinedMaskLayer;
  12. @property (nonatomic, assign) CGFloat xIncrement;
  13. @end
  14. @implementation KSSpectrumView
  15. - (instancetype)initWithFrame:(CGRect)frame {
  16. self = [super initWithFrame:frame];
  17. if (self) {
  18. [self setupView];
  19. }
  20. return self;
  21. }
  22. - (instancetype)initWithCoder:(NSCoder *)aDecoder {
  23. self = [super initWithCoder:aDecoder];
  24. if (self) {
  25. [self setupView];
  26. }
  27. return self;
  28. }
  29. - (void)setupView {
  30. self.barWidth = 3.0;
  31. self.space = 1.0;
  32. self.xIncrement = self.barWidth + self.space;
  33. self.combinedGradientLayer = [CAGradientLayer layer];
  34. self.combinedGradientLayer.colors = @[
  35. (__bridge id)HexRGBAlpha(0xffffff, 1.0f).CGColor,
  36. (__bridge id)HexRGBAlpha(0xffffff, 1.0f).CGColor
  37. ];
  38. self.combinedGradientLayer.locations = @[@0.6, @1.0];
  39. [self.layer addSublayer:self.combinedGradientLayer];
  40. self.combinedMaskLayer = [CAShapeLayer layer];
  41. }
  42. - (void)resetLayer {
  43. self.spectra = [NSArray array];
  44. }
  45. - (void)setSpectra:(NSArray<NSArray<NSNumber *> *> *)spectra {
  46. _spectra = spectra;
  47. if (spectra) {
  48. CGFloat viewHeight = self.bounds.size.height;
  49. CGFloat viewWidth = self.bounds.size.width;
  50. @weakObj(self);
  51. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  52. @strongObj(self);
  53. self.isModify = YES;
  54. NSUInteger spectraCount = [spectra[0] count];
  55. NSMutableArray<NSNumber *> *combinedSpectrum = [NSMutableArray arrayWithCapacity:spectraCount];
  56. // 取两个声道数据中的最大值
  57. for (NSUInteger i = 0; i < spectraCount; i++) {
  58. NSNumber *leftAmplitude = spectra[0][i];
  59. NSNumber *rightAmplitude = spectra.count > 1 ? spectra[1][i] : @0;
  60. CGFloat maxAmplitude = MAX(leftAmplitude.floatValue, rightAmplitude.floatValue);
  61. [combinedSpectrum addObject:@(maxAmplitude)];
  62. }
  63. CGFloat middleY = viewHeight / 2.0;
  64. CGFloat barHeight = (viewHeight) / 2.0;
  65. CGFloat cornerRadius = viewWidth / 2.0f;
  66. UIBezierPath *combinedPath = [UIBezierPath bezierPath];
  67. // Left channel
  68. for (NSUInteger i = 0; i < spectraCount; i++) {
  69. CGFloat x = i * self.xIncrement + self.space;
  70. CGFloat amplitudeValue = combinedSpectrum[i].floatValue;
  71. CGFloat height = amplitudeValue * barHeight;
  72. CGFloat y = middleY - height/2.0; // Centered vertically
  73. CGRect rect = CGRectMake(x, y, self.barWidth, height);
  74. UIBezierPath *barPath = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:cornerRadius];
  75. [combinedPath appendPath:barPath];
  76. }
  77. dispatch_async(dispatch_get_main_queue(), ^{
  78. self.combinedMaskLayer.path = combinedPath.CGPath;
  79. self.combinedGradientLayer.frame = CGRectMake(0, 0, viewWidth, viewHeight);
  80. self.combinedGradientLayer.mask = self.combinedMaskLayer;
  81. self.isModify = NO;
  82. });
  83. });
  84. }
  85. }
  86. - (CGFloat)translateAmplitudeToYPosition:(float)amplitude {
  87. CGFloat barHeight = amplitude * self.bounds.size.height;
  88. return self.bounds.size.height - barHeight;
  89. }
  90. - (void)dealloc {
  91. NSLog(@"---- KSSpectrumView dealloc");
  92. }
  93. - (NSArray<NSNumber *> *)computeFFTWithPCMBuffer:(float *)pcmBuffer frameCount:(NSUInteger)frameCount {
  94. NSUInteger log2n = log2(frameCount); // FFT 长度必须是 2 的幂
  95. NSUInteger fftSize = 1 << log2n; // 计算实际 FFT 点数
  96. // 创建 FFT 设置
  97. FFTSetup fftSetup = vDSP_create_fftsetup(log2n, FFT_RADIX2);
  98. if (!fftSetup) {
  99. NSLog(@"FFT Setup failed");
  100. return nil;
  101. }
  102. // 输入数据必须是复数形式,创建 SplitComplex 存储数据
  103. DSPSplitComplex splitComplex;
  104. float *real = malloc(sizeof(float) * fftSize / 2);
  105. float *imag = malloc(sizeof(float) * fftSize / 2);
  106. splitComplex.realp = real;
  107. splitComplex.imagp = imag;
  108. // 填充输入数据并进行窗口处理(如 Hanning 窗口)
  109. vDSP_ctoz((DSPComplex *)pcmBuffer, 2, &splitComplex, 1, fftSize / 2);
  110. vDSP_hann_window(real, fftSize / 2, vDSP_HANN_NORM);
  111. // 执行 FFT
  112. vDSP_fft_zrip(fftSetup, &splitComplex, 1, log2n, FFT_FORWARD);
  113. // 计算幅值(模长)
  114. float *amplitudes = malloc(sizeof(float) * fftSize / 2);
  115. vDSP_zvmags(&splitComplex, 1, amplitudes, 1, fftSize / 2);
  116. // 转换为 dB(可选)
  117. float scale = 1.0 / (2.0 * fftSize); // 归一化
  118. vDSP_vsmul(amplitudes, 1, &scale, amplitudes, 1, fftSize / 2);
  119. vDSP_vdbcon(amplitudes, 1, &scale, amplitudes, 1, fftSize / 2, 0);
  120. // 转换结果为 NSArray
  121. NSMutableArray<NSNumber *> *result = [NSMutableArray arrayWithCapacity:fftSize / 2];
  122. for (NSUInteger i = 0; i < fftSize / 2; i++) {
  123. [result addObject:@(amplitudes[i])];
  124. }
  125. // 清理内存
  126. free(real);
  127. free(imag);
  128. free(amplitudes);
  129. vDSP_destroy_fftsetup(fftSetup);
  130. return result;
  131. }
  132. /*
  133. // Only override drawRect: if you perform custom drawing.
  134. // An empty implementation adversely affects performance during animation.
  135. - (void)drawRect:(CGRect)rect {
  136. // Drawing code
  137. }
  138. */
  139. @end