請 Cortana 唱歌是我夢寐以求的功能,因爲 Amazon Alexa / Google Home 都能支援,但是之前 Cortana Skill 不支援。
微軟最新公佈 Add audio streaming to your skill 就讓我來介紹要怎麽使用。
如果您也研究過 Amazon Alexa 與 Google Home 會明白預設的 Music service (或稱 provider) 均是直接整合到系統裡,使用者只需要說:"Alexa, play music by [artist]."。
系統就會找到對應的歌手或是歌曲,再轉給整合的 Music service 拿到播放的 streaming。
本篇介紹是利用 Skill 的機制加入播放功能,跟 Cortana 設備提供的 Music Provider 機制不一樣。
Skill 支援播放 1 ~ N 首歌曲,在每一首歌曲播放完畢自動抓取下一首歌曲,沒有隨機播放, 完全依賴建立在 AudioCard 給的歌曲順序。
目前支援的格式:
Media | Extension |
Audio MP3 | mp3 |
Audio MP4 | m4a |
Audio MP4 | aac |
Audio WAV | wav |
因爲 TuneIn 可以在 Cortana 播放,所以我測試之後發現 HLS 也真的可以支援。
使用 Audio Card 的幾個重點:
- 一個 Audio Card attachment 只能放一首歌曲,多首歌曲需要建立多個 Audio Card attachment
- 每一個 Audio Card 只能設定一組 URL,如果設定多組 URL,Cortana 只會播放第一個 URL
- 設定在 Audio Card 預期播放的歌曲必須是: internet accessible & HTTPS protocol
- 利用 Audio Card 的 Media property 設定歌曲 URL 資訊; Title property 設定歌曲名稱;Cortana 會忽略其他 property (
非常不知道爲什麽)
使用範例如下:
// Create a reply message
Activity reply = activity.CreateReply();
// Set up attachments on the reply
reply.Attachments = new List();
// Add a single media URL for Cortana to play
MediaUrl murl = new MediaUrl("https://{yourstreamurl}");
MediaUrl[] medias = new MediaUrl[] {murl};
// Create a new AudioCard and attach your media URL
AudioCard audioCard = new AudioCard()
{
Media = medias,
};
// Add the attachment and send the reply
Attachment audioCardAttach = audioCard.ToAttachment();
reply.Attachments.Add(audioCardAttach);
reply.InputHint = InputHints.AcceptingInput;
await connector.Conversations.ReplyToActivityAsync(reply);
需注意 reply.InputHint = InputHints.AcceptingInput;,不應該使用 IgnoringInput 或 ExpectingInput。
如果使用非 AcceptingInput 會發生錯誤,因爲系統會一直等待您的下一個訊息造成音樂不會播放整個卡住。
需注意 Cortana 處理 Audio Card 訊息的方式:
- Cortana 不會主動通知 Skill 歌曲或是歌單播放完畢;
- 回傳的 message 包含 speech,Cortana 會讓 audio stream 到背景播放並降低音量,直到完成 speaking;
- 利用 Windows/iOS/Android 上面的 Cortana App 測試播放 audio,需要保持 Cortana 一直被開著,如果退到背景,音樂就會結束;
- 在 Windows 上,在用戶呼叫您的 Skill 到結束對話之前,Cortana 會傳送用戶的句子到您的 Skill。
結束對話後,Cortana 離開您的 Skill 繼續處理其他用戶輸入的句子; - Speaker-only devcies 上,Cortana 會處理所有用戶輸入的句子就算再播放歌曲期間,所以在播放歌曲期間想要讓 Skill 收到用戶輸入的句子,用戶必須引用您的 Skill 說: "Ask {skill invocation name} to ...";
接著説明一些 commands 從 Skill 角度與 User 角度要怎麽控制 Audio:
- User audio commands
下面指令只支援 speaker-only devices:
Command Description Stop Stops the audio streaming. Pause Pauses the audio streaming. Resume Resumes the audio streaming. Next Play the next track in a playlist. Previous Play the previous track in a playlist. 因爲是直接控制 Cortana 所以需要利用 "Hey Cortana" 起頭,例如:"Hey Cortana Pause"。
Cortana 會利用這些指令控制正在播放的 audio,這些指令不會傳給 Skill。
如果 device 有支援 Controller,例如:在 Windows/iOS/Android 上安裝的 Cortana App 也可以控制 (但是用語音指令無法控制),
如下圖:
- Getting the playback state
如果您的 Skill 是播放音訊,在 Cortana 傳送到您 Skill 的所有訊息,均會在訊息中的 channelData property 包含 CurrentAudioInfo 的物件。
範例情境,例如想知道現在播放的歌曲名稱,可以說: "Hey Cortana, ask {skill invocation name} what's playing"。
Skill 就能利用 CurrentAudioInfo 拿到現在播放的資訊,例如下面的程式:
JObject valueObj = JsonConvert.DeserializeObject
(activity.ChannelData.ToString()); string url = ""; JObject currentAudioObject = JsonConvert.DeserializeObject (valueObj["currentAudioInfo"]?.ToString() ?? ""); if(currentAudioObject!=null) { url = currentAudioObject["url"]?.ToString() ?? ""; } 需注意 CurrentAudioInfo 只支援 speaker-only device。
同樣地,上面提到 Cortana 播放歌曲或是歌單完畢之後不會通知 Skill 已經沒有下一首,這個時候,我們可以訂一個特定的 utterance,
例如:"Hey Cortana, ask {skill invocation name} to keep going",利用 CurrentAudioInfo 知道現在播放的最後一首歌曲是什麽,
再往下抓取其他的歌曲來播放。
那麽 channelData 擁有那些資訊,可以參考如下:{ "skillId": "18534e42-4a9b-4eb6-b788-6cae11ebe544", "skillProductId": "3b169322-d53c-4286-bf0c-bed462fdead", "isDebug": false, "currentAudioInfo": { "url": "https://xxx.azurewebsites.net/32fi_2o1.mp3" } }
- Skill audio commands
Skill 在某些情況下會傳送控制指令給 Cortana (例如:發現要播放的音訊已經不存在,需要請 Cortana 不要再來詢問播放)。
主要設定回傳訊息的 Type 為 ActivityTypes.Event,並給予 media/stop 的 Name,如下:
Activity reply = activity.CreateReply(); reply.Type = ActivityTypes.Event; reply.Name = "media/stop"; reply.InputHint = InputHints.AcceptingInput; await connector.Conversations.ReplyToActivityAsync(reply);
以上是介紹如何使用 Audio Card 讓 Cortana Skill 支援音訊播放的功能。
您也許會問,因爲 Cortana Skill 只有允許播放 internet accessible 與 HTTPS 是否代表需要完整提供播放的網址呢 (例如: https://xxx.com/audio/1.mp3)?
答案是不需要的。
那可以怎麽做呢?
- 在您的 Skill 多一個 Controller,命名為: RedirectAudioController,它負責處理 Cortana 請求 Audio stream 的回傳内容
- 在建立 Audio Card attachment 的 url 改指定為 https://localhost:3979/api/RedirectAudio/{query string}。
localhost 請換成對的 domian;
query string: 請利用加入 Bot Framework 的資訊,讓 RedirectAudioController 處理時可以做一些簡單的判斷跟處理;
參考下面的程式範例:
public static IMessageActivity GenerateAudioCards(IDialogContext context, List<TrackData> tracks) { // 把目前 converstaion 的資訊保存起來,做 Controller 驗證用 var conversationReference = context.Activity.ToConversationReference(); string stateToken = UrlToken.Encode(conversationReference); // 準備要回復的 message var reply = context.MakeMessage(); reply.Speak = reply.Text = "Prepare play music"; reply.InputHint = InputHints.AcceptingInput; reply.Attachments = new List
(); foreach (var item in tracks) { // 準備 query string string queryString = $"state={stateToken}&track={HttpUtility.UrlEncode(item.Id)}"; byte[] stringData = Encoding.UTF8.GetBytes(queryString); queryString = Convert.ToBase64String(stringData); var album = item.Album; // 填寫 subTitle 與 image,但是 Cortana 會忽略 (期待之後會加入) string subTitle = $"{album.Name} {album.Artist.Name}"; ThumbnailUrl image = new ThumbnailUrl(album.Image.Url, item.Name); // 把建立好的 query string 加入要處理的 Controller var mediaUrls = new List (); mediaUrls.Add(new MediaUrl($"http://localhost:3979/api/RedirectAudio/{queryString}", queryString)); var audioCard = new AudioCard(item.Name, subTitle, subTitle, image, mediaUrls, null, false, false, true); reply.Attachments.Add(audioCard.ToAttachment()); } return reply; } - 最後 RedirectAudioController 拿到真正的 mp3 url 之後,利用 HttpResponseMessage 重新請 Cortana 做 redirect。
如果您覺得再 redirect 一次太麻煩,也可以直接把歌曲的 byte array 寫到 HttpResponseMessage。
參考下面的程式範例:
public async Task
Get(string id) { try { // 將 query string 還原 var bytes = Convert.FromBase64String(id); string input = Encoding.UTF8.GetString(bytes); Dictionary keyValuePairs = input.Split('&').Select(value => value.Split('=')).ToDictionary(pair => pair[0], pair => pair[1]); // 從 state 參數轉換為原本的 ConversationReference ConversationReference conversationReference = UrlToken.Decode (keyValuePairs["state"]); string trackId = HttpUtility.UrlDecode(keyValuePairs["track"]); MusicProvider clientAPI = new MusicProvider(accessToken); var ticketResult = await clientAPI.GetAudioStreamAsync(trackId); if (ticketResult.Error != null) { return new HttpResponseMessage(HttpStatusCode.NotFound); } // 使用 Rediret 的機制通知 Cortana 下載歌曲 var url = ticketResult.Content.URL; var response = new HttpResponseMessage(HttpStatusCode.Redirect); response.Headers.Location = new Uri(url); // 或是選擇使用 直接給與 byteArrary //WebClient client = new WebClient(); //var fileStream = client.DownloadData(url); //var stream = new MemoryStream(); //// processing the stream. //stream.Write(fileStream, 0, fileStream.Length); //var response = new HttpResponseMessage(HttpStatusCode.OK) //{ // Content = new ByteArrayContent(stream.ToArray()) //}; //response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") //{ // FileName = $"0{id}.mp3" //}; //response.Content.Headers.ContentType = new MediaTypeHeaderValue("audio/mp3"); return response; } catch (Exception ex) { return new HttpResponseMessage(HttpStatusCode.NotFound); } }
[補充]
- 開發 Cortana Skill 現在分成兩種方式: Bot Framework 與 Knowledge Store 線上設計與開發,如果在 Knowledge Store 上怎麽回傳 audio card 可以參考: audio in renderers
- 有一些已知的問題可以參考:Know issues
======
在 Smart Speaker 最頻繁被使用的就是播放歌曲,剛好自己入手了 Harman Kardon Invoke (目前唯一支援 Cortana 的產品),
搭配本篇介紹的内容就可以讓 Invoke 唱歌,真的非常有趣。
如果您沒有 Invoke 也可以利用 Windows 10 IoT 的方式整合 Cortana Skill。
References:
- Get Cortana's channel data
- Add rich card attachments to messages
- Adaptive Cards for Bot Developers
- AudioCard Class
- VideoCard Class
- Activities overview
- Cortana Skills Kit now supports Adaptive Cards
- Creating and testing a Cortana Skill with Microsoft Bot Framework
- UWP SDK - Adaptive Card
- Adaptive Card overview
- The complete list of Alexa commands so far