HoloLens(第一代)输入 212:语音

重要

混合现实学院教程在制作时考虑到了 HoloLens(第一代)、Unity 2017 和混合现实沉浸式头戴显示设备。 因此,对于仍在寻求这些设备的开发指导的开发人员而言,我们觉得很有必要保留这些教程。 我们不会在这些教程中更新 HoloLens 2 所用的最新工具集或交互相关的内容,因此这些教程可能与较新版本的 Unity 不相符。 我们将维护这些教程,使之持续适用于支持的设备。 已经为 HoloLens 2 发布了一系列新教程

语音输入为我们提供了与全息影像交互的另一种方式。 语音命令的工作方式非常自然且简单。 请按照以下原则设计语音命令:

  • Natural
  • 容易记住
  • 与上下文相适应
  • 与同一上下文中的其他选项有足够大的差别

MR 基础知识 101 中,我们使用 KeywordRecognizer 生成了两个简单的语音命令。 在 MR 输入 212 中,我们将深入了解如何:

  • 设计针对 HoloLens 语音引擎优化的语音命令。
  • 让用户知道哪些语音命令可用。
  • 确认我们已听到用户的语音命令。
  • 使用听写识别器了解用户在说什么。
  • 使用语法识别器基于 SRGS(语音识别语法规范)文件聆听命令。

在本课程中,我们将再次用到在 MR 输入 210MR 输入 211 中生成的模型资源管理器。

重要

下面每一章中嵌入的视频是使用旧版 Unity 和混合现实工具包录制的。 虽然分步说明比较准确且是最新的,但你在相应视频中可能会看到已过时的脚本和视觉效果。 保留这些视频是为了供后来的读者参考,并且涉及的概念现在仍然适用。

设备支持

课程 HoloLens 沉浸式头戴显示设备
MR 输入 212:语音

开始之前

先决条件

项目文件

  • 下载项目所需的文件。 需要 Unity 2017.2 或更高版本。
  • 将文件解压缩到桌面或其他易于访问的位置。

注意

如果要在下载源代码之前查看它,可以在 GitHub 上查看

勘误表和备注

  • 需要在 Visual Studio 中的“工具”->“选项”->“调试”下禁用(取消选中)“启用仅我的代码”,以便在代码中命中断点

Unity 设置

说明

  1. 启动 “Unity”。
  2. 选择打开
  3. 导航到前面解压缩的“HolographicAcademy-Holograms-212-Voice”文件夹
  4. 找到并选择“Starting”/“Model Explorer”文件夹
  5. 单击“选择文件夹”按钮
  6. 在“项目”面板中,展开“Scenes”文件夹
  7. 双击“ModelExplorer”场景以在 Unity 中加载它

生成

  1. 在 Unity 中,选择“文件”>“生成设置”
  2. 如果“Scenes/ModelExplorer”未列在“生成中的场景”中,请单击“添加开放场景”以添加该场景
  3. 如果专门针对 HoloLens 进行开发,请将“目标设备”设置为“HoloLens”。 否则,请将此选项保留为“任何设备”
  4. 确保将“生成类型”设置为“D3D”,将“SDK”设置为“最新安装版本”(应该是 SDK 16299 或更高版本)
  5. 单击“生成”
  6. 创建名为“App”的新文件夹
  7. 单击“App”文件夹
  8. 按“选择文件夹”,然后 Unity 将开始生成适用于 Visual Studio 的项目

完成 Unity 设置后,将出现一个文件资源管理器窗口。

  1. 打开“App”文件夹。
  2. 打开“ModelExplorer Visual Studio 解决方案”

