はじめに
本記事はM5Stack Advent Calendar 2022の24日目です。
今回、M5Stack UnitV2の内蔵マイク音声を、ブラウザ上でリアルタイム再生出来るようにしてみました。
真っ暗な部屋などでは、UnitV2のカメラ映像も真っ黒になってしまいますが、
音声に明るさは関係ないので、夜中の防犯用途などにも使えそうです。
動作中の様子
以下は画像のため音声は聞こえませんが、波形や周波数スペクトルの表示が変化している様子が分かるかと思います。
音が小さい場合、波形の変化が分かりづらいため、ゲイン調整用のスライダーなども実装しています。
アプリ構成
アプリは以下のような構成です。
マイク音声をUnitV2からPCへWebSocketで送信し、ブラウザ上で再生しています。
Tips
複数プロセスから同時にマイク音声を取得可能にする
複数人が同時にアクセス出来るよう、バックエンドのサーバはGunicornを使用してマルチプロセスで起動しています。
この際、そのままでは複数プロセスから同時にマイクを使用出来ず、エラーになります。
ALSAのdsnoop
プラグインを使用すると、マイクの同時使用が可能になります。
UnitV2上に、以下の内容の/etc/asound.conf
を作成しておきます。
pcm.mixin {
type dsnoop
ipc_key 5978293
ipc_key_add_uid yes
slave {
pcm "hw:0,0"
channels 2
}
}
依存ライブラリのインストール
UnitV2のデフォルト環境を壊したくないので、Pythonの依存ライブラリはvenv
の仮想環境にインストールします。
内蔵ストレージは空きがほぼ無いので、SDカード上に作成します。
unitv2% cd /media/sdcard/適当なフォルダ
unitv2% sudo python -m venv venv
unitv2% . venv/bin/activate
(venv) unitv2% sudo python -m pip install fastapi gunicorn uvicorn websockets
この際、注意点がいくつかあります。
- 仮想環境の作成や、仮想環境へのライブラリインストールはroot権限で行う
-
pip
コマンドはpython -m pip
の形で実行する
仮想環境の作成時は内部的にchmod
が実行されますが、SDカード上のファイルは所有者がroot
で固定となるため、
root権限でないとこの処理に失敗し、仮想環境の作成途中でエラーになります。
また、python -m pip
ではなく単にpip
で実行すると、以下のようなエラーが出ます。1
(venv) unitv2% pip install 何か
Traceback (most recent call last):
File "/media/sdcard/適当なフォルダ/venv/bin/pip", line 5, in <module>
from pip._internal.cli.main import main
ModuleNotFoundError: No module named 'pip._internal.cli.main'
バックエンド実装(抜粋)
バックエンドはPython+FastAPIで実装しました。
arecord
で取得した生のマイク入力を、そのままWebSocketで送信しています。
先ほどのdsnoop
の設定を使用するため、arecord
の引数に-D mixin
を指定しておく必要があります。
import audioop
import subprocess
from fastapi import FastAPI, Request, WebSocket
CHUNK = 1024 * 1
COMMAND = ['arecord', '-D', 'mixin', '-f', 'S16_LE', '-c', '1', '-r', '48000', '-t', 'raw', '-q', '-']
AUDIO_SOURCE = subprocess.Popen(COMMAND, stdout=subprocess.PIPE)
app = FastAPI()
@app.websocket('/websocket')
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
key = websocket.headers.get('sec-websocket-key', '')
print(f'WebSocket {key} connected')
while True:
if AUDIO_SOURCE.poll() is not None:
await websocket.close()
print(f'WebSocket {key} closed')
break
AUDIO_SOURCE.stdout.flush()
buf = AUDIO_SOURCE.stdout.read(CHUNK)
try:
await websocket.send_bytes(buf)
except Exception as e:
await websocket.close()
print(f'WebSocket {key} closed: {e}')
break
フロントエンド実装
WebSocketで受信したマイク入力データを、Web Audio APIを使用して再生しています。2
波形データや周波数データへの変換にも、同じくWeb Audio APIを使用しています。
波形やスペクトル表示部には<svg>
の<path>
を使用しました。
Vue.jsで<path>
のd
を更新することで、リアルタイムでの波形描画を実現しています。3
ソースコードは上手く抜粋するのが難しかったので、直接GitHubを参照して下さい。
余談
今回、細かなソースコードの修正や微調整などは直接UnitV2上のVimで行っていましたが、
以前UnitV2用にビルドしたtmuxのおかげで快適に試行錯誤することが出来ました。
UnitV2上でtmuxを動作させる方法については、以下の記事に記載しています。
参考資料
- https://docs.m5stack.com/en/quick_start/unitv2/jupyter_notebook
- https://www.alsa-project.org/main/index.php/Asoundrc#dsnoop
- https://fastapi.tiangolo.com/deployment/server-workers/
- https://gist.github.com/ykst/6e80e3566bd6b9d63d19
- https://kuroeveryday.blogspot.com/2017/11/oscilloscope-with-web-audio-api-and-vuejs.html
ソースコード
今回作成したプログラムのソースコードは以下で公開しています。