編集履歴一覧に戻る
mandbjpのアイコン画像

mandbjp が 2022年09月25日23時37分02秒 に編集

fix heading

本文の変更

# モチベーション この度、 `2022年 SPRESENSE 活用コンテスト` に参加して、モニター提供を頂きました。 在宅ワークも当たり前となり、Web会議中に救急車がそばを通過してサイレン音で発言を中断することがしばしばあります。 大通りに面していることから、近距離で多く通っているんだろうとも予想していました。 そのため、一体1日で何回この救急車のサイレン音を聞いているんだろうか、ということが長らく疑問にありました。 今回、SpresenseをArduinoとして用いてマイクから録音し、サブコアを用いて音声処理(FFT)を行い、Eltresのネットワークを経由してCLIP Viewer Liteで可視化しました。 -- 今回 Spresense学習にあたり、 `SPRESENSEではじめるローパワーエッジAI` の電子書籍を購入しました。 https://www.oreilly.co.jp/books/9784873119670/ # ハードウェア編

-

### Spresenseと事前準備

+

## Spresenseと事前準備

今回は Spresense本体とSpresense拡張ボード、ELTRESアドオンボードを使用しました。また、DSPファイルのため4GBのMicroSDカードも使用しています 事前準備として、音声録音に必要なDSPファイルを `スケッチ例 > Spresense用スケッチ例 Audio > dsp_installer > src_instlaler` を初回に実行してインストールしておきます。

-

### マイク入力

+

## マイク入力