如果是部署到 HoloLens:

  1. 使用 Visual Studio 中的顶部工具栏,将目标从“调试”更改为“发布”,并从“ARM”更改为“x86”
  2. 单击“本地计算机”按钮旁边的下拉箭头,然后选择“远程计算机”
  3. 输入 HoloLens 设备 IP 地址,将“身份验证模式”设置为“通用(未加密协议)”。 单击“选择”。 如果你不知道自己的设备 IP 地址,可以在“设置”>“网络和 Internet”>“高级选项”中找到。
  4. 在顶部菜单栏中,单击“调试”->“开始执行(不调试)”或按 Ctrl + F5。 如果这是你第一次部署到设备,需要将设备与 Visual Studio 配对
  5. 部署应用后,使用“选择手势”关闭“工具箱”

如果部署到沉浸式头戴显示设备:

  1. 使用 Visual Studio 中的顶部工具栏,将目标从“调试”更改为“发布”,并从“ARM”更改为“x64”
  2. 确保部署目标设置为“本地计算机”
  3. 在顶部菜单栏中,单击“调试”->“开始执行(不调试)”或按 Ctrl + F5
  4. 部署应用后,通过拉动运动控制器上的触发器来关闭“工具箱”

注意

你可能会注意到 Visual Studio“错误”面板中以红色字体显示了一些错误。 可以放心地忽略这些错误。 切换到“输出”面板以查看实际生成进度。 需要修复“输出”面板中的错误(它们往往是脚本中的错误导致的)。

第 1 章 - 认识

目标

  • 了解语音命令设计的“宜做事项和禁忌事项”
  • 使用 “KeywordRecognizer” 添加基于视线的语音命令
  • 使用光标“反馈”让用户认识语音命令

语音指令设计

本章介绍如何设计语音命令。 创建语音命令时:

DO

  • 创建简洁的命令。 不适合使用“播放当前选择的视频”,因为该命令不简洁,用户很容易忘记它。 应该使用“播放视频”,因为此命令简洁且有多个音节
  • 使用简单的词汇。 始终尝试使用用户容易发现和记住的常用字词与短语。 例如,如果应用程序有一个可以在视图中显示或隐藏的便笺对象,则最好不要使用命令“显示招贴”,因为“招贴”是一个很少用的词语。 可以改用命令“显示便笺”,这样即可在应用程序中显示便笺
  • 保持一致。 语音命令应在整个应用程序中保持一致。 假设应用程序中有两个场景,这两个场景都包含一个用于关闭该应用程序的按钮。 如果第一个场景使用命令“退出”来触发该按钮,而第二个场景使用命令“关闭应用”,则用户会非常困惑。 如果在多个场景中保持同一功能,则应使用同一语音命令来触发该功能。

禁忌事项

  • 使用单音节命令。 例如,如果你要创建某个语音命令来播放视频,则应避免使用简单的命令“播放”,因为它只有一个音节,很容易被系统忽略。 应该使用“播放视频”,因为此命令简洁且有多个音节
  • 使用系统命令。 “选择”命令是系统保留的命令,用于对当前聚焦的对象触发点击事件。 不要在关键字或短语中重复使用“选择”命令,否则它可能不按预期方式工作。 例如,如果用于在应用程序中选择立方体的语音命令是“选择立方体”,但用户在发出该命令时正在注视某个球体,则就会选择该球体。 类似地,应用栏命令也支持语音。 不要在 CoreWindow 视图中使用以下语音命令:
    1. 返回
    2. 滚动工具
    3. 缩放工具
    4. 拖放工具
    5. 调整
    6. 删除
  • 使用类似的音效。 尽量避免使用押韵的语音命令。 如果你的购物应用程序支持使用“显示商店”和“显示床垫”作为语音命令,则最好是在使用其中一个命令时禁用另一个命令。 例如,可以使用“显示商店”命令打开商店,然后在显示商店后禁用该命令,以便可以使用“显示更多”命令来浏览更多

说明

  • 在 Unity 的“层次结构”面板中,使用搜索工具找到“holoComm_screen_mesh”对象
  • 双击“holoComm_screen_mesh”对象以在“场景”中查看它。 这是宇航员的手表,它将响应我们的语音命令。
  • 在“检查器”面板中,找到“语音输入源(脚本)”组件
  • 展开“关键字”部分以查看支持的语音命令:“打开通信器”
  • 单击右侧的齿轮图标,然后选择“编辑脚本”
  • 探索“SpeechInputSource.cs”,了解它如何使用“KeywordRecognizer”来添加语音命令

