iOS 绘制音频波形

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

图1.1 折线图
图1.2 柱状图
图 1.3 Siri波形效果
  • 先说一下图1.1 和图 1.2 的实现,下载这个Demo

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);


  • 图1.3 是github上的一个开源代码 waver

曾有过关于如何实现像 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();
}

完!,

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,024评论 4 62
  • 我太喜欢你们了,这样的文章好想你们看到: 有事回到30前读书的地方,记得一个同学在那儿,便询问。有人挺热心,去办公...
    焕能阅读 182评论 0 0
  • 在画画中寻找乐趣 我是花红火红(麦子熟了1981),欢迎你们的到来,希望多提宝贵意见
    黑白尘埃阅读 152评论 0 2
  • 一千公里的路途有多远 为何让我在万余个日夜心生抱怨 好似幼苗 对成长有执着的期盼 好似枯树 无人浇灌年轮却渐渐浮现...
    星之桥阅读 322评论 1 5