レベルメータ作成のまとめはこちら
はじめに
どうもtakaです。子育てダイエット期待していました。が、順調に体重増加中です。不思議だなあ(食べすぎ)
さてさて、
前回
はオーディオレベルメータの全体構成を示しました。今回から各要素の説明に入りたいと思います。今回は肝である音についてESP32を動作させながら確認していきます。
音とはなにか
いきなり哲学的ですが、レベルメータで音に合わせてLEDを点灯させたいとき、
音に合わせるとは一体どういうことなのか?
の解釈が肝になってきます。
私は音を”波”と捉え、波の特徴として次の2点に焦点を当ててレベルメータを作ることにします。
波の特徴その1:波の高さ(高い:大きな音)をLEDの個数
波の特徴その2:波の幅(広い:低い音)をLEDの色
単一波
一番シンプルな音は波の高さと幅が常に一定の所謂サイン波や正弦波と呼ばれる音です。
例えばこんな音です。
合成波
人の声や音楽など身の回りの音の波形は複雑な形状で何が何だか分からない波形をしています。
例えばこんな音です。
フーリエ変換とFFT
下記画像は私たちが耳にしている波形(D)を示しております。
この波形は低い音(A)と小さく高い音(B)が合わさった音の波形です。
上から
A:振幅1周波数10[Hz]の波形 B:振幅0.6周波数100[Hz]の波形 C:AとBを重ねて描画した波形 D:AとBを足し合わせた波形 |
フーリエ変換とは
我々が耳にしている音(D)からAとBを復元できれば、Aを赤色LED、Bを青色LEDの様に色に割り当てることが可能になります。
このように合成波を構成している単一波の振幅と周波数を求める方法こそがフーリエ変換です。例えば、Dをフーリエ変換すると10Hzの信号が振幅1、100Hzの信号が振幅0.6の割合で含まれていることが分かるようになります。
詳細は省きますが高校数学の範囲で出てくる話と思います(私は高校行っていないので正しくないかもです)。
FFTとは
FFTとは高速フーリエ変換の事です。フーリエ変換にとある制限(サンプルプログラムで説明)を加えると、コンピュータで高速に演算できるようになります。これが高速フーリエ変換と呼ばれており今回利用する手段となります。
ESP32でFFT
フーリエ変換自体は簡単な計算ですが今回はArudiono向けのFFTライブラリを用いることにします。
ここ
を参考に公式リポジトリからFFTライブラリ”arduinoFFT"を追加しておいてください。
サンプルプログラム
ESP32内で以下を実行します
・合成信号の生成(振幅1の1[kHz]と3[kHz]の合成信号)
・FFTの実行
・FFT結果の表示
とりあえずコピペして実行してみてください。
////////////////////////////////////////////////////////// //arduinoFFTライブラリを使用したFFTサンプル // //事前にarduinoFFTライブラリを登録しておくこと。 // ////////////////////////////////////////////////////////// #include "arduinoFFT.h" #define SAMPLING_FREQUENCY 30000.0 const unsigned int FFTsamples = 1024; //サンプリング点数(2のべき乗) double vReal[FFTsamples]; double vImag[FFTsamples]; arduinoFFT FFT = arduinoFFT(vReal, vImag, FFTsamples, SAMPLING_FREQUENCY); // FFTオブジェクトを作る //-------------------FFTテストデータ作成--------------------- //1000[Hz]と3000[Hz]の合成信号を生成する。 const double dpi=2*3.1415926535; const double dt=1.0/SAMPLING_FREQUENCY; const double f1=1000.0; const double f2=3000.0; const double w1=dpi*f1; const double w2=dpi*f2; void genFFTtestData(){ for(int n=0;n<FFTsamples;n++){ double t=dt*n; vReal[n]=sin(w1*t)+sin(w2*t); vImag[n]=0; } } //---------------------------------------------------------- //----------------------セットアップ関数---------------------- void setup() { Serial.begin(115200) ; delay(3000) ; } //---------------------------------------------------------- //----------------------メインループ関数---------------------- void loop() { genFFTtestData(); //FFTテストデータの生成 FFT.Windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD); //窓関数設定 FFT.Compute(FFT_FORWARD); // FFT実施(複素数で計算)。結果はvRealの前半に格納されている。 FFT.ComplexToMagnitude(); // 複素数を実数に変換 //FFT結果出力 int fftResIndexMax=FFTsamples/2; for(int n=0;n<fftResIndexMax;n++){ //FFT結果に2/サンプル数をかけるとテストデータと同じ次元になる //ただし正弦波は周期関数なので更に2倍しないとテストデータと同じ次元にはならない double vSpec=vReal[n] * 4.0 / FFTsamples; //結果を表示 //テストデータに含まれている周波数成分周辺にスペクトルが集中している事が確認できる Serial.printf("%.0f[Hz]:%f¥n",n*SAMPLING_FREQUENCY/FFTsamples,vSpec); } while(1){} }
実行結果
実行前にIDEのツール=>シリアルモニタでシリアルモニタを立ち上げbpsを115200に設定しておきます。実行するとシリアルモニタに周波数と数値の羅列が出てきたら成功です!
実行結果を見ると1[kHz]と3[kHz]の付近に1前後の値が確認できます。つまりサンプルコード内で生成した振幅1の1[kHz]と3[kHz]の合成波をFFTすることで元の周波数とその振幅を特定できていることが分かりますね♪
因みに合成波をグラフで見るとこのような波形です。
レベルメータではFFTの結果をもとに、周波数を色(低音:赤、中音:緑、高音:青)、振幅を点灯個数で表現します。そのためには音声信号を受け取る機能とLEDを制御する機能が必要ですので次回以降で説明していきます!
全投稿のまとめページはこちら
補足
サンプルプログラムを補足します。基本はコード内のコメントの通りです。
FFTを行うにはvReal配列に時系列データを格納します。今回はgenFFTtestData()で合成波を生成するのですが、SAMPLING_FREQUENCY [Hz]でFFTsamples個サンプリングしvRealに格納しています。本番ではvRealには音源から取得した音楽データを格納します。
FFTに必要なコードは下記3行のみです。
(genFFTtestData()はFFTの動作確認のために合成波を生成するための関数ですので本番では使用しません)
FFT.Windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD);
FFT.Compute(FFT_FORWARD);
FFT.ComplexToMagnitude();
この中でも実際にFFTを行っているのはFFT.Computeだけです。
FFT.WindowingはFFTを行うにあたり、窓関数を設定しています。窓関数とはデータの固まりから一定範囲のデータを抜き出すための関数です。いくつか種類があるのですが今回はよく使われるハミング窓を使用しています。窓関数はFFT結果に影響(精度)を与えます。解析したい信号の特徴に合わせて窓関数を選択するといいと思います。
FFT.ComplexToMagnitudeはFFT結果を実数に変換する処理です。本来FFT結果は複素平面で表現されます。
loop()内の上記以外の処理はFFTには不要です。例えばfor文はFFT結果をシリアルモニタに表示するための処理です。
0 件のコメント:
コメントを投稿