生成和部署

  • 在 Unity 中,使用“文件”>“生成设置”来重新生成应用程序
  • 打开“App”文件夹。
  • 打开“ModelExplorer Visual Studio 解决方案”

(如果在设置期间已在 Visual Studio 中生成/部署了该项目,则可以打开该 VS 实例并在出现提示时单击“全部重新加载”)。

  • 在 Visual Studio 中,单击“调试”->“开始执行(不调试)”或按 Ctrl + F5
  • 在应用程序部署到 HoloLens 后,使用隔空敲击手势关闭工具箱。
  • 凝视宇航员的手表。
  • 在手表上聚焦后,确认光标是否变为麦克风。 这提供了应用程序正在聆听语音命令的反馈。
  • 确认手表上是否出现了工具提示。 这有助于用户发现“打开通信器”命令
  • 凝视手表时,说出“打开通信器”以打开通信器面板

第 2 章 - 确认

目标

  • 使用麦克风输入录制一条消息。
  • 向用户提供应用程序正在聆听其语音的反馈。

注意

必须为应用声明“麦克风”功能才能从麦克风录音。 此操作已在 MR 输入 212 中完成,但请在你自己的项目中考虑到这一点。

  1. 在 Unity 编辑器中,导航到“编辑”>“项目设置”>“玩家”,转到“玩家设置”
  2. 单击“通用 Windows 平台”选项卡
  3. 在“发布设置”>“功能”部分,选中“麦克风”功能

说明

  • 在 Unity 的“层次结构”面板中,确认已选择“holoComm_screen_mesh”对象
  • 在“检查器”面板中,找到“宇航员手表(脚本)”组件
  • 单击蓝色小立方体,它已设置为“通信器预制件”属性的值
  • 在“项目”面板中,现在应已聚焦于“通信器”预制件
  • 单击“项目”面板中的“通信器”预制件,在“检查器”中查看其组件
  • 查看“麦克风管理器(脚本)”组件,我们可以使用它来录制用户语音
  • 请注意,“通信器”对象有一个“语音输入处理程序(脚本)”组件,该组件用于响应“发送消息”命令
  • 查看“通信器(脚本)”组件,并双击该脚本以在 Visual Studio 中将其打开

Communicator.cs 负责在通信器设备上设置正确的按钮状态。 这样,我们的用户便可以录制消息、播放消息并将其发送给宇航员。 它还可以启动和停止动画波形,以向用户确认已听到他们的语音。

  • 在“Communicator.cs”中,删除 Start 方法中的以下行(第 81 和 82 行)。 这会启用通信器上的“录制”按钮。
// TODO: 2.a Delete the following two lines:
RecordButton.SetActive(false);
MessageUIRenderer.gameObject.SetActive(false);

生成和部署

  • 在 Visual Studio 中,重新生成应用程序并将其部署到设备。
  • 凝视宇航员的手表并说出“打开通信器”以显示通信器
  • 按“录制”按钮(麦克风)开始录制要发送给宇航员的口头消息
  • 开始讲话,并确认通信器上正在播放波形动画,该动画向用户提供已听到其语音的反馈。
  • 按“停止”按钮(左侧方块),并确认波形动画停止运行
  • 按“播放”按钮(右侧三角形)以播放录制的消息并在设备上收听
  • 按“停止”按钮(右侧方块)以停止播放录制的消息
  • 说出“发送消息”以关闭通信器并接收来自宇航员的“收到消息”响应

第 3 章 - 理解和听写识别器

目标

  • 使用听写识别器将用户的语音转换为文本。
  • 在通信器中显示听写识别器的猜测结果和最终结果。

