今回の実装機能
今回で残りの機能すべてを実装、説明します。
機能実装
1. 最初から再生
case('AMAZON.StartOverIntent'):{constaudioPlayer=handlerInput.requestEnvelope.context.AudioPlayer;[playlistId,seed,,loop,,]=audioPlayer.token.split(':');break;}
1 曲目から再生するため track を 0
にしますが、トークンの値を無視することで初期値の 0
のままにしています。
再生する曲自体が変わるため、再生中の曲を繰り返し再生するリピートフラグもクリア(トークンの値を無視)します。
2. シャッフル再生オン・オフ
シャッフルした曲順を 1024 文字と有限なトークンに埋め込むのは無理があるので、同じシャッフル結果を再現するための情報、乱数シードをトークンに埋め込み、毎回プレイリストをシャッフルしてその○曲目を再生するようにします。
問題は JavaScript 標準の Math.random()
は乱数シードを指定できないという点で、乱数シードを指定できる疑似乱数生成器を実装します。
2.1. 疑似乱数生成器とシャッフル後曲番号取得
// ↓ rootFIleId 定義 (前回9行目) 辺りにこの行を追加constUINT32_MAX_NEXT=2**32;// ↓ getJson の下 (前回205行目) 辺りに以下のコードを追加// 0~1未満の実数の疑似乱数生成器constgetSeed=()=>Math.floor(Math.random()*(UINT32_MAX_NEXT-1))+1;constgetNext=(()=>{lets=Uint32Array.of(getSeed());returnseed=>{if(seed)s[0]=seed;s[0]^=s[0]<<13;s[0]^=s[0]>>17;s[0]^=s[0]<<5;returns[0]/UINT32_MAX_NEXT;};})();// シャッフル後の index 番目のトラック番号を取得する// 0~length-1 の連番をランダムに並べ替え、index 番目の数を返却するconstgetShuffledTrack=(length,index,seed)=>{getNext(seed);letseq=[...Array(length).keys()];while(length>index){constpick=Math.floor(getNext()*length--);[seq[length],seq[pick]]=[seq[pick],seq[length]];}returnseq[index];};
疑似乱数生成器は Xorshiftなどの擬似乱数をプロットして比較してみたの Xorshift 版のものを使いました。そして プレイリストの曲数分の連番を Fisher–Yates アルゴリズムでシャッフルし、その指定番目の値(元のプレイリストにおける曲番号)を返却します。
なお計算時間をケチるため、シャッフル後の指定番目の値が確定した時点で値を返すようにしています。(while (length > index)
の部分)
2.2. 再生リクエスト処理
case('AMAZON.ShuffleOnIntent'):case('AMAZON.ShuffleOffIntent'):{constaudioPlayer=handlerInput.requestEnvelope.context.AudioPlayer;[playlistId,,,loop,,]=audioPlayer.token.split(':');seed=requestTypeOrIntentName==='AMAZON.ShuffleOnIntent'?getSeed():0;handlerInput.responseBuilder.speak(`${seed?'シャッフル':'最初から順番に'}再生します。`);break;}
シャッフル再生オンの場合は乱数シードを新たに生成 getSeed()
し、オフの場合は 0
にします。上書きする乱数シード、オン・オフにより 1 曲目からの再生となるトラック番号、再生曲が変わり解除するリピートフラグについてはトークン値を無視します。
// addAudioPlayerPlayDirective を利用して AudioPlayer に音楽再生の指示を応答するconstidx=seed?getShuffledTrack(playlist.length,track,seed):track;consturl=makeDriveUrl(playlist[idx].id);
乱数シードから実際にプレイリストの何曲目を再生するかを求めます。前回 const idx = track;
となっていた部分ですが、シャッフル再生時(seed > 0
)はシャッフルした track 番目の曲番号、通常再生時は track 自体の番号を使って url を求めます。
3. 前の曲、次の曲
case('AMAZON.NextIntent'):case('AMAZON.PreviousIntent'):{constaudioPlayer=handlerInput.requestEnvelope.context.AudioPlayer;[playlistId,seed,track,loop,,]=audioPlayer.token.split(':');track=(+track)+(requestTypeOrIntentName==='AMAZON.NextIntent'?1:-1);break;}
発話によって曲を変更するため、すぐ指定した曲を再生するよう behavior
を REPLACE_ALL
のままにする点を除いて、前回説明した AudioPlayer.PlaybackNearlyFinished
とほぼ同じです。
トークンから取得したトラック番号に対し、次の曲なら +1
、前の曲なら -1
します。
再生する曲が変わるためリピートフラグはクリアします。
if(track>=playlist.length){returnhandlerInput.responseBuilder.addAudioPlayerClearQueueDirective('CLEAR_ALL').withShouldEndSession(true).getResponse();}elseif(track<0){track=0;}
前の曲や次の曲がプレイリストの範囲を超える場合はそれぞれ以下のようにします。
- 前の曲:1 曲目を再生します(
track = 0
) - 次の曲:再生を終了します(通常の全曲再生終了時と同じ)
このあとのループ再生でこの部分は手が入ります。
4. ループ再生オン・オフ
case('AMAZON.LoopOnIntent'):case('AMAZON.LoopOffIntent'):{constaudioPlayer=handlerInput.requestEnvelope.context.AudioPlayer;[playlistId,seed,track,,repeat,]=audioPlayer.token.split(':');loop=requestTypeOrIntentName==='AMAZON.LoopOnIntent'?'loop':'';offset=audioPlayer.offsetInMilliseconds;constspeakOutput=`ループ再生を${loop==='loop'?'オン':'オフ'}にします。`;handlerInput.responseBuilder.speak(speakOutput);break;}
ループ再生オンかオフかでループフラグを loop
か ''
にセットします。
ループフラグだけを変更したトークンをキューに登録しますが、そのままだと発話によって割り込んだ再生中の曲がまた最初からの再生になってしまうため、割り込み時の曲の位置 offset
を取得しておき、その位置からの再生を指示することで再開したように見せかけます。
if(track>=playlist.length){if(loop!=='loop'){returnhandlerInput.responseBuilder.addAudioPlayerClearQueueDirective('CLEAR_ALL').withShouldEndSession(true).getResponse();}track=0;}elseif(track<0){track=loop==='loop'?playlist.length-1:0;}
ループフラグがオンの場合、前の曲や次の曲がプレイリストの範囲を超えても循環するようにトラック番号を設定します。
5. リピート再生
case('AMAZON.RepeatIntent'):{constaudioPlayer=handlerInput.requestEnvelope.context.AudioPlayer;[playlistId,seed,track,loop,,]=audioPlayer.token.split(':');repeat='repeat';offset=audioPlayer.offsetInMilliseconds;constspeakOutput='現在の曲を繰り返し再生します。';handlerInput.responseBuilder.speak(speakOutput);break;}
リピートフラグに repeat
をセットします。ループオン・オフと同様、割り込んだ曲の再生位置オフセットを取得しておいて再開したように見せかけます。
case('AudioPlayer.PlaybackNearlyFinished'):{// 曲の終了間際の場合は再生中の曲をそのままにするため REPLACE_ENQUEUED でキューを置き換えるbehavior='REPLACE_ENQUEUED';constaudioPlayer=handlerInput.requestEnvelope.context.AudioPlayer;[playlistId,seed,track,loop,repeat,]=audioPlayer.token.split(':');track=(+track)+(repeat==='repeat'?0:1);repeat='';break;}
リピートフラグは「再生中の曲を繰り返し再生する」機能のため、曲の再生終了間際に次の曲をセットする処理で同じ曲番号をセットします。リピートフラグはクリアしないため同じ曲を無限再生します。リピート解除のインテントはありませんので、代わりに「次の曲」をリクエストすることで次の曲に移りつつリピートが解除できます。
6. 一時停止、再開
case('AMAZON.PauseIntent'):{returnhandlerInput.responseBuilder.addAudioPlayerStopDirective().withShouldEndSession(true).getResponse();}case('AMAZON.ResumeIntent'):{constaudioPlayer=handlerInput.requestEnvelope.context.AudioPlayer;[playlistId,seed,track,loop,repeat,]=audioPlayer.token.split(':');offset=audioPlayer.offsetInMilliseconds;break;}
一時停止は AudioPlayer に StopDirective
を送ります。再開示はトークン値とオフセットを取得してそのまま再生指示します。
ただし「Alexa、終了して」と発話した場合に AMAZON.StopIntent
ではなく AMAZON.PauseIntent
として扱われるみたいで、スキルを終了したつもりが一時停止になったりしますので、機能的に使わないなら実装しないか、明示的に「Alexa、プレイミュージックを終了して」と発話するか、などの対応が必要になります。
7. ヘルプ
case('AMAZON.HelpIntent'):{constlistNames=['お気に入り'].join('、');constspeakOutput=`利用可能なプレイリストは、${listNames}、です。`;returnhandlerInput.responseBuilder.speak(speakOutput).getResponse();}
ひとりで使う分には関係ないですが、設定したプレイリスト名に何があったかを確認できるよう、ヘルプに対してプレイリスト名を列挙して返答するようにします。なんなら root.json
にこれようのテキストを記述して読み込むように実装する手もあります。
8. 曲情報確認
case('AskInfoIntent'):{const[,,,,,info]=handlerInput.requestEnvelope.context.AudioPlayer.token.split(':');constspeakOutput=`この曲は、${info}、です。`;returnhandlerInput.responseBuilder.speak(speakOutput).getResponse();}
再生中の曲情報を確認するための処理で、プレイリスト json の info
プロパティに記述したテキストを読み上げます。
このインテントは標準インテントではなくカスタムインテントのため、単に「Alexa、曲名を教えて」と発話しても PlayMusic スキルに対するリクエストとして処理してくれません。少し面倒ですが「Alexa、プレイミュージックで曲名を教えて」などと スキル名
+接続詞
+発話サンプル
の形で発話します。
スキル名を言わなくて済む方法を探しましたが無理でした。Name-free Interactionsという仕組みに可能性を感じましたが日本では対応していないようで、スキル名を言う形で妥協しました。
8.1. AskInfoIntent
PlayMusicIntent
を作成した時と同様に AskInfoIntent
を作成します。
曲名を訪ねる際のサンプル発話を適当に設定してください。一括編集機能を使うと1行1サンプルで記述したテキストを貼り付けられるので楽です。ここで設定したのは以下の13パターンです。
曲名
この曲
この曲名
曲名を教えて
この曲を教えて
この曲名を教えて
曲名をおしえて
この曲をおしえて
この曲名をおしえて
この曲なに
この曲はなに
この曲何
この曲は何
9. その他修正箇所
canHandle(handlerInput){return['PlaylistIntent','AMAZON.StartOverIntent','AudioPlayer.PlaybackNearlyFinished','AMAZON.NextIntent','AMAZON.PreviousIntent','AMAZON.ShuffleOnIntent','AMAZON.ShuffleOffIntent','AMAZON.LoopOnIntent','AMAZON.LoopOffIntent','AMAZON.RepeatIntent','AMAZON.PauseIntent','AMAZON.ResumeIntent','AskInfoIntent','AMAZON.HelpIntent'].includes(getRequestTypeOrIntentName(handlerInput));},
canHandle(handlerInput){return['AMAZON.CancelIntent','AMAZON.StopIntent','SessionEndedRequest'].includes(getRequestTypeOrIntentName(handlerInput));},
対応したリクエストやインテントを CancelAndStopIntentHandler
から PlayRequestHandler
に移すだけです。
おわりに
これで Google Drive 版音楽プレイヤーの実装は完了です。
再生指示できるプレイリスト名を Playlist スロットタイプに登録する、Google Drive 上のファイル ID を調べてプレイリスト json ファイルを作る、root.json にそのプレイリストを記述する、など手間は掛かりますが機能的には使えるのではないかと思います。
プレイリストの作成補助として、Google Drive のフォルダからファイルを検索し、タグ情報のタイトルを読み取ってファイル ID とタイトルを列挙する Google スプレッドシート(Google Apps Script)も作ったので番外編として記事にするかも知れません。