算法系列之二十三:离散傅立叶变换之音频播放与频谱显示
算法系列之二十三:離散傅立葉變換之音頻播放與頻譜顯示
- 算法系列之二十三離散傅立葉變換之音頻播放與頻譜顯示
- 導語
- 什么是頻譜
- 1 頻譜的原理
- 2 頻譜的選擇
- 3 頻譜的計算
- 顯示動態頻譜
- 1 實現方法
- 2 雜項說明
- 結果展示
導語
頻譜和均衡器,幾乎是媒體播放程序的必備物件,沒有這兩個功能的媒體播放程序會被認為不夠專業,現在主流的播放器都具備這兩個功能,foobar 2000的十八段均衡器就曾經讓很多人著迷。在上一篇對離散傅立葉變換介紹的基礎上,本篇就進一步介紹一下頻譜是怎么回事兒,下一篇繼續介紹均衡器。
1 什么是頻譜
? ? ? ?頻譜是數字信號處理領域的一個專業術語,但是與本文要介紹的頻譜還不太一樣。我用Winamp播放音樂(AOL已經在2013年12月20日停止了Winamp的支持),最早吸引我的原因就是播放界面上那個跳動的頻譜(如圖 1 所示),相信幾乎所有主流的媒體播放軟件都有這個,這就是本文要介紹的頻譜。
圖 1 winmap上跳動的頻譜
1.1 頻譜的原理
? ? ? ?本篇先來說說頻譜。既然是頻譜,就一定和頻率有關系吧?是的,那個跳動的頻譜實際上就是當前播放的一小段片音頻信息在頻域上的功率分布。鼓聲和弦樂的頻率范圍相差很大,當音樂中有震耳的鼓聲時,頻譜中中低頻的部分就跳的很高,說明這部分頻率的功率比較高。同樣,當高亢的小提琴聲音響起時,頻譜中高頻的部分就跳的很高,說明高頻部分的功率比較高。正是因為這個關系,頻譜總是和正在播放的音樂“相映成趣”。
? ? ? ?要在播放器中顯示跳動的頻譜,就需要知道音頻數據中各個頻率對應的功率,常見的音頻數據都是時域信號,需要轉換成頻域信號才能進行分析。在《聽聲音破解電話號碼》一文中,我們介紹了離散傅立葉變換可以將時域的聲音信號轉換成頻域的頻率功率分布,并給出了相關的算法,這正是本篇要介紹的頻譜顯示的基礎。
1.2 頻譜的選擇
? ? ? ?《聽聲音破解電話號碼》一文中給出的PowerSpectrumS()函數,可以將44100Hz采樣率的音頻信號經過2048點離散傅立葉變換后,可以得到1024個點的有效頻率和功率分布(另外1024個點與之具有對稱性),對應的頻率映射范圍是0Hz到22050Hz。播放器軟件通常有一個很小巧的界面,在這個界面上用1024個波段全部顯示從0到22050Hz的頻譜是不現實的,也完全沒有必要,因為大部分人的耳朵聽力范圍在20Hz到20KHz之間,不在此范圍的頻率可以忽略。一般頻譜最多顯示32個波段(我用的winamp 2.91 版本只有19個頻譜波段),這就涉及到另一個問題,那就是如何從1024個頻譜數據中選擇32個用作頻譜的顯示。選取的原則是要選擇有代表性的頻率,兩個波段的中心頻率最好不要相差太小,可以是均勻選擇,也可以是不均勻選擇。可采用的方法很多,最簡單的方法,就是每隔32個頻率點選擇一個數據,剛好選擇32個點的功率值,然后映射到32個頻譜波段上顯示。44100Hz采樣率的音頻信號經過2048點離散傅立葉變換后,其頻譜分辨率是3.90625Hz,每32個點的頻域數據覆蓋的頻率寬度是125Hz。也就是說,這種方法每隔125Hz選擇一個頻率點,“簡單粗暴”地丟棄了太多的數據,會使得跳動的頻譜缺少一致的連貫性。
1.3 頻譜的計算
? ? ? ?本文介紹的方法是將1024個點分成32個波段,每個波段包含32個頻率點。在每個波段內找到中心頻率點,從中心頻率點向左和向右均勻地各取兩個頻率點,加上中心頻率點共采集5個頻率點的值進行計算。計算的方法是給這5個點賦予不同的權重,中間點權重最高,向兩邊依次降低,然后計算5個點的加權平均值,將加權平均值作為這個波段的頻譜功率映射到頻譜上顯示。這樣計算出來的加權平均值更能反映這個125Hz寬的頻率段的實際功率,從最終的頻譜顯示效果看,這種方法得到的頻譜跳動起來有比較好的連貫性。UpdateSpectrum()函數就是這個算法的體現,對于sampleData參數給出的一段音頻數據,首先調用PowerSpectrumS()函數得到這段音頻的功率分布,然后按照BAND_COUNT常量對其分段,最后對每段的頻域數據計算加權平均值。我們給這5個點分配的權重是:中央點0.5,緊鄰中央點的兩個點是0.15,最外邊的兩個點是0.1。SpectrumWnd是頻譜窗口對象,通過該對象SetBandLevel()函數將計算的結果傳遞給頻譜窗口。
void UpdateSpectrum(short *sampleData, int totalSamples, int channels) { float power[FFT_SIZE]; if(PowerSpectrumS(&m_hFFT, sampleData, totalSamples, channels, power)) { int fpFen = FFT_SIZE / 2 / BAND_COUNT; int level[BAND_COUNT]; for(int i = 0; i < BAND_COUNT; i++) { int centPos = i * fpFen + fpFen / 2; double bandTotal = power[centPos - 2] * 0.1 + power[centPos - 1] * 0.15 + power[centPos] * 0.5 + power[centPos + 1] * 0.15 + power[centPos + 2] * 0.1; level[i] = (int)(bandTotal + 0.5); } m_SpectrumWnd.SetBandLevel(level, BAND_COUNT); } }2 顯示動態頻譜
? ? ? ?動態頻譜就是以一定的時間間隔更新頻譜,使其看起來具有動態效果。圖2展示了一個仿Winamp的頻譜顯示窗口,從這個靜止的瞬間觀察,頻譜的每一個波段主要有三部分組成,分別是背景、當前強度級別和一條緩緩落下的細線(Top_Bar)。
2.1 實現方法
? ? ? ?頻譜顯示窗口的原理非常簡單,只要熟悉Windows GDI 編程,實現一個動態頻譜窗口非常容易。首先需要記錄32個波段的數據,每個波段的數據包含三部分,需要一個列表記錄當前各個波段的強度級別和當前Top_Bar的位置。每當一個buffer播放完成以后,UpdateSpectrum()函數會計算出相應波段的功率強度,并刷新當前各個波段的強度級別列表,根據選擇的播放緩沖區buffer大小,刷新的頻率應該在每秒5-10次左右。與此同時,內部的位置更新定時器也在周期地減少各個波段的強度級別的值,并降低Top_Bar的位置,為了使頻譜顯示平滑一點,更新定時器的頻率要大于強度級別的刷新頻率,一般應該在每秒15次以上。
? ? ? ?Top_Bar位置和強度級別的刷新就是一個不斷較少的過程,但是減少的方式不一樣。強度級別的減少可以是一個固定值,每次都減少一定的數量。Top_Bar則維持一個懸停時間,在懸停時間內位置不變化,懸停時間結束后,其值的減少是一個逐步加快的過程,并最終在強度級別減到0之前趕上強度級別的位置,這樣使得頻譜顯示看起來生動有趣。下面給出更新定時器的處理代碼,是本文的例子中使用的,僅供參考:
void CSpectrumWnd::UpdateLevelOnTimer() { for(int i = 0; i < BAND_COUNT; i++) { if(m_curLevel[i] >= m_levelStep) m_curLevel[i] -= m_levelStep; else m_curLevel[i] = 0; if(m_topBar[i].wait > 0) m_topBar[i].wait--; else { m_topBar[i].level = (m_topBar[i].level > m_topBar[i].step) ? (m_topBar[i].level - m_topBar[i].step) : 0; if(m_topBar[i].level <= m_curLevel[i]) m_topBar[i].level = m_curLevel[i]; if(m_topBar[i].step < 64) m_topBar[i].step += (m_topBar[i].step / 2); } } }? ? ? ?m_levelStep是強度值每次減少的點數,Top_Bar的wait屬性是懸停計數,用于控制懸停時間,當其減少到0時,則開始下降Top_Bar的位置,每次下降的點數是前一次下降點數的1.5倍,因此是一個逐步加快的過程。
2.2 雜項說明
? ? ? ?頻譜顯示窗口是一個需要高速繪圖的窗口,直接使用GDI函數畫頻譜窗口已經被證明是低效的方法,不推薦使用。一般都是采用位圖緩沖區的方式處理高速刷新的窗口,具體做法就是在一片位圖數據中直接通過顏色值控制“生成”頻譜顯示的位圖,然后用貼圖的GDI函數直接“貼”到窗口DC上。
? ? ? ?由于聲音和視覺信號在人類的神經和大腦之間傳導過程存在差異,會導致聲音和視覺在大腦中的反應有一個時間差,再加上聲和光的傳播速度本身也有很大的差異,因此,為了使頻譜顯示能有更好的感官體驗,需要對頻譜顯示的時機做一些調整。一般來說,應該先將聲音播放出來后再顯示頻譜,這就涉及一個問題,即聲音的音頻數據分段多長比較合適?這實際上是播放器音頻緩沖區大小的選擇問題,緩沖區不能太大,比如0.5秒以上的音頻緩沖區,等播放完0.5秒后再顯示頻譜,視覺體驗上就覺得對不上,鼓聲都響了半天了頻譜上才體現出來,這種感覺肯定不好。緩沖區太小也不好,首先離散傅立葉轉換計算量大,需要一定的時間對音頻數據進行處理,緩沖區太小的話就沒有足夠的時間進行計算,當然,現在的CPU都很強勁,這個不是主要問題,主要問題是如果緩沖器太小會導致頻譜刷新的太頻繁,這使得頻譜顯示看起來感覺不連貫,很機械。這方面我也沒有理論的數據支撐,根據實踐經驗,音頻緩沖區大小在0.05秒到0.2秒之間時,可以取得比較好的視覺體驗,本文給出的例子程序使用了0.1s的音頻緩沖區,對于我的感覺來說,效果還可以。朋友們如果有這方面的理論數據可以告訴我,本人將不勝感激。
3 結果展示
? ? ? ?本文在撰寫過程中創建的例子程序是一個Wave文件播放程序,播放并顯示一個跳動的頻譜,外觀仿Winamp的顯示效果,繪制出來的頻譜形狀比較接近Winamp的顯示,圖2 是演示程序最終的效果,就到這里吧,下一篇再接著講音頻均衡器的實現。
? ? ? ?最后廣播一下,應廣大熱心讀者的要求,本專欄全部文章對應的演示代碼,將提供打包下載,目前正在找存儲位置,請大家關注博客的更新。
圖 2 頻譜顯示演示窗口
總結
以上是生活随笔為你收集整理的算法系列之二十三:离散傅立叶变换之音频播放与频谱显示的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 自签名证书和CA机构颁发的证书的区别
- 下一篇: 安卓界面尺寸规范