junichi.minamino が 2024年01月31日03時55分49秒 に編集
初版
タイトルの変更
マイクに入力された複数のサイン波の組み合わせから音楽コードを判別してみた
タグの変更
SPRESENSE
spresense
FFT
マイク
メイン画像の変更
記事種類の変更
製作品
本文の変更
# 作品の概要 複数のサイン波の音源をマイクで受け取り、SPRESENSEでの演算で分析することで、複数のサイン波の組み合わせが想定していた音楽コードを判別できるか試してみました。 ### 目的 SPRESENSEを使用して、Arduinoなど低速のCPUでは実現できないような処理を加えたいことから、一例としてFFT(高速フーリエ変換)の処理を含んだ内容を考えてみたいと思い、今回のテーマを提案してみました。 ### 処理の流れ 1. 複数のサイン波の信号を出力する別のアプリを用意して、音楽コードと一致する周波数のサイン波の組み合わせを出力します。 2. マイクで受け取った「1」の信号をFFTで演算して、上位4つの周波数のピーク値を受け取ります。 3. 得られた周波数値から、キーを取得します。 4. 得られたキーの組み合わせから、音楽コードを取得します。 5. 得られた音楽コードをLCDに表示します。表示は都度更新します。 # 使用する部品 - SPRESENSE メインボード - SPRESENSE 拡張ボード - Mic&LCD KIT for SPRESENSE ``` 備考: LCDへの出力を正常に行いたい場合、電圧を5V → 3.3Vに設定変更する必要があります。 拡張ボードにあるジャンパーピンの位置を切り替えると変更されます。 ``` # 設計、実装 録音とFFTの演算部分の周辺は、参考資料のプログラムを使用させていただきました。 プログラム全体としては、MainCoreとSubCoreを使用しています。 ``` MainCore: FFTの演算、複数のピークの取得 SubCore: 各キーの取得、音楽コードを取得、LCDへの表示 ``` ### 複数のサイン波の信号の生成 iOSアプリとして作成しました。 鍵盤部分をタッチすると、指定した周波数のサイン波の信号を出力し続けます。鍵盤を離すと停止します。 ``` 例: 「Am」の場合、「ラ ド ミ」となることから、例えば 880Hz, 1047Hz, 1319Hz のサイン波の組み合わせを出力します。 周波数の計算方法: それぞれ、ラ:880 * 2^(0/12), ド:880 * 2^(3/12), ミ:880 * 2^(7/12) ``` ### 録音、FFTの演算 参考資料のプログラムを使用させていただきました。 (サンプリング周波数:48000Hz、サンプル数:1024、窓関数:ハミング窓) ### 複数のピークの取得 参考資料のプログラムを改変して実現しました。 ピーク値を求める関数(arm_max_f32)を簡易に使い回して実現したかったので、次の大きさのピーク値を求める際は、直前に得たピーク値とその前後の値を0にして、再度 arm_max_f32 関数を処理するというのを繰り返す方法で試しました。(今回は作業の時間が限られていたため、このような方法になりました。) なお、今回は3音と4音のコードを分析することにしました。(5音はコードの種類が多いため、また機会があれば確認したいと思います。) 以下が該当部分のプログラムです。 ```c:Maincore.ino // 周波数のピーク値を上位4つまで取得 int get_peak_frequency_list(float *pData, float *peakFs, float *maxValue) { uint32_t idx; float delta, delta_spr; // 周波数分解能(delta)を算出 const float delta_f = AS_SAMPLINGRATE_48000 / FFT_LEN; const int Max_Key_Num = 4; for (int i = 0; i < Max_Key_Num; i++) { if (i > 0) { pData[idx-1] = 0.0; pData[idx] = 0.0; pData[idx+1] = 0.0; } // 最大値と最大値のインデックスを取得 float maxValueTmp = 0.0; arm_max_f32(pData, FFT_LEN/2, &maxValueTmp, &idx); if (idx < 1) return i; float data_sub = pData[idx-1] - pData[idx+1]; float data_add = pData[idx-1] + pData[idx+1]; // 周波数のピークの近似値を算出 delta = 0.5 * data_sub / (data_add - (2.0 * pData[idx])); peakFs[i] = (idx + delta) * delta_f; // スペクトルの最大値の近似値を算出 delta_spr = 0.125 * data_sub * data_sub / (2.0 * pData[idx] - data_add); maxValue[i] = maxValueTmp + delta_spr; } return Max_Key_Num; } ``` ### キーの取得 周波数値から音楽のキーを取得する場合、計算の方針は以下になります。 |Key|周波数(Hz)| |:---:|:---| |A|880 * 2^(0/12)| |A#|880 * 2^(1/12)| |B|880 * 2^(2/12)| |C|880 * 2^(3/12)| |C#|880 * 2^(4/12)| |D|880 * 2^(5/12)| |...|...| ただ、ピークの周波数値の精度もばらつきが見られたので、ほぼ一致する値のみを採用していてはキーが取得できない可能性が高くなるため、以下のように範囲を持たせることで、(ハズレ無しになるように)必ずキーを取得するようにしました。 |Key|周波数の取りうる範囲(Hz)|計算結果(Hz)| |:---:|:---:|:---| |C|880 * 2^(5/24) ~ 880 * 2^(7/24)|1016 ~ 1077| |C#|880 * 2^(7/24) ~ 880 * 2^(9/24)|1077 ~ 1141| |D|880 * 2^(9/24) ~ 880 * 2^(11/24)|1141 ~ 1209| |...|...|...| 音楽のキーからのindexは以下のように定義しました。 |index|Key|周波数(Hz)| |:---:|:---:|:---| |0|C|440 * 2^(3/12)| |1|C# or Db|440 * 2^(4/12)| |2|D|...| |3|D# or Eb|| |4|E|| |5|F|| |6|F# or Gb|| |7|G|| |8|G# or Ab|440 * 2^(11/12)| |9|A|880| |10|A# or Bb|880 * 2^(1/12)| |11|B|880 * 2^(2/12)| |12|C|880 * 2^(3/12)| |13|C# or Db|880 * 2^(4/12)| |...|...|...| |21|A|1760| |22|A# or Bb|1760 * 2^(1/12)| |23|B|1760 * 2^(2/12)| |24|C|1760 * 2^(3/12)| 精度面から、上記の範囲の周波数値のみをキーの取得の対象としました。 また、音量の大きさについても、しきい値を設定して、それより小さい場合は対象外としました。 上記の点を満たす周波数値が3つまたは4つ存在した場合、音楽コードを取得できる対象としました。 ### 音楽コードの取得 上で得た、音楽のキーのセットに対して、小さい順に並び替えを行い、最も小さいindexを「ルートキー」とします。 音楽のキーのセットに対して、ルートキーの値で差分した結果の組み合わせが、以下のテーブルに一致するが存在すれば、音楽コードを取得することができます。 |Chord|key index set| |:---:|:---| |M|0, 4, 7| |m|0, 3, 7| |m(#5)|0, 3, 8| |aug|0, 4, 8| |dim|0, 3, 6| |sus4|0, 5, 7| |6|0, 4, 7, 9| |7|0, 4, 7, 10| |7(b5)|0, 4, 6, 10| |7(#5)|0, 4, 8, 10| |add9|0, 4, 7, 14| |M7|0, 4, 7, 11| |M7(b5)|0, 4, 6, 11| |M7(#5)|0, 4, 8, 11| |m6|0, 3, 7, 9| |madd9|0, 3, 7, 14| |m7|0, 3, 7, 10| |mM7|0, 3, 7, 11| |m7(b5)|0, 3, 6, 10| |m7(#5)|0, 3, 8, 10| |7sus4|0, 5, 7, 10| |M7sus4|0, 5, 7, 11| ``` 例: キーのセットが「ラ ド ミ」の場合、音楽のキーのセットは「9, 12, 16」となります。 ルートキーは「ラ=A」で値は「9」です。この値で差分すると「0, 3, 7」になり、下のテーブルからコードは「m」です。 これらから、「ラ ド ミ」の音楽コードは「Am」になります。 ``` ### 取得した音楽コードの表示 表示する項目とおおよその配置は以下になります。 ``` ピークの周波数と値(1番目) ピークの周波数と値(2番目) ピークの周波数と値(3番目) ピークの周波数と値(4番目) ルートキーのindex 音楽コードのindex 各ピークの音楽のキー 音楽コード ``` # 成果物 ### 動作確認(動画) 以下で、試したものが確認できます。 https://twitter.com/MJeeeey/status/1752403323838448079 ### ソースコード SPRESENSEのプログラム一式は、以下に置きました。 https://github.com/JunichiMinamino/spresense_fft_get_chord.git # 使い方 1. 複数のサイン波を再生するツールは、iOSアプリの「[マルチ信号発生器](https://apps.apple.com/jp/app/multi-wave-oscillator/id954158579)」で同等の確認ができます。 2. 「SPRESENSE 拡張ボード」に「SPRESENSE メインボード」と「Mic&LCD KIT for SPRESENSE」を取り付けます。(マイクは1つだけ取り付けます。) 3. メインボードにプログラムを書き込みます。(Maincore.ino は「MainCore」に、Subcore.ino は「SubCore 1」に対して書き込みます。) 4. 再生した複数のサイン波をマイクに近づけて、LCD表示を確認します。 # 考察 マイク経由から得られるピークの周波数値は、周波数帯域によって、精度面にばらつきが見られました。そのため、今回は以下のような条件を設けました。 - サイン波を音源として、複数組み合わせて出力する - キーを取得する範囲を十分に持たせることで、(ハズレ無しになるように)必ずキーを取得するように対応 - キーの取得の対象の周波数値を制限(C5 ~ C7 523Hz ~ 2093Hz) - 3音と4音のコードのみを分析 これにより、正解の音楽コードを取得する割合はかなり高まりました。 対象の周波数より低い値の場合、精度面はかなり落ちました。 精度面の課題の要因として、以下が考えられます。 - マイクの位置 - FFTのサンプル数を増やす(2048) - ピーク値を求める処理の部分の方法 また、LCDの表示の更新時に生じる点滅がやや目立つため、スムーズな表示の切り替えになるように改善したいです。 最近はAIで音楽コードを分析する手法も行われているようですので、その分野の知見を集めることができれば、SPRESENSEの環境でも試してみる機会が来る、かもしれないです。 # 参考資料 - SPRESENSEではじめるローパワーエッジAI/O'Reilly - https://github.com/TE-YoshinoriOota/Spresense-LowPower-EdgeAI