1. 项目概述与核心价值
最近在捣鼓一个Unity的桌面端应用,需要加一个“嘿,助手”这样的语音唤醒功能。一提到语音识别,很多人的第一反应就是去接科大讯飞、百度AI这些在线SDK。确实,对于需要高精度、大词汇量的连续语音识别,云端服务是首选。但我的需求很简单,也很明确:离线、低延迟、只识别几个预设的关键词,比如“开始”、“停止”、“下一个”。更重要的是,我希望用户下载即用,无需申请API密钥,也不用担心网络问题。
就在我琢磨是不是要自己折腾VAD和简单模型时,突然想起来Windows系统里其实藏着一个宝藏——Windows.Speech命名空间。这玩意儿是.NET Framework的一部分,从Win7时代就有了,它提供了一个本地的、离线的语音识别引擎。对于关键词唤醒这种场景,简直是量身定做。我花了点时间把它集成到Unity里,实测下来,从零到实现一个可用的关键词唤醒,真的只需要5分钟左右,而且完全免费,识别响应速度在本地几乎无延迟。
这个方案特别适合以下几种场景:
- 教育或工具类软件:需要语音指令控制课件翻页、工具开关。
- 体感或无障碍应用:用户双手被占用时,通过特定口令触发操作。
- 游戏中的快捷指令:实现类似“释放技能”、“使用道具”的语音命令。
- 任何对启动速度、隐私有要求,且指令集固定的离线应用。
如果你也在寻找一个轻量、快速、零依赖的Unity离线语音唤醒方案,那么直接使用Windows.Speech会是一个让人惊喜的选择。接下来,我就把整个集成过程、核心代码、以及我踩过的一些坑,毫无保留地分享出来。
2. 环境准备与原理浅析
2.1 为什么选择 Windows.Speech?
在深入代码之前,我们得先搞清楚Windows.Speech能做什么、不能做什么。它的核心是System.Speech.Recognition这个程序集,背后调用的是Windows内置的Windows Desktop Speech Technology识别引擎。这个引擎有几个关键特性决定了我们的使用方式:
- 离线工作:所有语音识别处理均在本地完成,不依赖网络,速度快且隐私性好。
- 基于语法规则:它不是一个通用的听写引擎,而是需要你预先定义好它要“听”什么。你可以定义一系列词语或短语,它就在这个有限的集合里进行匹配。这正好契合了“关键词唤醒”或“语音指令”的场景。
- 资源占用低:相比于加载一个庞大的AI模型,基于规则识别的开销要小得多。
所以,它的强项是离线、低延迟、高准确率的有限命令词识别。而它的弱项是不适合大词汇量、开放域的连续语音识别。如果你的需求是后者,那这个方案不合适。
2.2 Unity项目设置要点
由于Windows.Speech是.NET Framework的组件,我们的Unity项目需要与之兼容。
- 脚本后端:在
Player Settings->Other Settings->Configuration下,确保Scripting Backend设置为Mono。IL2CPP后端在调用一些特定的.NET库时可能会遇到问题,Mono的兼容性更好。 - API兼容级别:在同一个设置页面,将
Api Compatibility Level设置为.NET Framework。.NET Standard 2.0或.NET Core的Profile可能不包含我们需要的完整System.Speech库。 - 平台目标:显然,这个方案只适用于Windows Standalone平台(包括Win32和x64)。在Build Settings中确认平台已切换至PC, Mac & Linux Standalone,并在子选项中选择Windows。
注意:如果你之前项目用的是较新的.NET配置,切换回.NET Framework后,可能需要检查一下项目中是否有使用更新的C#语法或API,它们可能在旧的框架下不可用,需要进行适配。
2.3 添加必要的程序集引用
System.Speech默认并不在Unity的所有.NET配置文件中。我们需要手动在项目根目录的Packages文件夹里找到或创建csc.rsp文件(如果没有的话,在Assets同级的根目录创建即可)。
在csc.rsp文件中加入以下一行:
-r:System.Speech.dll这个文件的作用是告诉Unity的C#编译器(CSC)在编译时额外引用指定的程序集。保存后,需要重启Unity编辑器或重新加载项目,更改才会生效。
重启后,你就可以在C#脚本中使用using System.Speech.Recognition;了。如果编辑器没有报错,说明引用成功。
3. 核心代码实现与解析
3.1 构建语音识别引擎
我们创建一个名为OfflineKeywordRecognizer的C#脚本。核心类是SpeechRecognitionEngine。
using System.Speech.Recognition; using UnityEngine; public class OfflineKeywordRecognizer : MonoBehaviour { private SpeechRecognitionEngine recognizer; void Start() { InitializeSpeechEngine(); } void InitializeSpeechEngine() { try { // 1. 创建识别引擎实例 recognizer = new SpeechRecognitionEngine(); // 2. 创建语法规则(Choices) Choices keywords = new Choices(); keywords.Add("你好 Unity"); // 唤醒词示例 keywords.Add("开始游戏"); keywords.Add("退出"); keywords.Add("点击"); // 3. 构建语法构建器(GrammarBuilder)并包装Choices GrammarBuilder grammarBuilder = new GrammarBuilder(); grammarBuilder.Append(keywords); // 默认是任意一个词匹配 // 如果你想识别一个短语序列,可以这样: // GrammarBuilder commandGrammar = new GrammarBuilder(); // commandGrammar.Append("请"); // commandGrammar.Append(keywords); // “请” + (关键词之一) // 4. 创建语法对象 Grammar grammar = new Grammar(grammarBuilder); // 5. 加载语法到引擎 recognizer.LoadGrammar(grammar); // 6. 设置异步识别 recognizer.SetInputToDefaultAudioDevice(); // 使用系统默认麦克风 recognizer.RecognizeAsync(RecognizeMode.Multiple); // 持续监听模式 // 7. 订阅识别事件 recognizer.SpeechRecognized += OnSpeechRecognized; recognizer.SpeechRecognitionRejected += OnSpeechRejected; Debug.Log("离线语音识别引擎初始化成功,正在监听..."); } catch (System.Exception ex) { Debug.LogError($"语音引擎初始化失败: {ex.Message}"); } } }代码解析与注意事项:
RecognizeMode.Multiple:这个参数至关重要。它让引擎在识别出一个结果后继续监听,而不是只识别一次就停止。对于唤醒词场景,必须使用此模式。SetInputToDefaultAudioDevice():指定音频输入源。你也可以通过System.Speech.AudioFormat相关的类来指定特定的音频设备或格式,但对于大多数情况,默认设备就够了。- 异常处理:初始化过程可能因为麦克风被占用、权限问题或系统语音服务未启动而失败。务必用try-catch包裹,并给用户清晰的错误提示。
3.2 处理识别结果
识别到语音后,会触发SpeechRecognized事件。我们需要在这个事件的处理函数中判断识别出的文本,并执行相应的游戏逻辑。
private void OnSpeechRecognized(object sender, SpeechRecognizedEventArgs e) { // e.Result.Text 包含了识别出的文本 string recognizedText = e.Result.Text; Debug.Log($"识别成功: {recognizedText}"); // 根据识别到的关键词,分发处理逻辑 switch (recognizedText) { case "你好 Unity": OnWakeUpCalled(); break; case "开始游戏": StartGame(); break; case "退出": QuitApplication(); break; case “点击”: SimulateMouseClick(); break; default: // 理论上不会走到这里,因为语法限制了范围 Debug.LogWarning($"未处理的关键词: {recognizedText}"); break; } } private void OnSpeechRejected(object sender, SpeechRecognitionRejectedEventArgs e) { // 当语音无法被识别(置信度低)时触发 // 可以用来做日志记录,或者给用户一个“未听清”的视觉反馈 Debug.Log("语音未被识别"); } // 以下是示例响应函数 private void OnWakeUpCalled() { Debug.Log("唤醒词被触发!"); // 例如:激活你的语音助手UI,播放一个提示音 // GetComponent<AudioSource>().Play(); // wakeUpUI.SetActive(true); } private void StartGame() { /* 你的游戏开始逻辑 */ } private void QuitApplication() { Application.Quit(); } private void SimulateMouseClick() { /* 模拟鼠标点击事件 */ }关键点:
- 置信度:
e.Result.Confidence属性给出了本次识别的置信度(一个float值)。你可以设置一个阈值(比如0.7),只有当置信度高于阈值时才执行操作,以提高准确性,避免误触发。if (e.Result.Confidence < 0.7f) return; - UI更新:注意,语音识别事件是在后台线程中触发的。如果你需要在识别后更新Unity的UI(如Text、Image),必须使用
MainThreadDispatcher或者UnityEngine.Dispatcher等方式将操作抛回主线程,否则会报错。一个简单的方法是使用UnityMainThreadDispatcher这样的第三方工具,或者在事件中只设置一个标志位,在Update()里检查并执行UI操作。
3.3 优化语法与识别准确性
基础的Choices是“或”的关系。GrammarBuilder提供了更强大的语法构建能力。
1. 提高唤醒词准确性(追加语义)单纯的“你好Unity”可能容易被日常对话误触发。可以将其嵌入一个更复杂的语法中,降低误报率。
GrammarBuilder wakeGrammar = new GrammarBuilder(); wakeGrammar.Append(“嘿”); wakeGrammar.Append(“小助手”); // 识别“嘿 小助手”这个序列 // 你还可以设置可选的词 GrammarBuilder optionalGreeting = new GrammarBuilder(); optionalGreeting.Append(“你好”, 0, 1); // 最小0次,最大1次,即可选 wakeGrammar.Append(optionalGreeting); wakeGrammar.Append(“Unity”); recognizer.LoadGrammar(new Grammar(wakeGrammar));这样,只有说“嘿 小助手”或者“嘿 小助手 你好 Unity”才会触发,日常说“你好Unity”则不会。0,1表示这个词出现0次或1次。
2. 处理同义词和发音容错中文里,一个意思可能有多种说法。Choices本身就可以处理。
Choices startCommands = new Choices(); startCommands.Add(“开始”); startCommands.Add(“启动”); startCommands.Add(“go”); // 甚至支持中英文混合 GrammarBuilder startGrammar = new GrammarBuilder(startCommands);对于发音相近的词,引擎本身有一定的容错能力。但更可靠的做法是,将用户可能说出的所有变体都加入到Choices中。
3. 管理多个语法你可以加载多个Grammar对象到同一个引擎。例如,一个专门处理唤醒,另一个处理唤醒后的具体命令。引擎会并行监听所有已加载的语法。
4. 实战调试与性能优化
4.1 麦克风权限与设备选择
在Windows 10/11上,应用首次访问麦克风会触发系统权限弹窗。你需要在Unity打包的exe文件的Player Settings->Publishing Settings->Capabilities中勾选Microphone。也可以在应用启动时,用代码检查并引导用户开启权限。
如果用户有多个麦克风,你可以让用户选择:
using System.Speech.AudioFormat; using System.Speech.Recognition; // 获取所有音频输入设备 var audioDevices = SpeechRecognitionEngine.InstalledRecognizers(); // audioDevices 包含了识别器信息,但注意识别器和音频输入设备不是一一对应。 // 更直接的方法是用System.Windows.Forms或NAudio等库枚举录音设备,然后通过索引设置。 // recognizer.SetInputToAudioStream(stream, format); // 使用特定音频流不过,对于绝大多数单麦克风用户,SetInputToDefaultAudioDevice()是最简单可靠的选择。
4.2 降低CPU占用与误触发
持续监听麦克风会消耗一定的CPU资源。虽然Windows.Speech已经很轻量,但在笔记本上长期运行仍可能影响续航。可以通过以下方式优化:
- 设置识别超时:
recognizer.InitialSilenceTimeout = TimeSpan.FromSeconds(2);设置引擎在检测到静音后等待语音开始的最长时间。recognizer.BabbleTimeout设置识别过程中允许的静音间隔。合理设置可以减少无谓的等待。 - 动态开关监听:不要一直开着。例如,在游戏菜单界面开启,在紧张的战斗关卡关闭。或者在检测到第一次唤醒后,关闭通用监听,切换到更精确的命令语法模式。
void EnableListening() { if (recognizer != null && !isListening) { recognizer.RecognizeAsync(RecognizeMode.Multiple); isListening = true; } } void DisableListening() { if (recognizer != null && isListening) { recognizer.RecognizeAsyncStop(); // 停止异步识别 // 或者 recognizer.RecognizeAsyncCancel(); isListening = false; } } - 环境噪音基线:在
Start()方法初始化后,可以先让用户保持安静2秒,让引擎适应环境底噪。有些高级用法可以获取音频电平,但Windows.Speech的API对此支持有限。
4.3 处理引擎的休眠与恢复
当系统进入睡眠或麦克风被其他应用(如通讯软件)独占时,识别引擎可能会出错。一个好的实践是:
- 在
OnApplicationPause(对于PC平台,是失去焦点时)事件中,停止识别引擎。 - 在
OnApplicationFocus(重新获得焦点时)事件中,重新初始化或重启识别引擎。 - 在
OnDestroy中,一定要记得释放资源:void OnDestroy() { if (recognizer != null) { recognizer.RecognizeAsyncStop(); recognizer.SpeechRecognized -= OnSpeechRecognized; recognizer.SpeechRecognitionRejected -= OnSpeechRejected; recognizer.Dispose(); recognizer = null; } }
5. 常见问题排查与进阶技巧
5.1 问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 初始化失败,抛出异常 | 1. 未添加System.Speech.dll引用。2. 项目API级别不是 .NET Framework。3. Windows语音识别功能未启用。 | 1. 检查csc.rsp文件。2. 确认Player Settings设置。 3. 进入Windows“控制面板”->“语音识别”,设置并启用麦克风。 |
| 能初始化但无任何识别结果 | 1. 麦克风权限未开启。 2. 麦克风设备选择错误或损坏。 3. 语法(Grammar)未正确加载。 | 1. 检查系统麦克风权限,确保Unity.exe有权限。 2. 测试系统录音机是否可用。 3. 检查 LoadGrammar是否成功,关键词是否添加到Choices。 |
| 识别率低,经常误触发或无法触发 | 1. 环境噪音大。 2. 关键词太常见或太短。 3. 用户发音不标准或语速问题。 | 1. 建议用户在相对安静环境使用,或添加置信度过滤。 2. 使用更独特、更长的唤醒词,或组合成短语。 3. 在语法中加入同义词和常见变体。让用户多次训练。 |
| 触发事件后Unity卡顿或报错 | 在非主线程中直接操作了Unity对象。 | 将识别结果(如字符串)存入队列,在Update()中从主线程取出并处理UI和游戏逻辑。 |
| 打包后exe无法识别语音 | 1. 打包时未包含正确的.NET环境。 2. 系统缺少必要的语音库。 | 1. 确保目标机器安装了相应版本的.NET Framework(通常是4.x)。 2. 对于Windows 10/11,通常自带。Win7可能需要安装“Microsoft Speech Platform”。 |
5.2 进阶技巧:自定义语法与权重
GrammarBuilder和Choices可以构建非常复杂的语法树。例如,定义一个命令语法:“播放 [歌曲名]”,其中歌曲名来自一个列表。
Choices songChoices = new Choices("晴天", "七里香", "双截棍"); GrammarBuilder songGrammar = new GrammarBuilder(); songGrammar.Append("播放"); songGrammar.Append(songChoices); recognizer.LoadGrammar(new Grammar(songGrammar));你还可以为不同的选择设置语义值(SemanticResultValue),在识别结果中提取结构化的信息,而不仅仅是文本。
5.3 与Unity Input System集成
为了让语音控制更好地融入游戏,你可以将语音识别事件映射到虚拟的“输入动作”上。例如,当识别到“跳跃”时,触发一个名为VoiceJump的Input Action,这样你的角色控制脚本只需要监听这个Action,而无需关心输入是来自键盘、手柄还是语音。
- 在Unity Input System中创建一个
VoiceCommandAction Map。 - 在里面创建各种Action,如
WakeUp,Jump,Fire。 - 在语音识别事件处理中,调用对应Action的
Trigger()方法。
// 假设你有一个Input Action Reference public InputActionReference voiceJumpAction; private void OnSpeechRecognized(...) { if (e.Result.Text == “跳跃”) { voiceJumpAction.action.Trigger(); } }这种方式极大地解耦了输入源和游戏逻辑,是更优雅的架构。
5.4 关于“5分钟搞定”的实话
标题说“5分钟搞定”,指的是从写好代码到功能跑通的核心流程。但要达到产品级可用,你还需要投入时间在以下几方面:
- UI/UX反馈:识别时、识别成功/失败时,需要有视觉(如麦克风动画)或听觉(提示音)反馈。
- 唤醒词训练:可以引导用户对着麦克风多说几遍唤醒词,让Windows语音识别为当前用户做一次简单的适配,能显著提升识别率。这可以通过调用
recognizer.UpdateRecognizerSetting(“AdaptationOn”, 1)等相关设置(如果引擎支持)或引导用户去系统控制面板训练来实现。 - 降噪与增益:对于嘈杂环境,纯软件方案效果有限。可以考虑集成一个轻量级的软件降噪算法,或者在UI上提示用户调整麦克风距离和增益。
最后,记得在应用启动时或设置里,给用户一个清晰的开关,允许他们启用或禁用语音功能。不是所有人都喜欢或需要在所有场景使用语音交互。