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やBufferedWaveProviderでもいいかと思います. (BufferedWaveProviderの挙動がいまいちわからなかったので...)

//リングバッファです
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とすると
 \displaystyle{ f = \frac{k \times f_{sampling}}{N_{sample}}}
が成り立ち, その要素は周波数成分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でした.