MixedRealityToolkit-UnityのInputManagerを紐解く ~Voice編~

投稿者: | 2017-10-11

前々回からの続きでMixedRealityToolkit-UnityのInputManagerを使った音声入力を取り扱う方法をまとめます。過去のGazeやジェスチャー入力の内容を踏まえたエントリーになりますので、まだご覧になっていない方は以下を参照してください。


Unityが提供する音声認識API

UnityではWindowsストアアプリ用に主に2つの音声認識APIが用意されています。1つは予めキーワードを登録しておき、入力された音声とマッチングを行うKeywordRecognizer(以下、キーワードベース音声認識)と、自由文を検出できるDictationRecognizerです。

HoloLensShellと同じように特定の言葉(Select、Removeなど)を使った操作を実装したいのならば、キーワードベース音声認識であるKeywordRecognizerを使うのがよいと思います。本エントリでもそちらにスポットを当てて解説します。

キーワードベース音声認識の使い方

MixedRealityToolkit-Unityにて音声認識を行う場合は、何よりもまずSpeechInputSourceをシーンに配置する必要があります。SpeechInputSourceは音声用の入力ソースです。入力ソースについては前回のジェスチャー入力のエントリで解説した通りで、ユーザの入力を検出してそれをInputManagerに通知する機能を持ったスクリプトです。

どこに配置するかは重要ではありませんが、今回はジェスチャーの入力ソースと同じレベルに配置します。

シーンに配置したあとは、認識させたいキーワードを登録します。上の図の通りインスペクタから設定することができます。ここでは「bigger」「smaller」「change color」の3つを登録しました。

また、SpeechInputSourceではキーボード入力により音声の発生をエミュレーションする機能が備わっています。上の図の場合ですと、「B」を押すと「bigger」と発生したときと同じ状態になります。

認識させたいキーワードの登録が完了したあとは、キーワードを認識したときのイベントハンドラを実装します。イベントハンドラの実装はジェスチャー入力の時と同様に、特定のインタフェースを実装したスクリプトを用意します。音声認識の場合はISpeechHandlerとなります。なお、MixedRealityToolkit-UnityではISpeechHandlerを実装したSpeechInputHandlerというスクリプトがユーティリティとして用意されているので、今回はこちらを使ってみます。

音声で操作したいオブジェクト(ここではCube)にSpeechInputHandlerをアタッチします。

SpeechInputHandlerでは、SpeechInputSourceに登録されているキーワードが認識されたときに実行する処理をインスペクタから設定することができます。SpeechInputHandlerの「+」ボタンを押すと設定ボックスが1段追加されます。ここで「keyword」のリストボックスを選択すると、SpeechInputHandlerに登録されたキーワードの一覧が表示されます。

キーワードを選択したあとは、キーワードを認識したときの処理を設定します。今回は認識したときの処理として以下のようなものを用意しました。(こちらもCubeにアタッチしています)

using UnityEngine;

public class SpeechUnityEventSample : MonoBehaviour {

    public void OnRecognizedBigger()
    {
        transform.localScale *= 2.0f;
    }

    public void OnRecognizedSmaller()
    {
        transform.localScale /= 2.0f;
    }

    public void OnRecognizedChangeColor()
    {
        GetComponent<Renderer>().material.color = 
            new Color(Random.Range(0.0f, 1.0f), Random.Range(0.0f, 1.0f), Random.Range(0.0f, 1.0f));
    }
}

キーワードに処理を紐づける設定もインスペクタから実施することができます。

最終的には以下のようになりました。

以上で設定は完了です。
アプリを実行して、Cubeをフォーカスした状態で「bigger」「smaller」と発声するとCubeの大きさが変化します。「Change color」と発声すると色が変化します。

インスペクタ上での設定項目

SpeechInputSource.cs

Persistent Keyword
シーンをまたいでも音声認識の設定を引き継ぐかどうかを設定できます
Recognizer Start
音声認識機をどのタイミングで起動するかを指定します。「Auto Start」ではアプリ起動時に自動で起動します。「Manual Start」ではSpeechInputSource.StartKeywordRecognizer()をコールして起動する必要があります。
Recognition Confidence Level
音声認識時の認識率の閾値を設定します。「High」に近いほど正しく発声しないと認識しません。反対に「Low」に近いと誤認識の可能性が高くなります。詳しくはこちら

SpeechInputHandler.cs

