1. 程式人生 > >算法系列之二十三:離散傅立葉變換之音訊播放與頻譜顯示

算法系列之二十三:離散傅立葉變換之音訊播放與頻譜顯示

        頻譜和均衡器,幾乎是媒體播放程式的必備物件,沒有這兩個功能的媒體播放程式會被認為不夠專業,現在主流的播放器都具備這兩個功能,foobar 2000的十八段均衡器就曾經讓很多人著迷。我用Winamp播放音樂(AOL已經在20131220日停止了Winamp的支援),最早吸引我的原因就是播放介面上那個跳動的頻譜,如圖(1)所示。我一直想搞清楚這個實現原理是什麼,直到我知道有離散傅立葉變換這個東西存在的時候才恍然大悟。

圖(1winmap上跳動的頻譜

        本篇先來說說頻譜吧。既然是頻譜,就一定和頻率有關係吧?是的,那個跳動的頻譜實際上就是當前播放的一小段片音訊資訊在頻域上的功率分佈。鼓聲和絃樂的頻率範圍相差很大,當音樂中有震耳的鼓聲時,頻譜中中低頻的部分就跳的很高,說明這部分頻率的功率比較高。同樣,當高亢的小提琴聲音響起時,頻譜中高頻的部分就跳的很高,說明高頻部分的功率比較高。正是因為這個關係,頻譜總是和正在播放的音樂“相映成趣”。

        要在播放器中顯示跳動的頻譜,就需要知道音訊資料中各個頻率對應的功率,常見的音訊資料都是時域訊號,需要轉換成頻域訊號才能進行分析。在《聽聲音破解電話號碼》一文中,我們介紹了離散傅立葉變換可以將時域的聲音訊號轉換成頻域的頻率功率分佈,並給出了相關的演算法,這正是本篇要介紹的頻譜顯示的基礎。

        《聽聲音破解電話號碼》一文中給出的PowerSpectrumS()函式,可以將44100Hz取樣率的音訊訊號經過2048點離散傅立葉變換後,可以得到1024個點的有效頻率和功率分佈(另外1024個點與之具有對稱性),對應的頻率對映範圍是0Hz22050Hz。播放器軟體通常有一個很小巧的介面,在這個介面上用1024

個波段全部顯示從022050Hz的頻譜是不現實的,也完全沒有必要,因為大部分人的耳朵聽力範圍在20Hz20KHz之間,不在此範圍的頻率可以忽略。一般頻譜最多顯示32個波段(我用的winamp 2.91 版本只有19個頻譜波段),這就涉及到另一個問題,那就是如何從1024個頻譜資料中選擇32個用作頻譜的顯示。選取的原則是要選擇有代表性的頻率,兩個波段的中心頻率最好不要相差太小,可以是均勻選擇,也可以是不均勻選擇。可採用的方法很多,最簡單的方法,就是每隔32個頻率點選擇一個數據,剛好選擇32個點的功率值,然後對映到32個頻譜波段上顯示。44100Hz取樣率的音訊訊號經過2048點離散傅立葉變換後,其頻譜解析度是3.90625Hz,每32個點的頻域資料覆蓋的頻率寬度是125Hz。也就是說,這種方法每隔125Hz選擇一個頻率點,“簡單粗暴”地丟棄了太多的資料,會使得跳動的頻譜缺少一致的連貫性。

        本文介紹的方法是將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);
    }
}

        頻譜顯示視窗的設計沒什麼技術難度,只要熟悉Windows GDI 程式設計,實現一個頻譜視窗應該沒有問題。每一個波段的顯示主要分三部分,分別是背景、當前強度級別和一條緩緩落下的細線(Top_Bar)。除了需要一個列表記錄當前各個波段的強度級別之外,還需要一個列表記錄各個波段的Top_Bar的位置,每當一個buffer播放完成以後,UpdateSpectrum()函式會計算出相應波段的強度,並重新整理當前各個波段的強度級別列表,根據選擇的播放緩衝區buffer大小,重新整理的頻率應該在每秒510次左右。與此同時,內部的位置更新定時器也在週期地減少各個波段的強度級別的值,並降低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倍,因此是一個逐步加快的過程。

        頻譜顯示視窗是一個需要高速繪圖的視窗,直接使用GDI函式畫頻譜視窗已經被證明是低效的方法,不推薦使用。一般都是採用點陣圖緩衝區的方式處理高速重新整理的視窗,具體做法就是在一片點陣圖資料中直接通過顏色值控制“生成”頻譜顯示的點陣圖,然後用貼圖的GDI函式直接“貼”到視窗DC上。

        最後,是點題外話。由於聲音和視覺訊號在人類的神經和大腦之間傳導過程存在差異,會導致聲音和視覺在大腦中的反應有一個時間差,再加上聲和光的傳播速度本身也有很大的差異,因此,為了使頻譜顯示能有更好的感官體驗,需要對頻譜顯示的時機做一些調整。一般來說,應該先將聲音播放出來後再顯示頻譜,這就涉及一個問題,即聲音的音訊資料分段多長比較合適?這實際上是播放器音訊緩衝區大小的選擇問題,緩衝區不能太大,比如0.5秒以上的音訊緩衝區,等播放完0.5秒後再顯示頻譜,視覺體驗上就覺得對不上,鼓聲都響了半天了頻譜上才體現出來,這種感覺肯定不好。緩衝區太小也不好,首先離散傅立葉轉換計算量大,需要一定的時間對音訊資料進行處理,緩衝區太小的話就沒有足夠的時間進行計算,當然,現在的CPU都很強勁,這個不是主要問題,主要問題是如果緩衝器太小會導致頻譜重新整理的太頻繁,這使得頻譜顯示看起來感覺不連貫,很機械。這方面我也沒有理論的資料支撐,根據實踐經驗,音訊緩衝區大小在0.05秒到0.2秒之間時,可以取得比較好的視覺體驗,本文給出的例子程式使用了0.1s的音訊緩衝區,對於我的感覺來說,效果還可以。朋友們如果有這方面的理論資料可以告訴我,本人將不勝感激。

        本文在撰寫過程中建立的例子程式是一個Wave檔案播放程式,播放並顯示一個跳動的頻譜,外觀仿Winamp的顯示效果,繪製出來的頻譜形狀比較接近Winamp的顯示,圖(2)是演示程式最終的效果,就到這裡吧,下一篇再接著講音訊均衡器的實現。

