Chrome の SpeechRecognition API に WAVなどの音声ファイルを聴かせる

# はじめに

[前回記事](https://qiita.com/yoh1496/items/cc2fd729050ad2419a34) で

```
こちらMP3などの音源を食わせてHeadlessChrome・PupetterでSpeechRecognition APIを
稼働させる文字列を得ることはできないものでしょうか？
```

とコメントいただきました。
これってつまり、

別途用意した<u>動画ストリーム</u>をあたかも今<u>Webカメラで</u>撮っているように偽装する**「バーチャルWebカメラ」**のように（[この記事](https://qiita.com/yoh1496/items/c4ec35c97bd22862a081) 参照）、
別途用意した<u>音声ストリーム</u>をあたかも今<u>マイクで</u>録音しているかのように偽装する**「バーチャルマイク」**を作れないか、というお題に読み替えられますね。（無理やりすぎ？）


ではやっていきましょう。

# 要約

先に結論を言うと、
音源ファイルを再生できるバーチャルマイクの構成まではできましたが、
puppeteerからのChrome使用にそのマイクを使用することはできませんでした。

音源ファイルを翻訳するには翻訳APIを使うのが手っ取り早いですし、正しいやり方でしょうね（そりゃそうだ）

# 実現方法

Windowsでは「ステレオミキサー」なるデバイスを有効化することで、
スピーカー出力をマイクに入力させることが可能なようですが、
そもそもホストマシンに色々導入してどうこう、というのは面倒なのでdockerでやります。

## 概要

（あとで余裕があったら図を入れる）

- 使用するのは `node:12-slim` のイメージにChromeを導入したもの
- 上記コンテナにバーチャルマイク（`pulseaudio`の virtual-source）を導入する
- 前回記事同様、`puppeteer`でバーチャルマイクの内容をChromeに食わせる

## 準備

例によって下記環境で実施しました。

- OS: Windows 10 Pro 64bit
- RAM: 16GB

### Dockerコンテナの準備

`node:12-slim` ベースにgoogle-chromeを導入します。
あくまでオタメシなのでバージョンとかちゃんと考えてません。

```Dockerfile
FROM node:12-slim

RUN apt-get update
RUN apt-get install -y libappindicator1 fonts-liberation libasound2 libnspr4 libnss3 libxss1 lsb-release xdg-utils
RUN apt-get install -y wget
RUN wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
RUN apt install -y ./google-chrome*.deb
```

あとは下記を追記して、xvfbと今回重要になる `pulseaudio` を導入します。
xvfbは Chrome を「headlessじゃない状態で」コンテナで動かすために導入する、
「仮想X Server」と考えてください。

```dockerfile
RUN apt-get install -y  xvfb pulseaudio alsa-utils
```

（追記0423）
あ、あとは日本語フォントを導入します。

```dockerfile
RUN apt-get install -y fonts-ipafont
```
（追記0423ここまで）

ビルド。

```
docker build -t node-chrome .
```

（node, chrome以外に色々入ってるのにそのタグはなんだというツッコミはなしで。）

### コンテナの起動

今回調べきれなかったのですが、Chromeのオーディオのデバイス設定を
非GUI環境で行う方法がわかりませんでしたので、暫定対処として、
ホスト側のX Serverに chromeのGUI画面を出力して、設定を行います。

`VcXsrv` などをホストマシンで立ち上げた状態で、
環境変数 `DISPLAY` を `host.docker.internal:0.0` に設定して先ほどのコンテナを起動します。

vscode の 「Remote - Containers」エクステンションを使用している場合は、
`devcontainer.json` に下記を記載すればOK。

```json
    "containerEnv": {
        "DISPLAY": "host.docker.internal:0.0"
    }
```

起動したコンテナで下記コマンドを実行して、

```bash
google-chrome --no-sandbox
```

ホストでChromeのウィンドウが開けばおっけー。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/5da4730d-78eb-7578-c8a9-ae7922db0913.png)

（ｱｱｱｯ日本語フォント導入忘れて豆腐になっちゃった）

（追記0423）
直した

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/7f93ddb3-e53a-8d32-6e85-3e9f720bdfc4.png)

（追記0423ここまで）

### バーチャルマイクの設定