Is Global Listener
キーワード認識時の処理をグローバルリスナーとするかどうか(グローバルリスナーについては過去のエントリを参照)。チェックを入れていない場合はオブジェクトにフォーカスを当てていないと処理が実行されませんが、チェックを入れると発生するだけで処理が実行されます。ただしフォーカスを当てようが当てまいが関係なく実行されるので注意する必要があります。
Persistent Keyword
シーンをまたいでも音声認識の設定を引き継ぐかどうかを設定できます

実装

SpeechInputSource.cs

Unity標準APIのKeywordRecognizerをベースに作られています。

namespace HoloToolkit.Unity.InputModule
{
    public partial class SpeechInputSource : BaseInputSource
    {
        /// <summary>
        /// シーンをまたいで音声認識を有効化するか
        /// </summary>
        [Tooltip("Keywords are persistent across all scenes.  This Speech Input Source instance will not be destroyed when loading a new scene.")]
        public bool PersistentKeywords;

        public enum RecognizerStartBehavior { AutoStart, ManualStart }
        // 音声認識を自動で起動するか、マニュアル操作で起動するか
        [Tooltip("Whether the recognizer should be activated on start.")]
        public RecognizerStartBehavior RecognizerStart;

        // キーワードとキーコードの組
        [Tooltip("The keywords to be recognized and optional keyboard shortcuts.")]
        public KeywordAndKeyCode[] Keywords;

#if UNITY_WSA || UNITY_STANDALONE_WIN
        [Tooltip("The confidence level for the keyword recognizer.")]
        // 音声認識の精度のチューニング
        [SerializeField]
        private ConfidenceLevel recognitionConfidenceLevel = ConfidenceLevel.Medium;

        // キーワードベースの音声認識機
        private KeywordRecognizer keywordRecognizer;

キーボード入力をエミュレーションするため、キーワードとキーコードの組の配列を定義しています。

using System;
using UnityEngine;

namespace HoloToolkit.Unity.InputModule
{
    [Serializable]
    public struct KeywordAndKeyCode
    {
        [Tooltip("The keyword to recognize.")]
        public string Keyword;
        [Tooltip("The KeyCode to recognize.")]
        public KeyCode KeyCode;
    }
}

アプリ起動時にKeywordRecognizerをインスタンス化し、認識させたいキーワードを登録しています。

protected virtual void Start()
{
    if (PersistentKeywords)
    {
        DontDestroyOnLoad(gameObject);
    }
    
    int keywordCount = Keywords.Length;
    if (keywordCount > 0)
    {
        // キーワードとキーコードの組からキーワードだけ抽出
        var keywords = new string[keywordCount];
        for (int index = 0; index < keywordCount; index++)
        {
            keywords[index] = Keywords[index].Keyword;
        }
        // キーワードを登録
        keywordRecognizer = new KeywordRecognizer(keywords, recognitionConfidenceLevel);
        // キーワードを認識したときのイベントハンドラを登録
        keywordRecognizer.OnPhraseRecognized += KeywordRecognizer_OnPhraseRecognized;
        // 自動で起動するよう設定されていたら、音声認識機を起動
        if (RecognizerStart == RecognizerStartBehavior.AutoStart)
        {
            keywordRecognizer.Start();
        }
    }
    else
    {
        Debug.LogError("Must have at least one keyword specified in the Inspector on " + gameObject.name + ".");
    }
}

キーワード認識時のイベントハンドラは以下のようになっています。

private void KeywordRecognizer_OnPhraseRecognized(PhraseRecognizedEventArgs args)
{
    // InputManagerに処理をまかせる
    OnPhraseRecognized(args.confidence, args.phraseDuration, args.phraseStartTime, args.semanticMeanings, args.text);
}

protected void OnPhraseRecognized(ConfidenceLevel confidence, TimeSpan phraseDuration, DateTime phraseStartTime, SemanticMeaning[] semanticMeanings, string text)
{
    InputManager.Instance.RaiseSpeechKeywordPhraseRecognized(this, 0, confidence, phraseDuration, phraseStartTime, semanticMeanings, text);
}

ジェスチャーの時と同様、実際のハンドリングはInputManagerに委譲しています。

private static readonly ExecuteEvents.EventFunction<ISpeechHandler> OnSpeechKeywordRecognizedEventHandler =
    delegate (ISpeechHandler handler, BaseEventData eventData)
    {
        SpeechEventData casted = ExecuteEvents.ValidateEventData<SpeechEventData>(eventData);
        handler.OnSpeechKeywordRecognized(casted);
    };
    
public void RaiseSpeechKeywordPhraseRecognized(IInputSource source, uint sourceId, ConfidenceLevel confidence, TimeSpan phraseDuration, DateTime phraseStartTime, SemanticMeaning[] semanticMeanings, string text)
{
    // イベントデータの作成
    speechEventData.Initialize(source, sourceId, confidence, phraseDuration, phraseStartTime, semanticMeanings, text);
    // 音声認識イベントの通知
    HandleEvent(speechEventData, OnSpeechKeywordRecognizedEventHandler);
}

なお、キーボードによるエミュレーションについてはフレーム毎にキーの状態を確認して実施しています。

protected virtual void Update()
{
    if (keywordRecognizer != null && keywordRecognizer.IsRunning)
    {
        ProcessKeyBindings();
    }
}

/// <summary>
/// キーボードの入力を拾って音声認識をエミュレーション
/// </summary>
private void ProcessKeyBindings()
{
    for (int index = Keywords.Length; --index >= 0;)
    {
        // キーボード入力を拾って、それと紐づくキーワードをInputManagerに渡す
        if (Input.GetKeyDown(Keywords[index].KeyCode))
        {
            OnPhraseRecognized(recognitionConfidenceLevel, TimeSpan.Zero, DateTime.Now, null, Keywords[index].Keyword);
        }
    }
}

SpeechInputHandler.cs

音声認識時のハンドラを実装するときの参考になるのでチェックしておくことをおススメします。

インスペクタから設定できるようにする都合上、キーワードと処理(UnityEventとして定義する)の組を表す構造体を定義しています。

namespace HoloToolkit.Unity.InputModule
{
    public class SpeechInputHandler : MonoBehaviour, ISpeechHandler
    {
        // キーワードとそれに紐づく処理(UnityEvent)の組
        // インスペクタ上で設定できるようにするために定義
        [Serializable]
        public struct KeywordAndResponse
        {
            [Tooltip("The keyword to handle.")]
            public string Keyword;

