こちらの記事の続きでマイクなどの検証をしたコードと動作した様子を紹介していきます。
マイク検証 - 音を拾ってビジュアライズ
最初全く動いてなかった感じがしたけどマイクで音を拾ってビジュアライズすると動作がわかって良い感じでした。
#include <M5Unified.h>
#include <Wire.h>
#include <M5EchoBase.h>
// --- AtomS3 + EchoBase Pin Mapping ---
// AtomS3はESP32-S3ベースのため、従来のATOM Matrix/LiteとGPIO番号が異なります。
// EchoBaseの機能に割り当てられたピンをS3用にマッピングします。
static constexpr const uint8_t PIN_I2S_BCLK = 8; // Bit Clock
static constexpr const uint8_t PIN_I2S_WS = 6; // Word Select (LRCK)
static constexpr const uint8_t PIN_I2S_DOUT = 5; // Data Out (Speaker)
static constexpr const uint8_t PIN_I2S_DIN = 7; // Data In (Mic)
static constexpr const uint8_t PIN_I2C_SDA = 38;
static constexpr const uint8_t PIN_I2C_SCL = 39;
M5EchoBase echobase(I2S_NUM_0);
void setup() {
auto cfg = M5.config();
// AtomS3の内部スピーカーを使わないように設定(EchoBaseを使うため)
// ※M5Unifiedのバージョンによっては、外部I2S設定時に自動無効化されますが念のため。
cfg.internal_spk = false;
cfg.internal_mic = false;
M5.begin(cfg);
// EchoBaseのオーディオCODEC(ES8311)初期化用I2C
Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
// EchoBaseのES8311を初期化(公式サンプル準拠)
echobase.init(16000, PIN_I2C_SDA, PIN_I2C_SCL, PIN_I2S_DIN, PIN_I2S_WS, PIN_I2S_DOUT, PIN_I2S_BCLK, Wire);
echobase.setSpeakerVolume(70);
// --- Speaker (EchoBase NS4168) Config ---
{
auto spk_cfg = M5.Speaker.config();
spk_cfg.pin_bck = PIN_I2S_BCLK;
spk_cfg.pin_ws = PIN_I2S_WS;
spk_cfg.pin_data_out = PIN_I2S_DOUT;
spk_cfg.i2s_port = I2S_NUM_0; // S3はI2S_NUM_0推奨
spk_cfg.sample_rate = 16000; // サンプリングレート
M5.Speaker.config(spk_cfg);
}
// スピーカーの初期化開始(マイクは使わない)
M5.Speaker.begin();
// 音量設定 (0-255) ハウリング注意!
M5.Speaker.setVolume(128);
// --- Mic (EchoBase SPM1423) Config ---
{
auto mic_cfg = M5.Mic.config();
mic_cfg.pin_bck = PIN_I2S_BCLK;
mic_cfg.pin_ws = PIN_I2S_WS;
mic_cfg.pin_data_in = PIN_I2S_DIN;
mic_cfg.i2s_port = I2S_NUM_0;
mic_cfg.stereo = false;
mic_cfg.use_adc = false;
M5.Mic.config(mic_cfg);
}
M5.Mic.begin();
M5.Display.setTextSize(2);
M5.Display.setCursor(0, 0);
M5.Display.println("Hold Btn");
M5.Display.println("Mic Test");
}
void loop() {
M5.update();
// ボタンを押している間だけマイクレベルを表示
if (M5.BtnA.isHolding() || M5.BtnA.isPressed()) {
int16_t sound_data[256];
if (M5.Mic.record(sound_data, 256, 16000)) {
int32_t peak = 0;
for (int i = 0; i < 256; ++i) {
int32_t v = sound_data[i];
if (v < 0) v = -v;
if (v > peak) peak = v;
}
// 0..32767 を 0..60 にスケール
int32_t radius = (peak * 60) / 32767;
M5.Display.fillScreen(BLACK);
M5.Display.drawCircle(64, 64, 60, DARKGREY);
M5.Display.fillCircle(64, 64, radius, GREEN);
}
} else {
M5.Display.fillScreen(BLACK);
M5.Display.setTextSize(2);
M5.Display.setCursor(0, 0);
M5.Display.println("Hold Btn");
M5.Display.println("Mic Test");
}
}
録音 => 再生
マイクとスピーカーを動作させることができたので、録音と再生を試してみます。
周囲の音を拾って再生してくれますが、長い秒数だとメモリが足りなくなるので数秒です。
(ドラクエの戦闘曲もっと聞きたい)
#include <M5Unified.h>
#include <Wire.h>
#include <M5EchoBase.h>
#include <esp_heap_caps.h>
// --- AtomS3 + EchoBase Pin Mapping ---
// AtomS3はESP32-S3ベースのため、従来のATOM Matrix/LiteとGPIO番号が異なります。
// EchoBaseの機能に割り当てられたピンをS3用にマッピングします。
static constexpr const uint8_t PIN_I2S_BCLK = 8; // Bit Clock
static constexpr const uint8_t PIN_I2S_WS = 6; // Word Select (LRCK)
static constexpr const uint8_t PIN_I2S_DOUT = 5; // Data Out (Speaker)
static constexpr const uint8_t PIN_I2S_DIN = 7; // Data In (Mic)
static constexpr const uint8_t PIN_I2C_SDA = 38;
static constexpr const uint8_t PIN_I2C_SCL = 39;
M5EchoBase echobase(I2S_NUM_0);
static constexpr uint32_t SAMPLE_RATE = 8000;
static constexpr uint32_t RECORD_SECONDS = 8; //8秒録音
// 16kHz * 16bit * stereo(2ch) * seconds
static constexpr size_t RECORD_SIZE = SAMPLE_RATE * 2 * 2 * RECORD_SECONDS;
static uint8_t *record_buffer = nullptr;
void setup() {
auto cfg = M5.config();
// AtomS3の内部スピーカーを使わないように設定(EchoBaseを使うため)
// ※M5Unifiedのバージョンによっては、外部I2S設定時に自動無効化されますが念のため。
cfg.internal_spk = false;
cfg.internal_mic = false;
M5.begin(cfg);
// EchoBaseのオーディオCODEC(ES8311)初期化用I2C
Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
// EchoBaseのES8311を初期化(公式サンプル準拠)
echobase.init(SAMPLE_RATE, PIN_I2C_SDA, PIN_I2C_SCL, PIN_I2S_DIN, PIN_I2S_WS, PIN_I2S_DOUT, PIN_I2S_BCLK, Wire);
echobase.setSpeakerVolume(70);
// M5UnifiedのSpeaker/Micは使わず、EchoBaseのI2Sドライバのみを使う
M5.Display.setTextSize(2);
M5.Display.setCursor(0, 0);
M5.Display.println("Press Btn");
M5.Display.println("Rec 8s @8k");
record_buffer = static_cast<uint8_t *>(heap_caps_malloc(RECORD_SIZE, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
if (record_buffer == nullptr) {
record_buffer = static_cast<uint8_t *>(malloc(RECORD_SIZE));
}
if (record_buffer == nullptr) {
M5.Display.println("No Mem");
while (true) {
delay(1000);
}
}
}
void loop() {
M5.update();
static bool busy = false;
if (busy) {
return;
}
// ボタン押下で「8秒録音 → 再生」を1回実行
if (M5.BtnA.wasPressed()) {
busy = true;
M5.Display.fillScreen(BLACK);
M5.Display.setCursor(0, 0);
M5.Display.printf("Recording %us\n", RECORD_SECONDS);
echobase.setMute(true);
delay(10);
echobase.record(record_buffer, RECORD_SIZE);
delay(100);
M5.Display.fillScreen(BLACK);
M5.Display.setCursor(0, 0);
M5.Display.println("Playing...");
echobase.setMute(false);
delay(10);
echobase.play(record_buffer, RECORD_SIZE);
delay(100);
M5.Display.fillScreen(BLACK);
M5.Display.setCursor(0, 0);
M5.Display.println("Press Btn");
M5.Display.println("Rec 8s @8k");
busy = false;
}
}
指パッチン判定
マイク音量を取って閾値を超えたタイミングが一定フレーム内で2回あったら反応する。を作ってみました。
#include <M5Unified.h>
#include <Wire.h>
#include <M5EchoBase.h>
// AtomS3 + EchoBase pin map
static constexpr uint8_t PIN_I2S_BCLK = 8;
static constexpr uint8_t PIN_I2S_WS = 6;
static constexpr uint8_t PIN_I2S_DOUT = 5;
static constexpr uint8_t PIN_I2S_DIN = 7;
static constexpr uint8_t PIN_I2C_SDA = 38;
static constexpr uint8_t PIN_I2C_SCL = 39;
M5EchoBase echobase(I2S_NUM_0);
static constexpr uint32_t SAMPLE_RATE = 16000;
static constexpr size_t FRAME_SAMPLES = 256;
static constexpr size_t FRAME_BYTES = FRAME_SAMPLES * 2 * 2; // stereo 16bit
static constexpr int32_t CLAP_THRESHOLD = 9600; // 80%ライン相当(12000基準)
static constexpr uint32_t DOUBLE_WINDOW_MS = 800;
static constexpr uint32_t COOLDOWN_MS = 1500;
static constexpr uint32_t HOLD_MS = 500;
static constexpr uint8_t ABOVE_FRAMES = 2; //判定用連続超えフレーム数
static constexpr uint8_t BELOW_FRAMES = 2; //判定用連続下回りフレーム数
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
Serial.begin(115200);
randomSeed(millis());
M5.Display.setTextSize(2);
M5.Display.setCursor(0, 0);
M5.Display.println("Clap Debug");
M5.Display.println("See Serial");
Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
echobase.init(SAMPLE_RATE, PIN_I2C_SDA, PIN_I2C_SCL, PIN_I2S_DIN, PIN_I2S_WS, PIN_I2S_DOUT, PIN_I2S_BCLK, Wire);
echobase.setMicGain(ES8311_MIC_GAIN_6DB);
}
void loop() {
M5.update();
int16_t samples[FRAME_BYTES / 2];
static uint32_t last_trigger_ms = 0;
static uint32_t hold_until_ms = 0;
static uint32_t flash_until_ms = 0;
static uint16_t flash_color = 0;
static uint32_t last_log_ms = 0;
static uint32_t last_above_ms = 0;
static uint32_t prev_above_ms = 0;
static uint8_t above_count = 0;
static uint8_t below_count = 0;
static bool above_latched = false;
bool above_event = false;
const uint32_t now = millis();
if (echobase.record(reinterpret_cast<uint8_t *>(samples), FRAME_BYTES)) {
int32_t peak = 0;
for (size_t i = 0; i < FRAME_SAMPLES; ++i) {
int32_t v = samples[i * 2]; // left ch
if (v < 0) v = -v;
if (v > peak) peak = v;
}
if (now - last_log_ms >= 200) {
last_log_ms = now;
Serial.printf("peak,%ld\n", static_cast<long>(peak));
}
// ボリュームバー表示
const int32_t bar_max = 120;
int32_t bar = (peak * bar_max) / 12000;
if (bar > bar_max) bar = bar_max;
if (bar < 0) bar = 0;
M5.Display.fillRect(0, 50, 128, 30, BLACK);
M5.Display.drawRect(4, 60, 120, 12, DARKGREY);
M5.Display.fillRect(4, 60, bar, 12, GREEN);
// 閾値ライン表示
int32_t thr = (CLAP_THRESHOLD * bar_max) / 12000;
if (thr < 0) thr = 0;
if (thr > bar_max) thr = bar_max;
M5.Display.drawFastVLine(4 + thr, 60, 12, RED);
if (flash_until_ms != 0 && now < flash_until_ms) {
M5.Display.fillScreen(flash_color);
} else {
if (flash_until_ms != 0 && now >= flash_until_ms) {
flash_until_ms = 0;
}
M5.Display.setCursor(0, 0);
M5.Display.fillRect(0, 0, 128, 40, BLACK);
if (hold_until_ms != 0 && now < hold_until_ms) {
M5.Display.println("double_clap!");
} else {
if (hold_until_ms != 0 && now >= hold_until_ms) {
hold_until_ms = 0;
}
M5.Display.printf("Peak:%ld\n", static_cast<long>(peak));
}
}
// 閾値を一定フレーム以上超えたら「超えイベント」扱い
if (peak > CLAP_THRESHOLD) {
above_count++;
below_count = 0;
} else {
below_count++;
if (below_count >= BELOW_FRAMES) {
above_count = 0;
above_latched = false;
}
}
if (!above_latched && above_count >= ABOVE_FRAMES) {
above_latched = true;
prev_above_ms = last_above_ms;
last_above_ms = now;
above_event = true;
}
// 一定フレーム期間内に2回超えたらダブルクラップ(新しい超えイベント時のみ判定)
if (above_event && last_above_ms != 0 && prev_above_ms != 0) {
const uint32_t gap = last_above_ms - prev_above_ms;
if (gap <= DOUBLE_WINDOW_MS && (now - last_trigger_ms) > COOLDOWN_MS) {
last_trigger_ms = now;
Serial.println("double_clap");
hold_until_ms = now + HOLD_MS;
flash_until_ms = now + HOLD_MS;
flash_color = M5.Display.color565(random(0, 256), random(0, 256), random(0, 256));
// 連続誤発火防止で履歴をリセット
prev_above_ms = 0;
last_above_ms = 0;
}
}
} else {
Serial.println("record_failed");
delay(200);
}
}
指パッチン判定苦労したポイント
最初は「閾値を2回超えたらダブル」と単純にやったのですが、
音の揺らぎだけでダブル判定が連発してしまって失敗しました。
そこで「フレーム幅」を入れて、一定フレーム連続で超えたら1回カウントに変更。
これで「1フレームだけ超えるノイズ」を切ることができました。
static constexpr uint8_t ABOVE_FRAMES = 2;
static constexpr uint8_t BELOW_FRAMES = 2;
「超えた」判定も一度だけカウントされるように above_latched を使っています。
これを入れないと、同じ音で連続カウントされて誤検知が増えました。
if (!above_latched && above_count >= ABOVE_FRAMES) {
above_latched = true;
prev_above_ms = last_above_ms;
last_above_ms = now;
above_event = true;
}
もう1つ効いたのが 可視化。
ピーク値とバー、閾値ラインを出すことで「どのくらいで超えるか」を体感できて調整しやすくなりました。
int32_t thr = (CLAP_THRESHOLD * bar_max) / 12000;
M5.Display.drawFastVLine(4 + thr, 60, 12, RED);
最後に、ダブル判定が決まったらランダム色フラッシュ。
デモ映えと「判定できた」の確認が一気に楽になりました。
flash_color = M5.Display.color565(random(0, 256), random(0, 256), random(0, 256));
flash_until_ms = now + HOLD_MS;
USBシリアルでの注意
今回のデバッグで一番ハマったのがUSBシリアルでした。
PlatformIOでモニタが出ない=コードの問題とは限らないので注意です。
-
ポート固定は外したほうが良い
-
/dev/cu.usbmodemXXXは再接続で名前が変わりやすい - 固定すると「No such file」で詰まることがある
-
-
USB CDC が必要だった
-
build_flagsで以下を有効にしないとSerialが出ない環境があった
-
build_flags =
-D ARDUINO_USB_CDC_ON_BOOT=1
-D ARDUINO_USB_MODE=1
-
Monitor のポートが合っているか確認
-
platformio device monitorが拾っているポートと
実際の書き込み先がずれていると何も出ない
-
コード置き場
とりあえずここにシンプルなサンプルをまとめてます。
補足:今回やってよかったこと(メモ)
- 音量を可視化して閾値の位置が「見える」ようにした
- 連続フレーム判定を入れて「一瞬の揺らぎ」を切った
- デモ映えはランダムフラッシュが強い
終わりに
やれること増えてきたので家の中で使える仕組みとかに昇華させたいですね。
指パッチン判定は結構調整大変でしたけど精度よくできて達成感があります。
@haraxcraft さんや @Takaaki_Ono さんの実装をまねして音と物体検知みたいなあたりに応用してみたい。
https://qiita.com/haraxcraft/items/ffc38832f0c7f3bedbc5
https://qiita.com/Takaaki_Ono/items/1b8959ddc37ab5b95f5d