I've followed the sample code from https://github.com/alexa/skill-sample-nodejs-audio-player and gotten the example podcast player to play via my Amazon Echo.
How would one modify this code in order to tell Alexa to "Ask MySkill to Play $trackname". Instead of just playing from the top?
I'm very new to creating skills but I've read all the documentation and understand that this involves Audio Directives. However, I can't figure out what goes where.
In the sample code, the audioAssets.js contains a list of titles and urls. So for example, if I wanted to say "Play Episode 138" (one of the titles) - which files would I need to modify in order to do this?
'use strict';
var audioData = [
{
'title' : 'Episode 140',
'url' : 'https://feeds.soundcloud.com/stream/275202399-amazon-web- services-306355661-amazon-web-services.mp3'
},
{
'title' : 'Episode 139',
'url' : 'https://feeds.soundcloud.com/stream/274166909-amazon-web-services-306355661-aws-podcast-episode-139.mp3'
},
{
'title' : 'Episode 138',
'url' : 'https://feeds.soundcloud.com/stream/273105224-amazon-web-services-306355661-aws-podcast-episode-138.mp3'
},
{
'title' : 'Episode 137',
'url' : 'https://feeds.soundcloud.com/stream/272089501-amazon-web-services-306355661-aws-podcast-episode-137.mp3'
}
];
module.exports = audioData;
I'm assuming the code would go into stateHandlers.js but honestly I'm not sure.
'use strict';
var Alexa = require('alexa-sdk');
var audioData = require('./audioAssets');
var constants = require('./constants');
var stateHandlers = {
startModeIntentHandlers : Alexa.CreateStateHandler(constants.states.START_MODE, {
/*
* All Intent Handlers for state : START_MODE
*/
'LaunchRequest' : function () {
// Initialize Attributes
this.attributes['playOrder'] = Array.apply(null, {length: audioData.length}).map(Number.call, Number);
this.attributes['index'] = 0;
this.attributes['offsetInMilliseconds'] = 0;
this.attributes['loop'] = true;
this.attributes['shuffle'] = false;
this.attributes['playbackIndexChanged'] = true;
// Change state to START_MODE
this.handler.state = constants.states.START_MODE;
var message = 'Welcome to the AWS Podcast. You can say, play the audio to begin the podcast.';
var reprompt = 'You can say, play the audio, to begin.';
this.response.speak(message).listen(reprompt);
this.emit(':responseReady');
},
'PlayAudio' : function () {
if (!this.attributes['playOrder']) {
// Initialize Attributes if undefined.
this.attributes['playOrder'] = Array.apply(null, {length: audioData.length}).map(Number.call, Number);
this.attributes['index'] = 0;
this.attributes['offsetInMilliseconds'] = 0;
this.attributes['loop'] = true;
this.attributes['shuffle'] = false;
this.attributes['playbackIndexChanged'] = true;
// Change state to START_MODE
this.handler.state = constants.states.START_MODE;
}
controller.play.call(this);
},
'AMAZON.HelpIntent' : function () {
var message = 'Welcome to the AWS Podcast. You can say, play the audio, to begin the podcast.';
this.response.speak(message).listen(message);
this.emit(':responseReady');
},
'AMAZON.StopIntent' : function () {
var message = 'Good bye.';
this.response.speak(message);
this.emit(':responseReady');
},
'AMAZON.CancelIntent' : function () {
var message = 'Good bye.';
this.response.speak(message);
this.emit(':responseReady');
},
'SessionEndedRequest' : function () {
// No session ended logic
},
'Unhandled' : function () {
var message = 'Sorry, I could not understand. Please say, play the audio, to begin the audio.';
this.response.speak(message).listen(message);
this.emit(':responseReady');
}
}),
playModeIntentHandlers : Alexa.CreateStateHandler(constants.states.PLAY_MODE, {
/*
* All Intent Handlers for state : PLAY_MODE
*/
'LaunchRequest' : function () {
/*
* Session resumed in PLAY_MODE STATE.
* If playback had finished during last session :
* Give welcome message.
* Change state to START_STATE to restrict user inputs.
* Else :
* Ask user if he/she wants to resume from last position.
* Change state to RESUME_DECISION_MODE
*/
var message;
var reprompt;
if (this.attributes['playbackFinished']) {
this.handler.state = constants.states.START_MODE;
message = 'Welcome to the AWS Podcast. You can say, play the audio to begin the podcast.';
reprompt = 'You can say, play the audio, to begin.';
} else {
this.handler.state = constants.states.RESUME_DECISION_MODE;
message = 'You were listening to ' + audioData[this.attributes['playOrder'][this.attributes['index']]].title +
' Would you like to resume?';
reprompt = 'You can say yes to resume or no to play from the top.';
}
this.response.speak(message).listen(reprompt);
this.emit(':responseReady');
},
'PlayAudio' : function () { controller.play.call(this) },
'AMAZON.NextIntent' : function () { controller.playNext.call(this) },
'AMAZON.PreviousIntent' : function () { controller.playPrevious.call(this) },
'AMAZON.PauseIntent' : function () { controller.stop.call(this) },
'AMAZON.StopIntent' : function () { controller.stop.call(this) },
'AMAZON.CancelIntent' : function () { controller.stop.call(this) },
'AMAZON.ResumeIntent' : function () { controller.play.call(this) },
'AMAZON.LoopOnIntent' : function () { controller.loopOn.call(this) },
'AMAZON.LoopOffIntent' : function () { controller.loopOff.call(this) },
'AMAZON.ShuffleOnIntent' : function () { controller.shuffleOn.call(this) },
'AMAZON.ShuffleOffIntent' : function () { controller.shuffleOff.call(this) },
'AMAZON.StartOverIntent' : function () { controller.startOver.call(this) },
'AMAZON.HelpIntent' : function () {
// This will called while audio is playing and a user says "ask <invocation_name> for help"
var message = 'You are listening to the AWS Podcast. You can say, Next or Previous to navigate through the playlist. ' +
'At any time, you can say Pause to pause the audio and Resume to resume.';
this.response.speak(message).listen(message);
this.emit(':responseReady');
},
'SessionEndedRequest' : function () {
// No session ended logic
},
'Unhandled' : function () {
var message = 'Sorry, I could not understand. You can say, Next or Previous to navigate through the playlist.';
this.response.speak(message).listen(message);
this.emit(':responseReady');
}
}),
remoteControllerHandlers : Alexa.CreateStateHandler(constants.states.PLAY_MODE, {
/*
* All Requests are received using a Remote Control. Calling corresponding handlers for each of them.
*/
'PlayCommandIssued' : function () { controller.play.call(this) },
'PauseCommandIssued' : function () { controller.stop.call(this) },
'NextCommandIssued' : function () { controller.playNext.call(this) },
'PreviousCommandIssued' : function () { controller.playPrevious.call(this) }
}),
resumeDecisionModeIntentHandlers : Alexa.CreateStateHandler(constants.states.RESUME_DECISION_MODE, {
/*
* All Intent Handlers for state : RESUME_DECISION_MODE
*/
'LaunchRequest' : function () {
var message = 'You were listening to ' + audioData[this.attributes['playOrder'][this.attributes['index']]].title +
' Would you like to resume?';
var reprompt = 'You can say yes to resume or no to play from the top.';
this.response.speak(message).listen(reprompt);
this.emit(':responseReady');
},
'AMAZON.YesIntent' : function () { controller.play.call(this) },
'AMAZON.NoIntent' : function () { controller.reset.call(this) },
'AMAZON.HelpIntent' : function () {
var message = 'You were listening to ' + audioData[this.attributes['index']].title +
' Would you like to resume?';
var reprompt = 'You can say yes to resume or no to play from the top.';
this.response.speak(message).listen(reprompt);
this.emit(':responseReady');
},
'AMAZON.StopIntent' : function () {
var message = 'Good bye.';
this.response.speak(message);
this.emit(':responseReady');
},
'AMAZON.CancelIntent' : function () {
var message = 'Good bye.';
this.response.speak(message);
this.emit(':responseReady');
},
'SessionEndedRequest' : function () {
// No session ended logic
},
'Unhandled' : function () {
var message = 'Sorry, this is not a valid command. Please say help to hear what you can say.';
this.response.speak(message).listen(message);
this.emit(':responseReady');
}
})
};
module.exports = stateHandlers;
var controller = function () {
return {
play: function () {
/*
* Using the function to begin playing audio when:
* Play Audio intent invoked.
* Resuming audio when stopped/paused.
* Next/Previous commands issued.
*/
this.handler.state = constants.states.PLAY_MODE;
if (this.attributes['playbackFinished']) {
// Reset to top of the playlist when reached end.
this.attributes['index'] = 0;
this.attributes['offsetInMilliseconds'] = 0;
this.attributes['playbackIndexChanged'] = true;
this.attributes['playbackFinished'] = false;
}
var token = String(this.attributes['playOrder'][this.attributes['index']]);
var playBehavior = 'REPLACE_ALL';
var podcast = audioData[this.attributes['playOrder'][this.attributes['index']]];
var offsetInMilliseconds = this.attributes['offsetInMilliseconds'];
// Since play behavior is REPLACE_ALL, enqueuedToken attribute need to be set to null.
this.attributes['enqueuedToken'] = null;
if (canThrowCard.call(this)) {
var cardTitle = 'Playing ' + podcast.title;
var cardContent = 'Playing ' + podcast.title;
this.response.cardRenderer(cardTitle, cardContent, null);
}
this.response.audioPlayerPlay(playBehavior, podcast.url, token, null, offsetInMilliseconds);
this.emit(':responseReady');
},
stop: function () {
/*
* Issuing AudioPlayer.Stop directive to stop the audio.
* Attributes already stored when AudioPlayer.Stopped request received.
*/
this.response.audioPlayerStop();
this.emit(':responseReady');
},
playNext: function () {
/*
* Called when AMAZON.NextIntent or PlaybackController.NextCommandIssued is invoked.
* Index is computed using token stored when AudioPlayer.PlaybackStopped command is received.
* If reached at the end of the playlist, choose behavior based on "loop" flag.
*/
var index = this.attributes['index'];
index += 1;
// Check for last audio file.
if (index === audioData.length) {
if (this.attributes['loop']) {
index = 0;
} else {
// Reached at the end. Thus reset state to start mode and stop playing.
this.handler.state = constants.states.START_MODE;
var message = 'You have reached at the end of the playlist.';
this.response.speak(message).audioPlayerStop();
return this.emit(':responseReady');
}
}
// Set values to attributes.
this.attributes['index'] = index;
this.attributes['offsetInMilliseconds'] = 0;
this.attributes['playbackIndexChanged'] = true;
controller.play.call(this);
},
playPrevious: function () {
/*
* Called when AMAZON.PreviousIntent or PlaybackController.PreviousCommandIssued is invoked.
* Index is computed using token stored when AudioPlayer.PlaybackStopped command is received.
* If reached at the end of the playlist, choose behavior based on "loop" flag.
*/
var index = this.attributes['index'];
index -= 1;
// Check for last audio file.
if (index === -1) {
if (this.attributes['loop']) {
index = audioData.length - 1;
} else {
// Reached at the end. Thus reset state to start mode and stop playing.
this.handler.state = constants.states.START_MODE;
var message = 'You have reached at the start of the playlist.';
this.response.speak(message).audioPlayerStop();
return this.emit(':responseReady');
}
}
// Set values to attributes.
this.attributes['index'] = index;
this.attributes['offsetInMilliseconds'] = 0;
this.attributes['playbackIndexChanged'] = true;
controller.play.call(this);
},
loopOn: function () {
// Turn on loop play.
this.attributes['loop'] = true;
var message = 'Loop turned on.';
this.response.speak(message);
this.emit(':responseReady');
},
loopOff: function () {
// Turn off looping
this.attributes['loop'] = false;
var message = 'Loop turned off.';
this.response.speak(message);
this.emit(':responseReady');
},
shuffleOn: function () {
// Turn on shuffle play.
this.attributes['shuffle'] = true;
shuffleOrder((newOrder) => {
// Play order have been shuffled. Re-initializing indices and playing first song in shuffled order.
this.attributes['playOrder'] = newOrder;
this.attributes['index'] = 0;
this.attributes['offsetInMilliseconds'] = 0;
this.attributes['playbackIndexChanged'] = true;
controller.play.call(this);
});
},
shuffleOff: function () {
// Turn off shuffle play.
if (this.attributes['shuffle']) {
this.attributes['shuffle'] = false;
// Although changing index, no change in audio file being played as the change is to account for reordering playOrder
this.attributes['index'] = this.attributes['playOrder'][this.attributes['index']];
this.attributes['playOrder'] = Array.apply(null, {length: audioData.length}).map(Number.call, Number);
}
controller.play.call(this);
},
startOver: function () {
// Start over the current audio file.
this.attributes['offsetInMilliseconds'] = 0;
controller.play.call(this);
},
reset: function () {
// Reset to top of the playlist.
this.attributes['index'] = 0;
this.attributes['offsetInMilliseconds'] = 0;
this.attributes['playbackIndexChanged'] = true;
controller.play.call(this);
}
}
}();
function canThrowCard() {
/*
* To determine when can a card should be inserted in the response.
* In response to a PlaybackController Request (remote control events) we cannot issue a card,
* Thus adding restriction of request type being "IntentRequest".
*/
if (this.event.request.type === 'IntentRequest' && this.attributes['playbackIndexChanged']) {
this.attributes['playbackIndexChanged'] = false;
return true;
} else {
return false;
}
}
function shuffleOrder(callback) {
// Algorithm : Fisher-Yates shuffle
var array = Array.apply(null, {length: audioData.length}).map(Number.call, Number);
var currentIndex = array.length;
var temp, randomIndex;
while (currentIndex >= 1) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
temp = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temp;
}
callback(array);
}
You need to create a custom intent and custom slot to your skill.
Go to your interaction model
add this
In your sample Utterances add
This will let user to able to say
play episode 140
etc...Then in your
var stateHandlers = { startModeIntentHandlers
create a new function forPodcastIntent
That part is up to you , the code I write wont work but should give you some idea, maybe something like
check the https://github.com/alexa for the AMAZON.NUMBER intents...