5.3 FFTとオーディオの音程解析
マイクがまだ届いていないので、とりあえずオーディオファイルの音程を解析する部分を作った。
音程解析とは
オーディオファイルの音程を解析するには、以下の様な手順を踏む。
ノイズの影響を受けやすく、めっちゃ難しい。
FFT(高速フーリエ変換)
FFT(Fast Fourier Transform)とは、離散フーリエ変換の高速アルゴリズムである。具体的にはO(n^2)がO(nlogn)になる。これがどれほど凄いことかというと、たとえばn=1000のときを考えて見れば良い。前者は100万になり、後者は約1万になる*1。計算時間が100分の1になっている。リアルタイムに離散フーリエ変換を行う際、FFTはほぼ必須となってくる。
フーリエ変換とは、まぁ簡単に言うとx軸が時間・y軸が音圧だったものをx軸が周波数・y軸が分布に変えることである。つまり、オーディオファイルのある時間帯にどのくらいの周波数の音がどのくらい存在しているかが分かる。
詳しくはWikipediaに投げる。
Unityのココが凄い
Unityには、初めからオーディオファイルの高速フーリエ変換を行う関数が存在しています。
audio.GetSpectrumData(float[] samples, int channel, FFTWindow window);
これで、チャンネルchannelの*2オーディオファイルのFFT結果をsamplesに入れることが出来ます。samplesのサイズは2の乗数である必要があります。まぁ、1024を使うのが無難っぽい?windowは窓関数というものらしく、なんだかよく分かりませんがノイズを軽減する効果があるみたいです。だいたいFFTWindow.BlackmanHarrisを入れておけばいいと思います。
これで得られるデータは、Nをsamplesのインデックスナンバー・FをAudioSettings.outputSampleRateの値・Qをsamplesのサイズとすると、で与えられる周波数(Hz)です。
ピッチを探すには、samplesの中で最も大きな値を持っているものを探せば良いです。これがおおよそピッチの周波数になるのですが、配列の各要素は飛び飛びの周波数の分布を表しているので、周辺の分布にもピッチが影響している可能性があります。
そこで、なんだかよく分かりませんが魔法の数式を使います。ピッチとして検出したインデックスナンバーをNとし、samples[N](と略記します)にを足します。この式、本気で意味がわからないですが、確かに誤差が抑えられる効果を持っています。
ちなみに参考にしたのはここです。これのほぼ丸パクリになりますが、音程を解析するAnalyzeSound関数を定義します。
private int qSamples = 1024; //配列のサイズ private float threshold = 0.04f; //ピッチとして検出する最小の分布 private float pitchValue; //ピッチの周波数 private float[] spectrum; //FFTされたデータ private float fSample; //サンプリング周波数 void AnalyzeSound() { audio.GetSpectrumData(spectrum, 0, FFTWindow.BlackmanHarris); float maxV = 0; int maxN = 0; //最大値(ピッチ)を見つける。ただし、閾値は超えている必要がある for (int i = 0; i < qSamples; i++) { if (spectrum[i] > maxV && spectrum[i] > threshold) { maxV = spectrum[i]; maxN = i; } } float freqN = maxN; if (maxN > 0 && maxN < qSamples - 1) { //隣のスペクトルも考慮する float dL = spectrum[maxN - 1] / spectrum[maxN]; float dR = spectrum[maxN + 1] / spectrum[maxN]; freqN += 0.5f * (dR * dR - dL * dL); } pitchValue = freqN * (fSample / 2) / qSamples; }
これを使って、440Hzの正弦波の分布を見ると以下のようになります。
多少誤差はありますが、まぁまぁ正確な周波数が出ています。
周波数から数値音階への変換
さて、ここからはカラオケ採点として数値を楽に扱うために、周波数から数値音階への変換方法を考えます。
数値音階というのは私の造語です。ここでは以下のように定義します。
数値音階とは、以下の性質を満たす実数である。
- 110Hzを0とする
- ある数値音階から1大きい数値音階は、半音上の音階を表す
- ある数値音階から1小さい数値音階は、半音下の音階を表す
これは何をするためのものかというと、CROSSOのような歌唱線を作るためのものです。
ところで、音階と周波数に関する基礎知識を整理しましょう。
- 440HzはA(ラ)の音
- 周波数が2倍になるとちょうど1オクターブ高い音となる
以降、ドレミファソラシドをCDEFGABと表します。上の基礎知識は、つまり110Hz・220Hz・440Hz・880Hz…はAの音ということです。これをそれぞれA1・A2・A3・A4…と定義します。
A1からA2の間には、嬰音(#の付いた音)も考えると12音存在します。この12音は、110Hzから220Hzの間に均等に配置されています。
つまり、x=0をA1・x=1をA#1・x=2をB1…として、yを対応する周波数とすると、という関係式が出来ます。これを整理すると、となります。ここで、logは自然対数です。
また、数値音階を素に最も近い音階を文字列で表示できるようにすると便利でしょう。今回は四半音まで判断できるようにしました(たとえば、A1とA#1の間の音はA1+と表します)。
以下がコードになります。
// ヘルツから音階への変換 float ConvertHertzToScale(float hertz) { if (hertz == 0) return 0.0f; else return (12.0f * Mathf.Log(hertz / 110.0f) / Mathf.Log(2.0f)); } // 数値音階から文字音階への変換 string ConvertScaleToString(float scale) { // 12音階の何倍の精度で音階を見るか int precision = 2; // 今の場合だと、mod24が0ならA、1ならAとA#の間、2ならA#… int s = (int)scale; if (scale - s >= 0.5) s += 1; // 四捨五入 s *= precision; int smod = s % (12 * precision); // 音階 int soct = s / (12 * precision); // オクターブ string value; // 返す値 if (smod == 0) value = "A"; else if (smod == 1) value = "A+"; else if (smod == 2) value = "A#"; else if (smod == 3) value = "A#+"; else if (smod == 4) value = "B"; else if (smod == 5) value = "B+"; else if (smod == 6) value = "C"; else if (smod == 7) value = "C+"; else if (smod == 8) value = "C#"; else if (smod == 9) value = "C#+"; else if (smod == 10) value = "D"; else if (smod == 11) value = "D+"; else if (smod == 12) value = "D#"; else if (smod == 13) value = "D#+"; else if (smod == 14) value = "E"; else if (smod == 15) value = "E+"; else if (smod == 16) value = "F"; else if (smod == 17) value = "F+"; else if (smod == 18) value = "F#"; else if (smod == 19) value = "F#+"; else if (smod == 20) value = "G"; else if (smod == 21) value = "G+"; else if (smod == 22) value = "G#"; else value = "G#+"; value += soct + 1; return value; }
さらに、CROSSOみたいに波形が出るような機構を作りました。これについてのコードは省略します。
これを用いて、C2からC3までの正弦波を流しました。
いい感じだったので、試しに普通の楽曲を流してみました。これは初音ミクの激唱です。
う~ん、なかなか絶望的な感じ。なお、閾値以上の音が取れていない場合は最下点に持って行っています。
とにかくBGM(特にドラム)が妨害してくる感じなので、ボーカルのみの曲を探しました。
これなんか、解析できないと話にならなさそうですね。やってみました。
まぁまぁという感じでしょうか。ここまで綺麗なデータでもノイズの影響は出るみたいです。音程はそこそこちゃんと取れていますが、何故かオクターブ違いで取ってしまっている箇所がありますね。JOYSOUNDのアレって普通に起こり得る現象なんですね。
課題
- ノイズによって波形が乱れる現象について考える