Google Home でローカルの MP3 ファイルをプレイリスト再生する方法
前回の記事では ほりひろ 様による esp8266-google-home-notifier ライブラリと ESP32 ボードを使って構成した Google Home 用キャストエージェントとその利用例を紹介しました。記事ではあわせて同ライブラリをプライベートにカスタマイズした内容として以下を挙げました。
- 元の google-home-notifier に存在する ip 関数を追加
- 元の google-home-notifier に存在する play 関数 (public)を追加
- play 関数による Google Home での MP3 データ再生終了を検知するためのコールバック機構を追加
今回はみっつめの話題について紹介します。
話のきっかけ
Google Home Mini で Spotify など試しているうちに、昔買った音楽 CD をこういう手軽さで聴くことができればと思うようになりました。そのためには楽曲データを MP3 形式等に変換して Google Play Music へアップロードすれば無料で実現できると随所で紹介されています。なかなか魅力的なサービスではありますが、その一方で Google LLC 側の利用規約とのかねあいが気になりました。
手元では日常的に Google LLC の多くの優れたサービスの便宜を享受しており、ひとりのユーザとして同社には敬意と信頼感を持っています。しかし、自分にすべての権利のあるデータであればともかく、自分には所有権しかない楽曲のデータを丸ごとこの規約のもとへ預けることには個人的にためらいがありました。そこで注目したのが google-home-notifier の MP3 データ再生機能です。
google-home-notifier には URL で指定された MP3 データを Google Home へキャストすることのできる play 関数が用意されています。そのため、LAN 上にローカル Web サーバをを立ててそこへ所定の MP3 データを配置し、そのアドレスをこの関数へ渡せば手元の環境からデータを持ち出すことなく Google Home で再生することが可能です。このやり方なら市販の楽曲に対する自分の権利の範囲を踏み越える心配はないでしょう。
ただ、play 関数は一件のデータをワンショットでキャストすることを前提とするもので複数データの連続再生には対応していません。そのため何曲かを順番に流すといったことはできないのが残念です。そこで、この部分に手を加え、所定のプレイリストを参照して複数の MP3 データの順次再生・シャッフル再生ができるようにしてみました。
まずその動作の様子を動画で紹介します。自宅では向かって左側の Raspberry Pi Zero W をローカル Web サーバとして運用しています。このデモでは soundorbis 様によるフリーのジングル曲を使用させて頂いています。
動画:1分15秒
ちなみにここでは以下の IFTTT アプレット (クリックで可読大表示) 経由でローカル Web サーバから再生リスト「demo.txt」(内容)を読み込んで処理を行っています。
トリガー
|
アクション
|
再生リストとその周辺の話題は末尾の「エージェントプログラム」の項に記述しています。
以下、実現に至るまでの経緯を控えます。
対応方法の調査
google-home-notifier の処理内容
まず google-home-notifier の処理を追うことから始めました。以下はそのまとめです。esp8266-google-home-notifier はオリジナルの google-home-notifier が堅実に移植されたライブラリであるためここでは後者をターゲットとしています。
初期設定
- 呼び出し側から ip() 関数により対象とする Google Home デバイスの IP アドレスと言語が指定されればそれを設定
- 呼び出し側から device() 関数により対象 Google Home デバイスのデバイス名と言語が指定されればそれを保持
主処理
- notify() 関数は渡されたテキストを所定の言語で TTS 処理した結果の MP3 ファイルの URL を対象 Google Home デバイスへキャストする
- ここでの音声合成には getSpeechUrl() 関数経由で 「Google 翻訳」の TTS 機能が利用される => GitHub: google-tts
- play() 関数は指定された MP3 ファイルの URL をを対象 Google Home デバイスへキャストする
- notify(), play() はいずれも呼び出された時点で対象 Google Home デバイスの IP アドレスが未設定状態であれば前出 device() 関数経由で保持しているデバイス名で mDNS 照会により対象 Google Home デバイスの IP アドレスを得る => GitHub: mdns
キャストの手順
- notify(), play() はキャスト処理を onDeviceUp() 関数へ投げる。この短い関数が内部で以下の Google Cast v2 処理を呼び出している
- キャスト対象デバイスの 8009 番ポートへ TLS 接続
- Cast v2 プロトコルにそって次のみっつの名前空間の仮想チャネルをコネクション上に作成
- urn:x-cast:com.google.cast.tp.connection - 接続管理用
- urn:x-cast:com.google.cast.tp.heartbeat - 接続維持用
- urn:x-cast:com.google.cast.receiver - デバイスのプレイヤー起動/ 終了/ 音量設定/ 状態確認等
- キャスト対象デバイス標準のプレイヤーである DefaultMediaReceiver (AppID "CC1AD845") を launch() 、ここで仮想チャネル urn:x-cast:com.google.cast.media (コンテンツのロード/ 再生/ 停止/ 状態確認等) が作成される。このチャネル経由で対象 MP3 の URL を、バッファリング・自動再生指定つきで load() することにより Google Home デバイスでキャスト再生が実行される
- load() が終わったら各仮想チャネルを close してコネクションを切断
注目すべき点と考え方
ポイントは、上記「キャストの手順」の最後の「load() が終わったら各仮想チャネルを close してコネクションを切断」という部分にあります。該当箇所のソースコードを引用します。
- google-home-notifier.js (#L88-L96)
: var onDeviceUp = function(host, url, callback) { var client = new Client(); client.connect(host, function() { client.launch(DefaultMediaReceiver, function(err, player) { var media = { contentId: url, contentType: 'audio/MP3', streamType: 'BUFFERED' // or LIVE }; player.load(media, { autoplay: true }, function(err, status) { client.close(); callback('Device notified'); }); :
このように、Google Home 側の DefaultMediaReceiver へ再生対象データの引き渡し(player.load())を終えるとプログラムは直ちにデバイスとの接続を閉じそのまま終了しています。あとは Google Home まかせということになりますが、Google Home はデータ再生中に別のデータのキャストが指示されると現在の再生の終了を待つことなくあっさり新しいデータの再生を開始します。そのため、単に google-home-notifier を繰り返し実行しても、その都度データ再生が中断され別データの再生に移るだけで、プレイリスト再生のように「一件の再生が終了〜次のデータを再生」という動きにはなりません。
一方、手元で上記の player.load() 後も接続を維持した状態で観察したところ、Google Home 側でデータ再生が終了すると前掲の仮想チャネル「urn:x-cast:com.google.cast.media」経由でデバイス側からプログラムへ下記要領のメッセージが送出されることを確認しました。
{"type":"MEDIA_STATUS","status":[{"mediaSessionId":1,"playbackRate":1,"playerState":"IDLE","currentTime":0,"supportedMediaCommands":15,"volume":{"level":1,"muted":false},"currentItemId":1,"idleReason":"FINISHED"}],"requestId":0}
このように、接続を維持していればプログラムはデバイス側の再生終了の捕捉が可能であることがわかりました。
この件を含めここまでの一連の事情を踏まえると、google-home-notifier でデータの連続再生を実現するために必要な措置は大きく次のふたつであることがわかります。
なお、Google Cast の仕様にはデータの連続再生を行うためのネイティブの機構が存在するのではないかとも思うのですが、手元では今のところその方面の具体的な情報に行き着いておらず、いずれにせよここではあくまでも現行の google-home-notifier の機能を拡張することにしました。
実装まわりの話題
方針
実装にあたっては次の方針を基本としました。
処理のイメージ
左側のフローは手元での p8266-google-home-notifier とエージェントプログラムによる処理内容を要約したものです。右側はそこに調査から得られた情報を加味して MP3 データのリスト再生機能を追加したイメージで、ざっくり、赤文字の「キャスト処理」「再生監視タスク」の部分をライブラリ側の処理要素と想定しています。なお灰色のブロックは右フローとの共通部分です。 (クリックで可読大表示)
Google Home からのメッセージについて
上記右フローの「再生監視タスク」パート内の条件分岐部分でも触れていますが、データ再生の監視中には Google Home デバイスから仮想チャネル経由でさまざまなメッセージが送られてきます。手元での観察結果から、今回の実装では以下のメッセージを利用しています。
{"type":"MEDIA_STATUS","status":[{"mediaSessionId":1,"playbackRate":1,"playerState":"IDLE","currentTime":0,"supportedMediaCommands":15,"volume":{"level":1,"muted":false},"currentItemId":1,"idleReason":"FINISHED"}],"requestId":0}
{"type":"CLOSE"}
{"type":"PING"}
{"type":"PONG"}
{"type":"MEDIA_STATUS","status":[{"mediaSessionId":1,"playbackRate":1,"playerState":"PAUSED","currentTime":1.880214,"supportedMediaCommands":15,"volume":{"level":1,"muted":false},"activeTrackIds":[],"currentItemId":1,"repeatMode":"REPEAT_OFF"}],"requestId":388729138}
{"type":"MEDIA_STATUS","status":[{"mediaSessionId":1,"playbackRate":1,"playerState":"PLAYING","currentTime":15.665338,"supportedMediaCommands":15,"volume":{"level":1,"muted":false},"activeTrackIds":[],"currentItemId":1,"repeatMode":"REPEAT_OFF"}],"requestId":388729139}
なお「"requestId":0」のメッセージがデータの再生開始時点で一度発行されるため上記と混同しないよう注意
{"type":"MEDIA_STATUS","status":[{"mediaSessionId":1,"playbackRate":1,"playerState":"PLAYING","currentTime":1.384362,"supportedMediaCommands":15,"volume":{"level":1,"muted":false},"activeTrackIds":[],"currentItemId":1,"repeatMode":"REPEAT_OFF"}],"requestId":0}
ソースコード
カスタマイズした esp8266-google-home-notifier
オリジナルの esp8266-google-home-notifier ライブラリのコードへプライベートに手を加えています。変更箇所は「#ifdef TANABE」部分で、fork した GitHub リポジトリへ反映しています。変更内容はこの記事の冒頭、および前回の記事で触れたとおりです。
$ git clone -b private https://github.com/mkttanabe/esp8266-google-home-notifier.git
また、同リポジトリの examples/esp32/SimpleUsage/ 下に play 関数用のサンプルスケッチを加えました。どちらもオリジナルの SimpleUsage.ino のコピーを小さく書き替えたものです。
play 関数を再生終了通知用コールバック関数指定なしで呼び出すもの
play 関数を再生終了通知用コールバック関数指定つきで呼び出すもの
このふたつの追加サンプルを実行した様子の動画です。
エージェントプログラム
Arduino core for the ESP32 環境で作成したエージェントの Adruino スケッチです。前回分と互換性のある機能拡張版です。
- GoogleHomeNotifierESP32AgentEx - github.com/mkttanabe
GoogleHomeNotifierESP32Agent.ino の 以下の箇所を環境にあわせて書き替える
//------- ユーザ定義 ------------------
// Google Home デバイスの IP アドレス, デバイス名
// 有効にすれば指定 IP アドレスを直接使用
// 無効にすれば指定デバイス名で mDNS 照会
//#define USE_GH_IPADRESS
IPAddress myGoogleHomeIPAddress(192,168,0,110);
#define myGoogleHomeDeviceName "room02"
// WiFi アクセスポイント
#define ssid "ssid"
#define password "pass"
// Beebotte 情報
#define mqtt_host "mqtt.beebotte.com"
#define mqtt_port 8883
#define mqtt_topic "test01/msg"
#define mqtt_pass "token:" "token_************"
// MP3 データを配置する Web サーバ
#define dataServer "192.168.0.127"
#define dataServerPort 80
// MP3 データの URL
#define MP3DataFmt "http://" dataServer "/sound/%s.mp3"
// MP3 再生リストのパス
#define MP3ListFmt "/sound/list/%s.txt"
//-------------------------------------
上の記述の場合、自分の Beebotte アカウントの Channel "test01", Resource "msg" のメッセージの data キーの値に応じて以下が行われる- data 値の先頭文字がアスタリスクであれば音声合成用のテキストとみなして google-home-notifier の TTS 処理にかける
- 例) {"data":"*こんにちは"} => 「こんにちは」と発話
- data 値の先頭文字がアスタリスクまたは "@" でなければ MP3 ファイル名とみなし所定の Web サーバの URL に編集して再生
- 例) {"data":"file01"} => "http://192.168.0.127/sound/file01.mp3" をキャスト
- data 値の先頭文字が "@" であれば MP3 データ再生リスト名とみなし所定の Web サーバの URL に編集して読み込み、記述されている各 MP3 データエントリを再生
- 例) {"data":"@demo"} => "http://192.168.0.127/sound/list/demo.txt" を再生リストとして読み込んで処理
- 下記の指定があればシャッフル再生を行う
{"data":"@demo,1"} , {"data":"@list01,ランダム"}
- 上記以外ならリスト上の記述順に再生
- 下記の指定があればシャッフル再生を行う
- 再生リストの記述例
demo.txt# 各行の '#' 以降はコメントとして扱われる
# 空行は無視、有効行先頭末尾のスペース・タブは除去される
# soundorbis 様によるフリー BGM 作品より
#
# 【フリーBGM】リコーダージングル【01〜05】
# https://www.youtube.com/watch?v=ztm1CSZEpY8
#
# 利用規約
# https://www.soundorbis.net/license
#
demo/[1]nc150689
demo/[2]nc150690
demo/[3]nc150691
demo/[4]nc150692
demo/[5]nc150693
- 例) {"data":"@demo"} => "http://192.168.0.127/sound/list/demo.txt" を再生リストとして読み込んで処理
なお、上の「ユーザ定義」箇所において「#define USE_GH_IPADRESS」が無効な場合は前回版と同様に mDNS 照会によりデバイス名から IP アドレスを取得するが、このやや時間のかかる照会のコストを軽減するために、今回の拡張版においては一旦取得した IP アドレスを ESP32 のフラッシュメモリ領域 (SPIFFS) へ記録し次回はまずそれを参照する処理を加えている。当該アドレスへアクセスできない場合や当該アドレスのデバイスを Google Home と識別できない場合はあらためて照会と記録を行う。詳細は DeviceAddress.cpp を参照のこと - data 値の先頭文字がアスタリスクであれば音声合成用のテキストとみなして google-home-notifier の TTS 処理にかける
こういった経緯を経て形になったのが冒頭のデモ動画で稼働しているエージェントです。Raspberry Pi Zero W のローカル Web サーバとペアでとても快適に利用しています。
(tanabe)