どんぶらアニマル さんぽ道

CBR250RR(MC22)とNSR80(HCO6)とAPE50(AC16)を中心とした備忘録。

コード進行解析ツールを作れるか? その1 フーリエ変換を思い出す?

動機 

昔から2年に1回くらいギターをやろうと思って練習を始めては1か月くらいでフェードアウトを繰り返してる。今度こそは継続しようと思うんだけど、他に気になることが出てくるとそっちに集中しちゃって。。。

そんなこんなだけど、コードを知りたいときにはあっきーさんのWaveToneやYAMAHAのChord Trackerを使ってる。どっちも良くできてて、特にWaveToneは解析したデータを可視化してくれたり、キー、テンポ、コードの解析条件を変更できたり、ミキサーの機能等あると便利な機能が充実しているのでとても便利。一方Chord Trackerは全自動で結果の表示と再生のみでユーザが解析に関与できる余地が無いが、少しコード解析結果が良い傾向にある。

 
・あっきーさんのWaveTone
・YAMAHAのChord Tracker
 
あっきーさん、YAMAHAさん、ありがとうございます。

ただ、どちらも所々正しいコードが表示されないことがある。巷の話しでは自動で100%は不可能ということになっているようだ。それぞれ心血注いで作られたものなはずなのに何故うまくいかないことがあるのか、何故100%は無理なのか気になってきた。そしてまたギターに触らなくなってしまってる。

WaveToneは、解析結果をMIDIで鳴らすことができるので、原曲の再生をオフにしてMIDIだけならして聞くと原曲を想像することが難しい音が再生される。これではコードを推測することも難しいんじゃないか?とか使い方が悪いのかな?とか思ったり。

音楽素人の自分が耳コピの方法を調べてみて、コード解析に必要なことってこんな感じかなと。
 
キーを得る
BPM(小節区切り、音符の長さ)を得る
鳴っている音からコードを得る
鳴っている音、キー、前のコードから音楽理論的にコード推測する
もっと他の何か


多分3つ目は、様々な理由で必ずしもコードの構成音全てを認識することができるとは限らないと思われる。

他の音に埋もれて聞き取れない(聞き取り辛い)とか、そもそも演奏で鳴らしてないとか。

自分のように耳コピしようにもコードを聞き取れないという悩みを持つ人は多いみたいで、ググるとそんな人に対するアドバイスとして良く出てくるのが、ダイアトニックコードや五度圏等の理論に基づいた説明。

これが4つ目で、言っていることは何となくわかる(気がする)。けど、コードも確定した状態で後付けで理論を使った説明はできても理論からコードを確定できるようになるのはなかなか難しそうな気もしてる。

なんで難しいと思うかというと、理論は幅広い上、解釈の仕方も色々あるようなので、そのすべてをロジックとして実装する(身に着ける)ことはかなり困難な気が。特に理論の素人が調べながらでは全部を知るまでに途方もなく時間がかかる気がするから。

多分、教えてくれる人たちは、長く音楽と付き合っていく中で会得した多くの引き出しがあって、聞いてる人の状況に合わせて一部の引き出しで説明してくれてるんだと思う。教えてくれる人達にも得意分野などがあって、全ての人が全ての楽曲をやっつけられるわけではないのかも知れない。

まあ、無理だと思いながらも、気になってしまったので実際に何が難しいのか試してみようと思う。

数学出来ない、音楽理論知らない、楽器引けない状態からでどうなるのか。

音符の認識方法

 

キー、BPM、コードのどれを得るにもまずは楽曲中の音符の存在を認識しないと始まらない。音符の存在、音程を得るには、楽曲中のある瞬間のデータに含まれる周波数と各周波数のパワー(音量)を得ることから始まる。その方法としてメジャーなのが離散フーリエ変換(DFT)で、計算量を削減したのが高速離散フーリエ変換(FFT)。高速離散フーリエ変換を応用した測定器等はたまにつかっててFFTがどんなことをやるものかの感覚的なイメージは持ってるつもり。
 
タイムドメインアナライザ

でもフーリエ変換自体は、昔、職場の人から紹介されたヒッポファミリークラブのフーリエの冒険を読んで当時は納得したけどもう思い出せない。今もどこかにあるはずだけど、そこから始めると、また違った方向に行ってしまいそうなのでフーリエ変換はライブラリ的なものを使う。
 
で、そのライブラリ的なものを使ってるサンプルコードも使わせてもらって省力化したいと思ってググってみたらいいものが見つかった。それがtakesyhiさんの記事。
 
 
takesyhiさんありがとうございます。
 
