M5Stack UnitV2のマイク音声をリアルタイム再生するWebアプリ

# はじめに

本記事は[M5Stack Advent Calendar 2022](https://qiita.com/advent-calendar/2022/m5stack)の24日目です。  

今回、M5Stack UnitV2の内蔵マイク音声を、ブラウザ上でリアルタイム再生出来るようにしてみました。  
真っ暗な部屋などでは、UnitV2のカメラ映像も真っ黒になってしまいますが、  
音声に明るさは関係ないので、夜中の防犯用途などにも使えそうです。

# 動作中の様子

以下は画像のため音声は聞こえませんが、波形や周波数スペクトルの表示が変化している様子が分かるかと思います。  
音が小さい場合、波形の変化が分かりづらいため、ゲイン調整用のスライダーなども実装しています。

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2542709/be5b962c-3f53-29a9-bd7a-ebf7bf4068ad.gif)

# アプリ構成

アプリは以下のような構成です。  
マイク音声をUnitV2からPCへWebSocketで送信し、ブラウザ上で再生しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2542709/9d3197de-1b4e-464c-f129-acb07bc89258.png)

# Tips

## 複数プロセスから同時にマイク音声を取得可能にする

複数人が同時にアクセス出来るよう、バックエンドのサーバは[Gunicorn](https://gunicorn.org/)を使用してマルチプロセスで起動しています。  
この際、そのままでは複数プロセスから同時にマイクを使用出来ず、エラーになります。  
ALSAの`dsnoop`プラグインを使用すると、マイクの同時使用が可能になります。

UnitV2上に、以下の内容の`/etc/asound.conf`を作成しておきます。
```conf:/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カード上に作成します。

```shell
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`で実行すると、以下のようなエラーが出ます。[^nazo]
```
(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'
```

[^nazo]: とりあえず`python -m pip`で動いているので、原因は深追いしていません。


# バックエンド実装(抜粋)

バックエンドはPython＋FastAPIで実装しました。  
`arecord`で取得した生のマイク入力を、そのままWebSocketで送信しています。  
先ほどの`dsnoop`の設定を使用するため、`arecord`の引数に`-D mixin`を指定しておく必要があります。

```py:server.py
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を使用して再生しています。[^audio]  
波形データや周波数データへの変換にも、同じくWeb Audio APIを使用しています。

波形やスペクトル表示部には[`<svg>`の`<path>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths)を使用しました。  
Vue.jsで`<path>`の`d`を更新することで、リアルタイムでの波形描画を実現しています。[^oscilloscope]

ソースコードは上手く抜粋するのが難しかったので、直接[GitHub](#ソースコード)を参照して下さい。

[^audio]: [こちら](https://gist.github.com/ykst/6e80e3566bd6b9d63d19)のGistが参考になりました。
[^oscilloscope]: [こちら](https://kuroeveryday.blogspot.com/2017/11/oscilloscope-with-web-audio-api-and-vuejs.html)の記事が参考になりました。

# 余談

今回、細かなソースコードの修正や微調整などは直接UnitV2上のVimで行っていましたが、  
以前UnitV2用にビルドしたtmuxのおかげで快適に試行錯誤することが出来ました。  

![tmux.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2542709/5ac67fe5-b311-45d1-d191-5814b6c5e5d3.png)

UnitV2上でtmuxを動作させる方法については、以下の記事に記載しています。

[M5Stack UnitV2でtmuxを動かす](https://qiita.com/okmt765/items/24b4082d3f17d1e46487)

# 参考資料

- 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

# ソースコード

今回作成したプログラムのソースコードは以下で公開しています。

https://github.com/okmt765/unitv2-audio-server