ESP32マイクとリアルタイムスペクトログラム

声を「見ながら」合わせる。
ESP32マイク × Androidリアルタイムスペクトログラム

INMP441マイクを載せたESP32-S3で音声を取り込み、USB-CDC経由でAndroidに16kHz・16bit PCMをストリーミング。 スマホ側ではJetpack ComposeのCanvasで、0〜800Hzのスペクトログラムをリアルタイム描画します。
将来的には、お手本の歌声と自分の声のスペクトログラムを並べ、「どの周波数帯が足りないか・出し過ぎか」を視覚的にフィードバックするボーカルトレーニングツールを目指しています。:contentReference[oaicite:0]{index=0}

作品の説明

ただ録音して聞き返すだけでは、「何が違うのか」がわかりにくい。 そこで、声の周波数成分をリアルタイムに「絵」として見せて、 目で見ながら音域や発声を調整できる仕組みを作りました。

ESP32-S3側ではI2SでINMP441マイクから24bit左詰めの音声を取得し、 DCカット用の一次IIRハイパスフィルタをかけてから16bit PCMに変換。 このPCMをUSB-CDCシリアル経由でAndroidスマホに送り、 スマホ側でSTFT(短時間フーリエ変換)+dBスケールに変換して、 Canvas上に時系列スペクトログラムとして表示します。

  • ① ESP32-S3 + INMP441で16kHzサンプリング
  • ② 32bit → 16bit変換&DCカット(HPF)
  • ③ USB-CDCでAndroidにPCMストリーミング
  • ④ AndroidでSTFT(FFTサイズ256, HOP 64)
  • ⑤ 0〜800HzをCanvas上にリアルタイム描画
  • ⑥ mp3/m4aから「お手本」のスペクトログラムも算出
  • ⑦ 将来的に「お手本との差」を色で可視化して歌練習に活用

システム概要

リアルタイムスペクトログラムシステム構成図

左側にINMP441マイク搭載のESP32-S3ボード、右側にAndroidスマホ。 ESP32はI2Sでマイクから音声を取得し、16kHz・16bitモノラルのPCMとしてUSB-CDC経由で送信します。 AndroidアプリはUSBデバイスとしてESP32を認識し、取得したPCMをそのままSTFTに流し込んでスペクトログラムを描画します。:contentReference[oaicite:1]{index=1}

技術スタック

  • Embedded: ESP32-S3, INMP441, I2S, USB-CDC (Arduino core / ESP-IDF)
  • Android: Kotlin, Jetpack Compose, USB Host API, Coroutine (IOスレッドでUSB読み取り)
  • Audio / DSP: STFT, Hann窓, 16kHz/FFT=256, 0〜800HzのdBスケール表示
  • Media: MediaExtractor + MediaCodec でmp3/m4aをPCM16にデコード

主要コンポーネント

  • ESP32-S3 I2S入力コード: INMP441から24bit左詰めのサンプルを受信し、32bit → 16bit変換と一次IIRハイパスフィルタでDC成分を除去してからUSBに送信。
  • USB受信&STFTモジュール: Android側でCDCデバイスを検出し、bulkTransferでPCMを取得。フレームバッファに詰めてHann窓をかけ、FFTを実行。
  • スペクトログラム描画Canvas: 周波数軸×時間軸のグリッドに対して、各セルに対応するdB値を色にマッピングして描画。最新フレームが常に下に流れるUI。
  • 参照音声デコーダ: MediaExtractor/MediaCodecでmp3・m4aをPCM16に変換し、同じSTFT条件で「お手本スペクトログラム」を生成。

技術解説

EMBEDDED / I2S

ESP32-S3 + INMP441による16kHzサンプリング

フリーノーブESP32-S3 Liteボード上で、INMP441マイクをI2S接続しています。 INMP441は24bit左詰めで出力するため、ESP32側では32bit枠でサンプルを受信し、 右シフト(14ビット)によって16bitにスケーリング。 同時に、y[n] = a ( y[n-1] + x[n] - x[n-1] ) という一次IIRハイパスを実装し、 20Hz付近を目安にDC成分と超低域をカットすることで、メーターのふらつきを抑えています。

I2S: 16 kHz / 32bit / MONO, HPF_A = 0.995f(擬似的な20Hzカット) 出力フォーマット: リトルエンディアン16bit PCM
ANDROID / STFT

USB-CDC経由で受信し、その場でSTFT

Android側ではUSB Host APIでCDCデバイスを検出し、 Bulk INエンドポイントからバイト列を取得します。取得したデータをByteBufferShortArrayに変換し、 STFT用フレームバッファに順次格納。
フレーム長は256サンプル、ホップは64サンプル(75%オーバーラップ)でHann窓をかけた後、 自前実装のRadix-2 FFTで周波数領域に変換しています。:contentReference[oaicite:2]{index=2}

WIN = 256, HOP = 64, FFT_SIZE = 256, 表示帯域 = 0〜800 Hz(約13binを使用)
UI / CANVAS

「下から上に流れる」リアルタイムスペクトログラム

スペクトログラムは、縦軸=時間、横軸=周波数という形でCanvasに描画しています。 新しいフレームが来るたびに、既存の行を1つずつ上にシフトし、 一番下の行に最新フレームを書き込むことで、 常に「画面下がいま鳴っている音」になるようにしました。

userSpec[f][t]: f=0..FREQ_BINS-1(周波数)、t=0..TIME_STEPS-1(時間) t = TIME_STEPS-1 が最新版行、0が最古行
REFERENCE / MEDIA

mp3/m4aをPCM16にデコードして「お手本スペクトログラム」を生成

参照用の「お手本音声」は、MediaExtractorで音声トラックを選択し、MediaCodecでPCM16にデコードしています。 チャンネルが複数ある場合は単純平均してモノラル化し、 その後16kHzに線形補間でリサンプリング。 リアルタイム処理と同じ条件でSTFTを行い、refSpecとして2次元配列に格納します。

Decode → Mono → Resample(→16kHz) → STFT → refSpec[f][t] に保存 今後は refSpec - userSpec で差分カラー表示に発展予定。

実験・結果・課題

リアルタイム描画の動作確認

ホワイトノイズ、正弦波(100Hz, 300Hz, 600Hz)などをESP32側から入力し、 Android上のスペクトログラムに期待通りの帯が表示されるかを確認しました。

正弦波入力のスペクトログラム
(a) 正弦波入力(単一周波数の横線)
声のスペクトログラム
(b) 声を出したときのスペクトログラム

現在の課題

現状では、リアルタイムのスペクトログラムとレベルメータの表示は安定しており、 「音がどれくらい出ているか」を視覚的に確認できます。
一方で、「お手本スペクトログラムとの差分」を色で重ねるUIはまだ実装途中であり、 比較は頭の中で行う必要があります。

参照音声との差分表示UIは未実装 音程検出(F0推定)との連携を検討中

動画リンク

ESP32のマイクに向かって声を出し、そのスペクトログラムがスマホ上でリアルタイムに立ち上がる様子を撮影したデモ動画です。

マイク入力からスペクトログラム描画までの一連の流れ

まだできていない部分と今後

「お手本」と「自分」のギャップを色で教えてくれる先生に

目標は、好きな歌手やVtuberの音声を読み込んで、 「今の自分の声は、どの周波数帯が足りないか/出し過ぎか」をリアルタイムで色分けして教えてくれるアプリにすることです。

  • 短期: refSpecとuserSpecの差分を、黄色(足りない)・青(出し過ぎ)で可視化するUIの実装
  • 中期: F0(基本周波数)推定を組み合わせ、音程のズレも可視化
  • 長期: 区間ごとのフィードバックや、自動的に練習フレーズを提案する「歌練習支援アプリ」へ発展

これからの改善点と開発計画

ハード・DSP・UI・学習体験の4つの軸で改善していきます。

  • ハード: マイク固定具の改善、ノイズ源(ファン・キーボード)からの分離
  • DSP: ノイズゲートや簡易帯域フィルタの追加、F0推定アルゴリズムの実装
  • UI: 差分スペクトログラムのわかりやすいカラーマップ設計、記録・再生機能
  • 学習体験: 練習ログの保存、上達の可視化グラフなど
17
18
19
20
21
22
23
24
25
26
27
28
29
30
差分スペクトログラムUIの実装
F0推定(基本周波数トラッキング)
練習ログ保存&再生UI

このプロジェクトを通して

感じていること

マイコンとAndroidをつないでリアルタイム処理をするのは、 想像以上に「地味な不具合」との戦いでした。 USBのエンドポイントが見つからなかったり、I2Sのビットシフトを1つ間違えただけで、 スペクトログラムが真っ黒になってしまう。
その一方で、初めて自分の声がスペクトログラムとしてスマホに流れた瞬間は、 単純に「うわ、楽しい」と思いました。

今の自分へのメモ

  • ・まずは「音程・リズム・音色」のうち、どこを可視化したいのか焦点を絞ること。
  • ・録音データとスペクトログラム・コード片を必ず一緒に残しておくこと。

リファレンス & リンク

リファレンス

  • ESP32 Technical Reference Manual (I2S / USB Serial)
  • INMP441 Datasheet
  • Android Developers – USB Host & Accessory
  • Android Developers – MediaCodec / MediaExtractor
  • 信号処理の基礎(STFT / 窓関数 / dBスケール)

リンク