c#でマイク音声をFFTする

FFT

C#FFT(Fast Fourier Transform)するのに便利なライブラリーを探してみると、Math.NETというのが良さそうでした。今回はこれを使って音声信号をFFTしてみます。

音声入力

処理の対象物をどうしようかと悩んだんですが、いつも画像ばかり触っているので、たまには次元減らして音声処理をやってみます。ここで悩むのがどうやって音声を入力するかです。画像ならSystem.Drawing.Image.FromFile()とかcv::imread()が使えるんですが、音声はどうするんでしょう。
調べてみるとXNA.NET Voice Recorderを使えとStack over flowに書いてあったのでとりあえず後者を使います。
.NET Voice RecorderはNAudioを使っているのでNugetでNAudioをインストールします。

NAudioで音声取得

USBカメラにマイクがついていたのでそれを使います。
ちゃんとつながっているかは以下で確認できます。.NET Voice Recorderを参考にしてます。

for (int i = 0; i < WaveIn.DeviceCount; i++)
{
    var deviceInfo = WaveIn.GetCapabilities(i);
    this.label1.Text = String.Format("Device {0}: {1}, {2} channels", 
        i, deviceInfo.ProductName, deviceInfo.Channels) ;
}

音声の取得はイベントを使用して行います。サンプリングレートが8kHzでモノラルとしてます。

waveIn = new WaveIn()
    {
        DeviceNumber = 0, // Default
    };
waveIn.DataAvailable += WaveIn_DataAvailable;
waveIn.WaveFormat = new WaveFormat(sampleRate:8000, channels:1);
waveIn.StartRecording();
private void WaveIn_DataAvailable(object sender, WaveInEventArgs e)
{
    // 32bitで最大値1.0fにする
    for (int index = 0; index < e.BytesRecorded; index += 2)
    {
        short sample = (short) ((e.Buffer[index + 1] << 8) | e.Buffer[index + 0]);

        float sample32 = sample / 32768f;
        ProcessSample(sample32);
    }
}

とりあえずここまでで音声データを波形表示してみます。グラフはいけてるライブラリOxyplotを使いましょう。

private PlotModel _plotmodel = new PlotModel();
private LinearAxis _linearaxis1 = new LinearAxis
    {
        Position = AxisPosition.Bottom
    };
private LinearAxis _linearaxis2 = new LinearAxis
    {
        Minimux = -1.0,
        Maximum = 1.0,
        Position = AxisPosition.Left
    };
private LineSeries _lineSeries = new LineSeries();
List<float> _recorded = new List<float>(); // 音声データ

private void InitPlot()
{
    _plotmodel.Axes.Add(_linearaxis1);
    _plotmodel.Axes.Add(_linearaxis2);
    _plotmodel.Series.Add(_lineSeries);
    this.plotView1.Model = _plotmodel;
}

private void ProcessSample(float sample)
{
    _recorded.Add(sample);

    if (_recorded.Count == 1024)
    {
        var points = _recorded.Select((v, index) =>
                new DataPoint((double) index, v)
            ).ToList();
        _lineSeries.Points.Clear();
        _lineSeries.Points.AddRange(points);

        this.plotView1.InvalidatePlot(true);
        _recorded.Clear();
    }
}


実行するとこんな感じでいいですねー。
f:id:wildpie:20140923212343p:plain

FFT

FFTはMath.NET NumericsをNugetでインストールして実行します。
窓関数もラッキーなことに使えるので、ハミング窓をかけておきましょう。

var window = Window.Hamming(windowsize);
_recorded = _recorded.Select((v, i) => v * (float)window[i]).ToList();

FFTはComplex型の配列を入力します。_recordedはfloat型なので変換が必要です。Radix2Forward()は2のべき乗じゃないと動かないです。

Complex[] complexData = _recorded.Select(v => new Complex(v, 0.0)).ToArray();
       
Fourier.Forward(complexData, FourierOptions.Matlab); // arbitrary length
//Fourier.Radix2Forward(complexData.ToArray(), FourierOptions.Default);

表示は周波数表示用のグラフエリアを別に作って表示しましょう。対数軸でもいいけどそのままで。

var s = windowsize*(1.0/8000.0);
var point = complexData.Take(complexData.Count()/2).Select((v, index) =>
      new DataPoint((double)index/s, 
          Math.Sqrt(v.Real * v.Real + v.Imaginary * v.Imaginary))
).ToList();

_lineSeries2.Points.Clear();
_lineSeries2.Points.AddRange(point);
this.plotView2.InvalidatePlot(true);

できたのが下のソフトです。一番大きい値の周波数を表示して、それに最も近い音階を表示しています。

f:id:wildpie:20140924001409p:plain