マイクに入力された複数のサイン波の組み合わせから音楽コードを判別してみた
作品の概要
複数のサイン波の音源をマイクで受け取り、SPRESENSEでの演算で分析することで、複数のサイン波の組み合わせが想定していた音楽コードを判別できるか試してみました。
目的
SPRESENSEを使用して、Arduinoなど低速のCPUでは実現できないような処理を加えたいことから、一例としてFFT(高速フーリエ変換)の処理を含んだ内容を考えてみたいと思い、今回のテーマを提案してみました。
処理の流れ
- 複数のサイン波の信号を出力する別のアプリを用意して、音楽コードと一致する周波数のサイン波の組み合わせを出力します。
- マイクで受け取った「1」の信号をFFTで演算して、上位4つの周波数のピーク値を受け取ります。
- 得られた周波数値から、キーを取得します。
- 得られたキーの組み合わせから、音楽コードを取得します。
- 得られた音楽コードを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
使い方
- 複数のサイン波を再生するツールは、iOSアプリの「マルチ信号発生器」で同等の確認ができます。
- 「SPRESENSE 拡張ボード」に「SPRESENSE メインボード」と「Mic&LCD KIT for SPRESENSE」を取り付けます。(マイクは1つだけ取り付けます。)
- メインボードにプログラムを書き込みます。(Maincore.ino は「MainCore」に、Subcore.ino は「SubCore 1」に対して書き込みます。)
- 再生した複数のサイン波をマイクに近づけて、LCD表示を確認します。
考察
マイク経由から得られるピークの周波数値は、周波数帯域によって、精度面にばらつきが見られました。そのため、今回は以下のような条件を設けました。
- サイン波を音源として、複数組み合わせて出力する
- キーを取得する範囲を十分に持たせることで、(ハズレ無しになるように)必ずキーを取得するように対応
- キーの取得の対象の周波数値を制限(C5 ~ C7 523Hz ~ 2093Hz)
- 3音と4音のコードのみを分析
これにより、正解の音楽コードを取得する割合はかなり高まりました。
対象の周波数より低い値の場合、精度面はかなり落ちました。
精度面の課題の要因として、以下が考えられます。
- マイクの位置
- FFTのサンプル数を増やす(2048)
- ピーク値を求める処理の部分の方法
また、LCDの表示の更新時に生じる点滅がやや目立つため、スムーズな表示の切り替えになるように改善したいです。
最近はAIで音楽コードを分析する手法も行われているようですので、その分野の知見を集めることができれば、SPRESENSEの環境でも試してみる機会が来る、かもしれないです。
参考資料
- SPRESENSEではじめるローパワーエッジAI/O'Reilly
- https://github.com/TE-YoshinoriOota/Spresense-LowPower-EdgeAI
-
junichi.minamino
さんが
2024/01/31
に
編集
をしました。
(メッセージ: 初版)
ログインしてコメントを投稿する