有時(shí)候開(kāi)發(fā)中有繪制聲波圖形的需求儡率,找到類(lèi)似的demo借鑒了一下思路纹笼,下面是波形的效果圖踢故。
-
先說(shuō)一下圖1.1 和圖 1.2 的實(shí)現(xiàn)溯饵,下載這個(gè)Demo
1.首先,需要一個(gè)數(shù)組保存一段時(shí)間內(nèi)不同時(shí)間點(diǎn)音量大小
#define SOUND_METER_COUNT 40
int soundMeters[40];
2.開(kāi)始錄音背镇,或播放音頻時(shí)咬展,開(kāi)啟一個(gè)定時(shí)器timer不斷獲取
averagePowerForChannel,使用 soundMeters數(shù)組保存獲取的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.每次將音量數(shù)據(jù)加入隊(duì)未破婆,數(shù)組左移,注意添加 lastValue 是添加了兩次胸囱,左移也是兩次祷舀,這是為了下面處理數(shù)據(jù)方便。
- (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.最后一步是繪制數(shù)組保存的所有點(diǎn)的繪制邏輯烹笔,這里只展示波形繪制相關(guān)的代碼
4.1. 繪制折線(xiàn)圖使用UIBezierPath裳扯,如圖1.4 要先計(jì)算出頂點(diǎn) y, 因?yàn)榈谌街衛(wèi)astValue 是添加了兩次箕宙,所以相鄰兩個(gè) y點(diǎn)(例如y1 , y2點(diǎn)) 距離baseLine的距離是對(duì)稱(chēng)的 嚎朽,正好連成類(lèi)似波形的折線(xiàn)铺纽。
- (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);
// 基準(zhǔn)線(xiàn)
int baseLine = 250;
// 因數(shù)
int multiplier = 1;
// 音量最大值
int maxLengthOfWave = 50;
// 畫(huà)出的波形的最大值
int maxValueOfMeter = 70;
// 繪制一個(gè)類(lèi)似波形的折線(xiàn)圖
for(CGFloat x = SOUND_METER_COUNT - 1; x >= 0; x--)
{
// 基數(shù)位置的音量 設(shè)置為 -1
multiplier = ((int)x % 2) == 0 ? 1 : -1;
// y 是波形的頂點(diǎn) (波峰 或者 波谷) = baseLine + 波形的相對(duì)長(zhǎng)度 * 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 {
// 繪制線(xiàn)條
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 繪制柱狀圖同理,代碼如下狡门,把繪制折線(xiàn)的代碼替換掉就行了
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上的一個(gè)開(kāi)源代碼 waver
曾有過(guò)關(guān)于如何實(shí)現(xiàn)像 Siri 的聲波效果的討論陷寝,當(dāng)時(shí)提出的第一個(gè)解決方案是 [FFT]
( 如果想了解什么是傅里葉變換 這篇文章不錯(cuò)。)
但是這不是重點(diǎn)其馏,重點(diǎn)是怎么去實(shí)現(xiàn)邏輯凤跑。
首先對(duì)這個(gè)基本函數(shù)我們需要以下幾個(gè)操作做基本調(diào)整
- 函數(shù)周期變化的 x 范圍限制符合手機(jī)屏幕的寬度,假設(shè)為 320
- 在 x 內(nèi)變化的周期數(shù)限制假設(shè)我們需要 2 個(gè)周期變化
- 波峰限制叛复,我們需要峰值不超過(guò)我們 UIView 容器的高度仔引,所以假設(shè) UIView 搞是 20扔仓,那么峰值應(yīng)該限制在 10 以?xún)?nèi)
- 五個(gè)波紋依次波峰遞減 1/5
波紋的限制
上面已經(jīng)非常接近我們想要的效果了,但是還有一個(gè)比較重要的咖耘,就是最終出來(lái)的效果應(yīng)該是越靠近屏幕中間的位置翘簇,波峰越大,靠近屏幕邊緣的地方儿倒,無(wú)限接近于靜止版保。
那么我們還需要一個(gè)參數(shù)(一元二次方程)來(lái)調(diào)整。滿(mǎn)足在 x 的范圍內(nèi)夫否,值從 0 - 正數(shù)值 變化彻犁,那么這兩個(gè)函數(shù)相乘的時(shí)候,就能實(shí)現(xiàn)我們想要的效果凰慈。
Animate
- 一個(gè)用來(lái)調(diào)整波峰的參數(shù)把聲音的音量處理后作為參數(shù)傳入汞幢,于函數(shù)相乘。
- 循環(huán)進(jìn)行 x 變化的參數(shù)使用 CADisplayLink 作為循環(huán)器微谓,聲明一個(gè)位移量急鳄,每次循環(huán)的時(shí)候進(jìn)行遞增,然后傳入我們的函數(shù)堰酿。
那么簡(jiǎn)單分析一下代碼
使用 CAShapeLayer + UIBezierPath 實(shí)現(xiàn)疾宏,好處是更方便對(duì)初始形態(tài)進(jìn)行調(diào)整,像 Siri 那樣可以從圓形變成線(xiàn)條触创。
根據(jù)參數(shù)numberOfWaves 創(chuàng)建多個(gè) CAShapeLayer 保存在 waves中
使用 CADisplayLink 作為循環(huán)器坎藐,位移量遞增,回調(diào)block 獲得音頻的lavel 然后傳入函數(shù)計(jì)算波形哼绑。
將生成的波形(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();
}
完岩馍!,