Part 5: (離散)フーリエ変換
原文: Onset Detection Part 5: The (Discrete) Fourier Transform
警告: ここに書いてあるように私は離散フーリエ変換を理解したが何箇所かには誤りがあるかもしれない。フーリエ変換の正しい説明については http://www.dspguide.com/ch8.htm を読むことをおすすめする。
とうとう恐れていたフーリエ変換の記事までたどり着いてしまった。すでに知っていることをいくつか復習しておこう。最初に思い出してほしいのは、オーディオデータがなんらかの録音デバイスから取得されるか、デジタル合成されるということだ。どのような場合でも、私たちがそのデータというのは時間軸上の離散した点ごとに計測された振幅であり、それをサンプルと呼ぶ。サンプルにはモノラルのものもステレオのものもあり、後者の場合、1 つの点ごとに 1 つだけでなく 2 つのサンプルが存在する。サンプリングレート 44100Hz で A の音 (440Hz) の 1024 個のサンプルはこんな感じ:
(TODO: 画像)
注意: 左側の縦の線は実際の画像の一部ではない。wordpress のせいでこうなっている。
サンプリングレート 44100Hz の 1024 個のサンプルは時間で言うと 43.07ms の範囲に等しい。この画像からサイン波に非常によく似たサンプルだということが分かる。そして実際このサンプルはこのシリーズの最初に記事でやったように Math.sin() を使って生成したものだ。その記事では音というのは異なる周波数と振幅を持ったサイン波を合成したものにすぎないということを確証していた。
フーリエという賢い数学者がかつてこのような理論を提唱した。「可能なかぎりすべての関数はいわゆる いわゆるシヌソイド、つまり異なる周波数と振幅を持つサイン波で近似することができる。」 (実際にはガウスがすでにこのことを知っていたが名声を得たのはフーリエだった。) 彼はこの魔法をいわゆる連続関数のために考えた。連続関数というのは学校の解析コースでやったので覚えているだろう。(解析のことは覚えているよな?) これは離散関数へと拡張されたのであり、そして聞いてくれ、私たちのサンプルもゆくゆくは離散関数にすぎないのだ。さて、フーリエ変換は 1 つの信号を異なる周波数と振幅を持つシヌソイドの集合へ分解する。フーリエ変換を離散的な音声信号へ当てはめれば、時間領域の音声信号に対する周波数領域の表現が得られる。この変換は可逆なので、分解することも合成することも可能だ。ここからは音声信号に関するあまり複雑ではない事柄に対しての離散フーリエ変換についてのみ考えることにする。
時間領域の信号の周波数領域というのは何を教えてくれるだろうか? ふむ、簡単に説明すればそれはどの周波数が時間領域の信号に対してどのくらい貢献しているかを教えてくれる。すなわち周波数領域の信号は音声信号の中に 440Hz の音が存在するかと、すべての音に比べてその音がどのくらいの音量なのかを教えてくれる。これは非常に素晴らしいことである。というのはビート検出のために特定の周波数を詳しく調べることができるからだ。前の記事の周波数チャートから、さまざまな楽器がそれぞれ異なる周波数の音を出すということは分かっている。たとえば歌に焦点を合わせるなら、80Hz から 1000Hz の範囲の周波数を見ることになるだろう。もちろんさまざまな楽器の周波数の範囲が重なりあっているから、いくつか問題はあるだろう。しかしこれについてはあとで気にすればよい。
さて、これから周波数分析に利用するのは離散フーリエ変換というものだ。離散的な時間軸の信号、たとえば特定のサンプリングレートで計測されたサンプルがあれば、離散的な周波数領域を得ることができる。中間段階として、周波数領域のいわゆる実部と虚部を得る。1 つ前の文にはいくつかこわい用語が出てきたが、とりあえずさっさと終わらせよう。フーリエ変換は時間軸上の信号をサイン波の係数へ分解する。実部には特定の周波数のコサイン (コサインはシヌソイド) に関するフーリエ係数と、同じ周波数のサインに関するフーリエ係数を持つ。今回のユースケースではこの表現方法はまったく重要ではない。これから使うのは元のフーリエ変換の実部と虚部から計算される、いわゆるスペクトラムである。スペクトラムはそれぞれの周波数で元の時間領域の音声信号に対してその周波数がどれだけ貢献しているかを教えてくれる。これからはこのスペクトラムを計算済のものとする。計算はフレームワークの中の FFT というクラス (Minim から拝借した) が行ってくれるので、苦痛を緩和してくれる。元のフーリエ変換の実部・虚部からスペクトラムをどう計算していければいいかを理解したいなら、上記のリンクを読んでほしい。
変換が離散的な性質を持つため、スペクトラムに可能な限りすべての周波数が存在するわけではない。周波数はビンになる、つまり複数の周波数が 1 つの値にマージされている。また、離散フーリエ変換によって得られる周波数には最大値が存在し、これはナイキスト周波数と呼ばれる。それは時間領域の信号のサンプリングレートの半分に等しい。たとえば 44100Hz でサンプルされた音声信号なら、ナイキスト周波数は 22050Hz である。それゆえ信号を周波数領域へと変換するとき、22050Hz までの周波数のビンが得られる。どのくらいの数の周波数のビンが含まれるだろうか? サンプルの数の半分に 1 を足した数だ。1024 個のサンプルを変換すると 513 個の周波数のビンが得られ、それぞれはナイキスト周波数をビンの数で割った帯域幅 (周波数の幅) を持つ。例外として最初と最後のビンだけはその半分の帯域幅を持つ。たとえば 44100Hz でサンプルされた 1024 個のサンプルを変換するとしよう。最初のビンは 0 から 22050 を 513 で割り、さらに半分にした周波数、つまり 21.5Hz だ。 (先ほど書いたように、最初のビンは普通の帯域幅の半分だ。) 次のビンは 21.5Hz から 21.5Hz + 22050 / 513 (43Hz) = 64.5Hz までの周波数を表す (ここでも 21.5Hz から 64.5Hz ということで帯域幅は 43Hz。) 次のビンは 64.5Hz から 107.5Hz までの範囲というように続く。
音声データに対して離散フーリエ変換を行うときにはいつも、サンプルの幅に基づいている。たとえば 1024 が一般的な 1 つの幅に対するサンプル数だ。幅の数は生成されるスペクトラムの解像度、つまりスペクトラムの中で何個の周波数のビンが変換するサンプルの数に応じて線形に増えていくかを表す。サンプルの幅が大きければ大きいほど、瓶は精緻となり、帯域幅が小さくなる。スペクトラムを計算するのには高速フーリエ変換 (Fast Fourier Transform, FFT) というアルゴリズムを使う。これは 0(n log n) の計算量で実行できるので、O(n*n) かかる元々のフーリエ変換の実装よりかなり速い。FFT 実装のほぼすべては 2 のべき乗個のサンプルの幅を必要とする。つまり、256, 512, 1024, 2048 は大丈夫だが、273 ではダメ。今回のフレームワークで使う FFT の実装には「制限」も存在する。
まあこれ以上面倒なことは説明せずに上記の画像に対して描画されたサンプルのスペクトラムをお見せすることにしよう。
(TODO: 画像)
注意: 左側の縦の線は実際の画像の一部ではない。wordpress のせいでこうなっている。
そう、これだけ。x 軸は周波数の瓶に対応し、y 軸は周波数の瓶の振幅を表している。左側にピークが存在し、スペクトラムの残りはゼロであることがはっきりと分かる。このピークがは周波数 440Hz、つまり 1024 個のサンプルとして生成された A という音の周波数の瓶に対応している。また、すっきりとしたピークではなく、左と右の瓶もいくらか振幅を受け取っていることが分かる。これは漏れというものであり、解決することはできない。今回のケースではあまり大きな問題とはならないが、他のアプリケーションのシナリオでは問題を引き起こすこともあるだろう。上記 2 つの画像を生成するプログラムは下記のようになる:
public class FourierTransformPlot
{
public static void main( String[] argv )
{
final float frequency = 440; // Note A
float increment = (float)(2*Math.PI) * frequency / 44100;
float angle = 0;
float samples[] = new float[1024];
for( int i = 0; i < samples.length; i++ )
{
samples[i] = ((float)Math.sin( angle ));
angle += increment;
}
FFT fft = new FFT( 1024, 44100 );
fft.forward( samples );
Plot plotSamples = new Plot( "Samples", 512, 512 );
plotSamples.plot( samples, 2, Color.white );
Plot plotSpectrum = new Plot( "Spectrum", 512, 512);
plotSpectrum.plot(fft.getSpectrum(), 1, Color.white );
}
}
最初の 2,3 行はすでに慣れているだろうが、44000Hz のサンプリングレートで 1024 のサンプルを生成している。実際、このプログラムで興味深いのは 2 行だけだ。最初は FFT オブジェクトをインスタンス化するところ。コンストラクタのパラメータとしてはスペクトラムを生成するのに使うサンプルの幅のサイズと、サンプリングレートを渡す。次の行ではフーリエ変換とスペクトラムの計算を行っている。やらなければならないのは FFT のコンストラクタに指定したのと同じサイズの float 配列を渡すことだけ。これさえやれば、貴重な周波数の瓶を得ることができる。あとはサンプルとスペクトラムをプロットするだけ。fft.getSpectrum() を呼び出していることに注意してほしい。このメソッドは FFT.forward() を呼び出し行った細心のフーリエ変換のスペクトラムを返却する。
ふー、かなり汚い感じになった。本当に離散フーリエ変換に関する記事は各自で読んだほうがいい。すべてのオーディオエンジニアにとって必須の数学ツールだ。ほとんどのオーディオエンジニアは今回のようにある種のツールボックスを使うだろうが、仕組みを知っておいて損はない。
音の立ち上がり/ビート検出を動かすために必要なことはすべて装備できた。次回の記事までチャンネルはそのままで。