在本章中,我们将使用听写识别器创建一条要发送给宇航员的消息。 使用听写识别器时,请记住:

  • 必须连接到 WiFi 才能正常运行听写识别器。
  • 超时发生在设置的时间段之后。 需要注意两个超时:
    • 如果识别器启动后在前五秒钟内未听到任何音频,则它会超时。
    • 如果识别器已给出结果,但随后听到 20 秒的静音,则它会超时。
  • 每次只能运行一种类型的识别器(“关键字”或“听写”)。

注意

必须为应用声明“麦克风”功能才能从麦克风录音。 此操作已在 MR 输入 212 中完成,但请在你自己的项目中考虑到这一点。

  1. 在 Unity 编辑器中,导航到“编辑”>“项目设置”>“玩家”,转到“玩家设置”
  2. 单击“通用 Windows 平台”选项卡
  3. 在“发布设置”>“功能”部分,选中“麦克风”功能

说明

我们将编辑 “MicrophoneManager.cs” 以使用听写识别器。 这是我们要添加的内容:

  1. 按下“录制”按钮时,将“启动 DictationRecognizer”
  2. 显示 DictationRecognizer 理解的“猜测结果”
  3. 锁定 DictationRecognizer 理解的“结果”
  4. 检查 DictationRecognizer 的超时。
  5. 按下“停止按钮”或麦克风会话超时时,“停止 DictationRecognizer”
  6. 重启 “KeywordRecognizer”,它将聆听“发送消息”命令

现在就开始吧。 完成 “MicrophoneManager.cs” 中 3.a 的所有编程练习,或复制并粘贴下面已完成的代码

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System.Collections;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Windows.Speech;

namespace Academy
{
    public class MicrophoneManager : MonoBehaviour
    {
        [Tooltip("A text area for the recognizer to display the recognized strings.")]
        [SerializeField]
        private Text dictationDisplay;

        private DictationRecognizer dictationRecognizer;

        // Use this string to cache the text currently displayed in the text box.
        private StringBuilder textSoFar;

        // Using an empty string specifies the default microphone.
        private static string deviceName = string.Empty;
        private int samplingRate;
        private const int messageLength = 10;

        // Use this to reset the UI once the Microphone is done recording after it was started.
        private bool hasRecordingStarted;

        void Awake()
        {
            /* TODO: DEVELOPER CODING EXERCISE 3.a */

            // 3.a: Create a new DictationRecognizer and assign it to dictationRecognizer variable.
            dictationRecognizer = new DictationRecognizer();

            // 3.a: Register for dictationRecognizer.DictationHypothesis and implement DictationHypothesis below
            // This event is fired while the user is talking. As the recognizer listens, it provides text of what it's heard so far.
            dictationRecognizer.DictationHypothesis += DictationRecognizer_DictationHypothesis;

            // 3.a: Register for dictationRecognizer.DictationResult and implement DictationResult below
            // This event is fired after the user pauses, typically at the end of a sentence. The full recognized string is returned here.
            dictationRecognizer.DictationResult += DictationRecognizer_DictationResult;

            // 3.a: Register for dictationRecognizer.DictationComplete and implement DictationComplete below
            // This event is fired when the recognizer stops, whether from Stop() being called, a timeout occurring, or some other error.
            dictationRecognizer.DictationComplete += DictationRecognizer_DictationComplete;

            // 3.a: Register for dictationRecognizer.DictationError and implement DictationError below
            // This event is fired when an error occurs.
            dictationRecognizer.DictationError += DictationRecognizer_DictationError;

            // Query the maximum frequency of the default microphone. Use 'unused' to ignore the minimum frequency.
            int unused;
            Microphone.GetDeviceCaps(deviceName, out unused, out samplingRate);

            // Use this string to cache the text currently displayed in the text box.
            textSoFar = new StringBuilder();

            // Use this to reset the UI once the Microphone is done recording after it was started.
            hasRecordingStarted = false;
        }

