原文連結(jié):
作者:potato04
鏈接:https://juejin.im/post/5c1bbec66fb9a049cb18b64c
來源:掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。
一步一步教你實現(xiàn)iOS音頻頻譜動畫(一)
如果你想先看看最終效果再決定看不看文章 -> bilibili
示例代碼下載
第二篇:一步一步教你實現(xiàn)iOS音頻頻譜動畫(二)
基于篇幅考慮捉超,本次教程分為兩篇文章,本篇文章主要講述音頻播放和頻譜數(shù)據(jù)的獲取,下篇將講述數(shù)據(jù)處理和動畫繪制勿侯。
前言
很久以前在電腦上聽音樂的時候,經(jīng)常會調(diào)出播放器的一個小工具缴罗,里面的柱狀圖會隨著音樂節(jié)奏而跳動助琐,就感覺自己好專業(yè),盡管后來才知道這個是音頻信號在頻域下的表現(xiàn)面氓。
熱身知識
動手寫代碼之前兵钮,讓我們先了解幾個基礎(chǔ)概念吧
音頻數(shù)字化
采樣: 總所周知,聲音是一種壓力波舌界,是連續(xù)的掘譬,然而在計算機(jī)中無法表示連續(xù)的數(shù)據(jù),所以只能通過間隔采樣的方式進(jìn)行離散化呻拌,其中采集的頻率稱為采樣率屁药。根據(jù)奈奎斯特采樣定理 ,當(dāng)采樣率大于信號最高頻率的2倍時信號頻率不會失真。人類能聽到的聲音頻率范圍是20hz到20khz酿箭,所以CD等采用了44.1khz采樣率能滿足大部分需要复亏。
量化: 每次采樣的信號強(qiáng)度也會有精度的損失,如果用16位的Int(位深度)來表示缭嫡,它的范圍是[-32768,32767]缔御,因此位深度越高可表示的范圍就越大,音頻質(zhì)量越好妇蛀。
聲道數(shù): 為了更好的效果耕突,聲音一般采集左右雙聲道的信號,如何編排呢评架?一種是采用交錯排列(Interleaved):
LRLRLRLR
眷茁,另一種采用各自排列(non-Interleaved):LLL RRR
。
以上將模擬信號數(shù)字化的方法稱為脈沖編碼調(diào)制(PCM)纵诞,而本文中我們就需要對這類數(shù)據(jù)進(jìn)行加工處理上祈。
傅里葉變換
現(xiàn)在我們的音頻數(shù)據(jù)是時域的,也就是說橫軸是時間浙芙,縱軸是信號的強(qiáng)度登刺,而動畫展現(xiàn)要求的橫軸是頻率。將信號從時域轉(zhuǎn)換成頻域可以使用傅里葉變換實現(xiàn)嗡呼,信號經(jīng)過傅里葉變換分解成了不同頻率的正弦波纸俭,這些信號的頻率和振幅就是我們需要實現(xiàn)動畫的數(shù)據(jù)。
圖1 (from nti-audio) 傅里葉變換將信號從時域轉(zhuǎn)換成頻域
實際上計算機(jī)中處理的都是離散傅里葉變換(DFT)南窗,而快速傅里葉變換(FFT)是快速計算離散傅里葉變換(DFT)或其逆變換的方法揍很,它將DFT的復(fù)雜度從O(n2)降低到O(nlogn)。 如果你剛才點開前面鏈接看過其中介紹的FFT算法万伤,那么可能會覺得這FFT代碼該怎么寫女轿?不用擔(dān)心,蘋果為此提供了Accelerate框架壕翩,其中vDSP部分提供了數(shù)字信號處理的函數(shù)實現(xiàn)蛉迹,包含F(xiàn)FT。有了vDSP放妈,我們只需幾個步驟即可實現(xiàn)FFT北救,簡單便捷,而且性能高效芜抒。
iOS中的音頻框架
現(xiàn)在開始讓我們看一下iOS系統(tǒng)中的音頻框架珍策, AudioToolbox
功能強(qiáng)大,不過提供的API都是基于C語言的宅倒,其大多數(shù)功能已經(jīng)可以通過AVFoundation
實現(xiàn)攘宙,它利用Objective-C
/Swift
對于底層接口進(jìn)行了封裝。我們本次需求比較簡單,只需要播放音頻文件并進(jìn)行實時處理蹭劈,所以AVFoundation
中的AVAudioEngine
就能滿足本次音頻播放和處理的需要疗绣。
圖2 (from WWDC16) iOS/MAC OS X 音頻技術(shù)棧
AVAudioEngine
AVAudioEngine
從iOS8加入到AVFoundation
框架,它提供了以前需要深入到底層AudioToolbox
才實現(xiàn)的功能铺韧,比如實時音頻處理多矮。它把音頻處理的各環(huán)節(jié)抽象成AVAudioNode
并通過AVAudioEngine
進(jìn)行管理,最后將它們連接構(gòu)成完整的節(jié)點圖哈打。以下就是本次教程的AVAudioEngine
與其節(jié)點的連接方式塔逃。
圖3 AVAudioEngine和AVAudioNode連接圖
mainMixerNode
和outputNode
都是在被訪問的時候默認(rèn)由AVAudioEngine
對象創(chuàng)建并連接的單例對象,也就是說我們只需要手動創(chuàng)建engine
和player
節(jié)點并將他們連接就可以了料仗!最后在mainMixerNode
的輸出總線上安裝分接頭將定量輸出的AVAudioPCMBuffer
數(shù)據(jù)進(jìn)行轉(zhuǎn)換和FFT湾盗。
代碼實現(xiàn)
了解了以上相關(guān)知識后,我們就可以開始編寫代碼了立轧。打開項目AudioSpectrum01-starter
格粪,首先要實現(xiàn)的是音頻播放功能。
如果你只是想瀏覽實現(xiàn)代碼肺孵,打開項目
AudioSpectrum01-final
即可,已經(jīng)完成本篇文章的所有代碼
音頻播放
在AudioSpectrumPlayer
類中創(chuàng)建AVAudioEngine
和AVAudioPlayerNode
兩個實例變量:
接下來在init()
方法中添加如下代碼:
//1
:這里將player
掛載到engine
上颜阐,再把player
與engine
的mainMixerNode
連接起來就完成了AVAudioEngine
的整個節(jié)點圖創(chuàng)建(詳見圖3)平窘。
//2
:在調(diào)用engine
的strat()
方法開始啟動engine
之前,需要通過prepare()
方法提前分配相關(guān)資源
繼續(xù)完善play(withFileName fileName: String)
和stop()
方法:
//1
:首先需要確保文件名為fileName
的音頻文件能正常加載凳怨,然后通過stop()
方法停止之前的播放瑰艘,再調(diào)用scheduleFile(_:at:completionHandler:)
方法編排新的文件,最后通過play()
方法開始播放肤舞。
//2
:停止播放調(diào)用player
的stop()
方法即可紫新。
音頻播放功能已經(jīng)完成,運行項目李剖,試試點擊音樂右側(cè)的Play
按鈕進(jìn)行音頻播放吧芒率。
音頻數(shù)據(jù)獲取
前面提到我們可以在mainMixerNode
上安裝分接頭定量獲取AVAudioPCMBuffer
數(shù)據(jù),現(xiàn)在打開AudioSpectrumPlayer
文件篙顺,先定義一個屬性: fftSize
偶芍,它是每次獲取到的buffer
的frame數(shù)量。
將光標(biāo)定位至init()
方法中的engine.connect()
語句下方德玫,調(diào)用mainMixerNode
的installTap
方法開始安裝分接頭:
在分接頭的回調(diào) block 中將拿到的 2048 個 frame 的 buffer 交由fft
函數(shù)進(jìn)行計算匪蟀,最后將計算的結(jié)果通過delegate
進(jìn)行傳遞。
按照 44100hz 采樣率和 1 Frame = 1 Packet 來計算(可以參考這里關(guān)于channel宰僧、sample材彪、frame、packet的概念與關(guān)系),那么block將會在一秒中調(diào)用44100/2048≈21.5次左右段化,另外需要注意的是block有可能不在主線程調(diào)用嘁捷。
FFT實現(xiàn)
終于到實現(xiàn)FFT
的時候了,根據(jù)vDSP
文檔穗泵,首先需要定義一個FFT
的權(quán)重數(shù)組(fftSetup)
普气,它可以在多次FFT
中重復(fù)使用和提升FFT
性能:
不需要時在析構(gòu)函數(shù)(反初始化函數(shù))中銷毀:
最后新建fft
函數(shù),實現(xiàn)代碼如下:
通過代碼中的注釋佃延,應(yīng)該能了解如何從buffer
獲取音頻樣本數(shù)據(jù)并進(jìn)行FFT
計算了现诀,不過以下兩點是我在完成這一部分內(nèi)容過程中比較難理解的部分:
- 通過
buffer
對象的方法floatChannelData
獲取樣本數(shù)據(jù),如果是多聲道并且是interleaved
履肃,我們就需要對它進(jìn)行deinterleave
, 通過下圖就能比較清楚的知道deinterleave
前后的結(jié)構(gòu)仔沿,不過在我試驗了許多音頻文件之后,發(fā)現(xiàn)都是non-interleaved
的尺棋,也就是無需進(jìn)行轉(zhuǎn)換封锉。┑( ̄Д  ̄)┍
圖4 interleaved的樣本數(shù)據(jù)需要進(jìn)行deinterleave
-
vDSP
在進(jìn)行實數(shù)FFT
計算時利用一種獨特的數(shù)據(jù)格式化方式以達(dá)到節(jié)省內(nèi)存的目的,它在FFT
計算的前后通過兩次轉(zhuǎn)換將FFT
的輸入和輸出的數(shù)據(jù)結(jié)構(gòu)進(jìn)行統(tǒng)一成DSPSplitComplex
膘螟。第一次轉(zhuǎn)換是通過vDSP_ctoz
函數(shù)將樣本數(shù)據(jù)的實數(shù)數(shù)組轉(zhuǎn)換成DSPSplitComplex
成福。第二次則是將FFT
結(jié)果轉(zhuǎn)換成DSPSplitComplex
,這個轉(zhuǎn)換的過程是在FFT
計算函數(shù)vDSP_fft_zrip
中自動完成的荆残。第二次轉(zhuǎn)換過程如下:n位樣本數(shù)據(jù)(n/2位復(fù)數(shù))進(jìn)行fft計算會得到n/2+1位復(fù)數(shù)結(jié)果:{[DC,0],C[2],...,C[n/2],[NY,0]} (其中DC是直流分量奴艾,NY是奈奎斯特頻率的值,C是復(fù)數(shù)數(shù)組),其中[DC,0]和[NY,0]的虛部都是0内斯,所以可以將NY放到DC中的虛部中蕴潦,其結(jié)果變成{[DC,NY],C[2],C[3],...,C[n/2]},與輸入位數(shù)一致俘闯。
圖5 第一次轉(zhuǎn)換時潭苞,vDSP_ctoz函數(shù)將實數(shù)數(shù)組轉(zhuǎn)換成DSPSplitComplex結(jié)構(gòu)
再次運行項目,現(xiàn)在除了聽到音樂之外還可以在控制臺中看到打印輸出的頻譜數(shù)據(jù)真朗。
圖6 將結(jié)果通過Google Sheets繪制出來的頻譜圖
好了此疹,本篇文章內(nèi)容到這里就結(jié)束了,下一篇文章將對計算好的頻譜數(shù)據(jù)進(jìn)行處理和動畫繪制遮婶。
資料參考
[1] wikipedia秀菱,脈沖編碼調(diào)制, zh.wikipedia.org/wiki/%E8%84…
[2] Mike Ash蹭睡,音頻數(shù)據(jù)獲取與解析, www.mikeash.com/pyblog/frid…
[3] 韓 昊, 傅里葉分析之掐死教程, blog.jobbole.com/70549/
[4] raywenderlich, AVAudioEngine編程入門衍菱,www.raywenderlich.com/5154-avaudi…
[5] Apple, vDSP編程指南, developer.apple.com/library/arc…
[6] Apple, aurioTouch案例代碼,developer.apple.com/library/arc…