有时候开发中有绘制声波图形的需求,找到类似的demo借鉴了一下思路,下面是波形的效果图。
-
先说一下图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的距离是对称的 ,正好连成类似波形的折线。
- (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]
( 如果想了解什么是傅里叶变换 这篇文章不错。)
但是这不是重点,重点是怎么去实现逻辑。
首先对这个基本函数我们需要以下几个操作做基本调整
- 函数周期变化的 x 范围限制符合手机屏幕的宽度,假设为 320
- 在 x 内变化的周期数限制假设我们需要 2 个周期变化
- 波峰限制,我们需要峰值不超过我们 UIView 容器的高度,所以假设 UIView 搞是 20,那么峰值应该限制在 10 以内
- 五个波纹依次波峰递减 1/5
波纹的限制
上面已经非常接近我们想要的效果了,但是还有一个比较重要的,就是最终出来的效果应该是越靠近屏幕中间的位置,波峰越大,靠近屏幕边缘的地方,无限接近于静止。
那么我们还需要一个参数(一元二次方程)来调整。满足在 x 的范围内,值从 0 - 正数值 变化,那么这两个函数相乘的时候,就能实现我们想要的效果。
Animate
- 一个用来调整波峰的参数把声音的音量处理后作为参数传入,于函数相乘。
- 循环进行 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();
}
完!,