Alexa AudioPlayerで同じ音源をループ再生させるには

#はじめに
ちょっとした環境音スキルを作る時に躓いたポイントがあったので、簡単に記事として残したいと思います。

#Alexaで音を扱うには
大きく分けて2つの方法があります。

- SSMLのAudioタグを利用する
- AudioPlayerインターフェイス

## SSMLのAudioタグを利用する
通常の``speak``タグの中に``audio``タグを含めることで、スキルからのレスポンスで音声を再生することができます。

```xml
<speak>
    タクシー予約へようこそ。
    <audio src="soundbank://soundlibrary/transportation/amzn_sfx_car_accelerate_01" />
    配車または料金の見積もりをリクエストできます。
    どちらにしますか？
</speak>
```
[音声合成マークアップ言語(SSML)のリファレンスより引用](https://developer.amazon.com/ja/docs/custom-skills/speech-synthesis-markup-language-ssml-reference.html#audio)

### Audioタグのメリット
#### セッションが切れないので、1つの返答として扱える

セッションが切れないので、音声再生後に何らかのアクションを継続的に受け取ることができます。
もくもく会などでゲームスキルがよく取り扱われますが、ゲームのプレイヤーが考えている間に音楽を流してセッションを意図的に継続させるときによく使われています。
### Audioタグのデメリット
#### 音声の再生時間の上限がある
１つの返答内容にAudioタグは、**最大5つ**まで含めることができます。
また、5つのファイルの合計再生時間が**240秒(4分)**以内であることが必須です。

## AudioPlayerインターフェイス
>AudioPlayerインターフェースは、オーディオをストリーミングしたり再生状況を監視したりするディレクティブとリクエストを提供します。スキルは、ディレクティブを送信して再生を開始したり停止したりできます。AlexaサービスはAudioPlayerリクエストをスキルに送信し、トラックの終了間際や再生の開始時と停止時など、再生状況に関する情報を提供します。また、Alexaは、リモコンなどのハードウェアボタンや、画面付きのAlexa搭載デバイスの次へ／前へタッチコントロールに応答して、PlaybackControllerリクエストも送信します。

[AudioPlayerインターフェースのリファレンス](https://developer.amazon.com/ja/docs/custom-skills/audioplayer-interface-reference.html)

要は、Alexaスキルでジュークボックススキルを作ったりする時に利用する音楽再生用のインターフェイスのことです。

### AudioPlayerインターフェイスのメリット
#### 任意のhpptsホスティングしている音源を再生することができる。
Audioタグと異なり再生するファイル、再生時間の制約がありません。
なので、音源さえ用意できればスキル経由で、好きな音源を再生することができます。

### AudioPlayerインターフェイスのデメリット
#### セッションが継続できない
音源の再生開始後、セッションが完了されてしまうので、スキル側からの返答を行うことができません。
なので、音源再生開始後に``Alexa、ヘルプ！``と発話するとスキル側のヘルプインテントではなく、Alexaそのもののヘルプ時の返答が帰ってきます。
AudioPlayerを使うと、音を使ってスキルから返答するようなことは出来ないということです。

## Audioの使い分け
**スキル内での体験を向上させたい**
→ SSMLの``audio``タグを利用する
**スキルを通して音楽を利用したい**
→ AudioPlayerインターフェイスを活用しましょう

## AudioPlayerインターフェイスで同一音源をループ再生するには
今回の本題です。
最近流行り(流行ってないかも...)の環境音スキルなど、特定の環境を再現するために特定の音源をループ再生したいと思うこともあるかと思います。
そのような時、次の音源の読み込みには``PlaybackNearlyFinishedリクエスト``を用いて次の音源を処理することになります。

```javascript:audioplay-intent.js
//再生したい音源のURL
const soundUrl = "https://hogehoge/fugafuga.mp3";

//再生終了直前のインテント
const playbackNearlyFinishedHandler = {
    canHandle(handlerInput) {
        return Util.checkIntentTypeName(handlerInput, 'AudioPlayer.PlaybackNearlyFinished');
    },
    handle(handlerInput) {
        return playbackNearlyFinished(handlerInput);
    }
}

//再生終了直前のロジック部分
function playbackNearlyFinished(handlerInput) {
    let audioPlayer = handlerInput.requestEnvelope.context.AudioPlayer;
    let url = soundUrl; //再生音源URLの指定
    let token = audioPlayer.token; //オーディオストリームを識別するためのトークン
    let expectedPreviousToken = audioPlayer.token; //前のストリームの識別トークン
    return handlerInput.responseBuilder
        .addAudioPlayerPlayDirective('ENQUEUE', url, token, 0, expectedPreviousToken)
        .getResponse();
}
```
※``Util.checkIntentTypeName``では、RequestTypeとIntentNameの判別を行っています。

当初、同一音源なので、再生音源のURLとtokenは前のストリームと同じものを付与していました。
しかし、上記のコードでは新しいストリームとして生成することが出来ず、指定したファイルの再生時間後自動で再生が停止してしまいます。

### ループ再生するには
```javascript:audioplay-intent.js
//再生したい音源のURL
const soundUrl = "https://hogehoge/fugafuga.mp3";

//再生終了直前のインテント
const playbackNearlyFinishedHandler = {
    canHandle(handlerInput) {
        return Util.checkIntentTypeName(handlerInput, 'AudioPlayer.PlaybackNearlyFinished');
    },
    handle(handlerInput) {
        return playbackNearlyFinished(handlerInput);
    }
}

//再生終了直前のロジック部分
function playbackNearlyFinished(handlerInput) {
    let audioPlayer = handlerInput.requestEnvelope.context.AudioPlayer;
    let url = soundUrl; //再生音源URLの指定
>>  let token = String(Math.random()); //オーディオストリームを識別するためのトークン
    let expectedPreviousToken = audioPlayer.token; //前のストリームの識別トークン
    return handlerInput.responseBuilder
        .addAudioPlayerPlayDirective('ENQUEUE', url, token, 0, expectedPreviousToken)
        .getResponse();
}
```
原因は単純なことです。
token値の更新を行わず、前回のストリームと同一トークンを設定していたため、前回のストリームと新しいストリームの識別が出来ず、新しいストリームの再生が行えなかったということでした。
なので、非常に簡易的な方法として新しいTokenは``Math.random()``などで生成した乱数をStirngにキャストし、``addAudioPlayerPlayDirective``の新しいストリームのtoken値の引数に先程の乱数を渡すことで、前回のストリームと新しいストリームの識別が可能になり、同一音源のループ再生が可能になります。

非常に初歩的な見落としですが、audioplayerのスキルの開発事例がまだ少なく見落としがちなポイントでしたので記録として参考になれば幸いです。

## 最後に
毎月、Amazon Japan目黒本社にて「もくもく会」が開催されています。
もくもく会と聞くと、**スキルがある人**が**もくもく**と開発するようなイメージになりがちですが、全くAlexaスキルの開発経験がない方も多く参加していただいており、初学者向けのスキル開発講習なんかも冒頭に行われています。

最近参加人数が少し減り気味なので、興味がある方は是非参加してみてください！
詳細は下記からチェック！
[Amazon Alexa 関連のイベント ハンズオントレーニング、コミュニティーイベント](https://developer.amazon.com/ja/alexa/events)