Brilliant Labs の Frame のマイクで録音してみる

初めに

Brilliant LabsFrame シリーズの続きです。前回までで OLED 表示と写真撮影をやったので、今回はマイクから音声を録って、Mac に WAV として保存してみます。

ayousanz.hatenadiary.jp

ayousanz.hatenadiary.jp

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 アプリのセットで動かします。

  1. ホストから標準 Lua ライブラリ (data, code, audio) と audio_frame_app.lua を Frame に転送
  2. ホストから TxCode(value=1) を送って録音開始
  3. Frame は audio.start() で MIC をオンにして、サンプルを BLE 経由で連続送信
  4. ホスト側の RxAudio がキューに音声データを溜める
  5. ホストから TxCode(value=0) を送って録音停止
  6. キューから取り出して、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