1. 语音识别与语音朗读能做什么为什么我推荐C#来入门先说清楚标题里的“语音识别”和“语音朗读”不是一个鸡肋拼接而是一对天然互补的能力。语音识别解决的是“听懂用户”语音朗读解决的是“让电脑开口说话”两个功能放在一起就能做出一个非常像样的本地语音助手用户说一句话程序解析出意图再处理逻辑、播报结果。整条链路完全离线运行不依赖任何云端服务这在很多办公场景里特别实用比如操作辅助、无障碍适配、自动化控制台甚至是在不方便用手的时候用一个简单命令触发软件动作。我选择C#而不选Python写这个示例原因很直接Windows平台下的C#能直接调用系统内置的语音SDK也就是System.Speech命名空间。这个方案的好处是显而易见的——不需要注册任何第三方服务不需要联网不需要申请API密钥甚至不需要装额外的大体积运行库只要机器上装了.NET FrameworkWindows自带或较新的.NET运行时就能跑起来。对很多刚入门的项目来说少一个外部依赖就少一层折腾。这个项目也并不是只能做“Hello World”。真正把语音识别和语音朗读组装好之后你能很自然地延伸出几个可落地的方向用语音命令控制媒体播放、用语音调用某个后台脚本、给老人做一个语音服药提醒甚至给演示系统加一个“说一句话就能打开报表”的入口。我实际做过的一个内部演示工具就是用这套技术让操作人员用固定口令切换工单状态结果反馈相当好因为完全免去了登录菜单的繁琐步骤。这篇内容适合谁看两类的朋友一类是刚接触Windows语音开发的C#程序员想快速了解怎么让电脑识别固定词语并开口回答另一类是写过简单的Windows服务但没碰过语音模块的人想找一个基础扎实、能直接抄作业的起点。我会把环境准备、朗读模块、识别模块、组合成完整示例的整个过程都交代清楚每一步都说明我为什么这样写哪些地方容易踩坑让你看完之后不只是复制一段代码而是真正能拿去改造成自己的东西。2. 从项目创建到跑通第一个语音程序2.1 运行时版本怎么选才省心做这个语音示例我建议优先使用.NET Framework 4.8的Windows项目尤其是控制台应用或WinForms程序。原因很简单System.Speech在.NET Framework里是系统程序集项目一创建就能用不需要额外引用NuGet包。如果你用的是.NET 6、.NET 8这类新版本情况稍微复杂一点因为System.Speech不是跨平台库它只在Windows上有意义需要额外引Microsoft.Windows.Compatibility兼容包才能把命名空间补进来。不是说新版本不能做而是对新手来说老框架的摩擦力明显小得多。我用控制台项目来做演示有两点考虑。第一控制台程序结构简单没有界面候的代码干扰主流程一目了然第二语音识别引擎本身是异步回调的控制台程序只要保持主线程不退出事件驱动照样跑得很溜并不需要依赖界面控件的线程模型。当然如果你最终要做带界面的产品WinForms或WPF同样能用这些代码只是回调里操作控件时需要Invoke处理线程问题。2.2 先检查系统里有没有可用的中文语音资源这是一个特别容易被忽略的前提条件。语音识别和语音朗读都依赖语言包如果你的Windows系统没有安装中文语音识别组件或者声音列表里没有中文语音代码写再好也没用。我的习惯是写一个极小型的探测程序先列出当前电脑里有哪些安装的语音using System; using System.Speech.Synthesis; class Program { static void Main() { using (SpeechSynthesizer synth new SpeechSynthesizer()) { foreach (InstalledVoice voice in synth.GetInstalledVoices()) { VoiceInfo info voice.VoiceInfo; Console.WriteLine($名称: {info.Name}); Console.WriteLine($语言: {info.Culture}); Console.WriteLine($性别: {info.Gender}); Console.WriteLine(---); } } Console.ReadLine(); } }正常情况下你会看到至少一个“Microsoft Huihui Desktop”之类的中文语音。如果没有说明系统缺语音包需要先补装语言功能。同样语音识别用到的SpeechRecognitionEngine也需要有对应语言引擎如果程序尝试用中文文化初始化识别引擎时报“没有找到匹配的识别器”那就得去系统的“语音”设置里检查语言功能是否完整。这种事不必慌属于一次性的环境成本装好以后就一直能用。2.3 十行代码先让电脑开口说话第一次跑TTSText to Speech文本转语音比想象中简单。控制台程序里写这三行就能听到声音using System.Speech.Synthesis; SpeechSynthesizer synth new SpeechSynthesizer(); synth.Speak(你好这是一个语音朗读示例。);SpeechSynthesizer是朗读功能的核心类Speak是一个同步方法执行的时候会阻塞当前线程直到朗读完成为止。这个细节很关键因为在大一点的程序里你不能直接在UI线程里调用同步朗读否则界面会卡住声音也跟播报期间的操作冲突。后面我会单独讲异步方案这里先建立一个直觉认知Speak说话当前线程等着说完继续走。语音识别这边最简化的启动代码是using System.Speech.Recognition; using System.Globalization; SpeechRecognitionEngine recognizer new SpeechRecognitionEngine(new CultureInfo(zh-CN)); Grammar grammar new DictationGrammar(); recognizer.LoadGrammar(grammar); recognizer.SetInputToDefaultAudioDevice(); recognizer.SpeechRecognized (s, e) { Console.WriteLine(识别到 e.Result.Text); }; recognizer.RecognizeAsync(RecognizeMode.Multiple); Console.ReadLine();这段代码用自由听写模式也就是不限制内容用户说什么都转成文字。跑起来以后你会立刻感受到微软桌面语音引擎的真实水平识别简单短句还行但对含糊发音、噪声环境就表现一般偶尔出现同音字错乱。这不是你代码的问题而是引擎本身的能力边界。所以真正做产品的时候我给出的建议是能用固定语法就用固定语法别相信自由听写能稳定覆盖所有场景。2.4 理解识别引擎的运行规律是避免踩坑的前提SpeechRecognitionEngine并不是一个“你调用一次就识别一句然后结束”的类。它的工作模式更像一个流水线音频设备持续输入声音引擎在后台不断分析音频流匹配到语音后触发SpeechRecognized事件然后继续监听。RecognizeAsync是异步启动识别第一个参数用RecognizeMode.Multiple表示持续识别用RecognizeMode.Single表示只识别一次就自动停止。判断“识别引擎是否在工作”有个很简单的方法看它有没有正常触发事件。很多新手第一次写代码复制过来按下回车程序没反应第一反应是“是不是没支持麦克风”其实往往是因为底层音频设备或者语言引擎配置不对。这时候你会用到后面章节里我讲的诊断方法包括捕获SpeechDetected事件、SpeechRecognitionRejected事件和异常信息一步步确认问题出在哪个环节。3. 朗读功能里的声音控制与输出管理3.1 音色、语速、音量到底怎么设才有好效果朗读功能最简单但也最容易被忽视细节。SpeechSynthesizer提供了三个直观属性Rate是语速范围从-10到100是正常速度Volume是音量范围从0到100声音的切换用SelectVoice参数是系统里语音包的名称。比如SpeechSynthesizer synth new SpeechSynthesizer(); synth.Rate 1; synth.Volume 90; synth.SelectVoice(Microsoft Huihui Desktop); synth.Speak(现在语速比默认值稍快音量也调整过了。);这里我想解释一下为什么Rate要在-10到10的区间里取值。这个值不是简单的百分比而是引擎内部的速率换算刻度正负表示方向绝对值表示幅度。设置太高容易让语音变得含糊不清听起来像机器在抢时间设置太低又会让用户失去耐心。我做播报类工具时有个经验绝大多数应用场景用0到2之间的速率就够了播报提示信息或操作反馈时紧迫信息用2普通说明性内容用0让用户感觉自然且不焦躁。3.2 把TTS语音输出到文件一个没有声音时的救急方案开发语音功能时你经常会遇到一个烦人的问题机器上可能没有扬声器或者现场测试环境非常安静。这时把朗读结果输出成WAV文件既能验证语音内容又能在后续流程中当素材用。SpeechSynthesizer支持切换输出目标SpeechSynthesizer synth new SpeechSynthesizer(); synth.SetOutputToWaveFile(D:\output\hello.wav); synth.Speak(这句话已经导出到文件了); synth.SetOutputToDefaultAudioDevice(); synth.Speak(这句话直接播放到扬声器);使用SetOutputToWaveFile后后续的Speak输出会写入文件不再播放。重新调SetOutputToDefaultAudioDevice才切回扬声器。这对做自动化测试特别有用我可以批量把大量文本合成为语音文件再统一检查质量完全不需要人一直守在音箱前面。再说一个同样有价值的API——SetOutputToAudioStream它能将语音写入内存流方便后续进行音频处理比如转码、拼接或作为网络传输的音频流。如果你的项目需要做更复杂的音频处理这个入口会非常关键。3.3 异步朗读和SSML复杂交互场景的必备技能你迟早会遇到一个情况程序需要一边朗读语音一边继续执行其他逻辑。直接使用Speak会阻塞所以这里要用SpeakAsync。它把朗读操作投递到后台队列不阻塞当前线程而且每次调用SpeakAsync都会把新的文本加入队列按顺序逐条播放。还有一个更强大的功能是直接调用SpeakSsml或SpeakSsmlAsync来朗读SSML格式的语音标记文本。SSMLSpeech Synthesis Markup Language是语音合成标记语言它允许你在文本里插入非常精确的控制指令比如调节单句语速、加入停顿、强调某个词。一个典型例子string ssml speak version1.0 xmlnshttp://www.w3.org/2001/10/synthesis xml:langzh-CN voice nameMicrosoft Huihui Desktop 下面这句话会有停顿。 break time500ms/ 已经停完了。 /voice /speak; synth.SpeakSsml(ssml);对普通程序来说用简单属性调整全局语速就够了但做语音播报的现代应用时SSML几乎是必须掌握的。比如说播报金额时你希望数字之间有个顿挫播报短信时你希望先停顿提示“收到新消息”。这些都能用SSML非常优雅地完成而不需要自己拼接诡异字符串。3.4 两条线程同时占用语音设备怎么处理这是实际开发中比较高概率出现的问题你已经在用SpeechSynthesizer朗读同时SpeechRecognitionEngine还在监听麦克风输入。两个引擎都在访问音频设备别看一个输出一个输入在设备管理层面它们仍然可能产生冲突尤其是部分机器声卡驱动对多路音频流支持不好时会发生无法创建音频设备、或者听到爆音的问题。我采用的稳妥方案是把朗读和识别放到不同的逻辑阶段或者至少把麦克风输入端做成可以重置的。也就是说在朗读长文本之前先暂停识别朗读完成后再重新SetInputToDefaultAudioDevice并启动识别。如果你的场景需要一边播报一边候命那么就准备一个专门的“播报锁”在播报开始前告诉识别引擎先停下播报结束再通知它继续。这套思路能让程序在低配机器上也保持稳定代价是响应上有一点点延迟但对语音交互来说完全可以接受。4. 语音识别的两种关键模式与命令式交互4.1 听写模式与命令语法模式到底用哪种语音识别引擎最核心的分水岭是使用DictationGrammar还是自定义的Grammar。DictationGrammar对应的是自由听写用户说什么就识别什么适合录入长文本比如语音输入一封邮件自定义Grammar对应的是受限语法只识别你预先定义好的短语和组合准确率高出几个量级适合做命令控制。我做这个示例的核心思路就是给识别引擎绑定一个“命令词库”。用户只能说词库里有的词语或者词库支持的组合其他内容全部被当作不匹配。这样做有三个好处不会有乱七八糟的同音字识别速度更快业务逻辑更简单。你想想一个语音助手在控制模式里用户只需要说“打开”、“关闭”、“下一个”、“上一个”这种短词完全没必要让引擎去处理几千个汉字组合。DictationGrammar虽然看起来很美好但它对中文的支持不算特别强尤其对背景噪声敏感的场合输出经常出现让人哭笑不得的错误。我的建议是如果程序的功能是语音控制就不要使用听写模式如果必须使用听写也要给最终结果加一层人工确认或者至少允许用户纠正。4.2 用GrammarBuilder设计一个可扩展的识别词表命令识别的核心是GrammarBuilder和Choices。Choices用来收集同位置的可选词条GrammarBuilder用来组合整个语句结构。这里给一个完整例子设计一个能识别“打开报表”、“打开设置”、“关闭程序”的语法using System.Speech.Recognition; using System.Globalization; Choices actions new Choices(打开, 关闭); Choices targets new Choices(报表, 设置, 程序); GrammarBuilder builder new GrammarBuilder(); builder.Append(actions); builder.Append(targets); Grammar commandGrammar new Grammar(builder); SpeechRecognitionEngine recognizer new SpeechRecognitionEngine(new CultureInfo(zh-CN)); recognizer.LoadGrammar(commandGrammar);这个语法会识别出九种组合比如“打开报表”、“关闭程序”。识别结果的Text属性就是完整匹配到的字符串我可以用它来做精确分支。重点在于Choices类它是整个识别语法组合的积木支持嵌套更复杂的结构比如多个Choices之间加固定文本。若你希望用户说“请打开报表”也能识别可以在builder前面加一个固定词语“请”或者在Choices中加入“请打开”、“打开”等变体。一个小技巧是在设计命令词库时要特意把同义说法都包含进去比如“退出”和“关闭程序”、“下一个”和“下一条”。因为语音识别不是死板的字符串比较引擎会把音频匹配到最接近的语法条目但语法条目越接近日常说法成功率越高。我在实践里通常会把界面按钮上的文字、常用口头语、以及简短阉割版表达都加进Choices里做到“怎么说都能被接住”。4.3 事件驱动识别结果是怎么被程序接住的语音识别引擎的核心是它会在后台分析音频然后把关键节点暴露成事件。其中最常用的是SpeechRecognized当引擎成功匹配到语法时触发其次是SpeechRecognitionRejected当引擎认为用户说了话但匹配不上语法时触发还有SpeechDetected当引擎检测到有人发音时触发。成功识别的事件参数SpeechRecognizedEventArgs里Result字段包含了识别结果的全部信息包括文本Text、识别置信度Confidence一个0到1的浮点数、识别时间等。置信度这个值很有用我一般在命令交互里加一个阈值比如Confidence小于0.6的结果干脆忽略避免太含糊的发音触发错误操作。recognizer.SpeechRecognized (s, e) { if (e.Result null) return; float confidence e.Result.Confidence; string text e.Result.Text; if (confidence 0.5f) { Console.WriteLine(置信度过低忽略 text); return; } Console.WriteLine($识别成功{text}置信度{confidence:P0}); // 这里执行具体的业务逻辑 };很多人会忽略SpeechRecognitionRejected其实在交互体验上它很重要。当用户说话但没被识别时程序什么都不反馈用户会觉得很困惑。合理的做法是捕获这个事件给出友好提示比如说“抱歉我没有听清楚”再引导用户重新说一次。这也是我们做完整语音助手的必要组成部分。4.4 为什么命令心理上需要“模式切换”而不是一次性识别所有词条刚开始的时候可能会把所有命令全塞进一个Choices里觉得这样省事。但词条一旦膨胀到几十条识别准确率会下降尤其是一些相似发音的命令会互相干扰比如“报表”和“报告”、“结果”和“结束”。我踩过这个坑以后意识到一个更好的策略把命令按业务场景分组在软件里做“识别状态”的切换。比如一个软件有主页场景、编辑场景、管理员场景每个场景各自维护一个Grammar独立命令集。当前场景对应的语法是激活状态切换场景时把上一个Grammar卸载或禁用再加载新的。这能大幅降低同音词冲突提高识别准确率。SpeechRecognitionEngine的UnloadGrammar方法可以卸载语法LoadGrammarAsync可以异步加载大语法避免界面卡顿。把识别范围限制在情境相关的词组上是把它做成可靠交互的关键习惯。5. 完整示例做一个能听懂固定命令并开口回答的语音助手5.1 需求和场景设计这个示例围绕一个点子展开控制台程序在后台运行听到用户说“你好”就回答问候语听到“几点了”就播报当前时间听到“再见”就礼貌道别并退出程序。每个识别命令都对应一个自动朗读回复这条完整的链路——麦克风输入、语音识别、逻辑判断、语音合成播放——就是一个典型的语音交互闭环。项目结构非常简单主程序负责创建对象和启动识别识别成功事件里通过switch把文本转成业务动作动作里调用朗读函数最后利用识别引擎的RecognizeMode控制循环。所有代码都能写在一个文件里方便你理解整体流程。5.2 完整代码与逐段解释先看完整代码框架using System; using System.Globalization; using System.Speech.Recognition; using System.Speech.Synthesis; class VoiceAssistant { private static SpeechSynthesizer _synth; private static SpeechRecognitionEngine _recognizer; static void Main() { Console.OutputEncoding System.Text.Encoding.UTF8; Console.WriteLine(语音助手启动请说你好 / 几点了 / 再见); _synth new SpeechSynthesizer(); _synth.Rate 0; _synth.Volume 100; _synth.SelectVoice(Microsoft Huihui Desktop); _synth.SpeakAsync(语音助手已经启动你可以对我说你好几点了或者再见); _recognizer new SpeechRecognitionEngine(new CultureInfo(zh-CN)); Choices choices new Choices(); choices.Add(你好); choices.Add(几点了); choices.Add(再见); GrammarBuilder builder new GrammarBuilder(choices); Grammar grammar new Grammar(builder); _recognizer.LoadGrammar(grammar); _recognizer.SetInputToDefaultAudioDevice(); _recognizer.SpeechRecognized OnSpeechRecognized; _recognizer.RecognizeAsync(RecognizeMode.Multiple); Console.ReadLine(); } private static void OnSpeechRecognized(object sender, SpeechRecognizedEventArgs e) { if (e.Result null) return; string text e.Result.Text; Console.WriteLine($识别结果{text}置信度{e.Result.Confidence:P0}); switch (text) { case 你好: _synth.SpeakAsync(你好很高兴见到你。); break; case 几点了: _synth.SpeakAsync(现在时间是 DateTime.Now.ToString(HH点mm分)); break; case 再见: _synth.SpeakAsync(再见期待下次见面。); _recognizer.RecognizeAsyncStop(); Environment.Exit(0); break; } } }这段代码有四个关键点。第一SpeechSynthesizer在程序启动时立即创建并SpeakAsync一条语音提示这个提示其实也起到了“验证输出通道”的作用如果这条声音没听到后面配置也会出问题。第二SpeechRecognitionEngine加载一个只有三句话的语法识别范围极小所以准确率很稳。第三识别回调函数里调用SpeakAsync是异步的朗读不会阻塞识别循环只要程序不退出用户说完“你好”后马上又能识别下一句。第四识别到“再见”后我先调用RecognizeAsyncStop停止识别再退出程序防止退出时语音引擎还在等待音频输入而抛异常。5.3 异步朗读和持续识别之间怎么协作不打架这个示例里_synth.SpeakAsync和识别引擎的持续识别是同时工作的输出音频和输入音频并行。前面说过某些机器会在同时使用时产生音频设备冲突。如果真遇到问题可以用一种更稳妥的协作模式在朗读回复前先停止识别播放完成后再重新启动识别。我把这个模式写成模板private static void SpeakAndResume(string message) { _recognizer.RecognizeAsyncStop(); _synth.SpeakAsync(message); _synth.SpeakAsyncCompleted (s, e) { _recognizer.RecognizeAsync(RecognizeMode.Multiple); }; }这里用了SpeakAsyncCompleted事件也就是异步朗读完成“回调”这样识别引擎不会在朗读中途重新启动抢占音频资源。虽然这个写法会让程序在播报期间暂时不能接收新语音但对于大部分短命令场景完全够用而且稳定性大大提高。5.4 把示例扩展成真正能用的工具加上状态机和语音反馈这个纯命令示例如果直接用还是偏简陋。真正常规使用的语音交互程序至少要有两个状态空闲待命和工作处理中。空闲待命时程序可以持续识别工作处理中时识别到的命令不能马上执行得排队或反馈占位信息。我通常用一个bool变量标记是否在忙在SpeechRecognized事件里先检查状态忙的时候就只播放“请稍等”不执行真正的操作。private static bool _isBusy false; private static void OnSpeechRecognized(object sender, SpeechRecognizedEventArgs e) { if (_isBusy) { _synth.SpeakAsync(请稍等我还在处理上一个任务。); return; } _isBusy true; try { switch (e.Result.Text) { case 打开报表: OpenReport(); // 你的业务逻辑 _synth.SpeakAsync(报表已经打开。); break; case 关闭程序: _synth.SpeakAsync(正在关闭再见。); _recognizer.RecognizeAsyncStop(); Environment.Exit(0); break; } } finally { _isBusy false; } }这种状态机思维是语音程序从Demo走向可用的关键。因为识别是异步的用户完全可能在程序处理A事件时又说了一个B命令如果你不对“忙状态”做保护就会出现并发执行两个动作、播报声音重叠的乱象。6. 实际操作中遇到的典型问题与排查方法6.1 常见问题排查速查表我在做这个示例时几乎把新手会踩的坑都踩了一遍下面这表格来自大量调研把它存下来遇到问题直接照着查现象可能原因排查与解决程序启动没有语音播报系统语音包缺失或已失效在GetInstalledVoices里逐个打印输出看是否存在中文语音。初始化识别引擎抛异常未安装对应语言的识别引擎用SpeechRecognitionEngine.InstalledRecognizers()检查可用识别器确认zh-CN引擎存在。麦克风说话但识别无响应麦克风权限被禁用Windows设置中给应用开放麦克风权限确认默认录音设备选对。识别结果不稳定老是听错使用了自由听写或词条过多使用受限的Grammar减少词条数量增加置信度过滤。朗读时识别引擎发出强烈噪声输入输出音频设备冲突按5.3小节的方式播报前停止识别播报完成后再启动。SpeechRecognized回调里操作界面卡死回调线程与界面线程不同步在WinForms/WPF中用Invoke控件操作控制台程序一般不用管。6.2 排查识别问题的一个高效诊断套路当识别功能不正常时我最常用的做法不是看代码而是先做一个“最小化音频链路测试”。具体步骤是用系统自带的录音软件录一段包含清晰短句的音频然后把它作为输入源喂给识别引擎而不是直接用麦克风。SpeechRecognitionEngine有一个SetInputToWaveFile方法可以直接从WAV文件识别。如果文件识别正常说明引擎和语法没有大问题问题100%出在麦克风或设备配置上如果文件识别也失败则说明是语法、语言包或音频内容的问题。SpeechRecognitionEngine recognizer new SpeechRecognitionEngine(new CultureInfo(zh-CN)); recognizer.LoadGrammar(new DictationGrammar()); recognizer.SetInputToWaveFile(D:\test.wav); recognizer.SpeechRecognized (s, e) Console.WriteLine(文件识别结果 e.Result.Text); recognizer.Recognize();这个方法节省了我大量时间。它能精准区分“音频输入问题”和“引擎配置问题”的边界让你不会误改代码。类似的你可以用SetInputToNull关闭输入流方便地测试引擎内部状态。6.3 需要注意的三个细节习惯第一SpeechRecognitionEngine和SpeechSynthesizer都是需要释放的非托管资源用完后调用Dispose或者写在using块里。控制台程序结束时如果不释放有可能出现“音频设备还被占用”的后遗症程序再次启动时识别初始化失败。我见到的很多奇怪问题根因都是上一次进程没有释放音频设备。第二设置CultureInfo时一定要和系统安装的语音包装配。你代码里写zh-CN机器上却只有英文语音包就会初始化失败。稳妥做法是遍历InstalledRecognizers()动态选择一个支持中文的语言如果找不到再报错提示用户安装语言包。第三控制台程序的编码问题。中文识别结果在控制台可能显示乱码我在代码里加了Console.OutputEncoding System.Text.Encoding.UTF8;这能避免大部分终端下的乱码情况。如果你用Windows Terminal问题一般不大但老版控制台还是要处理。7. 长期实践后的几个真实体会用系统自带的语音库做项目最大的现实是它不是免费的语音识别平台而是一个本地的、可控的、不用联网的方案准确率和扩展能力都有边界你不得不接受它的局限性。但换个角度看这种局限性反而让它变得特别适合做“固定语料的语音控制”场景因为命令词条有限的局面下准确率可以保持得很理想而且完全无需考虑网络延迟和数据隐私问题。如果你想让识别更准可以买一个质量稍好的麦克风。这不只是玄学语音识别对音频质量很敏感指向性麦克风和桌面麦克风在背景噪声环境下的表现差异非常大。我做测试时用过两种麦克风同一个识别语法准确率能差出二十个百分点。关于后续扩展这个示例可以直接往几个方向发展。一是给语音识别增加更多语法结构比如支持数字加单位的组合命令这样“三分钟后提醒我”这类包含变量的语句也能被解析你需要用到GrammarBuilder.AppendDictation或者自定义语义标签。二是把识别结果和朗读回复集成为一个服务封装成类库供WinForms、WPF或者背景服务调用。三是把语音合成的结果接入到自动化流程中比如状态变更时自动播报提示声音。每个方向都不算复杂但都需要先把这个基础示例搞透彻。最后分享一个小技巧是我实际用了很多次的在开发语音程序初期就把“朗读的内容”和“识别到的内容”全部打印到控制台并且保存到日志文件。你永远不会知道用户会用什么口音、什么音量、什么语速去说话但日志会忠实地记录每一次识别的真实结果。等到功能上线后你会感谢自己当初留下了这些日志因为它们比任何代码分析工具都更直观地告诉你到底哪个命令词条不常用、哪个词条经常误识别进而有针对性地优化词库。语音开发这个领域的门槛其实不高难的是把交互体验调得自然。C#和System.Speech给你提供了一套很低门槛的起点剩下的就看你怎么把识别、朗读和业务逻辑组合成让人用着顺手的东西了。