C#语音朗读封装:VoiceSpeaker实现播放停止暂停继续
2026/9/3 4:37:10 网站建设 项目流程

简介:针对C#语音朗读功能封装的VoiceReader类资源,面向需要在WinForms/WPF等桌面应用中快速集成文本转语音能力的开发者。该类基于System.Speech.Synthesis命名空间下的SpeechSynthesizer实现,完整提供播放、停止、暂停、继续四项核心操作,并支持通过SelectVoice切换中英文语音,可调整语速、音调等参数。压缩包共24个文件,包含6个.cs源代码、2个.dll库文件以及可直接运行的exe演示程序,另有sln/csproj工程文件、resx资源和settings配置,方便直接打开项目调试或抽取类库复用,整体仅124KB,轻量易部署。已有1176人学习下载,适合有一定C#基础、希望避免重复造轮子的开发者。资源附带完整项目结构与可执行示例,能帮助读者快速理解SpeakAsyncCancelAll、GetPosition/SetPosition等API的暂停续读实现细节,并在此基础上扩展更多语音交互功能。 最近在给一个工控小工具加语音播报功能,需求很直接:报警的时候把设备状态用中文喊出来,用户手动点按钮可以随时停止、暂停、继续。网上搜了一圈,关于 C# 语音朗读类的资料很多都在重复讲SpeakSpeakAsync,真正把播放、停止、暂停、继续这四个动作做成一个能直接拿来用的类,并且把暂停/继续这两个坑讲清楚的,确实不多。

这篇文章把我封装VoiceSpeaker类的完整过程整理出来,从方案选型到完整代码再到实测问题,一次讲透。不管你是在做桌面工具、上位机、还是给文章阅读器加朗读功能,看完都能直接落地。

1. 需求拆解与方案选型

1.1 这个类的使用场景到底有哪些

先别急着写代码,明确一下这个语音朗读类到底解决什么问题。我做这个小工具时,用户要求是:程序后台检测到设备异常,自动朗读报警内容;操作员听到后,可以按“暂停”停止当前播报,处理完问题再按“继续”接着听;如果这条报警不需要了,直接“停止”清空。

这类需求在 C# 开发里非常常见。最常见的是 WinForms/WPF 桌面程序里的 TTS(Text To Speech,文本转语音)功能,典型场景包括:

  • 工控上位机:设备状态、报警信息语音播报,操作员不用一直盯屏幕;
  • 仓储管理系统:扫码、入库、出库操作提示音替换成语音;
  • 辅助阅读工具:把文章内容朗读出来,适合做“听书”功能;
  • 定时任务提醒:到点播报工作计划、闹钟提醒;
  • 门店叫号系统:呼叫号码和窗口号,替代人工喊话。

这些场景的共性需求就是四个操作:播放、停止、暂停、继续。区别只在于触发方式和界面不同。所以我们真正要交付的不是一段调用SpeechSynthesizer的散装代码,而是一个封装好的VoiceSpeaker类,对外只暴露几个方法,调用方不需要关心底层细节。

1.2 选型:System.Speech 还是其他方案

C# 里做语音朗读,并不是只有一条路。我实际调研过的方案有四种,这里直接列个对比表:

方案使用方式优点缺点适用场景
System.Speech.Synthesis托管类库,直接调用使用简单、文档多、离线可用仅支持 Windows;没有原生暂停/继续绝大多数 Windows 桌面程序
Windows.Media.SpeechSynthesisUWP/WinRT API语音质量好、支持在线语音异步 API 用起来绕,桌面程序要处理初始化现代 Windows 应用
SAPI COM(SpVoice)COM 互操作有原生 Pause/Resume 方法API 老、类型不友好需要引擎级暂停/继续时
第三方云 TTSHTTP 接口音质自然、支持多音色需要网络、有 API 费用、延迟在线语音播报、语音助手

这套需求里我们优先选第一种,System.Speech.Synthesis下的SpeechSynthesizer。原因很简单:

  1. 离线可用SpeechSynthesizer调用的是 Windows 自带的 SAPI(Speech API)语音引擎,不需要联网,也不依赖第三方服务,这在工控、内网环境下是刚需。
  2. 使用成本低SpeakAsync是现成的异步方法,不会阻塞 UI 线程,对 WinForms 开发非常友好。
  3. 中文支持完善:Windows 10/11 自带简体中文语音包(默认是 Microsoft Huihui),直接就能读中文。

不过它有一个绕不开的问题:SpeechSynthesizer类本身没有提供 Pause 和 Resume 方法。这也是网上很多教程没说透的地方。后面第三节我会详细讲怎么处理。