        void Update()
        {
            // 3.a: Add condition to check if dictationRecognizer.Status is Running
            if (hasRecordingStarted && !Microphone.IsRecording(deviceName) && dictationRecognizer.Status == SpeechSystemStatus.Running)
            {
                // Reset the flag now that we're cleaning up the UI.
                hasRecordingStarted = false;

                // This acts like pressing the Stop button and sends the message to the Communicator.
                // If the microphone stops as a result of timing out, make sure to manually stop the dictation recognizer.
                // Look at the StopRecording function.
                SendMessage("RecordStop");
            }
        }

        /// <summary>
        /// Turns on the dictation recognizer and begins recording audio from the default microphone.
        /// </summary>
        /// <returns>The audio clip recorded from the microphone.</returns>
        public AudioClip StartRecording()
        {
            // 3.a Shutdown the PhraseRecognitionSystem. This controls the KeywordRecognizers
            PhraseRecognitionSystem.Shutdown();

            // 3.a: Start dictationRecognizer
            dictationRecognizer.Start();

            // 3.a Uncomment this line
            dictationDisplay.text = "Dictation is starting. It may take time to display your text the first time, but begin speaking now...";

            // Set the flag that we've started recording.
            hasRecordingStarted = true;

            // Start recording from the microphone for 10 seconds.
            return Microphone.Start(deviceName, false, messageLength, samplingRate);
        }

        /// <summary>
        /// Ends the recording session.
        /// </summary>
        public void StopRecording()
        {
            // 3.a: Check if dictationRecognizer.Status is Running and stop it if so
            if (dictationRecognizer.Status == SpeechSystemStatus.Running)
            {
                dictationRecognizer.Stop();
            }

            Microphone.End(deviceName);
        }

        /// <summary>
        /// This event is fired while the user is talking. As the recognizer listens, it provides text of what it's heard so far.
        /// </summary>
        /// <param name="text">The currently hypothesized recognition.</param>
        private void DictationRecognizer_DictationHypothesis(string text)
        {
            // 3.a: Set DictationDisplay text to be textSoFar and new hypothesized text
            // We don't want to append to textSoFar yet, because the hypothesis may have changed on the next event
            dictationDisplay.text = textSoFar.ToString() + " " + text + "...";
        }

        /// <summary>
        /// This event is fired after the user pauses, typically at the end of a sentence. The full recognized string is returned here.
        /// </summary>
        /// <param name="text">The text that was heard by the recognizer.</param>
        /// <param name="confidence">A representation of how confident (rejected, low, medium, high) the recognizer is of this recognition.</param>
        private void DictationRecognizer_DictationResult(string text, ConfidenceLevel confidence)
        {
            // 3.a: Append textSoFar with latest text
            textSoFar.Append(text + ". ");

            // 3.a: Set DictationDisplay text to be textSoFar
            dictationDisplay.text = textSoFar.ToString();
        }

        /// <summary>
        /// This event is fired when the recognizer stops, whether from Stop() being called, a timeout occurring, or some other error.
        /// Typically, this will simply return "Complete". In this case, we check to see if the recognizer timed out.
        /// </summary>
        /// <param name="cause">An enumerated reason for the session completing.</param>
        private void DictationRecognizer_DictationComplete(DictationCompletionCause cause)
        {
            // If Timeout occurs, the user has been silent for too long.
            // With dictation, the default timeout after a recognition is 20 seconds.
            // The default timeout with initial silence is 5 seconds.
            if (cause == DictationCompletionCause.TimeoutExceeded)
            {
                Microphone.End(deviceName);

                dictationDisplay.text = "Dictation has timed out. Please press the record button again.";
                SendMessage("ResetAfterTimeout");
            }
        }

        /// <summary>
        /// This event is fired when an error occurs.
        /// </summary>
        /// <param name="error">The string representation of the error reason.</param>
        /// <param name="hresult">The int representation of the hresult.</param>
        private void DictationRecognizer_DictationError(string error, int hresult)
        {
            // 3.a: Set DictationDisplay text to be the error string
            dictationDisplay.text = error + "\nHRESULT: " + hresult;
        }

