重要
混合现实学院教程在制作时考虑到了 HoloLens(第一代)、Unity 2017 和混合现实沉浸式头戴显示设备。 因此,对于仍在寻求这些设备的开发指导的开发人员而言,我们觉得很有必要保留这些教程。 我们不会在这些教程中更新 HoloLens 2 所用的最新工具集或交互相关的内容,因此这些教程可能与较新版本的 Unity 不相符。 我们将维护这些教程,使之持续适用于支持的设备。 已经为 HoloLens 2 发布了一系列新教程。
语音输入为我们提供了与全息影像交互的另一种方式。 语音命令的工作方式非常自然且简单。 请按照以下原则设计语音命令:
- Natural
- 容易记住
- 与上下文相适应
- 与同一上下文中的其他选项有足够大的差别
在 MR 基础知识 101 中,我们使用 KeywordRecognizer 生成了两个简单的语音命令。 在 MR 输入 212 中,我们将深入了解如何:
- 设计针对 HoloLens 语音引擎优化的语音命令。
- 让用户知道哪些语音命令可用。
- 确认我们已听到用户的语音命令。
- 使用听写识别器了解用户在说什么。
- 使用语法识别器基于 SRGS(语音识别语法规范)文件聆听命令。
在本课程中,我们将再次用到在 MR 输入 210 和 MR 输入 211 中生成的模型资源管理器。
重要
下面每一章中嵌入的视频是使用旧版 Unity 和混合现实工具包录制的。 虽然分步说明比较准确且是最新的,但你在相应视频中可能会看到已过时的脚本和视觉效果。 保留这些视频是为了供后来的读者参考,并且涉及的概念现在仍然适用。
设备支持
| 课程 | HoloLens | 沉浸式头戴显示设备 |
|---|---|---|
| MR 输入 212:语音 | ✔ | ✔ |
开始之前
先决条件
- 一台安装了正确工具的 Windows 10 电脑。
- 基础的 C# 编程能力。
- 应已完成 MR 基础知识 101。
- 应已完成 MR 输入 210。
- 应已完成 MR 输入 211。
- 一台配置用于开发的 HoloLens 设备。
项目文件
- 下载项目所需的文件。 需要 Unity 2017.2 或更高版本。
- 将文件解压缩到桌面或其他易于访问的位置。
注意
如果要在下载源代码之前查看它,可以在 GitHub 上查看。
勘误表和备注
- 需要在 Visual Studio 中的“工具”->“选项”->“调试”下禁用(取消选中)“启用仅我的代码”,以便在代码中命中断点。
Unity 设置
说明
- 启动 “Unity”。
- 选择打开。
- 导航到前面解压缩的“HolographicAcademy-Holograms-212-Voice”文件夹。
- 找到并选择“Starting”/“Model Explorer”文件夹。
- 单击“选择文件夹”按钮。
- 在“项目”面板中,展开“Scenes”文件夹。
- 双击“ModelExplorer”场景以在 Unity 中加载它。
生成
- 在 Unity 中,选择“文件”>“生成设置”。
- 如果“Scenes/ModelExplorer”未列在“生成中的场景”中,请单击“添加开放场景”以添加该场景。
- 如果专门针对 HoloLens 进行开发,请将“目标设备”设置为“HoloLens”。 否则,请将此选项保留为“任何设备”。
- 确保将“生成类型”设置为“D3D”,将“SDK”设置为“最新安装版本”(应该是 SDK 16299 或更高版本)。
- 单击“生成”。
- 创建名为“App”的新文件夹。
- 单击“App”文件夹。
- 按“选择文件夹”,然后 Unity 将开始生成适用于 Visual Studio 的项目。
完成 Unity 设置后,将出现一个文件资源管理器窗口。
- 打开“App”文件夹。
- 打开“ModelExplorer Visual Studio 解决方案”。
如果是部署到 HoloLens:
- 使用 Visual Studio 中的顶部工具栏,将目标从“调试”更改为“发布”,并从“ARM”更改为“x86”。
- 单击“本地计算机”按钮旁边的下拉箭头,然后选择“远程计算机”。
- 输入 HoloLens 设备 IP 地址,将“身份验证模式”设置为“通用(未加密协议)”。 单击“选择”。 如果你不知道自己的设备 IP 地址,可以在“设置”>“网络和 Internet”>“高级选项”中找到。
- 在顶部菜单栏中,单击“调试”->“开始执行(不调试)”或按 Ctrl + F5。 如果这是你第一次部署到设备,需要将设备与 Visual Studio 配对。
- 部署应用后,使用“选择手势”关闭“工具箱”。
如果部署到沉浸式头戴显示设备:
- 使用 Visual Studio 中的顶部工具栏,将目标从“调试”更改为“发布”,并从“ARM”更改为“x64”。
- 确保部署目标设置为“本地计算机”。
- 在顶部菜单栏中,单击“调试”->“开始执行(不调试)”或按 Ctrl + F5。
- 部署应用后,通过拉动运动控制器上的触发器来关闭“工具箱”。
注意
你可能会注意到 Visual Studio“错误”面板中以红色字体显示了一些错误。 可以放心地忽略这些错误。 切换到“输出”面板以查看实际生成进度。 需要修复“输出”面板中的错误(它们往往是脚本中的错误导致的)。
第 1 章 - 认识
目标
- 了解语音命令设计的“宜做事项和禁忌事项”。
- 使用 “KeywordRecognizer” 添加基于视线的语音命令。
- 使用光标“反馈”让用户认识语音命令。
语音指令设计
本章介绍如何设计语音命令。 创建语音命令时:
DO
- 创建简洁的命令。 不适合使用“播放当前选择的视频”,因为该命令不简洁,用户很容易忘记它。 应该使用“播放视频”,因为此命令简洁且有多个音节。
- 使用简单的词汇。 始终尝试使用用户容易发现和记住的常用字词与短语。 例如,如果应用程序有一个可以在视图中显示或隐藏的便笺对象,则最好不要使用命令“显示招贴”,因为“招贴”是一个很少用的词语。 可以改用命令“显示便笺”,这样即可在应用程序中显示便笺。
- 保持一致。 语音命令应在整个应用程序中保持一致。 假设应用程序中有两个场景,这两个场景都包含一个用于关闭该应用程序的按钮。 如果第一个场景使用命令“退出”来触发该按钮,而第二个场景使用命令“关闭应用”,则用户会非常困惑。 如果在多个场景中保持同一功能,则应使用同一语音命令来触发该功能。
禁忌事项
- 使用单音节命令。 例如,如果你要创建某个语音命令来播放视频,则应避免使用简单的命令“播放”,因为它只有一个音节,很容易被系统忽略。 应该使用“播放视频”,因为此命令简洁且有多个音节。
- 使用系统命令。 “选择”命令是系统保留的命令,用于对当前聚焦的对象触发点击事件。 不要在关键字或短语中重复使用“选择”命令,否则它可能不按预期方式工作。 例如,如果用于在应用程序中选择立方体的语音命令是“选择立方体”,但用户在发出该命令时正在注视某个球体,则就会选择该球体。 类似地,应用栏命令也支持语音。 不要在 CoreWindow 视图中使用以下语音命令:
- 返回
- 滚动工具
- 缩放工具
- 拖放工具
- 调整
- 删除
- 使用类似的音效。 尽量避免使用押韵的语音命令。 如果你的购物应用程序支持使用“显示商店”和“显示床垫”作为语音命令,则最好是在使用其中一个命令时禁用另一个命令。 例如,可以使用“显示商店”命令打开商店,然后在显示商店后禁用该命令,以便可以使用“显示更多”命令来浏览更多。
说明
- 在 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 中完成,但请在你自己的项目中考虑到这一点。
- 在 Unity 编辑器中,导航到“编辑”>“项目设置”>“玩家”,转到“玩家设置”
- 单击“通用 Windows 平台”选项卡
- 在“发布设置”>“功能”部分,选中“麦克风”功能
说明
- 在 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 中完成,但请在你自己的项目中考虑到这一点。
- 在 Unity 编辑器中,导航到“编辑”>“项目设置”>“玩家”,转到“玩家设置”
- 单击“通用 Windows 平台”选项卡
- 在“发布设置”>“功能”部分,选中“麦克风”功能
说明
我们将编辑 “MicrophoneManager.cs” 以使用听写识别器。 这是我们要添加的内容:
- 按下“录制”按钮时,将“启动 DictationRecognizer”。
- 显示 DictationRecognizer 理解的“猜测结果”。
- 锁定 DictationRecognizer 理解的“结果”。
- 检查 DictationRecognizer 的超时。
- 按下“停止按钮”或麦克风会话超时时,“停止 DictationRecognizer”。
- 重启 “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 中完成,但请在你自己的项目中考虑到这一点。
- 在 Unity 编辑器中,导航到“编辑”>“项目设置”>“玩家”,转到“玩家设置”
- 单击“通用 Windows 平台”选项卡
- 在“发布设置”>“功能”部分,选中“麦克风”功能
说明
- 在“层次结构”面板中,搜索并选择“Jetpack_Center”。
- 在“检查器”面板中查找“跟随操作”脚本。
- 单击“要跟随的对象”字段右侧的小圆圈。
- 在弹出的窗口中,搜索“SRGSToolbox”并从列表中选择它。
- 查看“StreamingAssets”文件夹中的“SRGSColor.xml”文件。
- 可在此处的 W3C 网站上找到 SRGS 设计规范。
我们的 SRGS 文件中包含三种类型的规则:
- 有一条规则允许你从 12 种颜色的列表中说出一种颜色。
- 有三条规则侦听颜色规则以及三种形状之一的组合。
- 根规则 (colorChooser) 侦听三个“颜色 + 形状”规则的任意组合。 可按任意顺序说出任意数量(从一个到所有三个)的形状。 这是唯一受侦听的规则,因为它在文件顶部的初始 grammar 标记中指定为根规则<>。
生成和部署
- 在 Unity 中重新生成应用程序,然后在 Visual Studio 中生成并部署,以便在 HoloLens 上体验应用。
- 使用隔空敲击手势关闭工具箱。
- 凝视宇航员的喷气背包并执行隔空敲击手势。
- 开始讲话。 “语法识别器”将解译语音并根据识别结果更改形状的颜色。 示例命令包括“蓝色圆圈,黄色方块”。
- 再次执行隔空敲击手势以关闭工具箱。
结束
祝贺你! 现已完成“MR 输入 212:语音”。
- 你已了解语音命令宜做事项和禁忌事项。
- 你已了解如何使用工具提示来让用户认识语音命令。
- 你已了解用于确认听到了用户语音的多种反馈。
- 你已了解如何在关键字识别器与听写识别器之间切换,以及这两项功能如何理解和解译语音。
- 你已了解如何在应用程序中使用 SRGS 文件和语法识别器进行语音识别。