モチベーション
この度、 2022年 SPRESENSE 活用コンテスト
に参加して、モニター提供を頂きました。
在宅ワークも当たり前となり、Web会議中に救急車がそばを通過してサイレン音で発言を中断することがしばしばあります。
大通りに面していることから、近距離で多く通っているんだろうとも予想していました。
そのため、一体1日で何回この救急車のサイレン音を聞いているんだろうか、ということが長らく疑問にありました。
今回、SpresenseをArduinoとして用いてマイクから録音し、サブコアを用いて音声処理(FFT)を行い、Eltresのネットワークを経由してCLIP Viewer Liteで可視化しました。
--
今回 Spresense学習にあたり、 SPRESENSEではじめるローパワーエッジAI
の電子書籍を購入しました。
https://www.oreilly.co.jp/books/9784873119670/
ハードウェア編
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チャネル
という並びになっています。
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の空き箱がちょうどよく挟んで立てられました。
ソフトウェア編
(全体的なソースコードは記事の末尾に記載しています)
まず、今回実装したコードの全体の動作フローを記載します
音声処理
救急車のサイレン音の検出の肝となるFFT処理は、Spresense用のライブラリを使用しました。また実装としても書籍から学習させてもらった内容におんぶにだっこされています。
同書籍にはSpresenseのマルチコアプログラミングの例もあり、あわせ技として今回はメインコアで録音し、サブコアでFFT処理をしました。
(音声録音はメインコアのみで実行可能でした)
特記事項として、Subcore では Audio.h
を include することができないため、必要な定数を再定義しています
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);
}
}
// ...
}
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);
}
}
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回ぐらいでした。
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っぽくフラグに応じて動作させました。
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で送信したデータが以下になります
今回は、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
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);
}
}
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);
}
}
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
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);
}
}
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;
}
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
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
参考文献
-
SPRESENSEではじめるローパワーエッジAI - O'Reilly Japan
https://www.oreilly.co.jp/books/9784873119670/ -
引用: "90dB以上120dB以下という音圧レベルは、およそ列車が通過する時の高架下に相当しており、この環境下では殆ど会話が不可能になるという数値です"
https://www.patlite.co.jp/support/faq/detail00342.html -
dsPIC マイコンを用いた救急車のサイレン音の検出
https://www.jstage.jst.go.jp/article/jjiiae/2/1/2_11/_pdf -
5PCS PJ392 Stereo Female Sockect Jack 3.5 Audio Headphone Connector - AliExpress
https://www.aliexpress.com/item/32834368373.html -
SPRESENSE用ELTRESアドオンボード
https://www.switch-science.com/products/7580 -
ELTRESアドオンボード_取扱説明書_20220501
https://doc.switch-science.com/media/files/e66da46d-7b26-4d07-8186-2f2181e433cd.pdf -
周波数ジェネレータ - 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 -
ELTRESアドオンボード用ライブラリ_v1.1.x_説明書(ArduinoIDE版).pdf
(CLIP Viewer Liteログイン後ページ > コンテンツ ページ内) -
ペイロードデータフォーマット仕様説明書_Ver1.6.x.pdf
(CLIP Viewer Liteログイン後ページ > コンテンツ ページ内)
投稿者の人気記事
-
mandbjp
さんが
2022/09/25
に
編集
をしました。
(メッセージ: 初版)
-
mandbjp
さんが
2022/09/25
に
編集
をしました。
(メッセージ: 初版)
-
mandbjp
さんが
2022/09/25
に
編集
をしました。
(メッセージ: fix header)
-
mandbjp
さんが
2022/09/25
に
編集
をしました。
(メッセージ: fix heading)
-
mandbjp
さんが
2022/09/25
に
編集
をしました。
(メッセージ: publish)
ログインしてコメントを投稿する