            [Tooltip("The handler to be invoked.")]
            public UnityEvent Response;
        }
        [Tooltip("The keywords to be recognized and optional keyboard shortcuts.")]
        public KeywordAndResponse[] Keywords;

        // 内部ではKeywordAndResponseはDictionaryとして管理する
        [NonSerialized]
        private readonly Dictionary<string, UnityEvent> responses = new Dictionary<string, UnityEvent>();

        [Tooltip("Determines if this handler is a global listener, not connected to a specific GameObject.")]
        public bool IsGlobalListener;

        [Tooltip("Keywords are persistent across all scenes.  This Speech Input Handler instance will not be destroyed when loading a new scene.")]
        public bool PersistentKeywords

インスペクタ上で設定したキーワードと処理の組は、アプリ起動時に分解されてDictionaryとして管理されます。

protected virtual void Start()
{
    if (PersistentKeywords)
    {
        DontDestroyOnLoad(gameObject);
        SceneManager.sceneLoaded += OnSceneLoaded;
    }
    // KeywordAndResponse(キーワードと処理の組)を分解してDictionaryにする
    int keywordCount = Keywords.Length;
    for (int index = 0; index < keywordCount; index++)
    {
        KeywordAndResponse keywordAndResponse = Keywords[index];
        string keyword = keywordAndResponse.Keyword.ToLower();
        if (responses.ContainsKey(keyword))
        {
            Debug.LogError("Duplicate keyword '" + keyword + "' specified in '" + gameObject.name + "'.");
        }
        else
        {
            responses.Add(keyword, keywordAndResponse.Response);
        }
    }
    // グローバルリスナーに登録
    if (IsGlobalListener)
    {
        InputManager.Instance.AddGlobalListener(gameObject);
    }
}

キーワードが認識されると、オブジェクトにフォーカスを当てている、もしくはグローバルリスナーとして登録されている場合はInputManagerから次のメソッドがコールされます。

// InputManagerからコールされる
void ISpeechHandler.OnSpeechKeywordRecognized(SpeechEventData eventData)
{
     UnityEvent keywordResponse;
     // SpeechEventDataに認識したキーワードがセットされているので、
     // それをキーにDictionaryから処理を引っ張ってきて実行する
     if (enabled && responses.TryGetValue(eventData.RecognizedText.ToLower(), out keywordResponse))
     {
         keywordResponse.Invoke();
     }
}

まとめ

MixedRealityToolkit-Unityを使ったキーワード音声認識の使い方をまとめました。

個人的な感覚としてHoloアプリの入力として音声を使ってるケースはジェスチャーに比べて少ないと感じています。ただ初心者には音声認識の方が操作しやすい場合もあるので、上手に使い分けていきたいところです。