1.3 一个绕不开的坑:没有内置暂停/继续

SpeechSynthesizer具备的方法大概有:Speak(同步朗读)、SpeakAsync(异步朗读)、SpeakAsyncCancel(取消特定异步操作)、SpeakAsyncCancelAll(取消所有异步朗读)、Pause?——抱歉,真没有。

查一下System.Speech.Synthesis命名空间下的成员就知道,SpeechSynthesizer只有播放和取消两类能力。想做暂停/继续,必须通过其他手段绕过去。我当时试了三条路:

  • 路线A:调用 WinMM 音频设备接口,直接对系统默认音频输出设备执行暂停/恢复。优点是代码量小、现有朗读任务不会被破坏;缺点是它暂停的是整个音频输出设备,严格说不只是当前 TTS 任务,而且在部分音频驱动下可能失效。
  • 路线B:改用 SAPI COM 的 SpVoice 对象,它有原生的Pause()Resume()方法。这是引擎级暂停,最正规,但代码风格老派,需要引用 COM 库。
  • 路线C:自己按句子切分文本,用信号量控制暂停。跨平台稳定,但句子之间会有间隙,而且暂停点不够精确。

最终我选择了“路线A为主、路线B为备选”的组合方案,下面会给出两套完整代码。

2. VoiceSpeaker 类的设计思路与核心 API

2.1 对外暴露的四个方法

设计一个语音朗读类,最重要的不是把代码堆上去,而是让调用方觉得“顺手”。我最终对外暴露的方法只有四个,和标题一一对应:

方法功能说明调用后状态
Play(string text)开始朗读,或重新开始朗读新文本Speaking
Stop()停止所有朗读并清空队列Ready
Pause()暂停当前朗读Paused
Continue()从暂停位置继续朗读Speaking

实际使用中,调用方只需要这样操作:

VoiceSpeaker speaker = new VoiceSpeaker(); speaker.Play("设备一号发生高温报警,请立即处理"); speaker.Pause(); // 操作员暂时离开 speaker.Continue(); // 回来继续听 speaker.Stop(); // 这条播报不需要了

这里有个设计上的取舍:有些场景要求“播放”时如果已经在朗读,就自动打断旧内容、播报新内容。我的做法是Play内部先调用Stop再开新任务,保证语义清晰。没有把“打断播报”和“首次播报”做成两个方法,因为绝大多数前端 UI 只需要一个播放按钮。

2.2 状态管理与 UI 联动

WinForms 或 WPF 界面上,按钮是否可点取决于当前朗读状态。比如“暂停”和“继续”不能同时可用,应该是个互斥状态。如果每次状态变化都让外层自己去判断,代码很快就会乱。

所以我给VoiceSpeaker增加了两个只读属性:

public bool IsSpeaking { get; private set; } public bool IsPaused { get; private set; }

再配合一个StateChanged事件,状态一旦变化,UI 直接刷新按钮。典型的绑定逻辑如下:

speaker.StateChanged += (s, e) => { btnPlay.Enabled = !speaker.IsSpeaking || speaker.IsPaused; btnStop.Enabled = speaker.IsSpeaking; btnPause.Enabled = speaker.IsSpeaking && !speaker.IsPaused; btnContinue.Enabled = speaker.IsPaused; };

这套逻辑跑起来后,界面上四个按钮永远只有合理的那几个能点。比如正在朗读时,“播放”置灰,“暂停”亮着;暂停后“暂停”置灰,“继续”亮着。用户不会误操作。

2.3 基础引用与环境准备

使用SpeechSynthesizer,项目里需要添加引用:

  • .NET Framework:项目右键→添加引用→程序集→框架,勾选System.Speech
  • .NET Core / .NET 5+:System.Speech 不再随框架内置,需要通过 NuGet 安装:
Install-Package System.Speech

这里特别提醒一下:System.Speech只支持 Windows,跨平台项目不要选它。如果你的目标平台是 Linux 服务器,请直接跳到第 4 节看替代方案。

3. 核心功能实现与完整代码

3.1 播放:为什么必须用 SpeakAsync

SpeechSynthesizerSpeak(string text)同步方法,它会阻塞调用线程直到朗读完毕。如果你在 WinForms 的按钮点击事件里直接调Speak,界面会直接“卡死”在朗读这段时间里,窗口拖不动、按钮点不了,非常糟糕。

正确用法是SpeakAsync(string text),它把朗读任务丢到后台线程池执行,立即返回,UI 线程保持流畅:

