2013年5月18日土曜日

Androidで音声解析!Visualizerによる高速フーリエ変換(FFT)


今回は、音の周波数を解析して音階を特定するというのをやってみたいと思います。

音階の特定には、時間軸を周波数軸に変換するフーリエ変換という処理で周波数解析を行います。

\hat{f}(\xi) := \int_{-\infty}^{\infty} f(x)\ e^{- 2\pi i x \xi}\,dx

数式を載せてみたものの、これが本当にフーリエ変換の式なのかすらよく分かりません。
数学的な知識はまったく無いので、フーリエ変換についての説明や考察は一切しません。いや、出来ません。

基本的に勘で進めているので、間違った記述もあるかもしれませんが、取りあえず動いたので完全に間違いという事ではないでしょう。


まずはシンプルに440Hz(Aの音)のsin波を鳴らして、それが440Hzであることが分かればゴールとします。


フーリエ変換を行うには、波形を表示したりといったことにも活用出来るandroid.media.audiofx.Visualizerを利用します。
android.media.audiofxはAPIレベル9(Android2.3)で追加されたパッケージで、イコライザ機能などを簡単に実装するものです。


public Visualizer (int audioSession)

 int audioSession 分析対象のMediaPlayerやAudioTrackのセッションID



流れとしては、Visualizerのコンストラクタに解析したいMediaPlayerなどのセッションIDを渡してやり、setCaptureSize()メソッドでキャプチャサイズを設定し、setDataCaptureListener()でリスナーを登録します。

キャプチャサイズを設定するときに利用するVisualizer.getCaptureSizeRange()で返ってくる配列はlengthが2で、index[0]は128、index[1]は1,024です。
つまり、キャプチャサイズは128か1,024しか選択出来ないようです。
129や2,048など別の数字を入れてもエラーにはなりませんが、VisualizerのgetCaptureSize()で確認すると、1,024になっていたので、自動で修正されているようです。

public int setDataCaptureListener (Visualizer.OnDataCaptureListener listener, int rate, boolean waveform, boolean fft)

Visualizer.OnDataCaptureListener listener 登録するリスナー
int rate         キャプチャ更新のレート
boolean waveform trueならonWaveFormDataCapture()が呼ばれる
boolean fft      trueならonFftDataCapture()が呼ばれる


キャプチャ更新レートの最大値はVisualizer.getMaxCaptureRate()で20,000です。

登録するリスナーはVisualizer.OnDataCaptureListenerインターフェースを実装したもので、以下二つの抽象メソッドがあります。

public abstract void onFftDataCapture (Visualizer visualizer, byte[] fft, int samplingRate)

public abstract void onWaveFormDataCapture (Visualizer visualizer, byte[] waveform, int samplingRate)


onFftDataCapture()がフーリエ変換後のデータを利用するもので、onWaveFormDataCapture()は生のWaveデータを利用するものです。


それではいよいよonFftDataCapture()についてみていきましょう。

まずドキュメントでVisualizerのgetFft (byte[] fft)部分を見ると、index[1]を除くと、byte配列には実数部と虚数部が交互に格納されているようなことが書いてあります。

ソースをちゃんと見ていませんが、onFftDataCapture()で渡されてくるbyte配列も恐らくこれと同じでしょう。

実数?虚数?という感じですが、複素数平面に拡張するには訳があります。

ある波を複数のsin波の合計で表現することがフーリエ変換なのですが、位相のズレを表現するためには虚数が必要なんです。
位相のズレたsin波をsinやcosをフル活用して表現する的な感じですかね。

はい。

あんまり良く分かりません。

まぁここで重要なのは、onFftDataCapture()のbyte配列の実数部がcos波、虚数部がsin波を表すってところです。

※という割には実数部を見ても虚数部を見ても同じところにピークが来ていました。また、ためしにcos波を混ぜてみたらまったく想定していない結果になり、謎は深まるばかりです。


ちなみにどの程度細かく分析が可能かに触れていませんでしたが、実はそんなに細かく分析出来ません。

