2018年08月08日

Google Home でローカルの MP3 ファイルをプレイリスト再生する方法

はてなブックマークに登録

前回の記事では ほりひろ 様による esp8266-google-home-notifier ライブラリと ESP32 ボード (秋月版)を使って構成した Google Home 用キャストエージェントとその利用例を紹介しました。記事ではあわせて同ライブラリをプライベートにカスタマイズした内容として以下を挙げました

  1. 元の google-home-notifier に存在する ip 関数を追加
  2. 元の google-home-notifier に存在する play 関数 (public)を追加
  3. play 関数による Google Home での MP3 データ再生終了を検知するためのコールバック機構を追加

今回はみっつめの話題について紹介します。

話のきっかけ

Google Home Mini で Spotify など試しているうちに、昔買った音楽 CD をこういう手軽さで聴くことができればと思うようになりました。そのためには楽曲データを MP3 形式等に変換して Google Play Music へアップロードすれば無料で実現できると随所で紹介されています。なかなか魅力的なサービスではありますが、その一方で Google 側の利用規約とのかねあいが気になりました。

手元では日常的に Google の多くの優れたサービスの便宜を享受しており、ひとりのユーザとして同社には敬意と信頼感を持っています。しかし、自分にすべての権利のあるデータであればともかく、自分には所有権しかない楽曲のデータを丸ごとこの規約のもとへ預けることには個人的にためらいがありました。そこで注目したのが 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 デバイスへキャストする
  • play() 関数は指定された MP3 ファイルの URL をを対象 Google Home デバイスへキャストする
  • notify(), play() はいずれも呼び出された時点で対象 Google Home デバイスの IP アドレスが未設定状態であれば前出 device() 関数経由で保持しているデバイス名で mDNS 照会により対象 Google Home デバイスの IP アドレスを得る  =>  GitHub: mdns

キャストの手順

注目すべき点と考え方

ポイントは、上記「キャストの手順」の最後の「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 の機能を拡張することにしました。

実装まわりの話題

方針

実装にあたっては次の方針を基本としました。

  • 複数 MP3 データの連続再生はあくまでも拡張機能として取り回す。従来の TTS 機能および単一の MP3 再生機能との互換性を保ち両者の処理を衝突させない
  • デバイスからの再生終了メッセージを受け取るためのコネクション維持と監視は他の処理を止めないためにバックグラウンドタスク内で実施する
  • バックグラウンドタスクから再生終了通知を受け取るためにコールバック機構を設置する。ライブラリ側へ追加するのはこの「一件の MP3 データ再生が終了した場合、または中断・スキップされた場合にコールバック関数経由で通知する」処理にとどめ、それ以外の処理はアプリケーション側の要件としてエージェントプログラムへ実装する
  • つまりライブラリ側の改変は必要最小限の内容とする

処理のイメージ

左側のフローは手元での p8266-google-home-notifier とエージェントプログラムによる処理内容を要約したものです。右側はそこに調査から得られた情報を加味して MP3 データのリスト再生機能を追加したイメージで、ざっくり、赤文字の「キャスト処理」「再生監視タスク」の部分をライブラリ側の処理要素と想定しています。なお灰色のブロックは右フローとの共通部分です。 (クリックで可読大表示)

Google Home からのメッセージについて

上記右フローの「再生監視タスク」パート内の条件分岐部分でも触れていますが、データ再生の監視中には Google Home デバイスから仮想チャネル経由でさまざまなメッセージが送られてきます。手元での観察結果から、今回の実装では以下のメッセージを利用しています。

  • (右フロー中の「※1」) データの再生終了は、前掲のとおり当該デバイスからの「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}
  • (右フロー中の「※2」) デバイスが他のアプリ等とのキャストセッションを開始した場合には「urn:x-cast:com.google.cast.tp.connection」チャネルから以下のメッセージが届く。これを受け取ったらエージェント側は現在の連続再生処理を停止する
    {"type":"CLOSE"}
    • なお、Google Cast 機構においてはデバイス側との接続の維持・確認のために上記チャネル上で相互に PING, PONG メッセージの応酬を継続的に実施することが流儀のひとつ。バックグラウンドタスクでの待機中は 5秒毎に PING を送ることにしたが、タイミングによってはデバイス側から PING が届くこともある。その場合は PONG を送る
      {"type":"PING"}
      {"type":"PONG"}
  • (右フロー中の「※3」) ユーザが「OK, Google。ストップ」「OK, Google。やめて」等の再生停止を求めるフレーズを発した場合は「urn:x-cast:com.google.cast.media」チャネル経由でデバイスから下記要領のメッセージが届く。これを受け取ったらエージェント側は連続再生を停止する
    {"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}
  • (右フロー中の「※4」) ユーザが「OK, Google。スキップ」「OK, Google。次の曲」等のフレーズで次データへのスキップをリクエストした場合は「urn:x-cast:com.google.cast.media」チャネル経由でデバイスから下記要領のメッセージが届く。この状況でのステータスは「"playerState":"PLAYING"」かつ、「"requestId":」の値が 0 以外となる。 これを受け取ったらエージェント側は次のデータの再生へ移行する
    {"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 のコピーを小さく書き替えたものです。

このふたつの追加サンプルを実行した様子の動画です。
  

エージェントプログラム

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

    なお、上の「ユーザ定義」箇所において「#define USE_GH_IPADRESS」が無効な場合は前回版と同様に mDNS 照会によりデバイス名から IP アドレスを取得するが、このやや時間のかかる照会のコストを軽減するために、今回の拡張版においては一旦取得した IP アドレスを ESP32 のフラッシュメモリ領域 (SPIFFS) へ記録し次回はまずそれを参照する処理を加えている。当該アドレスへアクセスできない場合や当該アドレスのデバイスを Google Home と識別できない場合はあらためて照会と記録を行う。詳細は DeviceAddress.cpp を参照のこと

こういった経緯を経て形になったのが冒頭のデモ動画で稼働しているエージェントです。Raspberry Pi Zero W のローカル Web サーバとペアでとても快適に利用しています。


(tanabe)
klab_gijutsu2 at 09:54│Comments(0)IoT | スマートスピーカ

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント