#背景
2019年10月に999円で買ったAmazon Echo Dot。なんとなく使い道に困っていた。
「自前のMP3再生できればいいのに」そう思ってググったら、やってる人がいた。
Alexa Skill KitでAudioPlayerスキルを実装する(NodeJS編)
https://qiita.com/yoshikawaa/items/bbf2791f397ce1efe44e
Alexaのスキルを自作して手持ちの音楽ファイルを再生させよう
http://android-smart.com/2019/06/alexaskilldevelopmusic.html
私は、C/C++/C#をベースとしたソフトウェア開発を生業としているが、Node.JSやJavascriptは不慣れ。
Alexaスキルを開発すればまた違った楽しみ方があるはず。連休になったので、基礎から勉強してみようと思った。
自分のイメージするMP3プレーヤーにするために、上のサイトを参考にし、いい感じのものができました。
制限事項
なぜかS3に置いたmp3では音が鳴らないのだが(誰か教えて~、CloudWatchでデバッグできないんです)、外部サーバに置けば問題なし。
私のたどった道どりを書き留めておきます。どなたかの参考になればと思います。
#前提条件
アカウント作成やデバイス登録などは他サイトを参考にしてください。
まず経験のない方は、「Alexa道場」 Season4 までは見てください。
https://developer.amazon.com/ja-JP/alexa/alexa-skills-kit/get-deeper/webinars
#私のイメージするMP3プレーヤー
「アレクサ、〇〇〇で、あいみょんを再生して」 (〇〇〇はスキル名)
と言ったら、登録しておいた私のお気に入りのmp3の曲がシャッフル再生され、「次」「前」で曲の移動ができる、iPod shuffleのようなシンプルなもの。
Amazon Echo Showなどでは、カバーアートや背景画像が設定できるみたいですが、私は持っていないので、、、
上記2つのサイトのコードを組み合わせる必要がありました。
#スキル開発
・スキルの作成ボタンを押します
・スキル名は「テストプレーヤー」にしました。デフォルトのまま、「カスタム」で「Alexa-Hosted(Node.js)」で次に進みます。
・ハローワールドスキルのテンプレートで作成します。
##ビルドタブ
・しばらく待つと、スキルが作成されます。
・「インターフェース」で「Audio Player」をOnにします。「インターフェースを保存」して、「モデルをビルド」します。
・スロットタイプで「+スロットタイプ」ボタンを押し、「MyPlay_list」を入力して、「Next」を押します。
・スロット値に「ドレミ」と入力し、Enterキーを押します。
・IDに「001」を入力し、必要に応じて同義語を追加します。
・「インテント」で「HelloWorldIntent」をクリックします。
・インテントの名称を「HelloWorldIntent」⇒「PlaylistIntent」に変更し、サンプル発話の3つを削除します。
・サンプルに 「{MyPlay_list}を再生して」 と入力してEnterキーを押します。※{MyPlay_list}はスロットになります。わからない人は「Alexa道場」を見ましょう。
・スロットタイプを「MyPlay_list」にします。
・「モデルを保存」し、「モデルをビルド」します。
##コードエディタタブ
・コードエディタタブに移動し、index.jsファイルの中身を下のソースコードで置き換えます。
・フォルダを作成し、「lambda/playlists/001.json」ファイルを作成します。
・画面右下の「メディアストレージ」をクリックし、mp3ファイルをアップロードします。
・「001.json」ファイルを編集し、「保存」、「デプロイ」 します。
##テストタブ
・テストタブに移動し、「開発中」に変更します。Alexaシミュレータに「テストプレーヤーでドレミを再生して」と入力してテストします。
・うまくいけば右側のJSON出力に次のような表示がされ、右下に「Unsupported Directive」のメッセージが表示されます。これは、Audio PlayerはAlexaシミュレータでは動作しないというメッセージになります。
##実機(Echo Dot)でテスト
Echo Dotに向かって 「テストプレーヤーでドレミを再生して」 と言ってみましょう!
制限事項
なぜかS3に置いたmp3では音が鳴らないのです。外部サーバに置けば問題なし。※後述
#ソースコード
const token_prefix = 'testPlayer';
const Util = require('util.js');
const Alexa = require('ask-sdk-core');
const LaunchRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
},
handle(handlerInput) {
const speakOutput = 'どのプレイリストを再生しますか?';
return handlerInput.responseBuilder
.speak(speakOutput)
.reprompt(speakOutput)
.getResponse();
}
};
const PlaylistIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'PlaylistIntent';
},
handle(handlerInput) {
const resolvedSlot = handlerInput.requestEnvelope.request.intent.slots.playlist.resolutions.resolutionsPerAuthority[0].values;
if (resolvedSlot === undefined) {
return handlerInput.responseBuilder
.speak('プレイリストが検索できませんでした。')
.getResponse();
}
const behavior = 'REPLACE_ALL';
const file = resolvedSlot[0].value.id;
const playlist = require(`./playlists/${file}.json`);
const pos = 0;
const offset = 0;
//シャッフルリスト作成
var order = [], i;
for (i = 0; i < playlist.length; i++) {
order.push(i);
}
for (i = order.length - 1; i > 0; i--) {
var r = Math.floor(Math.random() * (i + 1)), t = order[i]; order[i] = order[r]; order[r] = t;
}
//URLとTOKENの作成
const url = playlist[order[pos] * 1].url;
const token = [token_prefix, file, pos, order.join(',')].join(':');
return handlerInput.responseBuilder.addAudioPlayerPlayDirective(
behavior,
Util.getS3PreSignedUrl(url), //url, //
token,
offset, void (0), null
).withShouldEndSession(true).getResponse();
}
};
exports.handler = Alexa.SkillBuilders.custom().addRequestHandlers(
LaunchRequestHandler,
PlaylistIntentHandler,
{
canHandle(input) { return true; },
handle(input) {
var behavior = 'REPLACE_ALL';
//tokenには ['プレフィックス':'プレイリストのファイル名':'現在の再生位置':'シャッフルされた曲順オーダー(カンマ区切り)']が入っている
//"token": "testPlayer:001:0:7,6,5,0,1,2,3,4"
var token = input.requestEnvelope.context.AudioPlayer.token;
const file = (token.split(/:/))[1];
const playlist = require(`./playlists/${file}.json`);
var pos = (token.split(/:/))[2] * 1;
const t3 = (token.split(/:/))[3];
var order = t3.split(/,/);
var offset = (input.requestEnvelope.context.AudioPlayer || {}).offsetInMilliseconds || 0;
switch (('IntentRequest' === input.requestEnvelope.request.type) ? input.requestEnvelope.request.intent.name : input.requestEnvelope.request.type) {
case ('AMAZON.StartOverIntent'):
case ('AMAZON.RepeatIntent'): {
offset = 0;
pos = 0;
break;
}
case ('AudioPlayer.PlaybackNearlyFinished'):
case ('AMAZON.NextIntent'):
case ('PlaybackController.NextCommandIssued'): {
pos++;
if (pos === playlist.length) {
pos = 0; //シャッフルオーダーの最後の場合最初に戻る
}
offset = 0;
if (input.requestEnvelope.request.type === 'PlaybackNearlyFinished') {
behavior = 'REPLACE_ENQUEUED';
}
break;
}
case ('AMAZON.PreviousIntent'):
case ('PlaybackController.PreviousCommandIssued'): {
pos--;
if (pos < 0) {
pos = playlist.length - 1; //先頭の場合、シャッフルオーダーの最後に設定
}
offset = 0;
break;
}
case ('AMAZON.StopIntent'):
case ('AMAZON.CancelIntent'): {
return input.responseBuilder.addAudioPlayerClearQueueDirective('CLEAR_ALL').withShouldEndSession(true).getResponse();
}
case ('PlaybackController.PauseCommandIssued'):
case ('AMAZON.PauseIntent'): {
return input.responseBuilder.addAudioPlayerStopDirective().withShouldEndSession(true).getResponse();
}
case ('PlaybackController.PlayCommandIssued'):
case ('AMAZON.ResumeIntent'): {
break;
}
case ('AudioPlayer.PlaybackFailed'): {
return input.responseBuilder.speak('再生できませんでした').withShouldEndSession(true).getResponse();
}
case ('System.ExceptionEncountered'): {
return input.responseBuilder.withShouldEndSession(true).getResponse();
}
case ('AMAZON.HelpIntent'):
case ('AudioPlayer.PlaybackStarted'):
case ('AudioPlayer.PlaybackFinished'):
case ('AudioPlayer.PlaybackStopped'):
case ('SessionEndedRequest'):
default: {
return input.responseBuilder.withShouldEndSession(true).getResponse();
}
}
//URLとTOKENの作成
const url = playlist[order[pos] * 1].url;
token = [token_prefix, file, pos, order.join(',')].join(':');
return input.responseBuilder.addAudioPlayerPlayDirective(
behavior,
Util.getS3PreSignedUrl(url), //url, //
token,
offset, void (0), null
).withShouldEndSession(true).getResponse();
}
}).addErrorHandlers({
canHandle() { return true; },
handle(input, error) {
return input.responseBuilder.speak('すみません わかりません').withShouldEndSession(true).getResponse();
}
}).lambda();
[
{"index": "000", "url":"Media/pianoC.mp3"},
{"index": "001", "url":"Media/pianoD.mp3"},
{"index": "002", "url":"Media/pianoE.mp3"},
{"index": "003", "url":"Media/pianoF.mp3"},
{"index": "004", "url":"Media/pianoG.mp3"},
{"index": "005", "url":"Media/pianoA.mp3"},
{"index": "006", "url":"Media/pianoB.mp3"},
{"index": "007", "url":"Media/pianoC2.mp3"}
]
#外部サーバ
なぜかS3に置いたmp3では音が鳴らないのです。CloudWatchで次のようなエラーが表示されて、デバッグできないんです。
次のようにソースを変更して、mp3ファイルを外部サーバに置けば音は鳴るので、なにかがおかしいはずなのですが、わかりません。
index.jsファイルの、次の部分を2か所を変更
url, //Util.getS3PreSignedUrl(url), //
プレイリストファイルを、httpsに対応したサイトに変更
[
{"index": "000", "url":"https://~~.ne.jp/~~/mp3/pianoC.mp3"},
{"index": "001", "url":"https://~~.ne.jp/~~/mp3/pianoD.mp3"},
{"index": "002", "url":"https://~~.ne.jp/~~/mp3/pianoE.mp3"},
{"index": "003", "url":"https://~~.ne.jp/~~/mp3/pianoF.mp3"},
{"index": "004", "url":"https://~~.ne.jp/~~/mp3/pianoG.mp3"},
{"index": "005", "url":"https://~~.ne.jp/~~/mp3/pianoA.mp3"},
{"index": "006", "url":"https://~~.ne.jp/~~/mp3/pianoB.mp3"},
{"index": "007", "url":"https://~~.ne.jp/~~/mp3/pianoC2.mp3"}
]
#最後に
Alexa道場ではCloudWatchにログが書かれて、デバッグができるようなことを言っていたのですが、私の環境ではなぜかログが出力されなくてエラーになってしまっています。AWSのアカウントを登録してみましたが、うまくいきません。どなたか原因のわかる方がいらっしゃったら、ご教授願います。