[Emulating microphone input to Chrome inside Docker container](https://superuser.com/questions/1223118/emulating-microphone-input-to-chrome-inside-docker-container)

ほぼ3年前に同じことやろうとしてる人がいるので、詳細はそちらを参照ください。

`entrypoint.sh`を作成します

```sh
# Load pulseaudio virtual audio source
pulseaudio -D --exit-idle-time=-1

# Create virtual output device (used for audio playback)
pactl load-module module-null-sink sink_name=DummyOutput sink_properties=device.description="Virtual_Dummy_Output"

# Create virtual microphone output, used to play media into the "microphone"
pactl load-module module-null-sink sink_name=MicOutput sink_properties=device.description="Virtual_Microphone_Output"

# Set the default source device (for future sources) to use the monitor of the virtual microphone output
pacmd set-default-source MicOutput.monitor

# Create a virtual audio source linked up to the virtual microphone output
pacmd load-module module-virtual-source source_name=VirtualMic
```

が、このまま entrypoint.sh を実行しようとすると

```
E: [pulseaudio] main.c: Daemon startup failed.
```

とでて失敗します。`pulseaudio -vvv` を実行すると

```
E: [pulseaudio] module-console-kit.c: Unable to contact D-Bus system bus: org.freedesktop.DBus.Error.FileNotFound: Failed to connect to socket /var/run/dbus/system_bus_socket: No such file or directory
E: [pulseaudio] module.c: Failed to load module "module-console-kit" (argument: ""): initialization failed.
```

というエラーが出ていることがわかります。ので、`module-console-kit`モジュールのロードをやめます（いいのか・・・？）。下記コマンドで`/etc/pulse/default.pa`の `load-module module-console-kit` している行をコメントアウトします。

```sh
sed -i 's/load-module module-console-kit/# load-module module-console-kit/g' /etc/pulse/default.pa
```

再度 `entrypoint.sh` を実行してエラーが出力されなければおっけー。（いいのか・・・？）

```
./entrypoint.sh
```

### バーチャルマイクを使用するように設定する

[Google Chrome でマイク設定を既定以外にする方法](http://www.okayulu.moe/2018/02/google-chrome.html)

`google-chrome --no-sandbox` でchromeを起動したら、上記サイトの通り実行し、
マイクを`Virtual Source VirtualMic on Monitor of Virtual_Microphone_Output` に設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/90031542-95e5-f15e-acb0-9746dc36d7c3.png)

# 実験

ここまでの構成を使用して実験してみます。

## バーチャルマイクの動作確認

先ほどのコンテナで新しいシェルを開き、
下記コマンドで音源を再生します。

```sh
paplay --device=MicOutput <再生したい音源ファイル>
```

再生中に先ほどのChromeの音声入力ボタンを押せば試せるはずです。
私は [青空朗読](http://aozoraroudoku.jp/)の音源ファイルをお借りし、入力してみました。

なお、音源ファイルは 16bit の PCM に変換して入力しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/f6c49849-0ff5-3d3e-c7f4-8defe8bf0463.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/025ddec8-27f1-ac1d-1146-c2e856d9b9b9.png)

バーチャルマイクに再生した音源をChromeが読み、翻訳できていることがわかりますね。

## puppeteerによる自動実行

今回の構成で前回のスクリプトを実行してみます。
するとうんともすんとも言わず、終了してしまいました。

下記サイトなどでも指摘されているんですが、
`--use-fake` 系のオプションを使用しているとデフォルトのデバイスが使用されるようです。

[chrome speech recognition WebKitSpeechRecognition() not accepting input of fake audio device --use-file-for-fake-audio-capture or audio file](https://stackoverflow.com/questions/50774560/chrome-speech-recognition-webkitspeechrecognition-not-accepting-input-of-fake)

実際に `puppeteer` で起動したChromeの設定を見てみると、
先ほど設定したはずの内容が元に戻っていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/37d66ec3-dcba-0b41-b4b3-551c75335031.png)

うーん？？ puppeteerで起動するとユーザー設定が読み込まれないんだろうか？
（もしかしてrootで全部やっているのがよくない？）

これより先はちょっと手に余るので、わかる方いらっしゃったら教えてください。


# 終わりに

今回無駄にdockerコンテナでやってしまいましたが、
pulseaudioを使用すると簡単に仮想マイクを実現できますよ。という記事でした。

あ、あと、vscode の 「Remote-Container」使用しましたが便利ですね。

# 参考URL

- [青空朗読 | 青空文庫に所蔵されている本を朗読しています](http://aozoraroudoku.jp/)
- [Google Chrome でマイク設定を既定以外にする方法](http://www.okayulu.moe/2018/02/google-chrome.html)
- [Emulating microphone input to Chrome inside Docker container](https://superuser.com/questions/1223118/emulating-microphone-input-to-chrome-inside-docker-container)