public void Play(string text) { if (string.IsNullOrWhiteSpace(text)) return; Stop(); // 先取消上一次任务 _synthesizer.SpeakAsync(text); }

有个细节要讲:SpeakAsync调用后,同一个SpeechSynthesizer实例会进入“Speaking”状态,但如果之前有未取消的任务,新任务会排队而不是立即打断。所以Play里必须先Stop,否则用户连续点两次“播放”,第二次会等第一次读完才读,达不到“更新播报内容”的效果。

再补充一个SpeakAsync的变体:SpeakAsync(PromptBuilder),它可以组合多段文本、书签、音频,适合做复杂播报。我们这个类里暂时用不上,但如果以后要插入提示音,可以在PromptBuilder上扩展。

3.2 停止:取消队列而不是清空文本

停止逻辑很简单,调用SpeakAsyncCancelAll()就行:

public void Stop() { Interlocked.Increment(ref _version); // 防止旧回调干扰 _synthesizer.SpeakAsyncCancelAll(); IsSpeaking = false; IsPaused = false; }

这里有两个注意点。

第一,SpeakAsyncCancelAll是异步取消,调用后不会立刻触发完成事件,所以不要在调用后用同步方式等待状态变回“Ready”。如果你的业务里需要“停止后立刻做某件事”,最好在SpeakAsyncCompleted事件回调里做。

第二,类的属性状态要主动复位。因为取消后StateChanged事件不一定马上触发,万一 UI 在取消后立刻查询IsSpeaking,可能还是true。我选择在Stop里直接复位状态,保证属性值和实际操作语义一致。

3.3 暂停/继续:两种实现方式

这一节是重点。SpeechSynthesizer没有原生暂停/继续,我用两种方式解决,分别应对不同场景。

方式一:调用 WinMM 音频设备接口(推荐)

思路是:Windows 有底层音频设备 API,winmm.dll里的waveOutPausewaveOutResume可以暂停和恢复默认音频设备输出。TTS 朗读的内容最终要经过音频设备播放,设备一暂停,声音自然就停了。

using System.Runtime.InteropServices; [DllImport("winmm.dll")] private static extern int waveOutPause(IntPtr hwo); [DllImport("winmm.dll")] private static extern int waveOutResume(IntPtr hwo); public void Pause() { if (!IsSpeaking || IsPaused) return; int result = waveOutPause(IntPtr.Zero); if (result == 0) { IsPaused = true; } } public void Continue() { if (!IsPaused) return; int result = waveOutResume(IntPtr.Zero); if (result == 0) { IsPaused = false; } }

传入IntPtr.Zero表示操作默认音频输出设备。实测下来,大部分 Windows 10/11 系统上这个方案是有效的。优点是改动小、不用换 TTS 引擎;缺点是它暂停的是整个系统音频输出,如果用户同时放着音乐,音乐也会跟着暂停。

方式二:使用 SAPI COM 的 SpVoice(引擎级暂停)

如果要求“只暂停当前朗读,不影响其他声音”,或者waveOutPause在你的目标机器上失效了,就该上 COM 方案。项目里引用Microsoft Speech Object Library(即 SpeechLib),然后这样操作:

using SpeechLib; SpVoice voice = new SpVoice(); // 异步朗读 voice.Speak("设备报警", SpeechVoiceSpeakFlags.SVSFlagsAsync); // 引擎级暂停 voice.Pause(); // 引擎级恢复 voice.Resume(); // 停止 voice.Speak(string.Empty, SpeechVoiceSpeakFlags.SVSFPurgeBeforeSpeak);

SpVoice是 SAPI 5 的 COM 封装,Pause()Resume()是原生方法,暂停后引擎会完整保留当前的朗读位置,继续时无缝衔接。不过它的缺点也很明显:API 风格老、没有现成的异步取消事件,做复杂封装时要自己处理线程回调。

我的建议是:你自己封装的类,优先用SpeechSynthesizer做播放和停止,用 WinMM 做暂停/继续,代码量最少,也最容易维护。只有当音频设备层面出现兼容性问题时,再整体替换为SpVoice方案。

3.4 中文音色、语速、音量调节

朗读类如果只能读默认音色,实用性会大打折扣。中文系统下,默认的语音引擎可能是Microsoft Huihui Desktop,但遇到英文系统或者语音包缺失,读中文会变成奇怪的英文发音。因此我建议在类里暴露三个调节方法:

public void SetVoice(string voiceName) { if (!string.IsNullOrEmpty(voiceName)) _synthesizer.SelectVoice(voiceName); } public void SetRate(int rate) { _synthesizer.Rate = Math.Max(-10, Math.Min(10, rate)); } public void SetVolume(int volume) { _synthesizer.Volume = Math.Max(0, Math.Min(100, volume)); }

Rate的取值范围是 -10 到 10,-10 最慢,10 最快,默认 0。Volume是 0 到 100,默认 100。如果不知道怎么选音色,可以用下面这段代码把所有可用音色打出来:

foreach (var voice in _synthesizer.GetInstalledVoices()) { var info = voice.VoiceInfo; Console.WriteLine($"名称:{info.Name},语言:{info.Culture.Name},性别:{info.Gender}"); }

然后挑一个中文音色,例如:

speaker.SetVoice("Microsoft Huihui Desktop");

这里有个小坑:SelectVoice如果传入了不存在的音色名称,会抛出异常。所以稳妥的做法是先用GetInstalledVoices过滤一遍,存在才设置。

3.5 完整代码

把上面的思路合并,就是一个开箱即用的VoiceSpeaker类。以下是我在项目里实际使用的版本,已经去掉业务耦合,可以直接复制:

using System; using System.Runtime.InteropServices; using System.Speech.Synthesis; using System.Threading; namespace SpeechDemo { /// <summary> /// 语音朗读类:播放、停止、暂停、继续 /// </summary> public class VoiceSpeaker : IDisposable { private SpeechSynthesizer _synthesizer; private int _version; public event EventHandler<StateChangedEventArgs> StateChanged; public bool IsSpeaking { get; private set; } public bool IsPaused { get; private set; } [DllImport("winmm.dll")] private static extern int waveOutPause(IntPtr hwo); [DllImport("winmm.dll")] private static extern int waveOutResume(IntPtr hwo); public VoiceSpeaker() { _synthesizer = new SpeechSynthesizer(); _synthesizer.StateChanged += OnStateChanged; _synthesizer.SpeakAsyncCompleted += OnSpeakAsyncCompleted; } public void Play(string text) { if (string.IsNullOrWhiteSpace(text)) return; // 取消上一次任务,避免排队 Stop(); // 如果之前是暂停状态,先恢复设备 waveOutResume(IntPtr.Zero); IsPaused = false; int currentVersion = Interlocked.Increment(ref _version); _synthesizer.SpeakAsync(text); } public void Stop() { Interlocked.Increment(ref _version); _synthesizer.SpeakAsyncCancelAll(); IsSpeaking = false; IsPaused = false; } public void Pause() { if (!IsSpeaking || IsPaused) return; int result = waveOutPause(IntPtr.Zero); if (result == 0) { IsPaused = true; } } public void Continue() { if (!IsPaused) return; int result = waveOutResume(IntPtr.Zero); if (result == 0) { IsPaused = false; } } public void SetVoice(string voiceName) { if (string.IsNullOrEmpty(voiceName)) return; bool voiceExists = false; foreach (var voice in _synthesizer.GetInstalledVoices()) { if (voice.VoiceInfo.Name == voiceName) { voiceExists = true; break; } } if (voiceExists) { _synthesizer.SelectVoice(voiceName); } } public void SetRate(int rate) { _synthesizer.Rate = Math.Max(-10, Math.Min(10, rate)); } public void SetVolume(int volume) { _synthesizer.Volume = Math.Max(0, Math.Min(100, volume)); } private void OnStateChanged(object sender, StateChangedEventArgs e) { IsSpeaking = e.State == SynthesizerState.Speaking || e.State == SynthesizerState.Paused; if (e.State == SynthesizerState.Ready) { IsPaused = false; } StateChanged?.Invoke(this, e); } private void OnSpeakAsyncCompleted(object sender, SpeakAsyncCompletedEventArgs e) { IsSpeaking = false; IsPaused = false; } public void Dispose() { if (_synthesizer != null) { _synthesizer.SpeakAsyncCancelAll(); _synthesizer.Dispose(); _synthesizer = null; } } } }

这个类有几个细节值得说明:

  1. Play里先调Stop,再调waveOutResume,原因是如果之前是暂停状态,直接SpeakAsync后 TTS 开始朗读了,但音频设备还挂着“暂停”,会没有声音。
  2. Stop后旧任务的SpeakAsyncCompleted事件可能还会触发,所以事件回调里只复位状态,不操作新任务。
  3. _version字段用来标记一个新任务版本,如果以后要扩展“朗读途中播放更高优先级内容”,可以靠版本号区分旧回调。

4. 实测记录与常见问题排查

4.1 不同环境下的实测表现

我分别在 Windows 10 和 Windows 11 上做了测试,覆盖 .NET Framework 4.7.2 和 .NET 6 环境,运行情况如下:

环境播放停止暂停继续备注
Win10 + .NET Framework 4.7.2正常正常正常正常中文音色需要装语音包
Win10 + .NET 6正常正常正常正常需要 NuGet 安装 System.Speech
Win11 + .NET 6正常正常正常正常默认使用 Microsoft Xiaoxiao 在线语音时要注意网络

这里有一个额外发现:Windows 11 部分系统默认的 TTS 音色可能从离线引擎切换到了“在线自然语音”。在线语音音质更好,但如果电脑离线,朗读会失败或者回退到本地音色。如果你的工具部署在离线环境,务必在初始化时检查GetInstalledVoices()列表,并显式指定一个离线语音。

4.2 暂停失效怎么办

waveOutPause这个方案最大的问题在于:它不是 TTS 引擎级的暂停,而是音频设备级的暂停。如果你遇到“点了暂停,声音还在继续”的情况,最常见的原因是:

  1. 系统音频设备变了:比如用户插拔了耳机、蓝牙音频设备切换,IntPtr.Zero指向的默认设备和实际播放设备不一致。
  2. TTS 走的是新的音频会话:部分语音引擎使用独立音频会话,不一定挂在waveOut设备上。
  3. 程序权限问题:某些精简版系统、安全软件会拦截底层音频调用。

排查时先调函数返回值。waveOutPause返回 0 表示成功,返回其他值(比如MMSYSERR_INVALHANDLE)说明设备句柄无效。如果确认是设备句柄问题,可以改用waveOutOpen打开默认设备后,再用返回的句柄调用暂停接口。这个做法更严谨,但代码量会大一些。

最省事的兜底方案:换成SpVoiceCOM 对象做暂停/继续。它的Pause是引擎级操作,不依赖系统音频设备句柄,稳定性高很多。我有两个项目都是因为现场主机音频驱动太杂,最终切到了SpVoice

4.3 UI 线程卡顿和事件回调问题

很多初学者会把Speak直接放在按钮点击事件里,界面卡死是必然的。另外,SpeakAsyncCompleted事件的回调线程是线程池线程,不是 UI 线程。你在回调里想更新TextBoxLabel时,要用InvokeBeginInvoke跳回 UI 线程:

speaker.StateChanged += (s, e) => { if (this.InvokeRequired) { this.BeginInvoke(new Action(() => UpdateButtons())); } else { UpdateButtons(); } };

还有一个常见的竞态问题:快速连点“停止”和“播放”,可能上一次朗读的SpeakAsyncCompleted事件在新一次播放之后才触发,把新任务的IsSpeaking误置为false。我代码里的_version字段就是为了处理这类问题。扩展一下,在事件回调里判断版本号是否最新,不是最新就直接丢弃:

private int _currentVersion; private void OnSpeakAsyncCompleted(object sender, SpeakAsyncCompletedEventArgs e) { if (Interlocked.CompareExchange(ref _currentVersion, 0, 0) != _latestVersion) return; IsSpeaking = false; }

4.4 常见问题速查表

把我在项目维护中遇到的高频问题整理成了一张表,方便你直接对照排查:

问题现象根本原因解决方法
读出来是英文发音当前音色不支持中文GetInstalledVoices检查并SelectVoice选择中文音色
朗读时界面卡死用了同步Speak换成SpeakAsync
连续点播放不生效SpeakAsync任务排队Play内部先SpeakAsyncCancelAll
暂停后没有声音音频设备句柄无效检查返回值,或换SpVoice方案
播放时没声音之前处于暂停状态播放前先waveOutResume
停止后状态没更新回调线程异步在事件回调里复位状态,Stop方法里同步复位
电脑离线后朗读失败默认语音是在线语音显式指定离线音色
程序退出还有声音在响未调用 Dispose窗体关闭时调用speaker.Dispose()

最后再分享一个小技巧:SpeechSynthesizer支持PromptBuilderAppendTextWithBookmark,如果你想对朗读进度做精确控制(比如高亮当前读到的句子),可以用BookmarkReached事件拿到书签位置。我在做“逐句跟读”功能时就是用这个方案,比按时间估算朗读位置可靠得多。

这个VoiceSpeaker类目前已经在我自己的两个工具里跑了大半年,每次改动基本都是在外围业务逻辑,四个核心方法几乎没动过。如果你在设计 UI 时有任何状态切换的疑问,建议先跑起来,用最小 Demo 验证 WinMM 暂停在你的目标系统上是否可靠,再决定要不要继续往下封装。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询