C# : NAudio と 高速フーリエ変換(FFT)
NAudioについての日本語の記事が少ないので備忘録も兼ねてNAudioでのFFTのやり方を書きます.
今回の目標はマイクから取得した音をフーリエ変換することです.
いかんせん自分が信号について素人なので、記事の対象読者は次の通りです。
- 理屈に詳しくないが、フーリエ変換を活用したい。
- NAudioの日本語での解説が見たい。
読者がゼロにならないことを願います。
こちらの記事を参考にさせていただきました.
1.使うライブラリ郡
適宜Nugetなどで取得してください.
DxLibDLLは可視化に使うだけですので, 自分の好きなライブラリに置き換えて結構です.
using System; using System.Collections.Generic; using NAudio.CoreAudioAPI; using NAudio.Dsp; using NAudio.Wave;
2.マイク関連
マイク入力はWaveInEventクラスを使います.
WaveInクラスとの違いはGitHubによると
WaveInEventクラスはコールバックイベントを用いてwaveIn apiを利用します.
非GUIアプリケーションを作る際に用いてください.
とのこと
https://github.com/naudio/NAudio/blob/master/NAudio/Wave/WaveInputs/WaveInEvent.cs
バッファリングは自作クラスを用います.
ここはList
//リングバッファです public class WaveBuffer { float[] buffer; int headIndex; //一番新しいデータのインデックス int tailIndex; //一番古いデータのインデックス public int BufferedLength { get { return headIndex - tailIndex; } } public WaveBuffer(int size = 2048) { buffer = new float[size]; headIndex = 0; tailIndex = 0; } //バッファにデータを追加します public void Add(float data) { buffer[headIndex++ % buffer.Length] = data; } //countだけバッファを消費します public float[] Read(int count) { var rv = new float[count]; for (int i = 0; i < count; i++) { rv[i] = buffer[tailIndex++]; } return rv; } }
Mainメソッドを実装していきます.
DoFourier(float[])はフーリエ変換をしてその結果を返します.
//フィールド宣言です readonly int FFTLength = 512;
//Mainメソッド内です using (var waveIn = new WaveInEvent()) { var waveBuffer = new WaveBuffer(); //WaveInEventのイベントの追加 //呼び出しタイミングはバッファが利用可能になった時 waveIn.DataAvailable += (object sender, WaveInEventArgs e) => { //バイト列を合成 //waveIn.WaveFormat.BlockAlignは1サンプルが何バイトかを示す //普通2バイトとされる(16bit = shortと同等) for (int i = 0; i < e.BytesRecorded; i += waveIn.WaveFormat.BlockAlign) { //リトルエンディアンの並びで合成 short sample = e.Buffer[i + 1] << 8 | e.Buffer[i + 0]; //最大値が1.0fになるようにする float data = sample / 32768f; //記録 waveBuffer.Add (data); } //バッファが十分溜まった if (waveBuffer.BufferedLength >= FFTLength) { //バッファから読みだしてフーリエ変換 var fftsample = waveBuffer.Read (FFTLength); var result = DoFourier(fftsample); //(お好みで)結果を描画 RenderSpectrum (result); } }; //マイクから音を取得します waveIn.StartRecording (); //ここにお好みの処理を書きます waveIn.StopRecording (); }
3.フーリエ変換
FastFourierTranform.FFTを使えばすぐにできます.
引数の解説がほぼないので解説
FastFourierTransform.FFT (bool forward, int m, Complex[] data)
forward : よくわかりませんがtrueにしておきます. (正変換のことかと. コメントお待ちしています)
m : サンプル数が2のm乗の時, そのmの値. (Math.log (sample.Length, 2)などとして求めます)
data : 変換したいデータを入れます. 結果もこれに入ります. (参照型だからrefとかはいらない?)
で, 結果はどう扱うかというと, 詳しくは解説書やWikipediaなどを当たると良いのですが, (そもそも知っているという人が大多数?)
ざっくり言うと要素のインデックスをkとすると
が成り立ち, その要素は周波数成分fに関する情報を持ちます.(ただしk < N /2)
その複素数の大きさは振幅の半分に相当します.
k < N / 2 なのは標本化定理からだそうです。
以下コードです
public static float[] DoFourier(float[] sample) { var fftsample = new Complex[FFTLength]; //ハミング窓をかける for (int i = 0; i < FFTLength; i++) { fftsample[i].X = (float)(sample[i] * FastFourierTransform.HammingWindow (i, FFTLength)); fftsample[i].Y = 0.0f; } //サンプル数のlogを取る var m = (int) Math.Log(FFTSamplenum, 2); //FFT FastFourierTransform.FFT(true, m, fftbuffer); //結果を出力 //FFTSamplenum / 2なのは標本化定理から半分は冗長だから var result = new float[FFTSamplenum / 2]; for (int k = 0; k < FFTSamplenum / 2; k++) { //複素数の大きさを計算 double diagonal = Math.Sqrt (fftbuffer [k].X * fftbuffer [k].X + fftbuffer [k].Y * fftbuffer [k].Y); result [k] = (float) diagonal; } return result; }
以上でRenderSpectrum以外のメソッドは実装し終えました.
なるべくコメントを多く書いたのですが,理解の助けになれば幸いです.(というより,自分の理解がまだ未熟なので書いておかないと...)
フーリエ変換をするよりも,その下準備が大変という印象でした.
それと, Nugetで取得したNAudioには説明が皆無なので,適宜GitHubのソースを読まないと辛いです.
以上Raptorでした.