junichi.minaminoのアイコン画像
junichi.minamino 2024年01月31日作成
製作品 製作品 閲覧数 387
junichi.minamino 2024年01月31日作成 製作品 製作品 閲覧数 387

マイクに入力された複数のサイン波の組み合わせから音楽コードを判別してみた

マイクに入力された複数のサイン波の組み合わせから音楽コードを判別してみた

作品の概要

複数のサイン波の音源をマイクで受け取り、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音はコードの種類が多いため、また機会があれば確認したいと思います。)

以下が該当部分のプログラムです。

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アプリの「マルチ信号発生器」で同等の確認ができます。
  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の環境でも試してみる機会が来る、かもしれないです。

参考資料

1
junichi.minaminoのアイコン画像
音楽系のアプリ(主にスマートフォン向け)を開発しています。
ログインしてコメントを投稿する