算法系列之二十四:離散傅立葉變換之音頻播放與均衡器

導語

在算法系列的第二十二篇,我們介紹了離散傅立葉變換算法的實現,將時域的音頻信號轉換到頻域進行分析,獲取撥號音頻的頻率特徵。這一篇我們將介紹一種頻域均衡器的實現方法,所謂的頻域均衡器,就是在頻域信號的基礎上對音頻數據進行調整,然後再將頻域信號轉換成時域信號在回放設備上播放,從而達到音色調節的目的。將頻域信號轉換成時域信號的算法,就是離散傅立葉逆變換算法。

1 離散傅立葉逆變換

有從時域轉換到頻域的方法,就必然有從頻域轉換到時域的方法,相對於離散傅里葉變換,這個反向轉換就是離散傅里葉逆變換(IDFT)。和離散傅里葉變換一樣,離散傅里葉逆變換也是連續傅里葉逆變換的離散形式,先來看看非週期信號連續傅里葉逆變換的公式:

x(t)=12π+X(ω)eiωtdω
(24-1)

連續傅里葉逆變換中的函數X(ω)是頻域連續的,現在假設在X(ω)的某一段連續區間上按照頻域抽取N個頻率,得到N個採樣點,則每個採樣點的離散傅里葉逆變換公式就是:

x(n)=1Nk=0N1X(k)ei2πNkn            n=0,1,,N1
(24-2)

如果引入常量WN ,式(24-2)可以簡單記爲:

x(n)=1Nk=0N1X(k)WnkN          n=0,1,,N1
(24-3)

1.1 快速傅立葉逆變換的推導

對應於前面介紹的快速傅里葉變換,也存在與之對應的快速傅里葉逆變換(Inverse Fast Fourier Transform,IFFT)。和快速發傅里葉變換算法的推導過程一樣,快速傅里葉逆變換算法的推導也是從式(24-3)開始,利用WN 的週期性和對稱性,將離散傅里葉逆變換逐級分解,減少計算量。具體的推導過程與快速傅里葉變換類似,讀者可參考離散傅立葉變換算法的推導過程自行推導,此處不再贅述。

就IFFT算法的實現而言,其過程和FFT算法的實現一樣,只需對FFT算法稍作修改,就成爲了IFFT算法。將離散傅立葉逆變換公式(24-3)與離散傅立葉變換的公式對比,可以看出,二者的區別主要有兩點:一個是蝶形變換的旋轉因子不同,另一個是IFFT算法需要對整體結果除以N。FFT算法的蝶形變換旋轉因子是 ,而IFFT算法的旋轉因子是 ,除此之外,二者蝶形變換的距離和位置關係都是一樣的,也就是說,最終位序重排的方法也一樣。

1.2 快速傅里葉逆變換的算法實現

快速傅里葉逆變換算法的蝶形變換旋轉因子是WnN ,其分解的複數形式中餘弦項(實部)與FFT算法的餘弦項相同,正弦項(虛部)的符號位與FFT算法的正弦項剛好相反,因此,算法實現仍然可以可用FFT_HANDLE中的正弦項和餘弦項表。IFFT的算法實現如下:

void IFFT(FFT_HANDLE *hfft, COMPLEX * FD2TD)
{
    int i,j,k,butterfly,p;

    int power = NumberOfBits(hfft->count);

    for(k = 0; k < hfft->count; k++)
        FD2TD[k] = FD2TD[k] / COMPLEX(hfft->count, 0.0);

    /*蝶形運算*/
    for(k = 0; k < power; k++)
    {
        for(j = 0; j < 1<<k; j++)
        {
            butterfly = 1 << (power-k);
            p = j * butterfly;
            int s = p + butterfly / 2;
            for(i = 0; i < butterfly/2; i++)
            {
                COMPLEX t = FD2TD[i + p] + FD2TD[i + s];
                FD2TD[i + s] = (FD2TD[i + p] - FD2TD[i + s]) * COMPLEX(hfft->wt[i*(1<<k)].re,
                    -hfft->wt[i*(1<<k)].im);
                FD2TD[i + p] = t;
            }
        }
    }
    /*----按照倒位序重新排列變換後信號----*/
    for (k = 0; k < hfft->count; k++) 
    {
        int r = BitReverise(k, power);
        if (r > k) 
        {
            COMPLEX t = FD2TD[k];
            FD2TD[k] = FD2TD[r];
            FD2TD[r] = t;
        }
    }
}

2 利用傅里葉變換實現頻域均衡器

調節均衡器改變聲音的回放效果,就像在湯裏放味精一樣,掩蓋了音樂原始的味道,也能獲得一些意想不到的效果。但是,同學,你關注過它的實現原理嗎?這一節,我們就來研究一下均衡器的實現原理,同時結合前面介紹的快速傅里葉變換和快速傅里葉逆變換,實現一個可以對各種頻率的聲音進行精準控制的頻域均衡器算法。

