Alexa上で音源(MP3)を制御するための知見 [SSML, AudioPlayer]
この記事はドワンゴ Advent Calendar 2018 2の17日目の記事です。
Alexa上でMP3を再生するスキルの開発で四苦八苦したので知見をまとめておきます。
宣伝
以下四点のAlexaスキルをリリースしました。良ければ使ってください。
前提
レスポンスはJSONで表現します。
node等のsdkも最終的にオブジェクトからJSONを組み立てているはずなので、本質的には変わらないはずです。
SSMLを用いてMP3を鳴らす場合
SSMLでMP3を再生するための記法
SSMLは、Speech Synthesis Markup Languageの略で、合成音声を
制御するための記法です。
XMLベースなのでSSMLを見たことがなくても意味はわかるのではないでしょうか。
- 「こんにちは」と発話する:
<speak>
こんにちは
</speak>
これを単にAlexaに喋らせる場合、最小で以下のようなレスポンスを返すことになります。
{
"shouldEndSession": false,
"response": {
"outputSpeech": {
"type": "ssml",
"ssml": "<speak>こんにちは</speak>"
},
}
}
shouldEndSession
は必須のパラメータではありませんが、これをfalseにしておかない場合、SSMLの内容が処理されたあとにユーザーの発話を待ってしまいます。
完全なレスポンスの形を書きましたが、以後は省略しSSMLの内容のみ記述します。
先程、SSMLは合成音声を制御するための記法と書きましたが、SSMLは 音声ファイルの再生 を仕様として定義しています。
-
http://example.com/voice.mp3
を再生する:
<speak>
<audio src="http://example.com/voice.mp3" />
</speak>
audio
タグの src
属性にURLを指定することでMP3を再生できます。
複数のMP3を連続で再生したいときは単純に複数並べれば良いです。ただし、Alexaの仕様上音源は5つまでと定められています。
- 5つのMP3を再生する:
<speak>
<audio src="http://example.com/voice1.mp3" />
<audio src="http://example.com/voice2.mp3" />
<audio src="http://example.com/voice3.mp3" />
<audio src="http://example.com/voice4.mp3" />
<audio src="http://example.com/voice5.mp3" />
</speak>
再生はほぼシームレスにつながるので、音源と音源の間が詰まりすぎてて気になる場合や、間隔を開ける必要がある場合は break
タグを利用すると良いでしょう
- MP3の間を100ミリ秒、または1秒空ける:
<speak>
<audio src="http://example.com/voice1.mp3" />
<break time="100ms"/>
<audio src="http://example.com/voice2.mp3" />
<break time="1s"/>
<audio src="http://example.com/voice3.mp3" />
</speak>
MP3の形式
SSMLを用いてAlexa上でMP3を再生する場合、指定の形式に沿う必要があります。
細かい仕様はさておき、MP3をこちらのコマンドに通すことでAlexaに適した形式になります。
ffmpeg -i <input-file> -ac 2 -codec:a libmp3lame -b:a 48k -ar 16000 <output-file.mp3>
SSMLに音声を渡しても正常に再生されない?
MP3ファイルのURLを指定すると言いましたが、Alexaに再生させたいコンテンツがオープンなものではない場合、ちょっとした詰まりポイントがあります。
S3にプライベートなリソースを置いてそれにAlexaからアクセスさせたい場合、S3のリソースに対して署名付きURLを発行することになるかと思いますが、この署名付きURLは単純にSSML内に入れるとSSMLのパースエラーになってしまいます。
これはSSML内で &
が特殊な文字として解釈されるためなので、 &
に対してHTMLエスケープ( &
に置き換える)を行うことで回避できます。
Breakのちょっとした罠
<break time="0s"/>
を挟んでもらえれば分かると思いますが、break
タグを挟おうとすること自体に処理時間が必要なようで、固定の間隔(数10ms程度)が開いてしまいます。
普通なら特に気にすることではないですが、音源と音源と間隔に細かい調整が必要な場合は、
break
タグで間隔を空けるのではなく、音源自体に空白をひっつけてしまうほうが良いかもしれません。
SSMLの制限
一部は上で触れていますが、SSMLを使ったMP3の再生には制限があります。
引っかかりそうなものは以下の3点でしょう
- 音質の制限(適切なフォーマットにする必要がある)
- 5ファイル制限
- 合計240秒以内
詳しくは 公式のドキュメント にて。
SSMLの良いところ
- 複数音源の再生が容易に行える
- 1つ目の音源から2つ目の音声を再生がシームレスにつながる
- スキルのセッションを切らずに続けられる(ユーザーにさらなる応答を促すことが可能)
前述の制限をクリアできるのなら、可能な限りSSMLを利用して音声の再生を行うことをおすすめします が、結構制約がきついのでAudioPlayerを使わざるをえないこともあるでしょう。
5ファイルの制限は、予めMP3を結合することで5ファイル以内に収めるのも有効かもしれません。
AudioPlayerを用いてMP3を鳴らす場合
お手軽にMP3を再生できるSSMLですが、諸々の制限があり、用途によってはもっと高機能な音源再生機能が欲しくなるかもしれません。そこで出てくるのがAudioPlayerです。
AudioPlayerで1つ目のMP3を再生するための記法
AudioPlayerでmp3の再生を行う最低限のレスポンスです。
-
http://example.com/voice.mp3
を再生する:
{
"response": {
"directives": [
{
"type": "AudioPlayer.Play",
"playBehavior": "REPLACE_ALL",
"audioItem": {
"stream": {
"url": "https://example.com/voice.mp3",
"token": "任意の文字列"
}
}
}
],
"shouldEndSession": true
}
}
一つのMP3を再生することだけを考えるなら
json["response"]["directives"][0]["audioItem"]["stream"]["url"]
でurlを指定する事だけを覚えておけば良いでしょう。
しかしながら、複数のMP3を続けて再生したい、ユーザーのリクエストにあわせて再生中の曲を制御したい、などの場合はこれでは不十分で、AudioPlayerの仕様をより深く理解する必要があります。
なぜならば、 一つのレスポンスで複数のMP3を返す、ということがそもそもできない からです。 なんということでしょう。
directivesはリストなので、複数のPlayディレクティブを羅列すれば複数再生できそうな気もするのですが、気の所為です。エラーになります。残念ながら。
少々本題からずれますが、 shouldEndSession
が真(つまりユーザーの応答を待たずに)と指定されていることについて。
ここを偽にするとMP3の再生が 始まらず ユーザーによる応答を待つことになります。デフォルトで真なので困ることはないとは思いますが、念の為。
AudioPlayerのもつライフサイクル
AudioPlayerは、SSMLを用いたレスポンスとは異なり、セッションを一度切ってから再生を始めます。
通常のレスポンスではスキルのセッションを手放したらそこまでで、ユーザーがスキルをもう一度呼び出さない限りはAlexa側になんのアクションを起こさせることもできません。
しかし、AudioPlayerのレスポンスを返した場合、スキルが明示的に呼び出されていないにもかかわらず、リクエストをスキルに対して飛ばすことがあります。
- 音楽の再生が開始する、再生が終了する等のイベントが発生する
- ユーザーがスキルを指定せずに「アレクサ、止めて」「アレクサ、次の曲」等の発話を行う
1つ目は、Alexaが音源の再生を始めた、再生が終了する等のタイミングで逐一リクエストを送信します。
この仕組を便宜上「ライフサイクル」と当ページでは呼称していますが、公式で使われている名称ではありませんのでご注意ください。
複数のMP3を再生するには、再生がもうすぐ終了する (PlaybackNearlyFinished) というライフサイクルのリクエストに対して次の音源を指定したレスポンスを返すことで2曲目以降の音源の再生を指示する必要があります
2つ目は、Echo端末を所持しており音楽、ラジオなどの再生に利用している人には馴染み深い挙動かと思います
Alexaは最後に再生した音源がどのスキル由来なのかを覚えており、「キャンセル」「一時停止」「次の曲」等が最後に音源を再生したスキルに送られます。こちらは本題から逸れるため詳細は述べません。
AudioPlayerで2つ目以降のMP3を再生するための記法
1つ目のMP3の再生が終了する間際に、 "AudioPlayer.PlaybackNearlyFinished" というリクエストが飛んできます。
{
...
"context": { ... },
"request": {
"type": "AudioPlayer.PlaybackNearlyFinished",
...
"token": "一つ目のときに与えたtoken",
...
}
}
このリクエストに対して以下のレスポンスを返すことで、次の曲を再生キューに入れることができます。
-
http://example.com/voice.mp3
を再生する:
{
"type": "AudioPlayer.Play",
"playBehavior": "ENQUEUE",
"audioItem": {
"stream": {
"url": "https://example.com/voice.mp3",
"token": "任意の文字列",
"expectedPreviousToken": "一つ目のときに与えたtoken"
}
}
}
json["response"]["directives"][0]
のみを抜粋しています
先ほどと変わった点は2点です。
-
playBehavior
がREPLACE_ALL
からENQUEUE
になった
キューをまっさらにして新しく再生を始めるのが REPLACE_ALL
ですが、 ENQUEUE
では 1つ目が終了した際に2つ目の再生が始まるように予約します。
-
expectedPreviousToken
が追加された
playBehavior
が ENQUEUE
のときの必須パラメーターです。
上記したリクエストを見ればわかるように、 request_json["request"]["token"]
で1つ目のときのtokenが渡ってくるので、それをそのまま返せば良いです。
ちなみに、「終了直前に次の曲を指定するだけなら、2つめも REPLACE_ALL
で良いのでは」と思うかもしれませんが、この終了間際というのが曲者で、早ければ一つ目の再生が数10秒ほど残っているタイミングでリクエストが飛んできて、「 REPLACE_ALL
を返したら一つめの終わり際が強制的に打ち切られてしまった」、という事態が発生します。
また、PlaybackNearlyFinished
以外にも、PlaybackStopped
, PlaybackStarted
などのライフサイクルに対してMP3の再生を指示したくなりますが、これはできません。これは各ライフサイクルに応じて返せるレスポンスが決まっているためで、詳しい説明は ドキュメント に譲ります。
再生を終了する
再生の終了をハンドルして再生を継続する方法はわかりましたが、再生を継続したくない場合にもPlaybackNearlyFinishedに対してレスポンスを返す必要があります。
この場合なのですが、空のレスポンス({}
)を返せばたぶん良いと思います。特にエラーも起きません。
本来ならば、 AudioPlayer.ClearQueue
等の何らかの意味あるレスポンスを返するべきなのか?とも考えましたが、これに関しては何が正解なのかわかりませんでした。再生が止まれば何でも良いのかもしれません。
変数を保存したい
曲の終了に合わせて次の曲のレスポンスを返せばよい、ということはわかりましたが、もう一つ知っておかなければならないことがあります。
それは、「前回再生した曲が何なのかを知る術が限られている」、ということです。
AudioPlayerでは再生を開始すると同時にスキルのセッションを切ってしまうので、対話中の変数保存に利用できた Session Attribute
は利用できません。ではどうすればよいのでしょうか。
選択肢は2つあります。
-
token
想像がついたかもしれませんが、先程から「任意の」と言って特にどう使うかを明言していなかったtoken
を用います。
最も単純な方法を考えるなら、tokenにMP3ファイルのパス、例えば"voice1.mp3"
を保存しておき、"voice1.mp3"
のtokenを受け取ったときら、次に"voice2.mp3"
を再生させtokenに"voice2.mp3"
と保存しておく、となるでしょう。
ただこれでは不十分です。プレイリストAではvoice1
の次はvoice2
だが、プレイリストBではvoice1
の次がvoice3
かもしれません。
色々な条件下で柔軟に返すMP3を変更する、ということをしたくなった際は別の方法を考える必要があります。
一番素直に考えるなら、変数を格納した辞書をJSONにdumpし、これをtokenとして渡すという方法ではないでしょうか。実際私が開発したスキルでもそのような仕様になっているので、よっぽど複雑なことをしようとしない限り破綻しないでしょう。
JSONにする際に、token
の文字数は 1024文字まで という制限があることは覚えておいたほうがいいかもしれません。クラス構造を文字列で表現しオブジェクトとしてデシリアライズする、とかをしだすと引っかかると思います。 誰もやらないかそんなこと -
DynamoDB 等の外部リソースに頼る
もう一つは、完全に外部に依存してしまうという解決策が考えられます。この場合、複数のユーザーから同時にスキルを呼び出されたときにどのリクエストがどのユーザーからのものなのかを判別する必要があり、ユーザーIDをキーとして保存することになると思います。が正直なところこちらの方法は採用していないのであまり知見がありません。tokenでどうしても間に合わない場合は外部に頼れば最悪なんとかなるということを覚えておけばいいでしょう。
デバッグが辛い
家で一人で開発する方には関係のない話
ちょっと難しさの毛色が違うのですが、AudioPlayerのスキルを開発する際に覚えておいてほしいこととして、 AlexaコンソールにあるシミュレータはAudioPlayerに対応していない ことが挙げられます
シミュレータが使えないということは、必然的に動作の確認にEchoかAlexaアプリを使う必要があります。これらの両方がシミュレータでは可能なテキストでのコマンドの送信に対応しておらず、何かしら喋る必要があります。オフィスでEchoに喋りかけるのはなかなか辛いものがあり(周りは気にしないであろうが、私が気にする)、音がならないよう試行錯誤した結果以下のようになりました。
- Macにイヤホンをつなぐ
- Echo dotの後ろにイヤホンをつなぐ
- Echo dotのマイクに密着させる
- ターミナルから
say
コマンドを送る - イヤホンからAlexaの応答を聞く
なかなか滑稽な図になりますので奮ってお試しください。
Echo dotの第2世代はマイクが一つなのでイヤホンを密着させることで外部の音をシャットアウトしやすいです。
イヤホンのイヤーピースはコンプライがおすすめです。密着させやすいので。
1つ目と2つ目のMP3の間に間隔が空いてしまう
1つ目の再生が終了し2つ目の再生が開始するまでには、1秒程度の、しかも通信環境などに左右され予測できない長さの間隔が空く、ということは覚えておいたほうがいいかもしれません。
AudioPlayerのいいところ
- 自由度の高さ
MP3の再生に特に制限がなく、SSMLの制限を気にしなくてすみます
・・・いいところをいっぱい書こうとはしたのですが、SSMLの制約を乗り越えるための手段であって、積極的に採用したくはないものなのであまり出てきませんでした。
まとめ
- 曲の終了をハンドルして、次の曲を送る
- tokenに変数を保存する
これがわかっていればなんとかなります