(良く分からない式とかあるけど)ほぼ、知りたいことが書かれている感じ。「ハミング窓と高速フーリエ変換」の部分のコードがそのまま使える気がする。
 

音声解析に使うFFTの基本

音声解析でのFFTは、ある瞬間の音声データに含まれる周波数成分を解析して、どんな周波数の音がどんな大きさで含まれているかを知るために使う。図のように音量と時間で表されている音源を時間軸で細切れにし、それをFFTにかけ、周波数とパワー(音量)で表したデータに変換する。
 
音声解析でのFFT

※ 実用にするには目的によって、(takesyhiさんのコードにも実装されているように)FFTにかける前に窓関数にかけたり、フレームをオーバーラップさせたりもする。
 
想定している楽譜との関連イメージはこんな感じ。真ん中のド(D4)の周波数は261.5Hzなので、このドが含まれているフレーム1のFFTの結果には261.5Hzにパワーが現れる。フレーム2にはレの293.7Hz、フレーム3にはミの329.6Hzが見える。フレーム4ように、フレームの長さが音符の時間的な長さより十分に短くなっていないと単音が和音として見えてしまう。
音符とFFTの関係

FFTの結果(出力)ってどんなデータになってるのか?

takesyhiさんのコードにある戻り値のresultにはどんなフォーマットのデータが入っているのか理解できてない。説明してくれてるんだけどresultに入ってる具体的なデータのイメージが付かない。FFTを理解してればイメージできるものなのだろう。
 
で、まずは音源ファイルの入り口から出口までFFT_HammingWindow_ver1()関数のコードを読んでみて、
samples配列には読み込んだオーディオファイルのデータが入る
fftLengthには、オーディオデータの1フレーム分のデータ数が定義されている
複素数buffer配列には、ハミング窓をかけた1フレーム分のデータが入る
FFTの結果はbuffer配列に上書きして1フレーム分入る
diagonal配列には、複素数から求めた大きさが1フレーム分入る
result配列には、diagonal配列のコピーが入る
という流れだということを理解した。
 
resultは二次元配列になっていて、第一添え字(result[第一添え字, 第二添え字])がフレーム番号、第二添え字がそのフレームのFFT結果のデータになっている。
 

やってみる

具体的な値の幅(大きさ)やresultに入っている各データと周波数との対応が良く分からないけど、グラフに描画すれば何か分かるだろうと思ってpictureBoxに描画させてみたけど全く思った値にならない。データの羅列をテキストで見ても全体像が想像しがたいので、データをファイルに出力するようにコードを追加してExcelでグラフにしたりして観察してみた。
 
入力にはいつも使ってる、ひろくんさんのWaveGeneで作成したwavファイルを使った。
 
 
ひろくんさんありがとうございます。
 
wavは、ラ(A4 440.000Hz)とレ(D5 587.330Hz)のサイン波で、44,100kHzサンプルで作った。
 
今回使った実験コードはtakesyhiさんのFFT_HammingWindow_ver1()関数をまるぱくりして、実験用コードを追加してる。言語はC#。
追加したframe変数は処理しているフレーム数を示し、66行目のif分で1フレーム目のresultのデータをlog.txtファイルに書き出してる。現時点で大量のデータを見ても意味が無さそうなので1フレーム目だけに絞った。
        private float[,] FFT_HammingWindow_ver0(string fileName)
        {
            int frame = 0;
            System.Text.Encoding enc = new System.Text.UTF8Encoding(false);
            StreamWriter writer = new StreamWriter(@"log.txt", false, enc);

            int samplesRead;
            AudioFileReader audioStream = new AudioFileReader(fileName);
            // コンストラクタを呼んだ際に、Positionが最後尾に移動したため、0に戻す
            audioStream.Position = 0;

            // 波形データを配列samplesに格納 // ステレオの音声ならば、偶数番目が左のデータで奇数番目が右となる
            float[] samples = new float[audioStream.Length / audioStream.BlockAlign * audioStream.WaveFormat.Channels];

            //1サンプルのデータ数
            int fftLength = 256;

            double df = (double)(44100.0 / fftLength);
            writer.WriteLine(fileName);
            writer.WriteLine("fs=441000hz、df(周波数分解能)="+df+ "、BL(サンプリング点数)=" + fftLength + "、D(時間窓長)="+ (double)(fftLength / 44100.0)*1000+"ms");

            //1サンプルごとに実行するためのイテレータ用変数
            int fftPos = 0;

            // フーリエ変換後の音楽データを格納する配列 (標本化定理より、半分は冗長)
            //    1次元目はFFTに掛けたフレームの番号、2次元目はそのフレームのFFT結果
            float[,] result = new float[samples.Length / fftLength, fftLength / 2];

            // 入力ファイルのデータを全て読み込む
            samplesRead = audioStream.Read(samples, 0, samples.Length);

            // 波形データにハミング窓をかけたデータを格納する配列
            //    データにハミング窓をかけてからFFTに渡す。 FFTへの入力データは複素数なので、虚部を0にした複素数に変換する。
            Complex[] buffer = new Complex[fftLength];
            for (int i = 0; i < samples.Length; i++)
            {
                // ハミング窓をかける
                buffer[fftPos].X = (float)(samples[i] * FastFourierTransform.HammingWindow(fftPos, fftLength));
                buffer[fftPos].Y = 0.0f;
                fftPos++;

                // 1サンプル分のデータが溜まったとき
                if (fftLength <= fftPos)
                {
                    fftPos = 0;

                    // サンプル数の対数をとる (高速フーリエ変換に使用)
                    int m = (int)Math.Log(fftLength, 2.0);
                    // 高速フーリエ変換
                    FastFourierTransform.FFT(true, m, buffer);

                    for (int k = 0; k < result.GetLength(1); k++)
                    {
                        // 複素数の大きさを計算
                        double diagonal = Math.Sqrt(buffer[k].X * buffer[k].X + buffer[k].Y * buffer[k].Y);
                        // デシベルの値を計算
                        double intensityDB = 10.0 * Math.Log10(diagonal);

                        const double minDB = -60.0;

                        // 音の大きさを百分率に変換
                        double percent = (intensityDB < minDB) ? 1.0 : intensityDB / minDB;
                        // 結果を代入
                        result[i / fftLength, k] = (float)diagonal;
                    }
                    if (frame < 1)
                    {
                        for (int l = 0; l < result.GetLength(1); l = l + 1)
                            writer.WriteLine(result[frame, l]);
                        // writer.WriteLine(l * df * 2 + "," + result[frame, l]);
                    }

                    writer.WriteLine("frame=" + frame + "--------------------------------------------------");
                    Console.WriteLine("frame=" + frame + "--------------------------------------------------");
                    frame++;
                }

            }

            writer.Close();
            return result;
        }
 
これで出てきたlog.txtをexcelに張り付けてグラフにしてみた。なんだか良く分からない。X軸はresultの添え字の番号(データの個数目)にしている。X軸を周波数にしないと想像もできない感じ。
FFT結果のグラフ
 
このあたりからはresult変数のデータ構成をフレームとの関係でイメージできていると考えやすくなる。
FFT結果データ

 
FFTの入出力について、いろいろ調べた結果、エヌティーアイジャパンさんの高速フーリエ変換 - 基礎編が分かりやすかった。周波数分解能df = fs / BLとなっている。fsはサンプリングレートで44,100Hz、BLは(1フレーム分の)サンプル点数なので(fftLength変数にセットされている)256としている。よって、df = 44,100 / 256 = 172.265625となる。この周波数分解能がresultの各データ間(x1とx2、x2とx3等)の周波数になる。
FFTの出力数は、入力のサンプル数の1/2になるのでX軸を第二添え字番号×172.265625×2としてグラフにしてみたのがこれ。×2は間違ってるかも。
何か思い出してきた気がする、気がするだけ。
FFTの結果その1
・Excelのデータ:sign-A4-D5-10sec.xlsx

ラ(A4)、レ(D5)共に1kHz以下なので、1.723Khz以下のとこにあり、このグラフでは読み取れない。とりあえずコード側には手を入れず、入力ファイルを10kHzのサイン波にしてみた。
 
その結果のグラフがこれ。10kHzあたりに出てるので合ってそうなんだけど自信なし。×2が間違ってたとしたら5kHzに出ていることになるが、FFTから出てくるのはサンプルレートの1/2の周波数までのような気もするし。。。
33kHzあたりにあるのは謎だけどサンプルレートが44,100Hzなので22,050Hz以上は無視していいなじゃないだろうかと思いつつ、後回し。
FFTの結果その2
・Excelのデータ:sign-Left10khz-10sec.xlsx

今日はここまで。サンプリング点数が256個では平均律の音程を分離できなさそうなことが解ったので、次回は点数を増やして周波数分解能を上げてみる。
 
気が向いたら感想をお願いします。(ログイン不要、ボタンを押すだけです)