在 UWP 的開發過程中,我們可能需要提供多種交互方式,例如滑鼠、鍵盤、觸摸、游戲手柄等,當然,語音也是一項很重要的功能。 眾所周知,在 Windows 中的許多個版本都包含有語音功能,特別是在 Windows 10 上,Cortana(小娜)更是非常智能。同時,對於開發者而言,我們也能非常方便的在 ...
在 UWP 的開發過程中,我們可能需要提供多種交互方式,例如滑鼠、鍵盤、觸摸、游戲手柄等,當然,語音也是一項很重要的功能。
眾所周知,在 Windows 中的許多個版本都包含有語音功能,特別是在 Windows 10 上,Cortana(小娜)更是非常智能。同時,對於開發者而言,我們也能非常方便的在其中融入我們的功能,不過本文並不是想說這個。這裡將介紹如何開發我們自己的 UWP 應用的語音交互,即,在我們的 UWP 內部,支持用戶的語音命令和語音輸入,並提供語音反饋。
準備工作
首先,在 Visual Studio 2015 Update 3 或更高版本中,創建一個 UWP 項目。併在 Package.appxmainfest 中,在 Capabilities 中勾選“麥克風”,或者直接用文本編輯器打開該文件,在 Capabilities
節點中,插入以下代碼。
<DeviceCapability Name="microphone" />
打開 MainPage.xaml.cs 文件,我們需要先在其中加入以下命名空間,這些將分別用於處理語音識別、語音合成和文件訪問。
using Windows.Media.SpeechRecognition;
using Windows.Media.SpeechSynthesis;
using Windows.Storage;
語音識別
現在,我們需要在 MainPage
類里實現一個方法,用於執行語音識別,並返回結果。在處理語音識別的過程當中,需要用到一個名為 SpeechRecognizer
的類所創建的實例,該實例可被覆用,以便多次處理語音識別任務。因此,我們還需要一個欄位來存儲這個實例,併在該方法首次調用時,初始化這個實例。另外,由於該實例可以添加一些語音識別約束,用於描述場景,所以在使用前,需要先對這些約束進行編譯,即使並沒有添加任何約束。該編譯過程和語音識別都是非同步的,因此我們需要將這個方法也聲明為非同步方法。
private SpeechRecognizer _speechRecognizer;
private async Task<SpeechRecognitionResult> SpeechRecognizeAsync() {
if (_speechRecognizer == null)
{
// 創建一個 SpeechRecognizer 實例。
_speechRecognizer = new SpeechRecognizer();
// 編譯聽寫約束。
await _commandSpeechRecognizer.CompileConstraintsAsync();
}
// 開始語音識別,並返回結果對象。
return await _speechRecognizer.RecognizeAsync();
}
另外,也可以在這個方法里初始化 _speechRecognizer
欄位的地方,對該實例的一些行為和狀態做一些控制,例如使用其 RecognitionQualityDegrading
事件監聽語音輸入的質量等。
現在,我們可以在界面中添加一個按鈕,綁定一個點擊事件,用以測試語音錄入功能。
<Button x:Name="SpeechButton" Content="語音" Click="Speech_Click" />
在綁定的點擊事件中,需要先調用前面寫的 SpeechRecognizeAsync()
成員方法獲取到識別後的結果,然後在識別後,出於測試目的,我們彈出一個對話框,顯示所識別的文本內容。在實際使用場景中,這個結果應該是被輸入到文本框,或者是用在其它場合。
private async void Speech_Click(object sender, RoutedEventArgs e)
{
// 獲取語音識別結果。
var speechRecognitionResult = await SpeechRecognizeAsync();
// 彈出一個對話框,用於展示識別出來的文本。
var messageDialog = new Windows.UI.Popups.MessageDialog(speechRecognitionResult.Text, "你剛說了");
await messageDialog.ShowAsync();
}
現在,按下 F5 運行,可以發現,界面上有一個按鈕“語音”,點擊後,說出一句話,稍等片刻,即會彈出一個標題為“你剛說了”的對話框,裡面包含了剛纔所說的內容。
使用圖形輸入界面
在剛纔的場景中,點一下按鈕後,其實界面看似並沒有任何反應,但其實已經開始進入聆聽我們說話的過程中了,這樣的用戶體驗並不好,可能需要加入一些提示,例如提示用戶開始聆聽等。
不過,其實系統也有提供預設的通用界面可供調用。這些界面雖然較為簡單,但在許多場景下也的確滿足需求。系統預設的界面會在開始聆聽時彈出一個對話框,顯示“正在聆聽”併發出提示音;當聆聽結束時,會在該彈出框中顯示所聆聽到的內容,並同時用合成語音進行反饋;而失敗時,也會在該彈出框中顯示出錯信息,並提供合成語音反饋。
使用系統預設的語音識別界面,僅需把 SpeechRecognizeAsync()
成員方法中最後一步的 speechRecognizer.RecognizeAsync()
調用,替換成 speechRecognizer.RecognizeWithUIAsync()
即可。
// 開始識別,並呈現系統預設界面。
return await _speechRecognizer.RecognizeWithUIAsync();
如果需要調整系統預設的界面中的內容,例如“正在聆聽”的顯示文案、示例提示等信息等信息,也可以在初始化 _speechRecognizer
欄位時,在執行編譯過程之前,即調用 await _speechRecognizer.CompileConstraintsAsync();
方法之前,通過對該欄位的 UIOptions
屬性進行設置來控制。
// 可以設置“正在聆聽”期間的文案。
_speechRecognizer.UIOptions.AudiblePrompt = "請問你打算要我做什麼?";
// 可以設置“正在聆聽”期間顯示的示例提示,用於提示用戶可以說什麼。
_speechRecognizer.UIOptions.ExampleText = "請嘗試說:開燈、關燈。";
// 當聆聽結束後,可以設置是否要通過合成語音讀出聽到的結果。
_speechRecognizer.UIOptions.IsReadBackEnabled = false;
// 可以設置是否要顯示聆聽成功後的確認消息框。
_speechRecognizer.UIOptions.ShowConfirmation = false;
除此之外,也可以對該欄位的 Timeouts
成員屬性里的屬性進行設置,以控制各類時長。
語音輸入場景
為了更精準的識別所期待的內容,如前面所說,SpeechRecognizer
類支持添加語音識別約束。語音識別至少需要一個約束,才能定義可識別的辭彙。如果未指定任何約束,將使用通用 Windows 應用的預定義聽寫語法。
語音識別約束必須實現 ISpeechRecognitionConstraint
介面,然後在 SpeechRecognizer
類的 Constraints
成員屬性中調用 Add
成員方法添加至約束列表中。可以添加多項語音識別約束,但是有的可能會有衝突。
通常情況下,可能會應用到以下這些內置約束。
SpeechRecognitionTopicConstraint
- 基於預定義語法的約束。SpeechRecognitionListConstraint
- 基於字詞或短語列表的約束。SpeechRecognitionGrammarFileConstraint
- 在語音識別語法規範(SRGS)文件中定義的約束。
添加這些約束必須在編譯約束之前,即必須放置於上方示例代碼中的 SpeechRecognizeAsync()
成員方法里 await _commandSpeechRecognizer.CompileConstraintsAsync();
調用之前。
語音識別約束中有一個 Tag
成員屬性,用於作為該約束的 ID,設置後,在之後的語音識別結束時,可以用於判斷時基於哪個約束進行識別的。例如,可能添加了多個識別約束,每個約束都有不同的預期以及對應的處理方式,當語音識別結束時,我們需要知道當前用戶所說的內容,是符合哪一個約束所對應的預期的,這樣我們才知道接下來應該如何處理其語音輸入的內容,這時,可以根據 ID 來進行簡單的判斷,因為語音識別結果 SpeechRecognitionResult
類中,會包含 Constraint
屬性,這個屬性即是之前所實際採用的約束。當然,也可以直接判斷約束實例本身,而非通過這個 ID 來區分。不過需要註意的是,結果中的這個屬性可能為空,所以可能要處理為空的可能。
指定預定義語法的約束
SpeechRecognitionTopicConstraint
類用於預期為 Web 搜索、聽寫或表單輸入類型,使用方式如下。
var webGrammar = new SpeechRecognitionTopicConstraint(SpeechRecognitionScenario.WebSearch, "webSearch", "web");
_speechRecognizer.Constraints.Add(webGrammar);
這個類的構造函數支持輸入3個參數,其中最後一個為選填。
- 第1個參數是場景類型,為一個枚舉。
- Web 搜索(
SpeechRecognitionScenario.WebSearch = 0
)。 - 聽寫(
SpeechRecognitionScenario.Dictation = 1
)。 - 表單輸入(
SpeechRecognitionScenario.FormFilling = 2
)。 - 第2個參數為具體子類型,例如,針對錶單輸入,可以定義為支持拼寫檢查的文本類型、日期類型、姓名類型等,詳見 MSDN 文檔。
- 第3個參數是該約束項的 ID,會被填入
Tag
成員屬性當中去,用於之後在判斷用戶所說的內容是在哪個約束中識別的作為依據。
指定編程列表約束
SpeechRecognitionListConstraint
類用於字詞或短語列表的語音識別約束,可以通過程式輸出一個字元串列表,然後添加至約束列表。
例如,假設我們需要添加一項功能,提供播放音樂和暫停的功能,我們可以至少添加2個這類約束,並刪除前面註冊預定義語法約束的代碼,以防止衝突。
// 獲取歌曲列表。
// 此處硬編碼了一些歌曲,實際應該通過程式去歌曲庫中讀取。
var songs = new [] { "曲目一", "另一首歌", "未命名歌曲" };
// 生成接受的語音列表。
var playCmds = songs.Select((item) => {
return string.Format("{0}{1}", "播放", item;
});
// 創建約束,並將預期的語音列表作為構造函數的參數傳入。
// 同時,可以選填其 Tag。
var playConstraint = new SpeechRecognitionListConstraint(playCmds, "play");
// 添加約束。
_speechRecognizer.Constraints.Add(playConstraint);
// 接下來,創建暫停和繼續播放所對應的功能,並添加該約束。
var pauseConstraint = new SpeechRecognitionListConstraint(new [] { "暫停", "繼續播放" }, "pauseAndResume");
_speechRecognizer.Constraints.Add(pauseConstraint);
至於應該根據何種情況來設定預期的語音指令,這需要根據現實的使用場景和整體的程式設計來進行規劃,許多情況下,並不需要像這個示例一樣使用2個約束實例,僅用1個也可以。此處示例僅為說明這個約束的使用方式。
指定 SRGS 語法約束
SpeechRecognitionGrammarFileConstraint
類用於引入一個 SRGS 文件作為語音識別約束。例如下方這樣,我們假設我們需要控制一個燈的開關,我們需要先讀取一個 SRGS 類型的文件(.grxml),然後作為該類的第1個參數傳入,第2個參數為可選的 Tag(即 ID)。
// 獲取文件。
var grammarFile = await GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/Light.grxml"));
// 創建 SRGS 文件約束。
var srgsConstraint = new SpeechRecognitionGrammarFileConstraint(grammarFile, "light");
// 添加約束。
_speechRecognizer.Constraints.Add(srgsConstraint);
接下來,需要去實現那個位於項目中 Assets 目錄下的 Light.grxml 文件。
SRGS 語法
在項目的 Assets 目錄下新建一個名為 Light.grxml 的文件,併在文件屬性中,將“數據包操作”設置為“內容”,以及將“複製到輸出目錄”則設置為“始終複製”。該文件里的內容需要符合 SRGS 語法。SRGS 是一套為語音識別創建 XML 格式語法的行業標準標記語言,可以處理較複雜的語音識別。該文件需要包含類似下方的 XML 節點。
<grammar xml:lang="zh-Hans" root="Control" version="1.0" tag-format="semantics/1.0" xmlns="http://www.w3.org/2001/06/grammar">
<rule id="Control">
</rule>
</gramma>
在這裡,grammar
根節點里的 xml:lang
屬性,用於定義這個 SRGS 文件所支持的語言,只有噹噹前語音識別的語音於這個值匹配的時候,該文件中描述的語音識別規則才有效;而 root
屬性里需要定義一個規則的 ID,在 grammar
根節點里可以定義多套規則,但是入口只有1個,即此屬性中定義的;其它屬性可參考 MSDN 文檔。在 grammar
根節點裡面,必須至少有一個 rule
節點,並需要定義其 id
屬性,用於檢索。id
值和 grammar
根節點 root
屬性定義的值相同的 rule
節點,即為此文件所指定的主規則;其它規則通常是被用於引用的。rule
節點中所支持的屬性可參閱 MSDN 文檔。
由於我們計劃控制燈的開關,首先,需要一項規則是打開燈,因此,我們在 grammar
根節點中新建一個 rule
節點,並指定一個 ID TurnOn
,然後在裡面來實現這個規則。具體的實現方式是,在裡面添加一個 item
節點,該節點的目的是指定預期可能說的話。
<rule id="TurnOn">
<item>開燈</item>
</rule>
然而,日常生活中,打開燈可能又多種說法,除了“開燈”外,可能還有“打開燈”、“把燈打開”等,像這一類屬於多種可能都通向一個結果的,可以採用 one-of
節點將其包起來。於是我們把這個節點修改為如下。
<rule id="TurnOn">
<one-of>
<item>打開</item>
<item>開燈</item>
<item>亮燈</item>
<item>打開燈</item>
<item>把燈打開</item>
<item>快開燈</item>
</one-of>
</rule>
事實上,有些人很禮貌,即使和電腦說話,也可能會帶上“請”之類的敬詞,因此,我們還需要對其進行改造以下,在這個多選一的預期的前面,添加可能會說的內容。由於不確定是否會說,因此我們可以通過添加一個 item
節點,並通過其 repeat
屬性來設置可能的重覆次數來做到,由於最多可能只會說一個“請”字,那麼重覆次數的範圍應該是0到1之間。經過優化,最後該節點可能如下所示。
<rule id="TurnOn">
<item repeat="0-1">請</item>
<item repeat="0-1">幫我</item>
<item>
<one-of>
<item>打開</item>
<item>開燈</item>
<item>亮燈</item>
<item>打開燈</item>
<item>把燈打開</item>
<item>快開燈</item>
</one-of>
</item>
</rule>
item
節點中還有一些其它屬性,可參閱 MSDN 文檔。
完成了開燈,那麼還有關燈。同理,如下。
<rule id="TurnOff">
<item repeat="0-1">請</item>
<item repeat="0-1">幫我</item>
<item>
<one-of>
<item>關閉</item>
<item>關燈</item>
<item>關上</item>
<item>把燈關了</item>
<item>把燈關上</item>
<item>快關燈</item>
</one-of>
</item>
</rule>
不過,寫到這裡,這些規則並未能被調用,因為剛纔說到,只有 root
指定的規則才是入口規則,所以我們需要改寫前面的 Control
規則。改寫的內容其實很簡單,也是一個多選一的情況,只要說出開燈或關燈的任一命令,那麼就執行。但如何指定執行哪個已定義的規則呢?可以使用 ruleref
節點,其 uri
屬性用於指定是引用哪個規則,其中的值採用類似 CSS 中的選擇器的語法方式,例如,用 # 開頭即表示後面跟著的是對應的 ID。
<rule id="Control">
<one-of>
<item>
<ruleref uri="#TurnOn"/>
</item>
<item>
<ruleref uri="#TurnOff"/>
</item>
</one-of>
</rule>
現在,寫完了 SRGS 文件。
識別 SRGS 語法約束的識別結果
當語音識別進入 SRGS 後,並得出結果,我們需要知道在執行結束後識別最後走進了哪一個規則,因為只有如此,才能決定接下來執行什麼實際的操作。與前面兩個約束不同,前面的兩個約束可以通過所執行到的約束類型和簡單分析識別到的語句來進行判斷;這個 SRGS 的結果顯得更為複雜。首先,我們需要根據結果返回的約束類型來判斷,是否識別出的結果為這個約束下的;然後,我們可以獲取其所執行的規則路徑,來明白其所識別的邏輯結果。
規則路徑其實是個字元串列表,即我們之前在 rule
中定義的 ID 的順序執行列表,例如,當我們說出“開燈”時,其返回的時 ["Control", "TurnOff"]
。我們來新寫一個方法來處理這個結果。由於第1個規則肯定時根節點,因此我們在這個示例中,只需要簡單判斷第2個路徑是什麼即可。
private Control(IList<string> path)
{
if (path.Count < 2) return;
switch (path[1])
{
case "TurnOn":
// Turn on.
break;
case "TurnOff":
// Turn off.
break;
}
}
然後,我們還需要改寫前面所寫的 Speech_Click()
方法,以在該 SRGS 約束被執行到時調用上述方法。
// 獲取語音識別結果。
var speechRecognitionResult = await SpeechRecognizeAsync();
// 根據所執行到的約束來決定執行的內容。
if (speechRecognitionResult.Constraint == null) return;
switch (speechRecognitionResult.Constraint.Tag)
{
case "light":
Control(speechRecognitionResult.RulePath.ToList());
break;
case "play":
// 播放某首歌。
var songName = speechRecognitionResult.Text.Substring("播放".Length).Trim();
break;
case "pauseAndResume":
// 暫停或繼續播放。
break;
}
語音合成
語音識別是將用戶說的話識別為文本或指令,同時,有的時候,我們又需要反過來,通過語音合成,將指定的內容通過聲音的形式反饋給用戶。例如,當用戶通過語音發出了某項指令後,電腦通過語音進行回覆,以模擬一個語音對話的模式。
在 UWP 中,如果希望播放出某一段聲音,其中一種做法是,在界面中先嵌入一個 MediaElement
控制項,然後在通過控制這個對象來播放聲音。例如,在 MainPage.xaml 文件中插入該控制項。
<MediaElement x:Name="mediaElement"/>
接下來,我們需要通過語音合成,來將一段指定的文本,以音頻的形式讓電腦朗讀出來。我們新寫一個方法,用於將一段文本轉為語音,並通過剛纔新建的 MediaElement
控制項播放出來。
private SpeechSynthesizer _synth = new SpeechSynthesizer();
private async void Speak(string value)
{
// 創建一個文本轉語音的流。
var stream = await synth.SynthesizeTextToStreamAsync(value);
// 將流發送到 MediaElement 控制項並播放。
mediaElement.SetSource(stream, stream.ContentType);
mediaElement.Play();
}
然後我們可以對前面所寫的 Control(IList<string> path)
成員方法中的內容進行改寫。
private Control(IList<string> path)
{
if (path.Count < 2) return;
switch (path[1])
{
case "TurnOn":
// Turn on.
Speak("燈已打開。");
break;
case "TurnOff":
// Turn off.
Speak("燈已關上。");
break;
}
}
現在,當你點擊界面上的“語音”按鈕後,在提示聆聽後,說出“開燈”,稍後便會聽到“燈已打開”的提示。
定製語音合成
剛剛的語音播放其實採用的是正常模式,但有時可能需要對內容的語音播放進行細節上的控制,例如調整音調、重讀、語速等,甚至字或詞的發音。這時候,我們需要用到語音合成標記語言 SSML 來進行處理。例如以下示例。
// 創建一個字元串,包含 SSML 描述內容,用於朗讀。
string Ssml =
@"<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='en-US'>" +
"<prosody rate='slow'>你好嗎?</prosody> " +
"<break time='500ms'/>" +
"</speak>";
// 用合成器根據文本創建音頻流。
var stream = await _synth.synthesizeSsmlToStreamAsync(Ssml);
// 將音頻播放出來。
mediaElement.SetSource(stream, stream.ContentType);
mediaElement.Play();
節點 prosody
用於控制朗讀的語速、音調、音量等,詳見 MSDN 文檔。