都說語音是人機交互的重要手段,雖然個人覺得在大庭廣眾之下,對着手機發號施令會顯得有些尷尬。但是在資源受限的物聯網應用場景下(無法外接鼠標鍵盤顯示器),如果能夠通過語音來控制設備,與設備進行交互,那還是很實用的。繼上一篇《Windows 10 IoT Serials 4 - 如何在樹莓派上使用Cortana語音助手》之后,本文將詳細講述如何為運行Windows 10 IoT Core系統的樹莓派添加語音識別和語音交互功能。
1. 硬件准備
- 樹莓派2/樹莓派3、5V/2A電源、TF卡(8GB以上)
- 麥克風:Microsoft LifeCam HD 3000(該攝像頭集成了麥克風),也可以使用其他麥克風,如Blue Snowball iCE Condenser Microphone, Cardioid, Sound Tech CM-1000USB Table Top Conference Meeting Microphone
- 受控對象:這里以兩個LED燈為例。用戶可以根據實際需求添加受控對象,比如添加繼電器模塊以后,可以控制強電設備。
- 音頻輸出設備(可選):Windows 10 IoT Core系統的樹莓派只支持3.5mm接口的音頻輸出,HDMI的音頻輸出不支持。所以,可以接一個普通的3.5mm接口的耳機就可以。
- 顯示設備(可選):可以接HDMI接口的顯示器,或者使用有源HDMI轉VGA模塊,轉接VGA接口的顯示器。
注意,這里音頻輸出設備和顯示設備是可選的,並不是必須的。
2. 硬件連接
這里將LED連接到樹莓派的GPIO5和GPIO6兩個引腳,同時,把麥克風設備插入到樹莓派的USB接口。如果准備了音頻輸出設備(如耳機或音響)和顯示設備(顯示器),請連接到樹莓派的3.5mm音頻接口和HDMI接口。
3. 程序編寫
本應用程序使用的開發環境是Windows 10+Visual Studio 2015 Community,注意,Visual Studio需要包含Universal Windows App Development Tools組件。
3.1 新建工程和添加資源
新建工程時,選用Universal模板,工程命名為RPiVoiceControl,如下圖所示。
因為要用到GPIO引腳控制LED,所以需要為工程添加Windows IoT Extension for UWP引用,如下圖所示。
由於需要使用Microphone,所以需要在工程的Package.appxmanifest文件中,勾選Microphone,如下圖所示。
另外,由於需要使用到語音識別、LED和UI控件等資源,需要為應用程序引入命名空間,如下:
using System;
using System.Diagnostics;
此處省略若干…
using Windows.Devices.Gpio; //LED
using Windows.Media.SpeechRecognition;//語音識別
using Windows.Media.SpeechSynthesis;
using Windows.Storage;
using Windows.ApplicationModel;
3.2 新建語音指令定義文件
為項目添加新的xml文件,命名為Grammar.xml,用於定義語音指令。項目中用到的語音指令符合Speech Recognition Grammar Specification Version 1.0 (SRGS)標准,其具體協議可以參考MSDN上的這個文檔:Create Grammars Using SRGS XML (Microsoft.Speech)。
之后,打開該文件,為其添加如下語音指令。
<?xml version="1.0" encoding="utf-8" ?>
<grammar
version="1.0"
xml:lang="en-US"
root="automationCommands"
xmlns="http://www.w3.org/2001/06/grammar"
tag-format="semantics/1.0">
<rule id="root">
<item>
<ruleref uri="#automationCommands"/>
<tag>out.command=rules.latest();</tag>
</item>
</rule>
此處省略代碼,具體請參考Github上項目的完整代碼。
<rule id="deviceActions">
<one-of>
<item>
light <tag> out="LIGHT"; </tag>
</item>
<item>
led <tag> out="LED"; </tag>
</item>
</one-of>
</rule>
</grammar>
3.3 程序界面設計
如果不准備給樹莓派接顯示器的可以直接忽略這一步,如果需要在程序運行過程中查看狀態的,可以加入一些簡單的控件,這里只是加入了兩個指示LED燈狀態的Ellipse 控件、兩個指示程序運行狀態的TextBlock 控件和一個MediaElement 控件,代碼如下。
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<Ellipse x:Name="bedroomLED" Fill="LightGray" Stroke="White" Width="100" Height="100" Margin="10"/>
<Ellipse x:Name="kitchenroomLED" Fill="LightGray" Stroke="White" Width="100" Height="100" Margin="10"/>
<TextBlock x:Name="GpioStatus" Text="Waiting to initialize GPIO..." Margin="10,50,10,10" TextAlignment="Center" FontSize="26.667" />
<TextBlock x:Name="VoiceStatus" Text="Waiting to initialize Microphone" Margin="10,50,10,10" TextAlignment="Center" TextWrapping="Wrap" />
<MediaElement x:Name="mediaElement"></MediaElement>
</StackPanel>
</Grid>
3.4 后台代碼
后台代碼中,首先需要定義應用程序使用的資源對象,如GPIO、畫刷、定時器、部分代碼如下,
private const int BedRoomLED_PINNumber = 5;
private GpioPin BedRoomLED_GpioPin;
private GpioPinValue BedRoomLED_GpioPinValue;
private DispatcherTimer bedRoomTimer;
private const int kITCHENLED_PINNumber = 6;
private GpioPin kITCHENLED_GpioPin;
private GpioPinValue kITCHENLED_GpioPinValue;
private DispatcherTimer kITCHENTimer;
private SolidColorBrush redBrush = new SolidColorBrush(Windows.UI.Colors.Red);
private SolidColorBrush grayBrush = new SolidColorBrush(Windows.UI.Colors.LightGray);
然后,在MainPage的構造函數中,添加資源的初始化,部分代碼如下:
public MainPage()
{
this.InitializeComponent();
Unloaded += MainPage_Unloaded;
// Initialize Recognizer
initializeSpeechRecognizer();
InitBedRoomGPIO();
InitKITCHENGPIO();
bedRoomTimer = new DispatcherTimer();
bedRoomTimer.Interval = TimeSpan.FromMilliseconds(500);
bedRoomTimer.Tick += BedRoomTimer_Tick;
kITCHENTimer = new DispatcherTimer();
kITCHENTimer.Interval = TimeSpan.FromMilliseconds(500);
kITCHENTimer.Tick += KITCHENTimer_Tick;
}
在initializeSpeechRecognizer函數中,完成語音識別狀態改變事件的添加、語音指令文件的加載,部分代碼如下:
private async void initializeSpeechRecognizer()
{
// Initialize recognizer
recognizer = new SpeechRecognizer();
// Set event handlers
recognizer.StateChanged += RecognizerStateChanged;
recognizer.ContinuousRecognitionSession.ResultGenerated += RecognizerResultGenerated;
// Load Grammer file constraint
string fileName = String.Format(SRGS_FILE);
StorageFile grammarContentFile = await Package.Current.InstalledLocation.GetFileAsync(fileName);
SpeechRecognitionGrammarFileConstraint grammarConstraint = new SpeechRecognitionGrammarFileConstraint(grammarContentFile);
// Add to grammer constraint
recognizer.Constraints.Add(grammarConstraint);
SpeechRecognitionCompilationResult compilationResult = await recognizer.CompileConstraintsAsync();
Debug.WriteLine("Status: " + compilationResult.Status.ToString());
// If successful, display the recognition result.
if (compilationResult.Status == SpeechRecognitionResultStatus.Success)
{
Debug.WriteLine("Result: " + compilationResult.ToString());
await recognizer.ContinuousRecognitionSession.StartAsync();
}
else
{
Debug.WriteLine("Status: " + compilationResult.Status);
}
}
之后,添加RecognizerResultGenerated和RecognizerStateChanged兩個事件的處理,主要用於語音識別結果和狀態發生變化的處理。部分代碼如下:
private async void RecognizerResultGenerated(SpeechContinuousRecognitionSession session, SpeechContinuousRecognitionResultGeneratedEventArgs args)
{
// Check for different tags and initialize the variables
String location = args.Result.SemanticInterpretation.Properties.ContainsKey(TAG_TARGET) ?
args.Result.SemanticInterpretation.Properties[TAG_TARGET][0].ToString() :
"";
String cmd = args.Result.SemanticInterpretation.Properties.ContainsKey(TAG_CMD) ?
args.Result.SemanticInterpretation.Properties[TAG_CMD][0].ToString() :
"";
String device = args.Result.SemanticInterpretation.Properties.ContainsKey(TAG_DEVICE) ?
args.Result.SemanticInterpretation.Properties[TAG_DEVICE][0].ToString() :
"";
Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
VoiceStatus.Text= "Target: " + location + ", Command: " + cmd + ", Device: " + device;
});
switch (device)
{
case "hiActivationCMD"://Activate device
SaySomthing("hiActivationCMD", "On");
break;
case "LIGHT":
LightControl(cmd, location);
break;
default:
break;
}
}
// Recognizer state changed
private async void RecognizerStateChanged(SpeechRecognizer sender, SpeechRecognizerStateChangedEventArgs args)
{
Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
VoiceStatus.Text = "Speech recognizer state: " + args.State.ToString();
});
}
定義函數SaySomthing,用於反饋的語音生成,這樣,用戶就可以聽到樹莓派的語音反饋了。部分代碼如下:
private async void SaySomthing(string myDevice, string State, int speechCharacterVoice = 0)
{
if (myDevice == "hiActivationCMD")
PlayVoice($"Hi Jack What can i do for you");
else
PlayVoice($"OK Jack {myDevice} {State}", speechCharacterVoice);
await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
VoiceStatus.Text = $"OK -> ===== {myDevice} --- {State} =======";
});
}
最后,在兩個定時器的溢出事件處理中,加入對LED燈的處理,部分代碼如下:
private void BedRoomTimer_Tick(object sender, object e)
{
if (BedRoomLED_GpioPinValue == GpioPinValue.High)
{
BedRoomLED_GpioPinValue = GpioPinValue.Low;
BedRoomLED_GpioPin.Write(BedRoomLED_GpioPinValue);
bedroomLED.Fill = redBrush;
}
else
{
BedRoomLED_GpioPinValue = GpioPinValue.High;
BedRoomLED_GpioPin.Write(BedRoomLED_GpioPinValue);
bedroomLED.Fill = grayBrush;
}
}
4. 應用調試
在Visual Studio中設置編譯的平台為ARM,調試設備為Remote Machine,在Debug選項卡中,設置樹莓派的IP地址,點擊調試。如下圖所示。
程序運行以后,用戶可以通過語音指令與樹莓派進行交互。
首先,用戶可以使用“Hi Jack”與設備交互,可以聽到設備有回復,用於確認應用程序是否正確運行。
其次,用戶可以使用“Turn On/Off Bedroom Light”和“Turn On/Off kitchen Light ”來控制兩個LED燈,同時,在應用程序的界面上還可以看到燈的狀態和語音識別的狀態,如下圖所示。
5. 代碼下載
本項目的代碼已經發布到Github上,鏈接如下:https://github.com/shijiong/RPiVoiceControl,歡迎下載使用。