初めに
Brilliant Labs の Frame シリーズの続きです。前回までで OLED 表示と写真撮影をやったので、今回はマイクから音声を録って、Mac に WAV として保存してみます。
Frame のマイクは ICS-41351 という MEMS マイクで、サンプリングレート 8kHz / 16kHz、ビット深度 8bit / 16bit から選べます。frame-msg のデフォルトは 8kHz / 8bit で、音質的にはかなり荒いですが、転送量が小さいので BLE 経由でリアルタイム送信できる、というバランスになっています。
開発環境
前回と同じです。
- macOS
- Python 3.12(uv 管理)
- frame-ble 1.1.1 / frame-msg 5.2.1
仕組み
写真の時と同じく、音声もホスト側の Python と Frame デバイス側の Lua アプリのセットで動かします。
- ホストから標準 Lua ライブラリ (
data,code,audio) とaudio_frame_app.luaを Frame に転送 - ホストから
TxCode(value=1)を送って録音開始 - Frame は
audio.start()で MIC をオンにして、サンプルを BLE 経由で連続送信 - ホスト側の
RxAudioがキューに音声データを溜める - ホストから
TxCode(value=0)を送って録音停止 - キューから取り出して、
RxAudio.to_wav_bytes()で WAV ヘッダ付きのバイト列に変換して保存
audio_frame_app.lua は公式の frame_examples_python(BSD-3-Clause)の frame_msg/lua/audio_frame_app.lua をそのまま samples/lua/ に置きました。
ホスト側のコード
samples/record_audio.py として置いています。録音した WAV をそのまま afplay で再生するところまで含めています。
"""Frameのマイクで5秒録音してWAVに保存し、afplayで再生するサンプル.""" from __future__ import annotations import asyncio import subprocess from datetime import datetime from pathlib import Path from frame_msg import FrameMsg, RxAudio, TxCode LUA_APP = Path(__file__).parent / "lua" / "audio_frame_app.lua" CAPTURES_DIR = Path(__file__).resolve().parent.parent / "captures" AUDIO_SUBS_MSG = 0x30 RECORD_SECONDS = 5.0 async def main() -> None: CAPTURES_DIR.mkdir(parents=True, exist_ok=True) frame = FrameMsg() try: await frame.connect() await frame.print_short_text("Loading...") await frame.upload_stdlua_libs(lib_names=["data", "code", "audio"]) await frame.upload_frame_app(local_filename=str(LUA_APP)) frame.attach_print_response_handler() await frame.start_frame_app() rx_audio = RxAudio() audio_queue = await rx_audio.attach(frame) print(f"Recording for {RECORD_SECONDS} seconds...") await frame.send_message(AUDIO_SUBS_MSG, TxCode(value=1).pack()) await asyncio.sleep(RECORD_SECONDS) await frame.send_message(AUDIO_SUBS_MSG, TxCode(value=0).pack()) audio_samples = await asyncio.wait_for(audio_queue.get(), timeout=15.0) wav_bytes = RxAudio.to_wav_bytes(audio_samples) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") out_path = CAPTURES_DIR / f"audio_{timestamp}.wav" out_path.write_bytes(wav_bytes) print(f"Saved {len(wav_bytes)} bytes to {out_path}") rx_audio.detach(frame) frame.detach_print_response_handler() await frame.stop_frame_app() except Exception as e: print(f"An error occurred: {e}") return finally: await frame.disconnect() print(f"Playing back via afplay: {out_path}") subprocess.run(["afplay", str(out_path)], check=False) if __name__ == "__main__": asyncio.run(main())
ポイントは以下の通りです。
upload_stdlua_libs(['data', 'code', 'audio'])で標準 Lua ライブラリを Frame に送るTxCode(value=1)で録音開始、TxCode(value=0)で録音停止というプロトコル- 録音停止メッセージを送ったあと、
audio_queue.get()で全サンプルがまとめて返ってくる RxAudio.to_wav_bytes()が WAV ヘッダ付きのバイト列を作ってくれるので、そのままファイルに書ける- macOS なので、disconnect した後に
afplayで再生まで自動でやらせる
実行
実機を装着して以下を実行します。実行中の5秒間で何か喋るなり音を鳴らすなりすると分かりやすいです。
uv run python samples/record_audio.py
Frame app is running Recording for 5.0 seconds... Saved 40236 bytes to /.../captures/audio_20260510_202854.wav Playing back via afplay: /.../captures/audio_20260510_202854.wav
保存された WAV ファイルの形式を file コマンドで確認するとこんな感じです。
$ file captures/audio_20260510_202854.wav
captures/audio_20260510_202854.wav: RIFF (little-endian) data, WAVE audio,
Microsoft PCM, 8 bit, mono 8000 Hz