サンプルレートが44,100Hzであれば、いわゆるナイキスト周波数は22,050Hz(44,100Hz / 2)で、キャプチャサイズが1,024であれば、分解能は21.5332031Hz(44,100Hz / 2 / 1,024)となります。

これはG♭3とG3ぐらいの差なので、音階を完全に特定するには十分とはいえない気がしますが、これが最大値なようなので仕方が無いです。
低周波になってくると音階の区別がつかなくなるってことでしょう。


つまり、サンプルレートが44,100Hzで、キャプチャサイズが1,024であれば、分解能は21.5332031Hzとなり、FFTの結果のbyte配列は恐らく以下のよう解釈すれば良いと思われます。

index[0]  実数部 0Hz~10.76660155Hz
index[1]  実数部 10.76660155Hz~21.5332031Hz
index[2]  実数部 21.5332031Hz~43.0664062Hz
index[3]  虚数部 21.5332031Hz~43.0664062Hz
index[4]  実数部 43.0664062Hz~64.5996093Hz
index[5]  虚数部 43.0664062Hz~64.5996093Hz




440HzのAの音であれば、以下のindex[41]にピークがくるということです。

index[41]  虚数部 430.664062Hz~452.1972651Hz

では実際にやってみましょう。
まずはFFTの結果を拾うところまで。


AudioTrack mAudioTrack;
Visualizer mVisualizer;

//サンプルレート
static int SAMPLE_RATE = 44100;
int bufSize = SAMPLE_RATE * 5;

//ストリームモードでAudioTrackを利用します
mAudioTrack = new AudioTrack(
  AudioManager.STREAM_MUSIC, SAMPLE_RATE,
  AudioFormat.CHANNEL_OUT_MONO,
  AudioFormat.ENCODING_DEFAULT,
  bufSize, AudioTrack.MODE_STREAM,
  420);//セッションID
mAudioTrack.play();


mVisualizer = new Visualizer(420);

// 1024
int captureSize = Visualizer.getCaptureSizeRange()[1];
mVisualizer.setCaptureSize(captureSize);


mVisualizer.setDataCaptureListener(
  new Visualizer.OnDataCaptureListener() {
   // Waveデータ
   public void onWaveFormDataCapture(
     Visualizer visualizer,
     byte[] bytes, int samplingRate) {
   }

   // フーリエ変換
   public void onFftDataCapture(
     Visualizer visualizer,
     byte[] bytes, int samplingRate) {

    //このbytesがFFT後のデータ
    //何度も呼ばれるのでこのデータを分析する

   }, Visualizer.getMaxCaptureRate(),
   false,//trueならonWaveFormDataCapture()
   true);//trueならonFftDataCapture()

mVisualizer.setEnabled(true);

byte[] audioData = new byte[bufSize];

// A単音
double freqA = 440;
double t = 0.0;
double dt = 1.0 / SAMPLE_RATE;
for (int i = 0; i < audioData.length; i++, t += dt) {
 audioData[i] = (byte) (Byte.MAX_VALUE * (Math.sin(2.0 *
   Math.PI * t * freqA)));
}
mAudioTrack.write(audioData, 0, audioData.length);


これでフーリエ変換後のデータを取得出来ました。
あとはピークがどこに来ているか確認し、分解能を元に周波数を特定すれば良いだけです。

ただし、結果を見てみると概ね正しく解析出来ますが、おかしな数字が出てくることもあります。

index[41] : 441.4306640625
index[41] : 441.4306640625
index[41] : 441.4306640625
index[73] : 785.9619140625
index[73] : 785.9619140625
index[141] : 1518.0908203125
index[169] : 1819.5556640625
index[123] : 1324.2919921875
index[41] : 441.4306640625
index[41] : 441.4306640625
index[41] : 441.4306640625
index[41] : 441.4306640625
index[49] : 527.5634765625
index[41] : 441.4306640625
index[149] : 1604.2236328125
index[149] : 1604.2236328125
index[41] : 441.4306640625
index[41] : 441.4306640625
index[41] : 441.4306640625

正しいindex[41]以外はバラバラですね。

虚数部しか見ていなかったので、位相のズレなどが正しく解釈出来ていないということでしょうか??

まぁちょっと勉強が必要ですね。

Read More...