圖(2)頻譜顯示演示視窗

相關推薦

系列十三離散變換音訊播放頻譜顯示

        頻譜和均衡器,幾乎是媒體播放程式的必備物件,沒有這兩個功能的媒體播放程式會被認為不夠專業,現在主流的播放器都具備這兩個功能,foobar 2000的十八段均衡器就曾經讓很多人著迷。我用Winamp播放音樂(AOL已經在2013年12月20日停止了Winamp的

數字影象處理維影象的變換(1)

1.1、訊號變化越快,說明頻率越大,訊號變化越慢,說明頻率越小。這裡的頻率不一定是通常意義上的頻率,通常的頻率是指週期的倒數,我們把通常意義上的頻率叫時間頻率。廣義上的頻率是指變化的快慢,比如圖片來說,從這個畫素到另外一個畫素的灰度值差距比較大,那麼頻率就比較高

c語言數字影象處理(六)離散變換

基礎知識 複數表示 C = R + jI 極座標:C = |C|(cosθ + jsinθ) 尤拉公式:C = |C|ejθ 有關更多的時域與複頻域的知識可以學習複變函式與積分變換,本篇文章只給出DFT公式,性質,以及實現方法 二維離散傅立葉變換(DFT) 其中f(x,y)為原影象,F(u,

數字影象處理成長路4 C語言離散變換(DFT)

這幾天一直學習傅立葉變換,看了很多國內外資料,網上講原理的很多,到了程式實現這塊大多是Matlab,opencv等,這些軟體的api對於我們理解DFT在計算機中的實現並沒有多大幫助。於是想用C/C++實現DFT,經過不斷的閱讀與程式設計實驗,最終程式有了還算滿意

離散變換以及濾波應用

一、二維離散傅立葉變換 二維離散傅立葉變換的公式:F(u,v)=1MN[∑m=0M−1∑n=0N−1f(m,n)WMumWNvn]∙RMN(u,v) F(u,v) = \frac{1}{MN}[\sum_{m=0}^{M-1}\sum_{n=0}^{N-1}f

OpenCV中對影象進行離散變換

#include<opencv2/opencv.hpp> #include <highgui.h> #include <iostream> #include <cv.h> #include <opencv2/core/c

離散變換中進行頻譜平移(MATLAB::fft2shift)的作用

 懶得自己敲文字描述了,直接摘取在一個資料上看到的截圖吧! ------------------------------------------- 影象處理開發資料、影象處理開發需求、影象

數字影象處理筆記——離散變換(2D Discrete Fourier Transform)

二維傅立葉變換 我們先來看看一維情況的傅立葉變換。在訊號系統中講過連續時間的傅立葉變換和離散時間的傅立葉變換,連續時間傅立葉變換在頻譜上時非週期的,離散時間傅立葉變換(DTFT)在頻譜上是週期的。在DSP中講了離散傅立葉變換,它的思想是將時域週期化,反映在頻域上就是對連續的週期頻譜進行抽樣

C++實現離散變換

在上一篇文章《C++實現一維離散傅立葉變換》中,我們介紹了一維訊號傅立葉變換的公式和C++實現,並闡述了頻域幅值的意義。 一維傅立葉變換只適用於一維訊號,例如音訊資料、心腦電圖等。 在影象處理中,影象訊號具有高度和寬度兩個屬性,屬於二維空間訊號。將影象訊號從空間域轉換到頻域

影象變換離散變換

影象傅立葉變換 二維離散傅立葉變換是將影象從空間域轉至頻域,在影象增強、影象去噪、影象邊緣檢測、影象特徵提取、影象壓縮等等應用中都起著極其重要的作用。理論基礎是任意函式都可以表示成正弦函式的線性組合的形式。公式如下 逆變換公式如下 令 R(u,v) 和 I(u,c) 分別表示 F

離散變換 matlab

  從數學意義上看,傅立葉變換試講一個影象轉換為一系列周期函式來處理的。從物理效果上看,傅立葉變換從空間域轉換到頻率域。換句話說傅立葉變換是將影象的灰度分佈函式轉換為影象的頻率分佈函式。實際上對影象進行二維傅立葉變換得到頻譜圖,就是影象梯度的分佈圖,傅立葉頻譜圖上看到的明暗不

C++實現影象維DFT離散(FFT)程式碼——等效於opencv

這篇文章寫的不錯: 首先是一個opencv的dft執行程式碼: 進行一些簡單處理: (1)去掉using namespace std(會和dft函式實現過程中的自定義complex複數結構體衝突,導致結構體不明確,驗證發現 後面並不需要std名稱空間,若需要自己

(一)連續變換離散變換級數(Fourier Series)

訊號的正交分解到傅立葉級數(FS)一、訊號分解為正交函式二、傅立葉級數的三角形式由(一)可知,可將一個週期為T的訊號f(T),在(t0,t0+T)內表示為三角函式集的線性組合,即:上式即為週期訊號f(t),在區間(t0,t0+T)內的三角傅立葉級數展開式。Ω=2π/T稱為基波

離散變換(DFT)和快速變換(FFT)原理實現

目錄 1、影象變換 2、離散傅立葉變換(Discrete Fourier Transform) 3、DFT性質 4、DFT與數字影象處理 5、FFT-快速傅立葉變換 6、DFT與FFT的演算法實現 1. 影象變換 — —數學領域中有很多種變換,如傅立葉變換、拉普拉斯變

拉普拉斯變換變換;Z變換離散時間變換(DTFT);離散變換(DFT)之間的關係及理解

頻域與時域之間的關係是: 時域離散——頻域週期; 時域週期——頻域離散; 對於連續時間訊號 1.拉普拉斯變換: X (

Python中使用numpy對序列進行離散變換DFT

看了大佬對DFT的介紹後感覺離散傅立葉變換對序列訊號的處理還是很有用的, 總結下來就是DFT可以增加有限長序列的長度來提高物理解析度。 自己用python中的numpy庫實現了一下: 其中繪相簿的使用請參考:Python繪圖 將有效長度為4的單位序列,變換為長度16的DFT譜線。

補零離散變換的解析度

     離散傅立葉變換(DFT)的輸入是一組離散的值,輸出同樣是一組離散的值。在輸入訊號而言,相鄰兩個取樣點的間隔為取樣時間Ts。在輸出訊號而言,相鄰兩個取樣點的間隔為頻率解析度fs/N,其中fs為取樣頻率,其大小等於1/Ts,N為輸入訊號的取樣點數。這也就是說,DF

《OpenCV3程式設計入門》——5.5.8 離散變換綜合示例程式(附程式碼)

綜合《OpenCV3程式設計入門》——5.5 離散傅立葉變換原理和 《OpenCV3程式設計入門》——5.5.2 離散傅立葉變換相關函式詳解兩篇文章對離傅立葉變換的詳細介紹,本篇將展示實現離散傅立葉變化的示例程式(本篇所涉及的所有知識均在上述兩篇博文裡有詳細解釋,請參考): //--------

《OpenCV3程式設計入門》——5.5.2 離散變換相關函式詳解

目錄 1、dft()函式 2、返回DFT最優尺寸大小:getOptimalDFTSize()函式 3、擴充影象邊界:copyMakeBorder()函式 4、計算二維向量的幅值:magnitude()函式 6、矩陣歸一化:normalize()函式 1、dft()函式

《OpenCV3程式設計入門》——5.5 離散變換原理

離散傅立葉變換(Discrete Fourier Transform,縮寫為DFT)指傅立葉變換在時域和頻域上都呈現離散的形式,將時域訊號的取樣變換為在離散時間傅立葉變換(DTFT)頻域的取樣。 形式上,變換兩端(時域和頻域)的序列是有限長的,而實際上這兩組序列都應該被認為是離散週期訊號的主值序