| 本文部分知識從以下文章學習: |
最近工作上在做關於音樂游戲的內容,其中需要分析音頻找節奏點(或者說是重音點)。
學習了一系列相關知識后,了解到一段音樂的波形圖可以分解成不同頻率的波形圖,也就是由時域到頻域的轉換。
借用其他博主的圖就比較容易理解了,如下所示。

波從時域到頻域的轉換可以通過傅里葉變換實現,關於傅里葉變換的知識可以從最上面的鏈接學習或者自行查找(傅里葉真厲害!!!)。
計算機處理的音頻在時域上是離散的數據,我們可以使用離散傅里葉變換DFT(傅里葉變換在時域和頻域上都呈離散的形式)獲得頻域上的離散數據。
快速傅立葉變換FFT是DFT的快速算法,其核心思路就是分治法的DFT,具體推導可以查看上面的第二個鏈接。
FFT 代碼如下:
static void BitReverse(Complex[] cpData, int n) { Complex temp; int lim = 0; while ((1 << lim) < n) lim++; for (int i = 0; i < n; i++) { int t = 0; for (int j = 0; j < lim; j++) { if (((i >> j) & 1) != 0) t |= (1 << (lim - j - 1)); } if (i < t) { temp = cpData[i]; cpData[i] = cpData[t]; cpData[t] = temp; } // i < t 防止交換兩次 } } static void FFT1(Complex[] cpData, bool forward) { var n = cpData.Length; BitReverse(cpData, n);//位反轉 Complex[] omg = new Complex[n]; for (int i = 0; i < n; i++) { omg[i] = new Complex((float)Math.Cos(2 * Math.PI * i / n), (float)Math.Sin(2 * Math.PI * i / n)); } Complex temp ; for (int step = 2; step <= n; step *= 2) { int m = step / 2; for (int j = 0; j < n; j += step) for (int i = 0; i < m; i++) {//蝶形運算 if(forward) temp = omg[n / step * i] * cpData[j + i + m]; else temp = omg[n / step * i].Conjugate() * cpData[j + i + m]; cpData[j + i + m] = cpData[j + i] - temp; cpData[j + i] = cpData[j + i] + temp; } } }
Complex是封裝的復數類,偷懶不是自己寫的,來自這位老哥https://blog.csdn.net/u011583927/article/details/46974341
這個FFT,new了好多對象,效率不是很高。。。
再貼一個直接把復數的實部虛部輪流放在一個數組里直接算的,能快一些。
static void Reverse(float[] data, int n) { int j = 0, k = 0; var top = n / 2; while (true) { var t = data[j + 2]; data[j + 2] = data[k + n]; data[k + n] = t; t = data[j + 3]; data[j + 3] = data[k + n + 1]; data[k + n + 1] = t; if (j > k) { t = data[j]; data[j] = data[k]; data[k] = t; t = data[j + 1]; data[j + 1] = data[k + 1]; data[k + 1] = t; t = data[j + n + 2]; data[j + n + 2] = data[k + n + 2]; data[k + n + 2] = t; t = data[j + n + 3]; data[j + n + 3] = data[k + n + 3]; data[k + n + 3] = t; } k += 4; if (k >= n) break; var h = top; while (j >= h) { j -= h; h /= 2; } j += h; } } static void FFT2(float[] data, bool forward) { var n = data.Length; n /= 2; Reverse(data, n); float sign = forward ? 1 : -1; var mmax = 1; while (n > mmax) { var istep = 2 * mmax; var theta = sign * (float)Math.PI / mmax; float wr = 1, wi = 0; var wpr = (float)Math.Cos(theta); var wpi = (float)Math.Sin(theta); for (var m = 0; m < istep; m += 2) { for (var k = m; k < 2 * n; k += 2 * istep) { var j = k + istep; var tempr = wr * data[j] - wi * data[j + 1]; var tempi = wi * data[j] + wr * data[j + 1]; data[j] = data[k] - tempr; data[j + 1] = data[k + 1] - tempi; data[k] = data[k] + tempr; data[k + 1] = data[k + 1] + tempi; } var t = wr; wr = wr * wpr - wi * wpi; wi = wi * wpr + t * wpi; } mmax = istep; } }
static void Main(string[] args) { int n =1024*512; float[] data = new float[2 * n]; for (int i = 0; i < n; i++) { data[2 * i] = i; data[2 * i + 1] = 0; } Complex[] cpData = new Complex[n]; for (int i = 0; i < n; i++) { cpData[i] = new Complex(data[2 * i], data[2 * i + 1]); } long s = DateTime.Now.Ticks; FFT1(cpData, true); Console.WriteLine("time:" + (DateTime.Now.Ticks - s) / 10000); s = DateTime.Now.Ticks; FFT2(data, true); Console.WriteLine("time:" + (DateTime.Now.Ticks - s) / 10000); Console.Read(); }
速度上還是差挺多的。。。

好了獲得頻率數據之后的流程就不再那么燒腦了(都怪自己早早把傅里葉變換還給課本了)。。。
找節奏點的邏輯大概如下(代碼有點多就不貼了):
1.根據采樣率依次獲取數據,每次通過FFT得到一組復數數組。
2.計算出復數的模長,可以表示此頻率下的聲音大小,可以把一定范圍的聲音累加起來,可以用來表示低音、中音、高音。
3.對比每一幀的數據變化就可以判斷出節奏點(聲音變化大,可以表示是一個節奏點)。
其實能得到頻域的值,針對不同的功能,大家后面就可以自由發揮了。