從應用角度理解,音樂均衡器有兩種常見類型,一種是圖示均衡器(Graphic Equalizer),另一種是參量均衡器(Parametric Equalizer)。圖示均衡器是一種按照一定的規律把全音頻20~20000 Hz劃分爲若干的頻段,每個頻段對應一個可以對電平進行增益或衰減的調節器,可以根據需要,對輸入的音頻信號按照特定的頻段進行單獨的增益或衰減。參量均衡器不劃分固定的波段,可對任意一個頻率點(包括頻點附近指定頻率帶寬內的所有點)進行控制,通過調整帶寬,使得調節控制可精確(小帶寬),也可模糊(大帶寬),非常靈活。參量均衡器操作控制不直觀,多用在對聲音精確控制的專業場合。像Winamp和Foobar這樣的音頻播放器,多采用圖示均衡器,通過一個帶調節器的圖形面板可以讓用戶很方便地對特定頻段進行調節。

從信號形態角度理解,均衡器又可以分爲時域均衡器和頻域均衡器兩種類型。時域均衡器對時域音頻信號通過疊加一系列濾波器實現對音色的改變,無論是傳統的音響設備還是衆多音樂播放軟件,絕大多數都是使用時域均衡器。時域均衡器通常由一系列二次IIR濾波器或FIR濾波器串聯組合而成,每個波段對應一個濾波器,各個濾波器可以單獨調節,串聯在一起形成最終的效果。但是,傳統的IIR濾波器具有反饋迴路,會出現相位偏差,而FIR濾波器會造成比較大的時間延遲。另外,如果使用IIR或者FIR濾波器,均衡器波段越多,需要串聯的濾波器的個數也越多,運算量也越大。頻域均衡器是在頻域內直接對指定頻率的音頻信號進行增益或衰減,從而達到改變音色的目的。頻域均衡器沒有相位誤差和時間延遲,而且不固定波段,可以對任意頻率進行調節,不僅適用於圖示均衡器,也適用於參量均衡器。特別是採用快速傅里葉變換這樣的算法,可以進行更快速的運算,即便是多段均衡器也不會引起運算量的增加。

2.1 頻域均衡器的實現原理

總體上說,頻域均衡器的實現原理很簡單,就是將時域音頻信號轉換到頻域,然後對特定頻率進行增益或衰減計算,最後再將結果轉換到時域,從而實現對音頻音色的修改。如果是多個音軌的音頻,需要對每個音軌單獨做上述轉換和調節。原理簡單,但是實現起來並不簡單,有很多細節問題需要解決。首先,用戶在圖示均衡器上拉動拉桿,調節了某個波段之後,這個調節的相對變化如何轉化爲對頻域信號的處理?

2.2 雜項說明

圖示均衡器允許用戶調節每個波段的增益和衰減,調節的單位通常是dB(分貝)。dB是一個相對比值,用於表示兩個值之間的比例關係。20 dB的信號的實際強度是0 dB信號的10倍,而-20 dB的信號的實際強度是0 dB信號的1/10。當用戶調節了某個波段的增益值後,如何將這個相對增量轉換成能在頻域內直接對頻域數據進行計算處理的增益強度,是頻域均衡器需要解決的重點問題。

2.2.1 頻域的增益和衰減

首先,增益或衰減是基於頻率(頻段)進行計算,所以這個問題需要在頻域內處理。處理的方法就是根據功率相對強度與頻域信號值的計算關係。與處理頻譜的方式不同,這裏要通過這個公式反推需要在頻域信號疊加什麼值才能使得功率達到指定的增益或衰減。具體來說,就是頻域信號的實部和虛部各需要疊加什麼值,以及這種疊加關係是什麼。

爲了給某個頻率的信號增益Pz 個dB(Pz 若是小於0,表示是對信號進行衰減),根據頻譜功率計算公式,需要疊加的量(x+yi) 。現在對x和y賦予不同的權重,不妨設實部的權重是0.75,虛部的權重是0.25,也就是令x=0.75k,y=0.25k。將x和y代入頻譜功率計算公式,簡化後可以得到:

Pz=20.0×log10(102Nk)
(24-4)

對於指定的增益(或衰減)值Pz ,可以利用上式計算出k的值。接下來,假設某個頻率在頻域的值是(a+bi) ,其相對強度是P0 ,如果給其疊加一個增益(或衰減)Pz ,需要的計算是:

P0+Pz=20.0×log10(a2+b2N/2)+20.0×log10(102Nk)

=20.0×log10((102Nka)2+(102Nkb)2N/2)

由上式可知,需要給原始信號進行疊加的值是10k/2N ,疊加的方式是複數乘法。

前面給出的快速傅里葉變換和逆變換都是複數變換,但是處理音頻數據時都只使用了複數的實部(虛部賦值爲0.0),因此,疊加值在頻域計算好之後,需要轉換到時域,將虛部清0,只保留實部的值,然後再轉換到頻域,此時的疊加值纔是最終參與複數乘法計算的值。根據增益值計算疊加量的算法實現如下:

