天天看點

Hololens 開發筆記(7)——Voice

本篇文章來學習 Hololens 的基礎開發之語音操作。

源碼位址:https://github.com/jitwxs/blog_sample

建立一個新的 Unity 項目 VoiceDemo,初始化項目:

  1. 導入 MRTK 包
  2. 應用項目設定為 MR 項目
  3. 使用

    HoloLensCamera

    替代預設相機
  4. 添加

    CursorWithFeedback

  5. 添加

    InputManager

  6. 設定 InputManager 的

    SimpleSinglePointerSelector

    腳本的 Cursor 屬性為添加的 CursorWithFeedback
  7. 添加一個 Cube,位置如下
    Hololens 開發筆記(7)——Voice

最終 Hierarchy 結構如下:

Hololens 開發筆記(7)——Voice

一、語音控制

在 Hierarchy 建立一個空的 gameObject 并重命名為

SpeechManager

,為其添加 MRTK 的

SpeechInputSource.cs

腳本,為其添加兩個關鍵字,分别是 start rotate 和 stop rotate。

  1. 後面對應的

    Key Shortcut

    ,字面意思是對應的鍵盤按鍵,但是我在實際程式中并沒有體會到有啥用,是以就随便設了兩個值,有知道的同學可以留言告訴我下。
  2. Hololens 目前已經支援中文語音,但需要手動安裝刷機,參考官方。
Hololens 開發筆記(7)——Voice
屬性 描述
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 腳本有關系,該腳本用于處理語音輸入。

Hololens 開發筆記(7)——Voice
屬性 描述
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 。

Hololens 開發筆記(7)——Voice

部署到真機上,通過說 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

元件,用于播放聲音,它的屬性使用預設值即可。

Hololens 開發筆記(7)——Voice

在真機中運作程式,我們能夠聽見麥克風的聲音,并且 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

    :如果沒有找到相應函數,不會報錯