本篇文章來學習 Hololens 的基礎開發之語音操作。
源碼位址:https://github.com/jitwxs/blog_sample
建立一個新的 Unity 項目 VoiceDemo,初始化項目:
- 導入 MRTK 包
- 應用項目設定為 MR 項目
- 使用
替代預設相機HoloLensCamera
- 添加
CursorWithFeedback
- 添加
InputManager
- 設定 InputManager 的
腳本的 Cursor 屬性為添加的 CursorWithFeedbackSimpleSinglePointerSelector
- 添加一個 Cube,位置如下
最終 Hierarchy 結構如下:
一、語音控制
在 Hierarchy 建立一個空的 gameObject 并重命名為
SpeechManager
,為其添加 MRTK 的
SpeechInputSource.cs
腳本,為其添加兩個關鍵字,分别是 start rotate 和 stop rotate。
- 後面對應的
,字面意思是對應的鍵盤按鍵,但是我在實際程式中并沒有體會到有啥用,是以就随便設了兩個值,有知道的同學可以留言告訴我下。
Key Shortcut
- Hololens 目前已經支援中文語音,但需要手動安裝刷機,參考官方。
屬性 | 描述 |
---|---|
PersistentKeywords | Keyword 在所有場景中都是持久的,此語音輸入源執行個體在加載新場景時不會被銷毀 |
RecognizerStart | 是否在啟動時激活識别器 |
recognitionConfidenceLevel | Keyword 識别器的置信度 |
建立一個腳本
CubRotate.cs
,并将其添加到 Cube 上。
using UnityEngine;
public class CubRotate : MonoBehaviour {
bool HasRotate = false;
void Update () {
if(HasRotate)
{
transform.Rotate(Vector3.up);
}
}
public void StartRotate()
{
HasRotate = true;
}
public void StopRotate()
{
HasRotate = false;
}
}
這個腳本十分簡單,調用
StartRotate()
方法就能夠使 Cube 開始旋轉,調用
StopRotate()
方法使 Cube 停止旋轉。
為 Cube 添加 MRTK 中的
SpeechInputHandler.cs
腳本,根據名字就可以看出和 SpeechInputSource.cs 腳本有關系,該腳本用于處理語音輸入。
屬性 | 描述 |
---|---|
PersistentKeywords | Keyword 在所有場景中都是持久的,此語音輸入源執行個體在加載新場景時不會被銷毀 |
IsGlobalListener | 确定該處理程式是否是一個全局偵聽器,而不是連接配接到特定的GameObject。 |
在該腳本中,我們添加了兩個要處理的關鍵字,也就是在 SpeechInputSource.cs 中設定的 start rotate 和 stop rotate。在對應的
Response()
中調用了 Cube 的 CubeRotate.StartRotate() 和 CubeRotate.StopRotate() 方法。
運作程式,因為使用到了語音,是以必須使用真機運作,在運作前,不要忘記添加 Microphone 的權限。在
Edit/Project Settings/Player/Publishing Settings/Capabilities
中勾選 Microphone 。
部署到真機上,通過說 start rotate 和 stop rotate,來觀察 Cube 的旋轉和停止。
二、操縱麥克風
下面實作一個在耳機中播放麥克風錄入的聲音,并能夠根據聲音音量調整 Cube 的大小。
建立一個腳本
CubeMic.cs
,并将其添加到 Cube 上。
using HoloToolkit.Unity.InputModule;
using UnityEngine;
public class CubeMic : MonoBehaviour {
// Cube原始大小
private Vector3 origScale;
// 目前麥克風"音量"
private float averageAmplitude = 0;
void Start()
{
// 儲存Cube原始大小
origScale = transform.localScale;
// 設定麥克風音量
MicStream.MicSetGain(10);
// 開啟麥克風
MicStream.MicStartStream(false, false);
}
// 聲音過濾
private void OnAudioFilterRead(float[] buffer, int numChannels)
{
// 将麥克風輸入到聲音過濾管線中,将麥克風的聲音從耳機播放出來
MicStream.MicGetFrame(buffer, buffer.Length, numChannels);
// 計算麥克風"音量"大小
float sumOfValues = 0;
for (int i = 0; i < buffer.Length; i++)
{
sumOfValues += Mathf.Abs(buffer[i]);
}
averageAmplitude = sumOfValues / buffer.Length;
}
void Update()
{
// 根據"音量"調整Cube大小
transform.localScale = origScale * (1 + averageAmplitude * 10);
}
}
總結一下代碼:
-
MicStream :
HoloToolkit提供的麥克風操作類,詳細的用法可參考工具包中的MicStreamDemo類
-
OnAudioFilterRead :
Unity引擎提供的聲音濾波函數,具體原理可參考官方文檔《OnAudioFilterRead》
-
MicStream.MicGetFrame(…) :
這個方法可以擷取到麥克風的幀資料(float[]),可以在類似 OnAudioFilterRead 或者 Update 等高頻事件中調用并擷取。因為擷取到的是麥克風最小資料單元,使用起來非常靈活。我們可以在 OnAudioFilterRead 中播放,也可以使用Socke實作遠端通話。
為 Cube 添加一個
Audio Souce
元件,用于播放聲音,它的屬性使用預設值即可。
在真機中運作程式,我們能夠聽見麥克風的聲音,并且 Cube 根據音量大小發生改變。
三、設定合理的關鍵字
- 不要使用單音節詞,避免被系統忽略。例如使用 Play Video 替代 Play。也要注意不要音節過多,增加使用者使用成本。
- 不要使用系統預置語音,防止歧義,例如 Select、Remove等。
- 避免押韻的語音,例如使用 Show Store 替代 Show More。
四、語音的底層實作
使用 MRTK 工具包,我們隻需要點點滑鼠就能夠實作語音的處理,有興趣的同學可以了解下它源碼的實作。
本節代碼來源于:MR Basics 101: Complete project with device: Chapter 4 - Voice
4.1 SpeechManager
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Windows.Speech;
public class SpeechManager : MonoBehaviour
{
KeywordRecognizer keywordRecognizer = null;
Dictionary<string, System.Action> keywords = new Dictionary<string, System.Action>();
// Use this for initialization
void Start()
{
keywords.Add("Reset world", () =>
{
// Call the OnReset method on every descendant object.
this.BroadcastMessage("OnReset");
});
keywords.Add("Drop Sphere", () =>
{
var focusObject = GazeGestureManager.Instance.FocusedObject;
if (focusObject != null)
{
// Call the OnDrop method on just the focused object.
focusObject.SendMessage("OnDrop", SendMessageOptions.DontRequireReceiver);
}
});
// Tell the KeywordRecognizer about our keywords.
keywordRecognizer = new KeywordRecognizer(keywords.Keys.ToArray());
// Register a callback for the KeywordRecognizer and start recognizing!
keywordRecognizer.OnPhraseRecognized += KeywordRecognizer_OnPhraseRecognized;
keywordRecognizer.Start();
}
private void KeywordRecognizer_OnPhraseRecognized(PhraseRecognizedEventArgs args)
{
System.Action keywordAction;
if (keywords.TryGetValue(args.text, out keywordAction))
{
keywordAction.Invoke();
}
}
}
(1)建立一個
Dictionary<string, System.Action>
的集合,向集合中添加
Reset world
和
Drop Sphere
:
keywords.Add("Reset world", () =>
{
this.BroadcastMessage("OnReset");
});
keywords.Add("Drop Sphere", () =>
{
var focusObject = GazeGestureManager.Instance.FocusedObject;
if (focusObject != null)
{
focusObject.SendMessage("OnDrop", SendMessageOptions.DontRequireReceiver);
}
});
在 Drop Sphere 中,如果凝聚對象非空的話,向其推送 OnDrop 消息。
(2)初始化一個語音識别器,将集合key值數組傳入。
(3)為 注冊回調并啟動識别器。
keywordRecognizer.OnPhraseRecognized += KeywordRecognizer_OnPhraseRecognized;
keywordRecognizer.Start();
private void KeywordRecognizer_OnPhraseRecognized(PhraseRecognizedEventArgs args)
{
System.Action keywordAction;
if (keywords.TryGetValue(args.text, out keywordAction))
{
keywordAction.Invoke();
}
}
keywords.TryGetValue(args.text, out keywordAction) 擷取使用者的語音,如果存在于集合中,傳回true,并将捆綁的Action存入 keywordAction,調用 Invoke() 反射執行集合中對應的方法。
4.2 SphereCommands
using UnityEngine;
public class SphereCommands : MonoBehaviour
{
Vector3 originalPosition;
void Start()
{
// 擷取啟動時球的初始位置
originalPosition = this.transform.localPosition;
}
// Called by GazeGestureManager when the user performs a Select gesture
void OnSelect()
{
// If the sphere has no Rigidbody component, add one to enable physics.
if (!this.GetComponent<Rigidbody>())
{
var rigidbody = this.gameObject.AddComponent<Rigidbody>();
rigidbody.collisionDetectionMode = CollisionDetectionMode.Continuous;
}
}
// Called by SpeechManager when the user says the "Reset world" command
void OnReset()
{
// If the sphere has a Rigidbody component, remove it to disable physics.
var rigidbody = this.GetComponent<Rigidbody>();
if (rigidbody != null)
{
rigidbody.isKinematic = true;
Destroy(rigidbody);
}
// Put the sphere back into its original local position.
this.transform.localPosition = originalPosition;
}
// Called by SpeechManager when the user says the "Drop sphere" command
void OnDrop()
{
// Just do the same logic as a Select gesture.
OnSelect();
}
}
(1)啟動時擷取球的初始位置。
(2)
OnDrop()
方法中直接調用
OnSelect()
方法。
(3)
OnReset()
方法中,擷取剛體元件,如果存在,開啟動力學開關,并将其銷毀。
if (rigidbody != null)
{
rigidbody.isKinematic = true;
Destroy(rigidbody);
}
(4)将球位置替換為初始位置。
4.3 SendMessage
介紹下上面代碼中使用到的
SendMessage
函數。
(1)SendMessage…
-
SendMessage
調用一個對象的methodName函數(公有 or 私有均可),後面跟一個可選參數(函數入參)。
-
SendMessageUpwards
類似于 SendMessage ,但是它不僅會向目前對象推送消息,也會向這個對象的父對象推送這個消息(周遊所有父對象推送)。
-
BroadcastMessage
類似于 SendMessage ,但是它不僅會向目前對象推送消息,也會向這個對象的子對象推送這個消息(周遊所有子對象推送)。
(2)SendMessageOptions
-
:如果沒有找到相應函數,會報錯RequireReceive
-
:如果沒有找到相應函數,不會報錯DontRequireReceive