讓 Cortana Skill 幫你唱歌

請 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 的幾個重點:

  1. 一個 Audio Card attachment 只能放一首歌曲,多首歌曲需要建立多個 Audio Card attachment
  2. 每一個 Audio Card 只能設定一組 URL,如果設定多組 URL,Cortana 只會播放第一個 URL
  3. 設定在 Audio Card 預期播放的歌曲必須是: internet accessible & HTTPS protocol
  4. 利用 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;,不應該使用 IgnoringInputExpectingInput。

如果使用非 AcceptingInput 會發生錯誤,因爲系統會一直等待您的下一個訊息造成音樂不會播放整個卡住。

需注意 Cortana 處理 Audio Card 訊息的方式:

  1. Cortana 不會主動通知 Skill 歌曲或是歌單播放完畢;
  2. 回傳的 message 包含 speech,Cortana 會讓 audio stream 到背景播放並降低音量,直到完成 speaking;
  3. 利用 Windows/iOS/Android 上面的 Cortana App 測試播放 audio,需要保持 Cortana 一直被開著,如果退到背景,音樂就會結束;
  4. 在 Windows 上,在用戶呼叫您的 Skill 到結束對話之前,Cortana 會傳送用戶的句子到您的 Skill。
    結束對話後,Cortana 離開您的 Skill 繼續處理其他用戶輸入的句子;
  5. 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"
      }
    }
    更多可以參考 Get Cortana's channel data
  • 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);
    如果對象是 speaker-only devices 建議不要送這指令,除非現在播放歌曲的 Skill 就是您自己,不然會造成使用上的不一致。

以上是介紹如何使用 Audio Card 讓 Cortana Skill 支援音訊播放的功能。

您也許會問,因爲 Cortana Skill 只有允許播放 internet accessible 與 HTTPS 是否代表需要完整提供播放的網址呢 (例如: https://xxx.com/audio/1.mp3)

答案是不需要的。

那可以怎麽做呢?

  1. 在您的 Skill 多一個 Controller,命名為: RedirectAudioController,它負責處理 Cortana 請求 Audio stream 的回傳内容
  2. 在建立 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;
    }
  3. 最後 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: