Alexa Audio Playerを用いて、セッション終了後もユニークなイベントを発生させる

最近普通にスキル開発せず何かしらトリッキーなスキル開発を考えていることが多い山手です。
今回は、Alexa Audio Playerを用いてセッション終了後に任意のイベントを発火させるトリックをご紹介したいと思います。

今回ご紹介するテクニックは、下記のリリース済みスキルでも用いています。
<img width="789" alt="スクリーンショット 2019-12-22 16.59.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114168/9cae5f2e-c313-7565-f65b-7b05e0337149.png">
[寝台列車の音](https://www.amazon.co.jp/YamateMasami-寝台列車の音/dp/B07TZM2FNQ)

# Alexaのセッション
Alexaスキル開発者にとってセッション持続時間との戦いは避けられないものです。
どんなに良い音声対話スキルのアイデアがあってもセッション持続時間の壁があることで、実現できないスキルも多くあると思います。
現時点でのAlexaスキルのセッション持続時間は下記のようになっています。

- 通常：8秒
- Audio Tagを活用：240秒(4分)
- Alexa Gadgets Toolkit：90秒(1分30秒)

通常であれば、8秒でセッションがタイムアウトし、リプロンプトを入れても更に8秒ユーザーからの応答がなければスキルが自動的に終了してしまい、今までのセッション情報は破棄されてしまいます。
テクニカルに作られたスキルはスキル終了時に今までのセッション情報をDynamoDBなどを持ちて永続情報として保持させ、再度スキル起動時に以前のセッション情報を引き出す仕組みを持っていたりします。

ただし、再度スキルを用いるには「Alexa、hogehogeを開いて」とユーザーが発話する必要がありユーザー体験としては微妙なものとなってしまいます。
理想は、Alexaが能動的に発話してユーザーに通知やスキルのゲーム状況などを伝えられることで、これが実現するとより人間の対話に近い会話ができるスキルになると考えています。

## セッションを永続化させる
11月のAAJUG京都にて発表しましたが、Alexa Gadgets tool kitを用いてAlexa GadgetとEchoデバイスの間で空リクエストを送付し続けることでセッションの永続化は可能です。
しかし、Gadgetとの連携が必須であることと、ストアの審査を現状ではクリアできないため万人向けの方法ではありません。
上記の詳細は下記のページをご覧ください。
[Alexa Gadgets Toolkitを使ってAlexaスキルのセッションを永続化させる](https://qiita.com/MYamate_jp/items/0271f5b5e0b993a083ae#alexaのセッション持続時間)

そのため、ストアで公開するスキルでセッションを永続化させることは現時点では不可能です。
しかし、Alexa Audio Playerを活用することで擬似的にセッションが継続しているように見せることは可能です。

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

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

Alexa Audio Playerは、一言で言うとオーディオ再生用のインターフェイスです。
なので、音楽スキルや環境音スキルなどで用いる前提でリファレンスも用意されています。
ただし、Alexa Audio Playerで再生するオーディオは、カスタムスキル外で再生されます。
つまり、カスタムスキルのセッション外となるため、セッションは再生開始後閉じられてしまいセッション情報を永続化することは出来ません。

## 擬似的にセッションを永続化させる
セッション自体は破棄されますが、Audio Playerで定義されているインテントリクエストは、カスタムスキルのLambda functionに対して送付されます。なので、Audio Playerのインテント処理内で何らかのパラメーターを取得できれば、意図したタイミングで特定の音源を再生することが可能です。**例えば、ゲームのイベントが完了するまでは環境音を再生しておいて、クエストや任務が完了したタイミングで、作業完了のイベントとなるオーディオデータを再生することが可能になります。

## Audio PlayerのTokenを活用する
前述の通り、Audio Playerを用いる限りは、セッション情報を永続化することは出来ませんが、オーディオを識別するためのTokenパラメーターがAudio Playerには用意されているので、Tokenにユーザーのステータス値などを付与することでセッション外でも擬似的にセッション情報を生かしておくことが可能です。

# 活用事例
私が提供している「寝台列車の音」では、下記のような動作で動いています。

1.ユーザーがタイマー時刻をセットし、環境音の再生を開始する
2.スキル側で環境音の再生を開始、Audio PlayerのTokenに対してタイマー時刻をセット
3.1分刻みの音源をループ再生させる(再生キューに追加時、Tokenに付与されている時刻とLambda実行時刻を比較)
4.Tokneに付与されている時刻と実行時刻が特定のしきい値以内の時間になったらチャイムを再生する
5.スキル終了

タイマースキルであるので、無理やり1分間の音源を無限ループさせて時間の判別を行っています。

```audio.js
//再生のロジック部分
function play(handlerInput) {
    let sessionAttr = handlerInput.attributesManager.getSessionAttributes();
    let url = environmental_sound;
    let token = getUuid() + "$" + sessionAttr.timer;
    return handlerInput.responseBuilder
        .speak('準備が整いました。出発いたします。')
        .addAudioPlayerPlayDirective('REPLACE_ALL', url, token, 0, null)
        .getResponse();
}

//中断のロジック部分
function pause(handlerInput) {
    return handlerInput.responseBuilder
        .addAudioPlayerStopDirective()
        .getResponse();
}

//再開のロジック部分
function resume(handlerInput) { 
    let audioPlayer = handlerInput.requestEnvelope.context.AudioPlayer;
    let url = environmental_sound;
    let token = audioPlayer.token;
    let offset = audioPlayer.offsetInMilliseconds;
    return handlerInput.responseBuilder
        .addAudioPlayerPlayDirective('REPLACE_ALL', url, token, offset, null)
        .getResponse();
}

//再生終了直前のロジック部分
function playbackNearlyFinished(handlerInput) {
    let audioPlayer = handlerInput.requestEnvelope.context.AudioPlayer;
    let url = environmental_sound;
    let timeParm = audioPlayer.token.split('$');
    let time = "";
    if (timeParm.length == 2) {
        time = timeParm[1];
    }
    //時間の判定を行います
    let hm = time.split(":");
    if (hm.length == 2) {
        let date = new Date();
        let hour = date.getHours() + 9;
        let min = date.getMinutes();
        if (hour == hm[0] && min == hm[1]) {
            //時間が一致した場合、再生する音源を変更する
            url = haikensu_sound;
            time = "";
        }
    }
    let token = getUuid() + "$" + time;
    let expectedPreviousToken = audioPlayer.token;
    return handlerInput.responseBuilder
        .addAudioPlayerPlayDirective('ENQUEUE', url, token, 0, expectedPreviousToken)
        .getResponse();
}

//再生停止のロジック部分
function stop(handlerInput) {
    return handlerInput.responseBuilder
        .addAudioPlayerStopDirective()
        .getResponse();
}

```

実際のコードは上記のようになっています。
設定時刻が確定後、``function play``が実行され、セッション情報に保持されている設定時刻とランダムに生成したUUIDを付与し、音源の再生終了直前に実行される``function playbackNearlyFinished``にてチャイムを再生するか否かを判別するようにしています。

## 注意点
### Lambdaの呼び出し回数
今提供しているスキルのような1分毎に時刻の確認を行うような機能だと、**Lambda functionの実行回数が非常に多くなります。**破産するような額にはなりませんが、かなり無駄なことになります。
そのため、特定の再生時間後にゲームの進捗を確認するなど長時間の作業後にイベントステータスを確認するような場合には新しい体験を音声を通じて提供することが可能になると思います。