        /// <summary>
        /// The dictation recognizer may not turn off immediately, so this call blocks on
        /// the recognizer reporting that it has actually stopped.
        /// </summary>
        public IEnumerator WaitForDictationToStop()
        {
            while (dictationRecognizer != null && dictationRecognizer.Status == SpeechSystemStatus.Running)
            {
                yield return null;
            }
        }
    }
}

生成和部署

  • 在 Visual Studio中重新生成应用并将其部署到设备。
  • 使用隔空敲击手势关闭工具箱。
  • 凝视宇航员的手表并说出“打开通信器”
  • 选择“录制”按钮(麦克风)录制消息
  • 开始讲话。 “听写识别器”将解译你的语音并在通信器中显示猜测的文本
  • 在录制消息时尝试说出“发送消息”。 将会发现,“关键字识别器”无响应,因为听写识别器仍处于活动状态
  • 停止说话几秒钟。 观察听写识别器完成其猜测并显示最终结果。
  • 开始讲话,然后暂停 20 秒。 这会导致“听写识别器”超时
  • 将会发现,在发生上述超时后,“关键字识别器”已重新启用。 通信器现在会响应语音命令。
  • 说出“发送消息”,将消息发送给宇航员

第 4 章 - 语法识别器

目标

  • 使用语法识别器可以根据 SRGS(语音识别语法规范)文件识别用户的语音。

注意

必须为应用声明“麦克风”功能才能从麦克风录音。 此操作已在 MR 输入 212 中完成,但请在你自己的项目中考虑到这一点。

  1. 在 Unity 编辑器中,导航到“编辑”>“项目设置”>“玩家”,转到“玩家设置”
  2. 单击“通用 Windows 平台”选项卡
  3. 在“发布设置”>“功能”部分,选中“麦克风”功能

说明

  1. 在“层次结构”面板中,搜索并选择“Jetpack_Center”
  2. 在“检查器”面板中查找“跟随操作”脚本
  3. 单击“要跟随的对象”字段右侧的小圆圈
  4. 在弹出的窗口中,搜索“SRGSToolbox”并从列表中选择它
  5. 查看“StreamingAssets”文件夹中的“SRGSColor.xml”文件
    1. 可在此处的 W3C 网站上找到 SRGS 设计规范。

我们的 SRGS 文件中包含三种类型的规则:

  • 有一条规则允许你从 12 种颜色的列表中说出一种颜色。
  • 有三条规则侦听颜色规则以及三种形状之一的组合。
  • 根规则 (colorChooser) 侦听三个“颜色 + 形状”规则的任意组合。 可按任意顺序说出任意数量(从一个到所有三个)的形状。 这是唯一受侦听的规则,因为它在文件顶部的初始 grammar 标记中指定为根规则<>。

生成和部署

  • 在 Unity 中重新生成应用程序,然后在 Visual Studio 中生成并部署,以便在 HoloLens 上体验应用。
  • 使用隔空敲击手势关闭工具箱。
  • 凝视宇航员的喷气背包并执行隔空敲击手势。
  • 开始讲话。 “语法识别器”将解译语音并根据识别结果更改形状的颜色。 示例命令包括“蓝色圆圈,黄色方块”。
  • 再次执行隔空敲击手势以关闭工具箱。

结束

祝贺你! 现已完成“MR 输入 212:语音”

  • 你已了解语音命令宜做事项和禁忌事项。
  • 你已了解如何使用工具提示来让用户认识语音命令。
  • 你已了解用于确认听到了用户语音的多种反馈。
  • 你已了解如何在关键字识别器与听写识别器之间切换,以及这两项功能如何理解和解译语音。
  • 你已了解如何在应用程序中使用 SRGS 文件和语法识别器进行语音识别。