征服iOSiOS动画设计学习

iOS 绘制音频波形

2016-07-23  本文已影响5471人  怪小喵

有时候开发中有绘制声波图形的需求,找到类似的demo借鉴了一下思路,下面是波形的效果图。

图1.1 折线图 图1.2 柱状图 图 1.3 Siri波形效果

1.首先,需要一个数组保存一段时间内不同时间点音量大小

#define SOUND_METER_COUNT       40
int soundMeters[40];

2.开始录音,或播放音频时,开启一个定时器timer不断获取
averagePowerForChannel,使用 soundMeters数组保存获取的power值。

timer = [NSTimer scheduledTimerWithTimeInterval:WAVE_UPDATE_FREQUENCY target:self selector:@selector(updateMeters) userInfo:nil repeats:YES];

- (void)updateMeters {
    [recorder updateMeters];
    recordTime += WAVE_UPDATE_FREQUENCY;
    [self addSoundMeterItem:[recorder averagePowerForChannel:0]];
}

3.每次将音量数据加入队未,数组左移,注意添加 lastValue 是添加了两次,左移也是两次,这是为了下面处理数据方便。

- (void)addSoundMeterItem:(int)lastValue {
    [self shiftSoundMeterLeft];
    [self shiftSoundMeterLeft];
    soundMeters[SOUND_METER_COUNT - 1] = lastValue;
    soundMeters[SOUND_METER_COUNT - 2] = lastValue;
    
    [self setNeedsDisplay];
}

- (void)shiftSoundMeterLeft {
    for(int i=0; i<SOUND_METER_COUNT - 1; i++) {
        soundMeters[i] = soundMeters[i+1];
    }
}

4.最后一步是绘制数组保存的所有点的绘制逻辑,这里只展示波形绘制相关的代码

4.1. 绘制折线图使用UIBezierPath,如图1.4 要先计算出顶点 y, 因为第三步中lastValue 是添加了两次,所以相邻两个 y点(例如y1 , y2点) 距离baseLine的距离是对称的 ,正好连成类似波形的折线。

图1.4 绘制原理
- (void)drawRect:(CGRect)rect {
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    UIColor *strokeColor = [UIColor colorWithRed:0.886 green:0.0 blue:0.0 alpha:0.8];
    UIColor *fillColor = [UIColor colorWithRed:0.5827 green:0.5827 blue:0.5827 alpha:1.0];
    UIColor *gradientColor = [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.8];
    UIColor *color = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];

  
    // 绘制波形
    [[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.4] set];
    CGContextSetLineWidth(context, 3.0);
    CGContextSetLineJoin(context, kCGLineJoinRound);

    // 基准线
    int baseLine = 250;
    // 因数
    int multiplier = 1;
    // 音量最大值
    int maxLengthOfWave = 50;
    // 画出的波形的最大值
    int maxValueOfMeter = 70;
    
    
    // 绘制一个类似波形的折线图
    for(CGFloat x = SOUND_METER_COUNT - 1; x >= 0; x--)
    {
        // 基数位置的音量 设置为 -1
        multiplier = ((int)x % 2) == 0 ? 1 : -1;
        // y 是波形的顶点 (波峰 或者 波谷) = baseLine + 波形的相对长度 * multiplier
        CGFloat y = baseLine + ((maxValueOfMeter * (maxLengthOfWave - abs(soundMeters[(int)x]))) / maxLengthOfWave) * multiplier;
        
        if(x == SOUND_METER_COUNT - 1) {
            CGContextMoveToPoint(context, x * (HUD_SIZE / SOUND_METER_COUNT) + hudRect.origin.x + 10, y);
            CGContextAddLineToPoint(context, x * (HUD_SIZE / SOUND_METER_COUNT) + hudRect.origin.x + 7, y);
        }
        else {
            // 绘制线条
            CGContextAddLineToPoint(context, x * (HUD_SIZE / SOUND_METER_COUNT) + hudRect.origin.x + 10, y);
            CGContextAddLineToPoint(context, x * (HUD_SIZE / SOUND_METER_COUNT) + hudRect.origin.x + 7, y);
        }
    }
    CGContextStrokePath(context);
}

4.2 绘制柱状图同理,代码如下,把绘制折线的代码替换掉就行了