手元にPJ-392 という3.5mmヘッドフォンジャックの在庫がありました。 https://www.aliexpress.com/item/32834368373.html 端子側を手前にし、上に長い端子を向けた状態で、上①: GND、右②: Lチャネル、左③: Rチャネル という並びになっています。 ![PJ392 Pin](https://camo.elchika.com/4305aac1a6dc0f4204d24477fb0f24aa30ddfd1b/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f61323531376133342d323761652d343061392d386432332d3939313964626134643133342f64306662316637352d383265332d343230642d393538322d313764333139653539653338/) ① GND、② L、③ R Spresenseとの接続には拡張ボードにある、`GND` PINを①の配線、`MICA`PINを②の配線、 `MIC_BIASA` PINをバイアス抵抗2.2kohmを経由して②に配線し接続します。 (③は使用しません) マイクの動作確認には、`スケッチ例 > Spresense用のスケッチ Audio > application > voice_effector` が便利でした。 拡張ボードヘッドフォンジャックにイヤホンを接続し、MICA等に接続したマイクにめがけてスマホ音楽を流すことで、リアルタイムにイヤホンに音楽が聞こえればOKです。 ## Eltresアドオンボードの接続 + アンテナ Eltresアドオンボードの接続については、SwitchScienceの販売ページのマニュアルが役立ちました。 - SPRESENSE用ELTRESアドオンボード https://www.switch-science.com/products/7580 - ELTRESアドオンボード_取扱説明書_20220501 https://doc.switch-science.com/media/files/e66da46d-7b26-4d07-8186-2f2181e433cd.pdf Spresense本体の1PINとEltresアドオンボードの1PINを合わせるように接続します。 LPWAのアンテナも立てるように案内があり、Spresenseの空き箱がちょうどよく挟んで立てられました。 ![Spresense・Eltress・マイク](https://camo.elchika.com/0cc636cacede37c425407e49b65836812677814b/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f61323531376133342d323761652d343061392d386432332d3939313964626134643133342f38356234323334662d366364612d343830372d616236662d363161333233663063643934/) # ソフトウェア編 (全体的なソースコードは記事の末尾に記載しています) まず、今回実装したコードの全体の動作フローを記載します ![Spresense_Eltres_FFT_Multicore](https://camo.elchika.com/cf9a912147421eb297a0f3631baa8ce80970802c/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f61323531376133342d323761652d343061392d386432332d3939313964626134643133342f33633637623934332d396333392d343364322d626630662d613133343538663664363939/) [フローを mermaid.live で表示する](https://mermaid.live/edit#pako:eNqdWF9v2zYQ_yqEgBQd0A7rax42JLYcBHDswg46dJER0BLjCJFEV6K6Gkm--3gkRR0pSgnmJ_F4v_vH493Rz1HKMxadRw8F_zd9pLUgt5dJlVRE_m5oXs14zbZC0u-SCNYklQSiKEm003zbdo_Z5NLn0nxxIWrWxD9ZJWaXklGviSKQGS2KPU2frNSrqkG8V6vtdshJPn_-EzOuyxwsyDipuHjMqwPSfnZ2RgrOj8YWAAByw1KW_2TPSXSnaDuiVBly9lcSvSKENvk6KzAAlkG-LauyDaPZCTEDjSiihmjQrMxUtKRaXmfAPuNlSatsp6NI9EavRQH4McjPjw57SMOioIf1YgGnhcQTIBNJt4fgotCnj7xos5wPYCBPM8izkBAl3xLIeoXOx_EI22fdGTGvw_RfHixgGz86pmlVvnVGkYZdN3ZHhjtvekYcZUgyJ38GmQ9KdN7JFEB6DUoSHQ81EgBbZkK_XnUYc-YEnYsbUzAHsjNoBWwgA2AZUA3k_6MaisVNK9ivKyaWPH2CO8wEgU-rAdxVh7OoackkBxD0cRFFspxW2oYVjDbMSDQrV-qKi_zhJKsQFCHJpNfEEJCJhvI3zcUNaxp6AG5YkVIvcYUDzrf8MWzxL5YuFrdQ4eSXxBC5Csp6hzeGHcrGomY_WlalJ8hvKCNHRp-IpSLPTPZhgCF57DqzLe2i3LcFrVKmU9zSid3oKxCkwZwJlgq72SWHJvcgLyNtWQympd1FudkXTSdDFcW59l5udjtdalqQtpBl2GGFDTiqtTDxlZ4KTrPvrFHxF8QQiODki2eT5V7xIfMfXTw0omn3h5oeH6WXoj1K9jvI9h1pYPnxNysZflleS7tzXqk23VGdRg098SWJ9pwLEK1z_sVp0tNI05ZrdsgbwWqS2n774rZw8gH3Xi1UHlR31NYvBGps29dLK3zSz54O6YPHhn4HSbXlFWIZL2838fb-cn2xmd_H327vodDeb-JZfP0t3qGy6-hH0qAABgVdz5exbv5jUJvKQfw2Xs2lIRfz73gqQHdlqnOE2sZqzI6u2rulfpIdmz64gqtxI62BajQbs3400CFU38DGwhtCeUUG5aWbwx60GWTvUs6M_aWECfKdudrnv818nfMfugrVOHKmZIVG1sBu37JHN1FcXB531AvsdcXUB6LJyN-adgBiP-nEJINz-lOujOzb3jDYdhwauoJSyZYkJ3XGfJW1VTYPr4z68yBKSG_qG4h3AzUq3cx5SLA3040IdgI8Kh23a6XCa8q9aG_DjXu4K7uah434nfgVD8AlEfk9lTLBp9MUy5uSnMfHwIxwhELvowmON8T4zx8_owM3AF0L91z8Ud-mQT_bK1JoiFcbzsTeK3TISuVlISFS_8tgwlVyQpMstn-46zoSGGm7uKBL75r1u9Q7fEgEu0j_JLkzn-_oJx11qMM918ATJchgHifjYO9wBkzOS2QQnBCTipF_YAAwEJhz1fkt1d8zwz-V7KaRHn2KSlaXNM-i8-gZ-JNIPDJ4L57Lz4w90LYQSZRUr5KVtoJvT1UanT_QomGfovaYUcHmOZVHU1rqkVb_cN6vWZYLXt_ov8fUv2Sv_wFcTECs) ## 音声処理 救急車のサイレン音の検出の肝となるFFT処理は、Spresense用のライブラリを使用しました。また実装としても書籍から学習させてもらった内容におんぶにだっこされています。 同書籍にはSpresenseのマルチコアプログラミングの例もあり、あわせ技として今回はメインコアで録音し、サブコアでFFT処理をしました。 (音声録音はメインコアのみで実行可能でした) 特記事項として、Subcore では `Audio.h` を include することができないため、必要な定数を再定義しています ```cpp:Spresense_Eltres_FFT_Multicore_Main/Spresense_Eltres_FFT_Multicore_Main.ino // Multi core #ifdef SUBCORE #error "Core selection is wrong!!" #endif #include <MP.h> #include <MPMutex.h> MPMutex mutex(MP_MUTEX_ID0); int subcore = 1; // use subocre 1 // Audio #include <Audio.h> #include <SDHCI.h> SDClass SD; #define FFT_LEN 1024 AudioClass* theAudio = AudioClass::getInstance(); // global static const uint32_t buffering_time = FFT_LEN*1000/AS_SAMPLINGRATE_48000; static const uint32_t buffer_size = FFT_LEN*sizeof(int16_t); static char buff[buffer_size]; uint32_t read_size; static uint8_t cnt700hz = 0; static uint8_t cnt900hz = 0; // ... void setup() { // ... Serial.begin(115200); while (!SD.begin()) { MPLog("Insert SD card"); } MPLog("Init Audio Recorder\n"); theAudio->begin(); theAudio->setRecorderMode(AS_SETRECDR_STS_INPUTDEVICE_MIC); // 16bit RAW PCM format, 48000Hz Mono int err = theAudio->initRecorder(AS_CODECTYPE_PCM, "/mn/sd0/BIN", AS_SAMPLINGRATE_48000, AS_CHANNEL_MONO); if (err != AUDIOLIB_ECODE_OK) { MPLog("RecorderInitialize error\n"); while(1); } MP.begin(subcore); // boot subcore 1 MP.RecvTimeout(MP_RECV_BLOCKING); // setup_eltress(); } void loop() { // ... // loop logics if (isRecording) { int ret; do { ret = mutex.Trylock(); } while (ret != 0); ret = theAudio->readFrames(buff, buffer_size, &read_size); if (ret != AUDIOLIB_ECODE_OK && ret != AUDIOLIB_ECODE_INSUFFICIENT_BUFFER_AREA) { MPLog("Error err = %d\n", ret); theAudio->stopRecorder(); while(1); } mutex.Unlock(); if (read_size < buffer_size) { // wait until data ready in FIFO delay(buffering_time); return; } int8_t sndid = 100; int8_t rcvid; uint32_t rcvdata; // send pointer address ret = MP.Send(sndid, &buff, subcore); if (ret < 0) { MPLog("MP.Send error = %d\n", ret); } // receive from Maincore ret = MP.Recv(&rcvid, &rcvdata, subcore); if (ret < 0) return; if (700 < rcvdata && rcvdata < 1000) { cnt700hz = (700 < rcvdata) && (rcvdata < 800) && (cnt700hz < 30) ? cnt700hz + 1 : cnt700hz; cnt900hz = (900 < rcvdata) && (rcvdata < 1000) && (cnt900hz < 30) ? cnt900hz + 1 : cnt900hz; } else { cnt700hz = cnt700hz == 0 ? 0 : cnt700hz - 1; cnt900hz = cnt900hz == 0 ? 0 : cnt900hz - 1; } MPLog("%5d Hz | 700Hz: %2d | 900Hz: %2d | Ambulance %d\n", rcvdata, cnt700hz, cnt900hz, hadAmbulance); int isAmbulance = (cnt700hz + cnt900hz) == 24; if (isAmbulance != 0) { hadAmbulance = true; digitalWrite(LED_AMBULANCE, HIGH); MPLog("isAmbulance! %d\n", isAmbulance); } } // ... } ``` ```cpp:Spresense_Eltres_FFT_Multicore_Subcore/Spresense_Eltres_FFT_Multicore_Subcore.ino // Multi core #if (SUBCORE != 1) #error "Core selection is wrong!!" #endif #include <MP.h> #include <MPMutex.h> MPMutex mutex(MP_MUTEX_ID0); // FFT #include <FFT.h> #define AS_SAMPLINGRATE_48000 48000 // https://github.com/sonydevworld/spresense-exported-sdk/blob/master/sdk/modules/include/audio/audio_common_defs.h#L230 #define AS_CHANNEL_MONO 1 // https://github.com/sonydevworld/spresense-exported-sdk/blob/master/sdk/modules/include/audio/audio_common_defs.h#L169 #define FFT_LEN 1024 // initalize FFT with mono, 1024 samples FFTClass<AS_CHANNEL_MONO, FFT_LEN> FFT; void setup() { FFT.begin(WindowHamming, AS_CHANNEL_MONO, (FFT_LEN / 2)); MP.begin(); // notify to main core MP.RecvTimeout(MP_RECV_BLOCKING); } void loop() { static const int ch_index = AS_CHANNEL_MONO-1; static char *buff; static float pDst[FFT_LEN]; int ret; int8_t rcvid; // receive from Maincore ret = MP.Recv(&rcvid, &buff); if (ret < 0) return; // wait Lock do { ret = mutex.Trylock(); } while (ret != 0); // MPLog(" Got lock\n"); FFT.put((q15_t*)buff, FFT_LEN); // exec FFT FFT.get(pDst, ch_index); // collect data from ch0 mutex.Unlock(); avgFilter(pDst); float maxValue; float peakFs = get_peak_frequency(pDst, &maxValue); // MPLog("peak freq: %f Hz\n", peakFs); // MPLog("Spectrum : %f", maxValue); uint32_t snddata = (uint32_t)(peakFs); ret = MP.Send(++rcvid, snddata); if (ret < 0) { MPLog("MP.Send error = %d\n", ret); } } ``` ```cpp:Spresense_Eltres_FFT_Multicore_Subcore/utils.ino #define AVG_FILTER (8) void avgFilter(float dst[FFT_LEN]) { static float pAvg[AVG_FILTER][FFT_LEN/2]; static int g_counter = 0; if (g_counter == AVG_FILTER) g_counter = 0; for(int i=0; i<FFT_LEN/2; ++i) { pAvg[g_counter][i] = dst[i]; float sum = 0; for(int j=0; j < AVG_FILTER; ++j) { sum += pAvg[j][i]; } dst[i] = sum / AVG_FILTER; } ++g_counter; } float get_peak_frequency(float *pData, float *maxValue) { uint32_t idx; float delta, delta_spr; float peakFs; const float delta_f = AS_SAMPLINGRATE_48000 / FFT_LEN; arm_max_f32(pData, FFT_LEN/2, maxValue, &idx); if (idx < 1) return 0.0; // get peak frequency delta = 0.5 * (pData[idx-1] - pData[idx+1]) / (pData[idx-1] + pData[idx+1] - 2.0 * pData[idx]); peakFs = (idx + delta) * delta_f; // スペクトルの最大値の近似値を算出 delta_spr = 0.125 * (pData[idx-1] - pData[idx+1]) * (pData[idx-1] - pData[idx+1]) / (2.0*pData[idx] - (pData[idx-1] + pData[idx+1])); *maxValue += delta_spr; return peakFs; } ``` コア間のデータのやり取りは書籍のノウハウの通りなのであまり自分から解説することはありませんが、 `static char buff[buffer_size];` でメイン・サブ両コアから参照可能なグローバル領域に確保したメモリに `theAudio->readFrames(..)` で録音した音声を溜め込み、バッファがいっぱいになったら `MP.Send(sndid, &buff, subcore);` でサブコアにバッファのポインタアドレスを送ります。 サブコアは受け取ったポインタアドレスへアクセスし、FFT処理を行います。ピーク周波数を求めたら、再度メインコアに周波数値を返します。 FFT動作確認としては、Androidアプリの周波数ジェネレータで生成した音を聞かせて確認しました。770hzを与えると765hz前後がSerialに出力されていました。 - 周波数ジェネレータ - Google Play ストア https://play.google.com/store/apps/details?id=com.boedec.hoel.frequencygenerator&hl=ja&gl=US また、救急車のサイレン音としては以下動画の活用させていただきました。 - 救急車サイレン - YouTube https://www.youtube.com/watch?v=Suq-yk3kpao 救急車のサイレン音はピー(960hz) ポー(770hz) が1.2秒周期で繰り返されるそうです。*文献 サイレン音を聞かせながらSerial出力を眺めていると、それぞれの音が だいたい12回~15回ぐらいでした。 ```txt:Serial [Main] 0 Hz | 700Hz: 0 | 900Hz: 0 | Ambulance 0 [Main] 0 Hz | 700Hz: 0 | 900Hz: 0 | Ambulance 0 [Main] 26 Hz | 700Hz: 0 | 900Hz: 0 | Ambulance 0 [Main] 947 Hz | 700Hz: 0 | 900Hz: 1 | Ambulance 0 [Main] 947 Hz | 700Hz: 0 | 900Hz: 2 | Ambulance 0 [Main] 962 Hz | 700Hz: 0 | 900Hz: 3 | Ambulance 0 [Main] 958 Hz | 700Hz: 0 | 900Hz: 4 | Ambulance 0 [Main] 958 Hz | 700Hz: 0 | 900Hz: 5 | Ambulance 0 [Main] 961 Hz | 700Hz: 0 | 900Hz: 6 | Ambulance 0 [Main] 958 Hz | 700Hz: 0 | 900Hz: 7 | Ambulance 0 [Main] 959 Hz | 700Hz: 0 | 900Hz: 8 | Ambulance 0 [Main] 963 Hz | 700Hz: 0 | 900Hz: 9 | Ambulance 0 [Main] 962 Hz | 700Hz: 0 | 900Hz: 10 | Ambulance 0 [Main] 959 Hz | 700Hz: 0 | 900Hz: 11 | Ambulance 0 [Main] 961 Hz | 700Hz: 0 | 900Hz: 12 | Ambulance 0 [Main] 961 Hz | 700Hz: 0 | 900Hz: 13 | Ambulance 0 [Main] 959 Hz | 700Hz: 0 | 900Hz: 14 | Ambulance 0 [Main] 962 Hz | 700Hz: 0 | 900Hz: 15 | Ambulance 0 [Main] 962 Hz | 700Hz: 0 | 900Hz: 16 | Ambulance 0 [Main] 959 Hz | 700Hz: 0 | 900Hz: 17 | Ambulance 0 [Main] 962 Hz | 700Hz: 0 | 900Hz: 18 | Ambulance 0 [Main] 759 Hz | 700Hz: 1 | 900Hz: 18 | Ambulance 0 [Main] 758 Hz | 700Hz: 2 | 900Hz: 18 | Ambulance 0 [Main] 757 Hz | 700Hz: 3 | 900Hz: 18 | Ambulance 0 [Main] 757 Hz | 700Hz: 4 | 900Hz: 18 | Ambulance 0 [Main] 758 Hz | 700Hz: 5 | 900Hz: 18 | Ambulance 0 [Main] 757 Hz | 700Hz: 6 | 900Hz: 18 | Ambulance 0 [Main] isAmbulance! 1 [Main] 756 Hz | 700Hz: 7 | 900Hz: 18 | Ambulance 1 [Main] 757 Hz | 700Hz: 8 | 900Hz: 18 | Ambulance 1 [Main] 756 Hz | 700Hz: 9 | 900Hz: 18 | Ambulance 1 [Main] 756 Hz | 700Hz: 10 | 900Hz: 18 | Ambulance 1 [Main] 755 Hz | 700Hz: 11 | 900Hz: 18 | Ambulance 1 [Main] 753 Hz | 700Hz: 12 | 900Hz: 18 | Ambulance 1 [Main] 951 Hz | 700Hz: 12 | 900Hz: 19 | Ambulance 1 ... +14count 900hz [Main] 761 Hz | 700Hz: 13 | 900Hz: 30 | Ambulance 1 ... +13count 700hz ``` 多少幅を持たせて700~800hz と 900-1000hz の範囲でカウントし、一定数検出したところで、救急車が通ったと判定しました。 以上で、救急車のサイレン音の検出ができました。 # 可視化 - Eltres通信 検出の結果を可視化するため、EltresのCLIP Viewer Liteを使わせていただきました。 ### ライブラリのインストール ライブラリのインストールについて、自分のMacのArduinoでは `.ZIP形式のライブラリをインストール`がうまく行かず、ZIPを手動で解凍し、 `~/Documents/Arduino/libraries/.` にコピーしました。 ## Eltres実装 サンプルコードをみて最初は癖があるように感じましたが、各種マニュアルPDFを読むと実際はシンプルで素直な仕様でした。 - `ELTRESアドオンボード用ライブラリ_v1.1.x_説明書(ArduinoIDE版)` - `ペイロードデータフォーマット仕様説明書_Ver1.6.x.pdf` 触ってみて便利に感じたところが、 - Eltresからイベント通知があるとコールバックが呼ばれる (割り込み?) - `begin` したあとはEltresが非同期で動いてくれるので、`loop() {..}` に自前の処理を比較的自由にかけるので便利。 - その間に各種センサーの処理をしておき、LPWA通信の 1分/3分 送信周期の直前にコールバックが起きるので、そのときにpayloadをセットおけば送信される - グラフへの可視化には `ペイロードデータフォーマット仕様説明書_Ver1.6.x.pdf` に従いpayloadを準備することで可能 逆に少しだけ不便に感じたことは、 - Eltresアドオンボードを使用する上で、緯度経度の情報を使うかどうかに関係なく、必ずGNSSの電波が受信できる状態が有効である必要。おそらく正確な時間を取得するため? - コールバック方式なので、ESPのWiFi処理やMQTTのように `loop() {..}` 内でイベントを受け取ってハンドリングする、といった慣れた実装は書けない(最初に癖があると感じた点) - `ペイロードデータフォーマット仕様説明書_Ver1.6.x.pdf` に `Float` や `Int16` と記載があるが、実際にやってみると、BigEndian <--> LittleEndianの変換が必要 Eltresがイベントをコールバックで通知してくれるため、 各種イベントフラグとコマンドフラグを用意して `loop()` 内はStateMachineっぽくフラグに応じて動作させました。 ```cpp:Spresense_Eltres_FFT_Multicore_Main/Spresense_Eltres_FFT_Multicore_Main.ino // commands bool startRecord = false; bool isRecording = false; bool stopRecord = false; bool terminate = false; bool hadAmbulance = false; // Eltres #include <EltresAddonBoard.h> // eltres events bool event_send_ready = false; bool event_idle = false; bool event_gnss_lost = false; bool event_gnss_receive = false; // Eltres payload uint8_t payload[16]; /** * @brief Eltres Event callback * @param event */ void eltres_event_cb(eltres_board_event event) { switch (event) { case ELTRES_BOARD_EVT_GNSS_TMOUT: // GNSS Timeout MPLog("gnss wait timeout error.\n"); break; case ELTRES_BOARD_EVT_IDLE: // Idle MPLog("waiting sending timings.\n"); event_idle = true; break; case ELTRES_BOARD_EVT_SEND_READY: // Prepare payload MPLog("Shortly before sending, so setup payload if need.\n"); event_send_ready = true; break; case ELTRES_BOARD_EVT_SENDING: MPLog("start sending.\n"); break; case ELTRES_BOARD_EVT_GNSS_UNRECEIVE: // GNSS Lost MPLog("gnss wave has not been received.\n"); event_gnss_lost = true; break; case ELTRES_BOARD_EVT_GNSS_RECEIVE: // GNSS Got signal MPLog("gnss wave has been received.\n"); event_gnss_receive = true; break; case ELTRES_BOARD_EVT_FAULT: // Error MPLog("internal error.\n"); break; } } /** * @brief GGA signal callback * @param gga_info pointer to gga_info */ void gga_event_cb(const eltres_board_gga_info *gga_info) { if (gga_info->m_pos_status) { // gga ready // do nothing } else { // gga not ready MPLog("[gga] invalid data.\n"); } } /** * @brief memcopy with Little Endian */ void memcpyLittleEndian(uint8_t *p, void *value, int size) { for(int i=0; i<size; i++) { memcpy(p+i, value + size - i - 1, 1); } } /** * @brief Set Eltres payload */ void set_payload(float airPressure, float pressure, float illuminance, uint16_t distance) { // init payload memset(payload, 0x00, sizeof(payload)); // set payload type to No.5 payload[0] = 0x85; memcpyLittleEndian(payload + 1, &airPressure, sizeof(airPressure)); memcpyLittleEndian(payload + 5, &pressure, sizeof(pressure)); memcpyLittleEndian(payload + 9, &illuminance, sizeof(illuminance)); memcpyLittleEndian(payload + 13, &distance, sizeof(distance)); } void setup_eltress() { // init Eltress eltres_board_result ret = EltresAddonBoard.begin( ELTRES_BOARD_SEND_MODE_1MIN, eltres_event_cb, gga_event_cb ); if (ret != ELTRES_BOARD_RESULT_OK) { terminate = true; MPLog("cannot start eltres board.\n"); } else { // eltres ready } } void setup() { pinMode(LED_GNSS, OUTPUT); digitalWrite(LED_GNSS, LOW); pinMode(LED_RECORD, OUTPUT); digitalWrite(LED_RECORD, LOW); pinMode(LED_SEND, OUTPUT); digitalWrite(LED_SEND, LOW); pinMode(LED_AMBULANCE, OUTPUT); digitalWrite(LED_AMBULANCE, LOW); // ... setup_eltress(); } void loop() { if (terminate) { MPLog("Program terminated.\n"); while(1); } // handle eltas event if (event_gnss_receive) { event_gnss_receive = false; startRecord = true; digitalWrite(LED_GNSS, HIGH); } if (event_gnss_lost) { event_gnss_lost = false; stopRecord = true; digitalWrite(LED_GNSS, LOW); } if (event_idle) { event_idle = false; startRecord = true; hadAmbulance = false; digitalWrite(LED_SEND, LOW); digitalWrite(LED_AMBULANCE, LOW); } if (event_send_ready) { event_send_ready = false; stopRecord = true; digitalWrite(LED_SEND, HIGH); // setup payload if (hadAmbulance) { hadAmbulance = false; set_payload(1, 1, 1, 1); } else { set_payload(0, 0, 0, 0); } EltresAddonBoard.set_payload(payload); } // handle command if (startRecord) { startRecord = false; MPLog("Start Recorder\n"); theAudio->startRecorder(); isRecording = true; digitalWrite(LED_RECORD, HIGH); } if (stopRecord) { stopRecord = false; MPLog("Stop Recorder\n"); theAudio->stopRecorder(); isRecording = false; digitalWrite(LED_RECORD, LOW); } // loop logics if (isRecording) { // ... } if (!isRecording) { delay(10); } } ``` ## CLIP Viewer Lite 結果 Eltresで送信したデータが以下になります ![CLIP Viewer Lite 結果](https://camo.elchika.com/ae849aa6d757e397f57e22a27e43b486bb374e7a/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f61323531376133342d323761652d343061392d386432332d3939313964626134643133342f38333238373833612d316632322d346231382d396334352d623130643562613833373031/) 今回は、CLIIP Viewer上のグラフで可視化したく、No.5のペイロードデータフォーマットで16byteを詰めました。 サイレン音を検出したら、気圧、圧力、照度、距離に `1` を、そうでない場合に `0`を BigEndian <--> LittleEndianの変換をして `payload` につめて送信しました。 # 考察 YouTubeのサイレン音は100%に検出できるのですが、実際の救急車のサイレン音の検出率は 3回に1度程度でした。 考察すると、 - 救急車が赤信号の交差点進入で警告を高めるためにサイレン音が半音+ボリュームが上がり、検出が成功する - 交差点青信号で直進する際は、半音上がらず、ボリュームも上がらないため周波数が検出できない 開発がギリギリになってしまい、1.5日しかモニタリングできておらずサンプル数も多く撮れていません。 今後、マイクを窓付近に設置して集音性を高めて継続して計測してみたいと思っています。 # 最後に 今回は在宅ワークで長らく疑問に思っていた1日に救急車のサイレン音を聞く回数を調べる足がかりとして、Spresenseを活用させていただきました。音声処理の肝となるFFTがライブラリとして提供されており、かつとても参考になる参考書もあります。 いくつかスケッチ例を試すだけでも、Spresenseのポテンシャルの高さを感じられました。 今回複数コアを活用したく、サブコアでFFT処理をしていますが、その間メインコアはブロッキングで通知待ちしているだけで並列処理はできていません。 また、Eltresも送信準備(5秒前通知)〜送信完了の10秒程度の間は、割り込みの競合 & 録音バッファオーバーを避けるため録音を停止しサイレン音が検出できない期間があります。これらも工夫すれば詰められますが今回は時間切れとなりました。 今回の記事から、マルチコアでのFFT処理ノウハウ、EltresのNo.5 ペイロード作成 `set_payload(airPressure, pressure, illuminance, distance)` ・ BigEndian <--> LittleEndianの変換 `memcpyLittleEndian(...)` が参考になれば幸いです。 # 全体ソースコード ## Main core ```cpp:Spresense_Eltres_FFT_Multicore_Main/Spresense_Eltres_FFT_Multicore_Main.ino // Spresense #define LED_GNSS PIN_LED0 #define LED_RECORD PIN_LED1 #define LED_SEND PIN_LED2 #define LED_AMBULANCE PIN_LED3 // Multi core #ifdef SUBCORE #error "Core selection is wrong!!" #endif #include <MP.h> #include <MPMutex.h> MPMutex mutex(MP_MUTEX_ID0); int subcore = 1; // use subocre 1 // Audio #include <Audio.h> #include <SDHCI.h> SDClass SD; #define FFT_LEN 1024 AudioClass* theAudio = AudioClass::getInstance(); // global static const uint32_t buffering_time = FFT_LEN*1000/AS_SAMPLINGRATE_48000; static const uint32_t buffer_size = FFT_LEN*sizeof(int16_t); static char buff[buffer_size]; uint32_t read_size; static uint8_t cnt700hz = 0; static uint8_t cnt900hz = 0; // commands bool startRecord = false; bool isRecording = false; bool stopRecord = false; bool terminate = false; bool hadAmbulance = false; // Eltres #include <EltresAddonBoard.h> // eltres events bool event_send_ready = false; bool event_idle = false; bool event_gnss_lost = false; bool event_gnss_receive = false; // Eltres payload uint8_t payload[16]; /** * @brief Eltres Event callback * @param event */ void eltres_event_cb(eltres_board_event event) { switch (event) { case ELTRES_BOARD_EVT_GNSS_TMOUT: // GNSS Timeout MPLog("gnss wait timeout error.\n"); break; case ELTRES_BOARD_EVT_IDLE: // Idle MPLog("waiting sending timings.\n"); event_idle = true; break; case ELTRES_BOARD_EVT_SEND_READY: // Prepare payload MPLog("Shortly before sending, so setup payload if need.\n"); event_send_ready = true; break; case ELTRES_BOARD_EVT_SENDING: MPLog("start sending.\n"); break; case ELTRES_BOARD_EVT_GNSS_UNRECEIVE: // GNSS Lost MPLog("gnss wave has not been received.\n"); event_gnss_lost = true; break; case ELTRES_BOARD_EVT_GNSS_RECEIVE: // GNSS Got signal MPLog("gnss wave has been received.\n"); event_gnss_receive = true; break; case ELTRES_BOARD_EVT_FAULT: // Error MPLog("internal error.\n"); break; } } /** * @brief GGA signal callback * @param gga_info pointer to gga_info */ void gga_event_cb(const eltres_board_gga_info *gga_info) { if (gga_info->m_pos_status) { // gga ready // do nothing } else { // gga not ready MPLog("[gga] invalid data.\n"); } } /** * @brief Set Eltres payload */ void set_payload(float airPressure, float pressure, float illuminance, uint16_t distance) { // init payload memset(payload, 0x00, sizeof(payload)); // set payload type to No.5 payload[0] = 0x85; memcpyLittleEndian(payload + 1, &airPressure, sizeof(airPressure)); memcpyLittleEndian(payload + 5, &pressure, sizeof(pressure)); memcpyLittleEndian(payload + 9, &illuminance, sizeof(illuminance)); memcpyLittleEndian(payload + 13, &distance, sizeof(distance)); // Serial.println("payload content"); // Serial.print(" airPressure "); // Serial.print(airPressure); // Serial.print(", pressure "); // Serial.print(pressure); // Serial.print(", illuminance "); // Serial.print(illuminance); // Serial.print(", distance "); // Serial.print(distance); // Serial.println(); // for(int i=0; i<16; i++) { // Serial.print(" "); // Serial.print(payload[i], HEX); // } // Serial.println(); } // void setup_eltress() { // init Eltress eltres_board_result ret = EltresAddonBoard.begin( ELTRES_BOARD_SEND_MODE_1MIN, eltres_event_cb, gga_event_cb ); if (ret != ELTRES_BOARD_RESULT_OK) { terminate = true; MPLog("cannot start eltres board.\n"); } else { // eltres ready } } // void setup() { pinMode(LED_GNSS, OUTPUT); digitalWrite(LED_GNSS, LOW); pinMode(LED_RECORD, OUTPUT); digitalWrite(LED_RECORD, LOW); pinMode(LED_SEND, OUTPUT); digitalWrite(LED_SEND, LOW); pinMode(LED_AMBULANCE, OUTPUT); digitalWrite(LED_AMBULANCE, LOW); Serial.begin(115200); while (!SD.begin()) { MPLog("Insert SD card"); } MPLog("Init Audio Recorder\n"); theAudio->begin(); theAudio->setRecorderMode(AS_SETRECDR_STS_INPUTDEVICE_MIC); // 16bit RAW PCM format, 48000Hz Mono int err = theAudio->initRecorder(AS_CODECTYPE_PCM, "/mn/sd0/BIN", AS_SAMPLINGRATE_48000, AS_CHANNEL_MONO); if (err != AUDIOLIB_ECODE_OK) { MPLog("RecorderInitialize error\n"); while(1); } MP.begin(subcore); // boot subcore 1 MP.RecvTimeout(MP_RECV_BLOCKING); setup_eltress(); } void loop() { if (terminate) { MPLog("Program terminated.\n"); while(1); } // handle eltas event if (event_gnss_receive) { event_gnss_receive = false; startRecord = true; digitalWrite(LED_GNSS, HIGH); } if (event_gnss_lost) { event_gnss_lost = false; stopRecord = true; digitalWrite(LED_GNSS, LOW); } if (event_idle) { event_idle = false; startRecord = true; hadAmbulance = false; digitalWrite(LED_SEND, LOW); digitalWrite(LED_AMBULANCE, LOW); } if (event_send_ready) { event_send_ready = false; stopRecord = true; digitalWrite(LED_SEND, HIGH); // setup payload if (hadAmbulance) { hadAmbulance = false; set_payload(1, 1, 1, 1); } else { set_payload(0, 0, 0, 0); } EltresAddonBoard.set_payload(payload); } // handle command if (startRecord) { startRecord = false; MPLog("Start Recorder\n"); theAudio->startRecorder(); isRecording = true; digitalWrite(LED_RECORD, HIGH); } if (stopRecord) { stopRecord = false; MPLog("Stop Recorder\n"); theAudio->stopRecorder(); isRecording = false; digitalWrite(LED_RECORD, LOW); } // loop logics if (isRecording) { int ret; do { ret = mutex.Trylock(); } while (ret != 0); ret = theAudio->readFrames(buff, buffer_size, &read_size); if (ret != AUDIOLIB_ECODE_OK && ret != AUDIOLIB_ECODE_INSUFFICIENT_BUFFER_AREA) { MPLog("Error err = %d\n", ret); theAudio->stopRecorder(); while(1); } mutex.Unlock(); if (read_size < buffer_size) { // wait until data ready in FIFO delay(buffering_time); return; } int8_t sndid = 100; int8_t rcvid; uint32_t rcvdata; // send pointer address ret = MP.Send(sndid, &buff, subcore); if (ret < 0) { MPLog("MP.Send error = %d\n", ret); } // receive from Maincore ret = MP.Recv(&rcvid, &rcvdata, subcore); if (ret < 0) return; if (700 < rcvdata && rcvdata < 1000) { cnt700hz = (700 < rcvdata) && (rcvdata < 800) && (cnt700hz < 30) ? cnt700hz + 1 : cnt700hz; cnt900hz = (900 < rcvdata) && (rcvdata < 1000) && (cnt900hz < 30) ? cnt900hz + 1 : cnt900hz; } else { cnt700hz = cnt700hz == 0 ? 0 : cnt700hz - 1; cnt900hz = cnt900hz == 0 ? 0 : cnt900hz - 1; } MPLog("%5d Hz | 700Hz: %2d | 900Hz: %2d | Ambulance %d\n", rcvdata, cnt700hz, cnt900hz, hadAmbulance); int isAmbulance = (cnt700hz + cnt900hz) == 24; if (isAmbulance != 0) { hadAmbulance = true; digitalWrite(LED_AMBULANCE, HIGH); MPLog("isAmbulance! %d\n", isAmbulance); } } if (!isRecording) { delay(10); } } ``` ```cpp:Spresense_Eltres_FFT_Multicore_Main/utils.ino /** * @brief memcopy with Little Endian */ void memcpyLittleEndian(uint8_t *p, void *value, int size) { for(int i=0; i<size; i++) { memcpy(p+i, value + size - i - 1, 1); } } ``` ```sh:Spresense_Eltres_FFT_Multicore_Main/build_run.sh #!/bin/sh export FQBN="SPRESENSE:spresense:spresense:Core=Main,Memory=768,Debug=Disabled,UploadSpeed=115200" export PORT="/dev/cu.usbserial-14140" export SPEED="115200" ../build_run.sh $* ``` ## Sub core ```cpp:Spresense_Eltres_FFT_Multicore_Subcore/Spresense_Eltres_FFT_Multicore_Subcore.ino // Multi core #if (SUBCORE != 1) #error "Core selection is wrong!!" #endif #include <MP.h> #include <MPMutex.h> MPMutex mutex(MP_MUTEX_ID0); // FFT #include <FFT.h> #define AS_SAMPLINGRATE_48000 48000 // https://github.com/sonydevworld/spresense-exported-sdk/blob/master/sdk/modules/include/audio/audio_common_defs.h#L230 #define AS_CHANNEL_MONO 1 // https://github.com/sonydevworld/spresense-exported-sdk/blob/master/sdk/modules/include/audio/audio_common_defs.h#L169 #define FFT_LEN 1024 // initalize FFT with mono, 1024 samples FFTClass<AS_CHANNEL_MONO, FFT_LEN> FFT; void setup() { FFT.begin(WindowHamming, AS_CHANNEL_MONO, (FFT_LEN / 2)); MP.begin(); // notify to main core MP.RecvTimeout(MP_RECV_BLOCKING); } void loop() { static const int ch_index = AS_CHANNEL_MONO-1; static char *buff; static float pDst[FFT_LEN]; int ret; int8_t rcvid; // receive from Maincore ret = MP.Recv(&rcvid, &buff); if (ret < 0) return; // wait Lock do { ret = mutex.Trylock(); } while (ret != 0); // MPLog(" Got lock\n"); FFT.put((q15_t*)buff, FFT_LEN); // exec FFT FFT.get(pDst, ch_index); // collect data from ch0 mutex.Unlock(); avgFilter(pDst); float maxValue; float peakFs = get_peak_frequency(pDst, &maxValue); // MPLog("peak freq: %f Hz\n", peakFs); // MPLog("Spectrum : %f", maxValue); uint32_t snddata = (uint32_t)(peakFs); ret = MP.Send(++rcvid, snddata); if (ret < 0) { MPLog("MP.Send error = %d\n", ret); } } ``` ```cpp:Spresense_Eltres_FFT_Multicore_Subcore/Spresense_Eltres_FFT_Multicore_Subcore.ino #define AVG_FILTER (8) void avgFilter(float dst[FFT_LEN]) { static float pAvg[AVG_FILTER][FFT_LEN/2]; static int g_counter = 0; if (g_counter == AVG_FILTER) g_counter = 0; for(int i=0; i<FFT_LEN/2; ++i) { pAvg[g_counter][i] = dst[i]; float sum = 0; for(int j=0; j < AVG_FILTER; ++j) { sum += pAvg[j][i]; } dst[i] = sum / AVG_FILTER; } ++g_counter; } float get_peak_frequency(float *pData, float *maxValue) { uint32_t idx; float delta, delta_spr; float peakFs; const float delta_f = AS_SAMPLINGRATE_48000 / FFT_LEN; arm_max_f32(pData, FFT_LEN/2, maxValue, &idx); if (idx < 1) return 0.0; // get peak frequency delta = 0.5 * (pData[idx-1] - pData[idx+1]) / (pData[idx-1] + pData[idx+1] - 2.0 * pData[idx]); peakFs = (idx + delta) * delta_f; // スペクトルの最大値の近似値を算出 delta_spr = 0.125 * (pData[idx-1] - pData[idx+1]) * (pData[idx-1] - pData[idx+1]) / (2.0*pData[idx] - (pData[idx-1] + pData[idx+1])); *maxValue += delta_spr; return peakFs; } ``` ```sh:Spresense_Eltres_FFT_Multicore_Subcore/build_run.sh #!/bin/sh export FQBN="SPRESENSE:spresense:spresense:Core=Main,Memory=768,Debug=Disabled,UploadSpeed=115200" export PORT="/dev/cu.usbserial-14140" export SPEED="115200" ../build_run.sh $* ``` ## other ```sh:build_run.sh #!/bin/sh #################### # arduino-cli util # # usage ./build_run.sh -b -u -m # - -b <path> : build path (default path ".") # - -u <path> : upload path (default path ".") # - -m : serial monitor #################### # FQBN="SPRESENSE:spresense:spresense:Core=Main,Memory=768,Debug=Disabled,UploadSpeed=115200" # PORT="/dev/cu.usbserial-14140" # SPEED="115200" # @ref https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash while [[ $# -gt 0 ]]; do case $1 in -b|--build|-c|--compile) if [[ $2 == -* ]] then BUILDPATH="." shift # past argument else BUILDPATH="${2:-.}" # default as . shift # past argument shift # past value fi echo "--BUILD: $BUILDPATH" arduino-cli compile --fqbn=$FQBN $BUILDPATH || { exit 1; } ;; -u|--upload) if [[ $2 == -* ]] then UPLOADPATH="." shift # past argument else UPLOADPATH="${2:-.}" # default as . shift # past argument shift # past value fi echo "--UPLOAD: $UPLOADPATH" arduino-cli upload --fqbn=$FQBN -p $PORT $UPLOADPATH ;; -m|--monitor) shift # past argument arduino-cli monitor -p $PORT -c baudrate=$SPEED ;; -*|--*) echo "Unknown option $1" exit 1 ;; *) POSITIONAL_ARGS+=("$1") # save positional arg shift # past argument ;; esac done ``` # 参考文献 1. SPRESENSEではじめるローパワーエッジAI - O'Reilly Japan https://www.oreilly.co.jp/books/9784873119670/ 2. 引用: "90dB以上120dB以下という音圧レベルは、およそ列車が通過する時の高架下に相当しており、この環境下では殆ど会話が不可能になるという数値です" https://www.patlite.co.jp/support/faq/detail00342.html 3. dsPIC マイコンを用いた救急車のサイレン音の検出 https://www.jstage.jst.go.jp/article/jjiiae/2/1/2_11/_pdf 4. 5PCS PJ392 Stereo Female Sockect Jack 3.5 Audio Headphone Connector - AliExpress https://www.aliexpress.com/item/32834368373.html 5. SPRESENSE用ELTRESアドオンボード https://www.switch-science.com/products/7580 6. ELTRESアドオンボード_取扱説明書_20220501 https://doc.switch-science.com/media/files/e66da46d-7b26-4d07-8186-2f2181e433cd.pdf 7. 周波数ジェネレータ - Google Play ストア https://play.google.com/store/apps/details?id=com.boedec.hoel.frequencygenerator&hl=ja&gl=US 8. 救急車サイレン - YouTube https://www.youtube.com/watch?v=Suq-yk3kpao 9. ELTRESアドオンボード用ライブラリ_v1.1.x_説明書(ArduinoIDE版).pdf (CLIP Viewer Liteログイン後ページ > コンテンツ ページ内) 10. ペイロードデータフォーマット仕様説明書_Ver1.6.x.pdf (CLIP Viewer Liteログイン後ページ > コンテンツ ページ内)