iOS 繪制音頻波形

有時(shí)候開(kāi)發(fā)中有繪制聲波圖形的需求儡率,找到類(lèi)似的demo借鑒了一下思路纹笼,下面是波形的效果圖踢故。

圖1.1 折線(xiàn)圖
圖1.2 柱狀圖
圖 1.3 Siri波形效果
  • 先說(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)铺纽。

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

    // 基準(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)整

  1. 函數(shù)周期變化的 x 范圍限制符合手機(jī)屏幕的寬度,假設(shè)為 320
  2. 在 x 內(nèi)變化的周期數(shù)限制假設(shè)我們需要 2 個(gè)周期變化
  3. 波峰限制叛复,我們需要峰值不超過(guò)我們 UIView 容器的高度仔引,所以假設(shè) UIView 搞是 20扔仓,那么峰值應(yīng)該限制在 10 以?xún)?nèi)
  4. 五個(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

  1. 一個(gè)用來(lái)調(diào)整波峰的參數(shù)把聲音的音量處理后作為參數(shù)傳入汞幢,于函數(shù)相乘。
  2. 循環(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();
}

完岩馍!,

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末抖韩,一起剝皮案震驚了整個(gè)濱河市蛀恩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌茂浮,老刑警劉巖双谆,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異席揽,居然都是意外死亡顽馋,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)幌羞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)寸谜,“玉大人,你說(shuō)我怎么就攤上這事属桦⌒艹眨” “怎么了他爸?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)果善。 經(jīng)常有香客問(wèn)我讲逛,道長(zhǎng),這世上最難降的妖魔是什么岭埠? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任盏混,我火速辦了婚禮,結(jié)果婚禮上惜论,老公的妹妹穿的比我還像新娘许赃。我一直安慰自己,他們只是感情好馆类,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布混聊。 她就那樣靜靜地躺著,像睡著了一般乾巧。 火紅的嫁衣襯著肌膚如雪句喜。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天沟于,我揣著相機(jī)與錄音咳胃,去河邊找鬼。 笑死旷太,一個(gè)胖子當(dāng)著我的面吹牛展懈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播供璧,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼存崖,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了睡毒?” 一聲冷哼從身側(cè)響起来惧,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎演顾,沒(méi)想到半個(gè)月后供搀,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡偶房,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年趁曼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片棕洋。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖乒融,靈堂內(nèi)的尸體忽然破棺而出掰盘,到底是詐尸還是另有隱情摄悯,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布愧捕,位于F島的核電站奢驯,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏次绘。R本人自食惡果不足惜瘪阁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望邮偎。 院中可真熱鬧管跺,春花似錦、人聲如沸禾进。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)泻云。三九已至艇拍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間宠纯,已是汗流浹背卸夕。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留婆瓜,地道東北人娇哆。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像勃救,于是被迫代替她去往敵國(guó)和親碍讨。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)蒙秒、插件勃黍、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,105評(píng)論 4 62
  • 我太喜歡你們了覆获,這樣的文章好想你們看到: 有事回到30前讀書(shū)的地方,記得一個(gè)同學(xué)在那兒瓢省,便詢(xún)問(wèn)弄息。有人挺熱心,去辦公...
    煥能閱讀 191評(píng)論 0 0
  • 在畫(huà)畫(huà)中尋找樂(lè)趣 我是花紅火紅(麥子熟了1981)勤婚,歡迎你們的到來(lái)摹量,希望多提寶貴意見(jiàn)
    黑白塵埃閱讀 157評(píng)論 0 2
  • 一千公里的路途有多遠(yuǎn) 為何讓我在萬(wàn)余個(gè)日夜心生抱怨 好似幼苗 對(duì)成長(zhǎng)有執(zhí)著的期盼 好似枯樹(shù) 無(wú)人澆灌年輪卻漸漸浮現(xiàn)...
    星之橋閱讀 326評(píng)論 1 5