bool UpdateFilter(EQUALIZER_HANDLE *hEQ, float *gain, int count)
{
    if((hEQ->hfft.count / 2) < count)
        return false;

    for(int i = 0; i < hEQ->hfft.count / 2; i++)
    {
        double dbk = pow(10.0, gain[i]/20.0);
        hEQ->filter[i].re = (float)(dbk * 0.75);
        hEQ->filter[i].im = (float)(dbk * 0.25);
        hEQ->filter[hEQ->hfft.count - 1 - i].re = hEQ->filter[i].re;
        hEQ->filter[hEQ->hfft.count - 1 - i].im = hEQ->filter[i].im;
    }

    IFFT(&hEQ->hfft, hEQ->filter); //to time-domain
    for(int i = 0; i < hEQ->hfft.count; i++)
    {
        hEQ->filter[i].im = (float)0.0;
    }
    FFT(&hEQ->hfft, hEQ->filter); //to freq-domain

    return true;
}

算法中只計算了前一半的疊加量,後一半採用的是對稱賦值,這是由頻域信號的對稱性決定的。

2.2.2 應用三次樣條曲線插值算法平滑增益與衰減

對均衡器調節,對應的是一個波段,不是一個頻率。因此,在頻域進行增益(或衰減)計算時,不應僅考慮一個頻率,而應考慮以這個頻率爲中心的整個波段。當然,也不是整個波段都進行相同的增益(或衰減),最好的方法是波段的中心頻率點執行最大增益(或衰減),然後按照波段帶寬,從中心到邊緣逐步降低增益(或衰減)的值。

從波段中心到邊緣的變化可以採用線性方式,從示意圖看起來就是多條折線。當然,也可以採用當前流行的方法,就是採用曲線插值的方法,使示意圖起來想一條平滑的曲線。說到曲線插值,本系列會另起一篇專門介紹。本篇的均衡器例子就使用三次樣條曲線插值算法,得到一條平滑的增益(或衰減)值曲線。生活中到處都是算法,不是嗎?

2.3 均衡器的實現——仿Foobar的18段均衡器

有了以上的分析,均衡器算法的實現就水到渠成了,將音頻數據按照FFT算法一次能處理的最大數據分塊,對每一塊音頻數據用FFT算法將其轉換到頻域,對信號進行計算,然後在用IFFT算法將音頻數據轉換到時域,每一塊的處理算法如下:

  FFT(&hEQ->hfft, leftData);
  if(channels > 1)
  {
        FFT(&hEQ->hfft, rightData);
  }

  SampleDataMpGain(leftData, rightData, hEQ->hfft.count, channels, hEQ->filter);
  IFFT(&hEQ->hfft, leftData);
  if(channels > 1)
  {
        IFFT(&hEQ->hfft, rightData);
  }

SampleDataMpGain()函數負責增益(或衰減)的計算,就是將信號與filter逐個做複數乘法運算,最終的結果將在逆變換後得到的音頻數據中得到體現。

最後需要注意的是,對信號進行增益(或衰減)計算可能會導致信號超出合法值的範圍,從聽覺上理解就是會導致調整後的聲音聽起來有雜音,因此需要在轉換過程中消除這種現象。在音頻處理領域,有很多專門的算法應對這種情況,很多個人和組織都申請了很多這樣的專利。如果要實現一個專業的均衡器,你需要研究這些算法,本章的例子只是爲了演示用FFT算法實現頻域均衡器的原理,所以採用了一種簡單的處理方法,就是當有信號值越界後,簡單調整成最大值。這個調整在ComplexToSampleData()函數中實現,這個函數與SampleDataToComplex()函數對應,用於將調整後的信號轉換成PCM格式的音頻數據。

void ComplexToSampleData(COMPLEX *cdl, COMPLEX *cdr, int channels, short *sampleData)
{
    if(cdl->re > 1.0)
        cdl->re = 1.0;
    if(cdl->re < -1.0)
        cdl->re = -1.0;

    *sampleData = short(cdl->re * 32768.0);
    if(channels != 1)
    {
        if(cdr->re > 1.0)
            cdr->re = 1.0;
        if(cdr->re < -1.0)
            cdr->re = -1.0;
        *(sampleData + 1) = short(cdr->re * 32768.0);
    }
}

至此,所有的核心算法都已經完成,按照慣例,可以做個例子演示一下了。是的,來一個仿Foobar的18段均衡器吧,順帶體現一下三次樣條曲線插值算法的價值。圖24-1就是演示程序,均衡器曲線是我隨便調的,沒人會這麼調均衡器吧?完整的示例程序代碼包含在本章的隨書代碼中,除了算法核心代碼之外,剩下的都是常規的Windows編程,請大家自己研究吧。

這裏寫圖片描述

圖24-1 一個仿Foobar的18段均衡器的例子

3 總結

這一系列文章介紹了離散傅里葉變換及其快速算法(FFT)的幾個應用例子,都是生活中常見的功能,背後隱藏的卻是如此簡單的算法實現。其實離散傅里葉變換在工業和信號處理領域有非常廣泛的應用,並不僅限於本章的例子。本章給出的算法不算最高效的算法實現,但是中規中矩,是研究算法原理的好例子,讀者還可以從互聯網上找到處理實數的更高效的FFT算法來研究。我的目的是讓大家再次瞭解生活中隱藏的算法,解除對算法的神祕感,不知道是否達到了?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章