Node-REDからGoogle HomeへMP3のキャスト

## はじめに

自宅のスマートホーム化において、当初はGoogle HomeをTTSで喋らせていたのですが、機械的でそっけなく、しばしば聞き取りづらい場面もあったので音声ファイルを再生させる方法を模索しました。その備忘録です。
※事前に音声ファイルを準備する必要があるため、動的な内容を喋らせたい場合は参考になりません。

## 構築した環境
- Raspberry Pi 3B+
- Raspbian Stretch (kernel 4.14.98-v7+)
- Node-RED環境 nodered/node-red-docker:rpi (Node.js v8.1.3 + Node-RED 0.20.5)

## 音声ファイルの準備
声素材は「[あみたろの声素材工房](https://amitaro.net/)」から頂いています。
とても可愛い声ですし、組み合わせ音声も作れる程の種類が豊富です。

音声出力する際に逐一ネットワークから音声ファイルをダウンロードしたくないので、ブラウザで音声ファイルのURLをコピーして、`wget`コマンドでローカルにダウンロードします。
コマンドが無い場合は、`sudo apt install wget`でインストールしてください。

```
$ wget http://www14.big.or.jp/~amiami/happy/download/voice/001_aisatsu/hajimemasite_01.wav
　　　　　　　　　　　　　　　　　(略)
hajimemasite_01.wav 100%[===================================>] 80.61K 62.3KB/s in 1.3s    

2019-05-06 01:17:15 (62.3 KB/s) - `hajimemasite_01.wav' へ保存完了 [82542/82542]
```
ダウンロードしたwavファイルを、`lame`コマンドでMP3に変換します。
コマンドが無い場合は、`sudo apt install lame`でインストールしてください。

```
$ lame hajimemasite_01.wav 
LAME 3.99.5 32bits (http://lame.sf.net)
Using polyphase lowpass filter, transition band: 16538 Hz - 17071 Hz
Encoding hajimemasite_01.wav to hajimemasite_01.mp3
Encoding as 44.1 kHz single-ch MPEG-1 Layer III (11x)  64 kbps qval=3
    Frame          |  CPU time/estim | REAL time/estim | play/CPU |    ETA 
    37/37    (100%)|    0:00/    0:00|    0:00/    0:00|   9.2181x|    0:00 
---------------------------------------------------------------------------------------------------------------------------------------------------------------
   kbps       mono %     long switch short %
   64.0      100.0        94.6   2.7   2.7
Writing LAME Tag...done
ReplayGain: -8.1dB
```

また、時報などで複数の音声ファイルを繋げる場合は、`sox`コマンドを利用すると良いです。
「１時」と「ちょうど」と「だよ」を繋げて、out.wavにするには以下のようにコマンドを実行します。
コマンドが無い場合は、`sudo apt install sox`でインストールしてください。

```
$ sox jihou_1ji_01.wav choudo_01.wav dayo_01.wav out.wav
```

無音部分を入れる場合は[この記事](https://qiita.com/ironsand/items/76c8f96a27b53b6677ba)を参考にしました。

## Node-REDでのフロー作成
今回追加するノードは以下の「node-red-contrib-cast」ですが、MP3がキャストできるノードなら、他のノードでも代用可能だと思います。

・[node-red-contrib-cast] (https://flows.nodered.org/node/node-red-contrib-cast)

ノードの配置は以下のようになります。
![スクリーンショット 2019-05-06 2.43.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/48839860-8f11-be5f-9d85-962f9f8ac756.png)

フローを書き出したコードは以下です。
Node-REDの待ち受けIPアドレスとポートは「192.168.15.5:1880」、Google HomeのIPアドレスは「192.168.15.29」となっているのでご自分の環境に合わせて変更してください。
Google HomeのIPアドレスは、Google Homeアプリで確認してください。

```
[{"id":"c7f7e44f.7f5318","type":"http in","z":"105517e4.eb0498","name":"","url":"/hajimemasite.mp3","method":"get","upload":false,"swaggerDoc":"","x":130,"y":60,"wires":[["ceb4b1fe.d12ea"]]},{"id":"ceb4b1fe.d12ea","type":"file in","z":"105517e4.eb0498","name":"","filename":"/data/voice/hajimemasite_01.mp3","format":"","chunk":false,"sendError":false,"encoding":"none","x":410,"y":60,"wires":[["854eb9f1.a6ba88"]]},{"id":"854eb9f1.a6ba88","type":"http response","z":"105517e4.eb0498","name":"","statusCode":"200","headers":{"content-type":"audio/mpeg"},"x":650,"y":60,"wires":[]},{"id":"b7aa287c.48ea88","type":"inject","z":"105517e4.eb0498","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":110,"wires":[["bb31bd70.5c383"]]},{"id":"cbbe9f61.4ec81","type":"cast-to-client","z":"105517e4.eb0498","name":"","url":"","contentType":"","message":"","language":"en","ip":"","port":"","volume":"","x":520,"y":110,"wires":[[]]},{"id":"bb31bd70.5c383","type":"change","z":"105517e4.eb0498","name":"","rules":[{"t":"set","p":"ip","pt":"msg","to":"192.168.15.29","tot":"str"},{"t":"set","p":"url","pt":"msg","to":"http://192.168.15.5:1880/hajimemasite.mp3","tot":"str"},{"t":"set","p":"contentType","pt":"msg","to":"audio/mp3","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":110,"wires":[["cbbe9f61.4ec81"]]}]
```

上記で単純な音声出力はできるのですが、困った事にGoogle HomeはDHCPのみしか対応しておらず、DHCPサーバ側で固定する設定が無ければIPアドレスが変わってしまう事があります。
Google HomeのIPアドレスの取得方法として、multicast-dnsを使用する方法があるようなので、Node-REDのfunctionノードで使用できるようにします。

```
$ sudo npm install -g multicast-dns
```

次に、Node-REDの設定ファイル(settings.js)の functionGlobalContext で、multicast-dns が使えるように設定します。

```
    functionGlobalContext: {
        mdns: require('multicast-dns')
    },
```

設定できたら、Node-REDを再起動してください。
multicast-dnsでのGoogle HomeのIPアドレス検出については、いくつか[記事](https://qiita.com/vanx2/items/3c20bf8e4111da9eb68d)があったのですが、responseの処理が止まってくれなかったりして、Node-REDのfunctionノードで動かすのには少し改変が必要でした。

```javascript:functionノード記載例
var mdns = new global.get('mdns')();

//応答が来たら、Google Homeのものか判断する。
//Google Homeからの応答ならIPアドレスをmsg.payloadに入れて終了。
mdns.on('response', function(response) {
    if(/^Google-Home-.*/.test(response.answers[0].data)){
        var ip = response.additionals[2].data;
        node.send({payload:ip});
        mdns.destroy();
    }
});

//Node-REDのデプロイなどで呼ばれた際のソケットの破棄（らしい）
node.on('close', function() {
    mdns.destroy();
});

//mDNSでの検索クエリ発行 ※2019/8/20修正
mdns.query({
    questions: [{
        name: '_googlecast._tcp.local',
        type: 'PTR'
    }]
});

return msg;
```

Google Homeが複数台ある方は、if内の正規表現の一致部分を広げる必要があるかもしれません。
私はChromecastも持っており、正規表現は、`/^Chromecast-.*/`としています。ChromecastはMP4の動画もキャストできるのですが、まだ用途を考えついていません。

頻繁に更新されるようなものでもないので、私はこれをインジェクトノードでNode-RED起動時にファイルに保存しています。インジェクトノードであれば、手動でも実行できるので使い勝手が良いです。

![スクリーンショット 2019-05-06 3.41.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/a908e67f-58c9-537b-19aa-1a9ba1c92645.png)

フローを書き出したコードは以下です。

```
[{"id":"ca273bee.0cbc78","type":"file","z":"f5ed8a05.870628","name":"","filename":"/data/status/GoogleHomeMini","appendNewline":false,"createDir":false,"overwriteFile":"true","x":540,"y":70,"wires":[["e944542a.71cf68"]]},{"id":"e944542a.71cf68","type":"debug","z":"f5ed8a05.870628","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":770,"y":70,"wires":[]},{"id":"89ce224.3ad88e","type":"function","z":"f5ed8a05.870628","name":"Google HomeのIP検索","func":"var mdns = new global.get('mdns')();\n\nmdns.on('response', function(response) {\n    if(/^Google-Home-.*/.test(response.answers[0].data)){\n        var ip = response.additionals[2].data;\n        node.send({payload:ip});\n        mdns.destroy();\n    }\n});\n\nnode.on('close', function() {\n    mdns.destroy();\n});\n\nmdns.query({\n    questions: [{\n        name: '_googlecast._tcp.local',\n        type: 'PTR'\n    }]\n});\n\nreturn msg;","outputs":1,"noerr":0,"x":280,"y":70,"wires":[["ca273bee.0cbc78"]]},{"id":"ce414c85.f1b79","type":"inject","z":"f5ed8a05.870628","name":"","topic":"","payload":"","payloadType":"str","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":100,"y":70,"wires":[["89ce224.3ad88e"]]}]
```

後はファイルからGoogle HomeのIPアドレスを持ってくるようにすれば、DHCPによるIPアドレス変更問題点は解消します。
ノードの配置は以下のようになります。

![スクリーンショット 2019-05-06 3.45.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/70df73a9-8a75-2e87-47dd-8486f1e2a8a7.png)

なんだか長くなってしまっていますが、下の段はサブフロー化する事で、楽に使えるようになります。

![スクリーンショット 2019-05-06 3.56.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/e851cf42-f912-2f37-7add-aa2973da9864.png)

フローを書き出したコードは以下です。

```
[{"id":"6607a5e8.72444c","type":"subflow","name":"Google Home","info":"","category":"","in":[{"x":-10,"y":80,"wires":[{"id":"910e74ae.7cdba8"}]}],"out":[{"x":860,"y":80,"wires":[{"id":"334544df.0ece3c","port":0}]}],"env":[]},{"id":"910e74ae.7cdba8","type":"file in","z":"6607a5e8.72444c","name":"","filename":"/data/status/GoogleHomeMini","format":"utf8","chunk":false,"sendError":false,"x":200,"y":80,"wires":[["1dfe2318.ad556d"]]},{"id":"dc5b8ed2.c24d6","type":"function","z":"6607a5e8.72444c","name":"URL生成","func":"\nmsg.url = \"http://192.168.15.5:1880/\" + msg.mp3;\nmsg.contentType = \"audio/mp3\";\n\nreturn msg;","outputs":1,"noerr":0,"x":590,"y":80,"wires":[["334544df.0ece3c"]]},{"id":"334544df.0ece3c","type":"cast-to-client","z":"6607a5e8.72444c","name":"","url":null,"contentType":"","message":null,"language":"en","ip":"","port":"","volume":null,"x":740,"y":80,"wires":[[]]},{"id":"1dfe2318.ad556d","type":"change","z":"6607a5e8.72444c","name":"IPアドレス","rules":[{"t":"set","p":"ip","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":430,"y":80,"wires":[["dc5b8ed2.c24d6"]]},{"id":"7e60f467.016e4c","type":"change","z":"105517e4.eb0498","name":"はじめまして","rules":[{"t":"set","p":"mp3","pt":"msg","to":"hajimemasite.mp3","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":330,"y":230,"wires":[["51240cc4.11e084"]]},{"id":"7e51851.d644f7c","type":"inject","z":"105517e4.eb0498","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":140,"y":230,"wires":[["7e60f467.016e4c"]]},{"id":"51240cc4.11e084","type":"subflow:6607a5e8.72444c","z":"105517e4.eb0498","name":"","env":[],"x":520,"y":230,"wires":[[]]}]
```

# おわりに

今回は初投稿ですが、今後も自宅のスマートホーム化で得た情報をメインに載せていきますので、よろしくお願いします。