「AmbientでIoTをはじめよう」、第16回はM5Stackを使って音を測ります。
音は設備の異常検知などをする際の重要なデーターです。 音による異常検知では、音の大きさや周波数成分が正常時と大きく異なるときに異常と判断します。 正常値のデーターを学習し、異常値を検出するのは機械学習などでおこないます。
今回は、M5Stackにマイクモジュールをつないで、音の大きさと周波数成分を測ります。
マイクは言うまでもなく音を電気信号に変換するものです。 マイコンに接続するマイクとしてはエレクトレットコンデンサーマイクとMEMS技術で作られたマイクがよく使われます。
写真の左はMEMSマイクの「ADMP401搭載MEMSマイクモジュール」、 右はエレクトレットコンデンサーマイクを使ったマイクモジュールです。
エレクトレットコンデンサーマイクはコンデンサーマイクの一種です。 コンデンサーの電極の一方を薄膜にして、音によって生じる膜の振動を、電圧の変化として取り出すものです。 電極にエレクトレット素子(半永久的に電荷を蓄える高分子化合物)を用いることで低い電圧で動作可能になっています。
MEMSマイクはエレクトレットコンデンサーマイクと同じ原理ものをMEMS(Micro Elerctronics Mechanical System)技術で構成したものです。 非常に小型で、振動、衝撃、温度変化に強いといった特徴があります。
今回はMEMSマイクの「ADMP401搭載MEMSマイクモジュール」を使いました。
| 電源電圧 | 1.5〜3.3V |
| 出力 | アナログ出力 (無音時Vccの1/2) |
| 周波数特性 | 100Hz〜15kHzまでフラット |
マイクの出力はアナログ信号なので、M5Stackに搭載されているESP32のADコンバーターで値を読めます。 マイクとM5Stackは次のように接続します。
| M5Stack | ADMP401搭載MEMSマイクモジュール |
| 36 | AUD |
| 3V3 | Vcc |
| GND | GND |
ジャンパーワイヤーでも接続できますが、今回は小さな基板を作って接続してみました。 こうするとコンパクトなセンサー端末になります。
マイクの出力はArduinoであればanalogRead()関数で値を読めます。
音は振動なので周期的に値が変化します。 音を記録するためには音の周期の2倍以上の周期で値を測定(サンプリング)する必要があります。 人が聞こえる音の周波数範囲は、個人差や年齢差はあるものの一般的には20Hzから20kHz程度と言われています。 そこで、次のプログラムでは40kHzの周期でマイクの信号を読み、メモリーに保存して、シリアルに出力してみます。
40kHzというのは1秒間に4万回、25マイクロ秒毎に1回測定することになります。 このプログラムでは500回測定しているので、時間にすると12.5ミリ秒間の音を測定しています。
| /* | |
| * M5Stack+ADMP401を使い、SAMPLING_FREQUENCYで音を測定し、値をシリアルにプリント。 | |
| */ | |
| #include <M5Stack.h> | |
| #define MIC 36 | |
| #define SAMPLES 500 | |
| #define SAMPLING_FREQUENCY 40000 | |
| void setup() { | |
| M5.begin(); | |
| M5.Speaker.write(0); // スピーカーをオフする | |
| Serial.begin(115200); | |
| while (!Serial) ; | |
| M5.lcd.setBrightness(20); // LCDバックライトの輝度を下げる | |
| unsigned int sampling_period_us = round(1000000 * (1.0 / SAMPLING_FREQUENCY)); | |
| pinMode(MIC, INPUT); | |
| delay(1000); | |
| int buff[SAMPLES]; | |
| for (int i = 0; i < SAMPLES; i++) { | |
| unsigned long t = micros(); | |
| buff[i] = analogRead(MIC); | |
| while ((micros() - t) < sampling_period_us) ; | |
| } | |
| for (int i = 0; i < SAMPLES; i++) { | |
| Serial.println(buff[i]); | |
| } | |
| } | |
| void loop() { | |
| } |
26行目のfor文で500回繰り返して、28行目のanalogRead()でマイクの出力を測定し、メモリーに保存しています。 周期処理は周期時間内に収まる必要があります。 M5StackでのanalogRead()の実行時間を測ったところ約9.5マイクロ秒でしたので、周期時間25マイクロ秒よりも短い時間で実行できていました。 analogRead()の実行には10マイクロ秒程度かかるので、 10マイクロ秒単位の周期処理をおこなう時は処理時間が周期時間に収まっているかを確認することは重要です。
25マイクロ秒の周期処理は29行目のwhile文でおこないます。 周期処理の先頭(27行目)でmicros()関数で経過時間を測り、while文で経過時間が先頭から25マイクロ秒経つまで時間調整しています。
M5Stackにマイクモジュールをつなぎ、このプログラムをビルドして、動かします。 Arduino IDEのシリアルモニタの代わりにシリアルプロッタを立ち上げてみましょう。
特に音源を用意していなければ、測定している場所の環境雑音が測定され、次のような出力が得られます。 シリアルプロッタは数字の列をグラフ表示してくれるので、センサーの値を確認するときなどに便利です。
M5StackのAD変換は12ビットの分解能で、0〜3.6vの入力に対して0〜4095の値が返されます。 「ADMP401搭載MEMSマイクモジュール」は無音時にVccの1/2の出力になるので、2000前後の値が読めています。
プログラムの16行目で M5.Lcd.setBrightness(20); でLCDの輝度を下げています。 M5StackのLCDはLEDのバックライトがついていて、LCDの輝度はこのLEDをPWM(パルス幅変調)することで制御しています。 このPWMの周波数が10kHzで、デフォルトの輝度だとこの信号をADCが拾ってしまい、次のように無音時でも10kHzの信号が測定されてしまいました。 LCDの輝度を下げることでPWMの影響を低減させています。
次にスマートフォンの音生成アプリを使い、リファレンス音を生成して、測定してみます。 音生成アプリとしては「Audio Tone Generator LITE」などが便利です。
ATG LITEを使って1000Hzの正弦波(sine波)を発生させます。
先ほどのプログラムを動かして、マイクで音を測定し、シリアルプロッタで出力を表示してみます。 こんな感じの波形が表示されます。
シリアルプロッタは最後の500件のデーターを表示します。 画面を見ると約12.5回の波が記録されています。 25マイクロ秒周期で500回、12.5ミリ秒の時間に約12.5回の波なので、波の周期は約1ミリ秒、周波数1000Hzの音が測定できています。
音の周波数を500Hzにして、同じように測定してみると、次のような波形が表示されます。
波の間隔が2倍程度に伸びており、500Hzの音が測定できています。
音を測定して波形が確認できたので、この波形をLCDに表示します。
M5StackのLCDの横幅が320ピクセルなので、300回データーを測定してメモリーに保存し、表示しました。 グラフを描画する部分のプログラムは次のようになります。
| double minY = 0.0; | |
| double maxY = 3.6; | |
| #define DRANGE 2.0 | |
| #define X0 10 | |
| int d2y(double d, double minY, double maxY, int HEIGHT) { | |
| return HEIGHT - ((int)((d - minY) / (maxY - minY) * (float)HEIGHT) + 1); | |
| } | |
| void drawChart() { | |
| int HEIGHT = M5.Lcd.height(); | |
| double newmin = 3.6; | |
| double newmax = 0; | |
| for (int i = 1; i < SAMPLES; i++) { | |
| int d0 = d2y(buff[i - 1], minY, maxY, HEIGHT); | |
| int d1 = d2y(buff[i], minY, maxY, HEIGHT); | |
| M5.Lcd.drawLine(i - 1 + X0, d0, i + X0, d1, WHITE); | |
| if (newmin > buff[i]) newmin = buff[i]; | |
| if (newmax < buff[i]) newmax = buff[i]; | |
| } | |
| if ((newmax - newmin) < DRANGE) { | |
| minY = newmin - DRANGE / 2; | |
| maxY = newmax + DRANGE / 2; | |
| } else { | |
| minY = newmin - (newmax - newmin) / 2; | |
| maxY = newmin + (newmax - newmin) / 2; | |
| } | |
| } |
グラフ描画はM5.Lcd.drawLine()でおこなっています。 横軸は左に10ピクセルの余白を置き、10ピクセルから1ピクセルずつ増やしていきます。 縦軸は1周期の最小値と最大値を求め、次の周期のグラフの縦軸の最小値と最大値にしています。
Audio Tone Generator LITEを使って1000Hzの正弦波(sine波)を発生させた時の M5Stackの画面は次のようになりました。
一般的に音はいくつもの周波数の音の組み合わせでできています。 サンプリングした波形を高速フーリエ変換(FFT: Fast Fourier Transform)することで、音を構成する主な周波数の成分を調べられます。
Arduinoで動くFFTライブラリはいくつかあります。 Arduino IDEのライブラリマネージャからインストールできるものとしてはarduinoFFTがあります。
Arduino IDEのスケッチ→ライブラリをインクルード→ライブラリを管理...を選択してライブラリマネージャを立ち上げ、右上の検索窓に「fft」と入力します。 いくつかFFTライブラリが表示されますが、その中の「arduinoFFT by Enrique Condes」をインストールします。 「More info」をクリックすると図のように「インストール」ボタンが現れるので、それをクリックすればインストール完了です。
ライブラリーの使い方は簡単です。
| #include "arduinoFFT.h" | |
| const uint16_t FFTsamples = 256; // サンプル数は2のべき乗 | |
| double vReal[FFTsamples]; // vReal[]にサンプリングしたデーターを入れる | |
| double vImag[FFTsamples]; | |
| arduinoFFT FFT = arduinoFFT(vReal, vImag, FFTsamples, SAMPLING_FREQUENCY); // FFTオブジェクトを作る | |
| FFT.Windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD); // 窓関数 | |
| FFT.Compute(FFT_FORWARD); // FFT処理(複素数で計算) | |
| FFT.ComplexToMagnitude(); // 複素数を実数に変換 |
ヘッダーファイルarduinoFFT.hをインクルードし、作業用のバッファーvReal[]とvImag[]を用意します。 バッファーサイズはサンプル数で2のべき乗にします。 バッファー、サンプル数、サンプリング周波数を指定してFFTオブジェクトを生成します。 サンプリングしたデーターをvReal[]に入れ、vImag[]は0クリアします。
FFTの計算はWindowing()で窓関数を適用し、Compute()でFFTの計算をおこない、ComplexToMagnitude()で複素数から実数に変換します。 これでvReal[]の半分に周波数ごとの信号の大きさが計算されます。
なお、ライブラリーについてくるサンプルプログラムは次のようなインターフェースでライブラリーを呼んでいますが、 ライブラリーのソースコードを見るとこれらの呼び方は非推奨(deprecated)と書かれているので、推奨されているインターフェースを使っています。
| /* | |
| * サンプルプログラムでは使われているが、非推奨なインターフェース | |
| */ | |
| arduinoFFT FFT = arduinoFFT(); | |
| FFT.Windowing(vReal, samples, FFT_WIN_TYP_HAMMING, FFT_FORWARD); | |
| FFT.Compute(vReal, vImag, samples, FFT_FORWARD); | |
| FFT.ComplexToMagnitude(vReal, vImag, samples); |
プログラムは、loop関数の中で音をサンプリングし、FFT処理で音の周波数成分を求め、LCDに周波数ごとの音の強さを表示しています。 プログラム全体は次のようになります。
| /* | |
| * M5StackでSAMPLING_FREQUENCYでマイクを測定し、FFTして周波数成分をLCDにグラフ表示。 | |
| */ | |
| #include <M5Stack.h> | |
| #include "arduinoFFT.h" | |
| #define MIC 36 | |
| #define SAMPLING_FREQUENCY 40000 | |
| const uint16_t FFTsamples = 256; // サンプル数は2のべき乗 | |
| double vReal[FFTsamples]; // vReal[]にサンプリングしたデーターを入れる | |
| double vImag[FFTsamples]; | |
| arduinoFFT FFT = arduinoFFT(vReal, vImag, FFTsamples, SAMPLING_FREQUENCY); // FFTオブジェクトを作る | |
| unsigned int sampling_period_us; | |
| void sample(int nsamples) { | |
| for (int i = 0; i < nsamples; i++) { | |
| unsigned long t = micros(); | |
| vReal[i] = (double)analogRead(MIC) / 4095.0 * 3.6 + 0.1132; // ESP32のADCの特性を補正 | |
| vImag[i] = 0; | |
| while ((micros() - t) < sampling_period_us) ; | |
| } | |
| } | |
| int X0 = 30; | |
| int Y0 = 20; | |
| int _height = 240 - Y0; | |
| int _width = 320; | |
| float dmax = 5.0; | |
| void drawChart(int nsamples) { | |
| int band_width = floor(_width / nsamples); | |
| int band_pad = band_width - 1; | |
| for (int band = 0; band < nsamples; band++) { | |
| int hpos = band * band_width + X0; | |
| float d = vReal[band]; | |
| if (d > dmax) d = dmax; | |
| int h = (int)((d / dmax) * (_height)); | |
| M5.Lcd.fillRect(hpos, _height - h, band_pad, h, WHITE); | |
| if ((band % (nsamples / 4)) == 0) { | |
| M5.Lcd.setCursor(hpos, _height + Y0 - 10); | |
| M5.Lcd.printf("%.1fkHz", ((band * 1.0 * SAMPLING_FREQUENCY) / FFTsamples / 1000)); | |
| } | |
| } | |
| } | |
| void setup() { | |
| M5.begin(); | |
| M5.Speaker.write(0); // スピーカーをオフする | |
| Serial.begin(115200); | |
| while (!Serial) ; | |
| M5.lcd.setBrightness(20); | |
| pinMode(MIC, INPUT); | |
| sampling_period_us = round(1000000 * (1.0 / SAMPLING_FREQUENCY)); | |
| } | |
| void DCRemoval(double *vData, uint16_t samples) { | |
| double mean = 0; | |
| for (uint16_t i = 1; i < samples; i++) { | |
| mean += vData[i]; | |
| } | |
| mean /= samples; | |
| for (uint16_t i = 1; i < samples; i++) { | |
| vData[i] -= mean; | |
| } | |
| } | |
| void loop() { | |
| sample(FFTsamples); | |
| DCRemoval(vReal, FFTsamples); | |
| FFT.Windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD); // 窓関数 | |
| FFT.Compute(FFT_FORWARD); // FFT処理(複素数で計算) | |
| FFT.ComplexToMagnitude(); // 複素数を実数に変換 | |
| M5.Lcd.fillScreen(BLACK); | |
| drawChart(FFTsamples / 2); | |
| } |
プログラムをビルドして、M5Stackに転送し、動かします。 音源を用意していない状態、Audio Tone Generator LITEで1000Hz、4000Hz、16kHzの音を出した時の画面は次のようになりました。
次の動画はスマートフォンの「トーンジェネレーター」というアプリで1000Hzから10kHzまでの周波数の音を出したときの、FFTの画面です。
音の周波数変化に対応してグラフが変化しているのが確認できます。
私達の身の回りは環境雑音も含めて様々な音にあふれています。 測定したり周波数成分を調べてグラフ化すると、今まで気づかなかった音に気がつくこともあります。 皆さんもマイコンとセンサーと簡単なプログラムで調べてみてはいかがでしょうか?
この記事はアンビエントデーターの下島が担当しました。