CGFloat y1 = baseLine + ((maxValueOfMeter * (maxLengthOfWave - abs(soundMeters[(int)x]))) / maxLengthOfWave) * 1;        
CGFloat y2 = baseLine + ((maxValueOfMeter * (maxLengthOfWave - abs(soundMeters[(int)x]))) / maxLengthOfWave) * -1;
CGContextMoveToPoint(context, x * (HUD_SIZE / SOUND_METER_COUNT) + hudRect.origin.x + 7, y1);
CGContextAddLineToPoint(context, x * (HUD_SIZE / SOUND_METER_COUNT) + hudRect.origin.x + 7, y2);


曾有过关于如何实现像 Siri 的声波效果的讨论,当时提出的第一个解决方案是 [FFT]
( 如果想了解什么是傅里叶变换 这篇文章不错。)
但是这不是重点,重点是怎么去实现逻辑。

首先对这个基本函数我们需要以下几个操作做基本调整

  1. 函数周期变化的 x 范围限制符合手机屏幕的宽度,假设为 320
  2. 在 x 内变化的周期数限制假设我们需要 2 个周期变化
  3. 波峰限制,我们需要峰值不超过我们 UIView 容器的高度,所以假设 UIView 搞是 20,那么峰值应该限制在 10 以内
  4. 五个波纹依次波峰递减 1/5
    波纹的限制
    上面已经非常接近我们想要的效果了,但是还有一个比较重要的,就是最终出来的效果应该是越靠近屏幕中间的位置,波峰越大,靠近屏幕边缘的地方,无限接近于静止。
    那么我们还需要一个参数(一元二次方程)来调整。满足在 x 的范围内,值从 0 - 正数值 变化,那么这两个函数相乘的时候,就能实现我们想要的效果。

Animate

  1. 一个用来调整波峰的参数把声音的音量处理后作为参数传入,于函数相乘。
  2. 循环进行 x 变化的参数使用 CADisplayLink 作为循环器,声明一个位移量,每次循环的时候进行递增,然后传入我们的函数。

那么简单分析一下代码

使用 CAShapeLayer + UIBezierPath 实现,好处是更方便对初始形态进行调整,像 Siri 那样可以从圆形变成线条。
根据参数numberOfWaves 创建多个 CAShapeLayer 保存在 waves中
使用 CADisplayLink 作为循环器,位移量递增,回调block 获得音频的lavel 然后传入函数计算波形。
将生成的波形(Path)赋值给CAShapeLayer显示

下面是主要的绘制逻辑

- (void)updateMeters
{
 self.waveHeight = CGRectGetHeight(self.bounds);
 self.waveWidth  = CGRectGetWidth(self.bounds);
 self.waveMid    = self.waveWidth / 2.0f;
 self.maxAmplitude = self.waveHeight - 4.0f;
 
    UIGraphicsBeginImageContext(self.frame.size);
    
    for(int i=0; i < self.numberOfWaves; i++) {

        UIBezierPath *wavelinePath = [UIBezierPath bezierPath];

        // Progress is a value between 1.0 and -0.5, determined by the current wave idx, which is used to alter the wave's amplitude.
        CGFloat progress = 1.0f - (CGFloat)i / self.numberOfWaves;
        CGFloat normedAmplitude = (1.5f * progress - 0.5f) * self.amplitude;

        for(CGFloat x = 0; x<self.waveWidth + self.density; x += self.density) {
            
            //Thanks to https://github.com/stefanceriu/SCSiriWaveformView
            // We use a parable to scale the sinus wave, that has its peak in the middle of the view.
            CGFloat scaling = -pow(x / self.waveMid  - 1, 2) + 1; // make center bigger
            CGFloat y = scaling * self.maxAmplitude * normedAmplitude * sinf(2 * M_PI *(x / self.waveWidth) * self.frequency + self.phase) + (self.waveHeight * 0.5);
            
            if (x==0) {
                [wavelinePath moveToPoint:CGPointMake(x, y)];
            }
            else {
                [wavelinePath addLineToPoint:CGPointMake(x, y)];
            }
        }
        
        CAShapeLayer *waveline = [self.waves objectAtIndex:i];
        waveline.path = [wavelinePath CGPath];
    }
    
    UIGraphicsEndImageContext();
}

完!,

上一篇下一篇

猜你喜欢

热点阅读