简介:一份基于微软TTS引擎的C++语音合成入门示例,打包在TTS.rar中,内附唯一的TTS.cpp源文件,大小仅521字节,结构精炼,适合开发者快速掌握文本转语音的编程思路。示例代码主要演示了调用微软TTS接口完成语音合成的全流程,包括创建语音引擎对象、设置语速与音调、加载语音库、读取待转换文本以及输出音频文件,可直接编译查看效果;其中涉及语音属性调整、发音词典处理等关键点,对理解TTS系统组成很有帮助。目前已吸引335人学习,说明该示例在语音入门领域具有一定实用价值。通过研读这份代码,不仅能夯实TTS基础,还可为后续使用微软SAPI或Azure TTS服务,满足多语言、自然语音合成需求,进而集成到无障碍工具、智能助手、自动语音播报等真实场景中。
1. 一个 TTS.cpp 能拆出多少东西
拿到一个名为TTS.rar的压缩包,里面只有一个TTS.cpp时,别急着双击运行。这个文件大概率是微软 SAPI(Speech Application Programming Interface)的极简示例:创建SpVoice对象、调用Speak()、把一句话变成语音输出。真正的价值不在那几十行代码,而在你能否借此把 TTS 的调用链、线程模型和音频输出机制摸清。很多人在网上搜“微软tts”“tts 语音”找到这类资源,结果卡在CoInitialize没调用、语音引擎未注册或者中文读不出来,浪费半天时间。这篇文章就围绕这个TTS.cpp展开,把 SAPI 合成流程从初始化到落盘 WAV 全部拆开,最后给出一套可以直接迁移到 Windows 服务、后台任务和跨进程场景的写法。
2. 微软 TTS 与 SAPI:选型与核心机制
2.1 为什么是 SAPI 而不是 Azure SDK
微软的 TTS 方案大致分三代:老的SAPI 5.4(SpVoice)、新的Windows.Media.SpeechSynthesis(WinRT API),以及云端的Azure Cognitive Services Text-to-Speech。TTS.cpp这类老代码通常指向 SAPI 5,原因很现实:SAPI 是 Windows 系统级 COM 组件,xp到Windows 11都支持,不依赖网络,不需要 API Key,离线可用。它通过注册表暴露系统已安装的语音包,比如Microsoft Huihui Desktop、Microsoft Kangkang Desktop,或者新版系统的Microsoft Xiaoxiao Online(在线)与Microsoft Huihui(离线)。
选用 SAPI 而不是 Azure SDK 的决策依据很简单:如果你要处理的是本地语音播报、不需要神经网络音色、或者对数据隐私有要求,SAPI 能覆盖 90% 的场景;如果你要生成多语种、情感丰富、接近真人的声音,并且接受按字符计费,才需要考虑 Azure。另外要注意,SAPI 的离线中文语音在 Windows 10/11 上可能默认没有安装,需要去“设置-时间和语言-语言-中文-语音”里手动添加。不然TTS.cpp运行时会抛0x80045004之类的声音资源错误。
2.2 语音合成对象与事件流
SpVoice是 SAPI 的核心 COM 对象,类型库定义在sapi.h中。它和普通 COM 对象最大的区别是:Speak()默认是同步阻塞的,调用线程会被挂起直到语音播完或合成完毕。若你希望一边合成一边做其他事,必须把SPF_ASYNC标志传给Speak,并监听SpVoice的事件(如SPFEI_END_STREAM)。大多数新手把Speak写成一个死循环,或者在事件循环里直接播放,导致界面卡死,原因就是没理解它的同步模型。
事件流上,SAPI 支持SetNotifyCallbackFunction或SetNotifyWindowMessage,后者适合 MFC/Win32 窗口程序。如果你用的是控制台程序,最简单的做法是把Speak(..., SPF_ASYNC)放到一个独立线程,然后用WaitForSingleObject等待事件句柄,或者轮询Status属性判断语音是否播放完毕。下面会给出一个可运行的示例。
2.2.1 SpVoice 的四个关键参数
SpVoice暴露了几个粗粒度属性,注意它们不是分贝和赫兹,而是相对值:
| 属性 | 类型 | 有效范围 | 默认值 | 说明 |
|---|---|---|---|---|
Rate | long | -10 ~ 10 | 0 | 语速,每档约 20% 变化 |
Volume | long | 0 ~ 100 | 100 | 整体音量百分比 |
Voice | ISpObjectToken | 枚举值 | 系统默认 | 当前语音引擎 |
AudioOutput | ISpAudio | 设备或文件流 | 默认声卡 | 输出目标 |
Rate的负值会变得拖沓,正值容易含糊,中文语音在 0~2 之间比较自然。Volume只影响SpVoice自身的混合音量,不会改变系统主音量。Voice属性必须用SetVoice设置一个ISpObjectToken,而不能直接传语音名称字符串,这是初学者最容易踩的坑。
3. 用 C++ 把文本变成 WAV:TTS.cpp 的骨架
3.1 初始化与 CoInitialize
SpVoice是一个 COM 组件,所以在调用它之前必须初始化 COM 单元。这里有个细节:如果你的程序最终要在 UI 线程使用 SAPI,建议用CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);如果是后台工作线程,用COINIT_MULTITHREADED更合适,但SpVoice的线程亲和性很强,同一线程创建的SpVoice最好只在该线程使用。下面是TTS.cpp的最小启动逻辑。
#include <sapi.h> #include <sphelper.h> #include <windows.h> #include <iostream> #pragma comment(lib, "ole32.lib") #pragma comment(lib, "sapi.lib") int wmain() { // 初始化 COM,失败直接退出 HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); if (FAILED(hr)) { std::cerr << "CoInitializeEx failed: 0x" << std::hex << hr << std::endl; return 1; } CComPtr<ISpVoice> cpVoice; hr = cpVoice.CoCreateInstance(CLSID_SpVoice); if (FAILED(hr)) { std::cerr << "Create SpVoice failed: 0x" << std::hex << hr << std::endl; CoUninitialize(); return 1; } // 合成并播放 hr = cpVoice->Speak(L"你好,这是一条来自微软 TTS 的语音。", SPF_DEFAULT, nullptr); if (FAILED(hr)) { std::cerr << "Speak failed: 0x" << std::hex << hr << std::endl; } cpVoice.Release(); CoUninitialize(); return 0; }CoCreateInstance里的CLSID_SpVoice是 SAPI 注册表中的标准类 ID,sapi.h里已经定义好。CComPtr来自atlbase.h,如果你不想引入 ATL,可以用裸指针手动Release()。注意Speak的第三个参数pNotify传nullptr表示同步执行,此时Speak返回时语音已经播放完毕,或者至少已经提交给音频设备。
3.2 设置语音、语速与音调
要改变发声人,必须通过语音令牌。SpFindBestToken是一个便捷函数,它根据SPCAT_VOICES和属性匹配来查找语音。比如查找名为 "Microsoft Huihui Desktop" 的中文语音,可以这样:
CComPtr<ISpObjectToken> cpToken; hr = SpFindBestToken(SPCAT_VOICES, L"Language=804;Gender=Female", L"", &cpToken); if (SUCCEEDED(hr)) { cpVoice->SetVoice(cpToken); }Language=804是简体中文的 LCID 十六进制表示,Gender=Female指定女性音色。SpFindBestToken的第二个参数是“必须满足”的属性,第三个是“优选”属性(可为空)。如果你只想要系统默认语音,把第二个参数传L""即可。设置语速和音量很简单:
cpVoice->SetRate(1); // 比默认稍快 cpVoice->SetVolume(90); // 音量 90%需要说明的是,SetRate接收的是long,但底层按千分比存储。SpVoice内部会把这个值映射到引擎支持的语速范围,所以不同引擎相同数值下实际语速可能有差异。这里建议用1或2做中文播报,超过4容易吞字。
3.3 保存到文件与直接播放
TTS.cpp里最常见的需求是把语音保存为 WAV,而不是实时播放。SAPI 用ISpStream配合SPF_SAVE_TO_FILE实现语音落盘。核心代码如下:
CComPtr<ISpStream> cpStream; CSpStreamFormat fmt; fmt.AssignFormat(SPSF_22kHz16BitMono); // 设置音频格式 hr = cpStream->Open(L"output.wav", SPFM_CREATE_ALWAYS, &fmt.FormatId(), fmt.WaveFormatExPtr()); if (SUCCEEDED(hr)) { cpVoice->SetOutput(cpStream, TRUE); cpVoice->Speak(L"保存成 WAV 文件", SPF_DEFAULT, nullptr); cpStream->Close(); }SPSF_22kHz16BitMono是一个枚举值,表示采样率 22.05kHz、16 位、单声道。这个格式体积适中,播报清晰;如果你要更高质量,可以改成SPSF_44kHz16BitMono,代价是文件体积翻倍。SetOutput第二个参数TRUE表示释放当前输出流,避免文件被占用。注意一定要在Speak完成后Close(),否则文件可能损坏。
3.4 编译与链接
在 Visual Studio 中编译这个TTS.cpp,需要设置三件事:项目属性 -> C/C++ -> 附加包含目录里加上 Windows SDK 的um和shared目录(一般默认就有);链接器 -> 输入 -> 附加依赖项里添加sapi.lib和ole32.lib;字符集改为“使用 Unicode 字符集”,因为L"..."是宽字符串。如果用命令行编译,直接执行:
cl /EHsc /D "UNICODE" TTS.cpp sapi.lib ole32.lib/EHsc启用 C++ 异常处理,/D "UNICODE"让wmain入口正确。如果报sphelper.h找不到,说明你的 Windows SDK 版本过低,建议升级到 Windows 10/11 SDK。
4. 参数调优与多语音选择
4.1 语音令牌枚举
很多从TTS.rar拿到代码的人想换语音却发现SetVoice总是失败。原因可能是语音名称没有注册,或者你猜的名字和系统里的不一致。枚举所有已安装语音是定位问题最快的方法:
CComPtr<IEnumSpObjectTokens> cpEnum; ULONG count = 0; SpEnumTokens(SPCAT_VOICES, nullptr, nullptr, &cpEnum); cpEnum->GetCount(&count); for (ULONG i = 0; i < count; i++) { CComPtr<ISpObjectToken> cpToken; cpEnum->Next(1, &cpToken, nullptr); CSpDynamicString strDesc; cpToken->GetStringValue(L"Description", &strDesc); wcout << L"语音 " << i << L": " << strDesc.m_psz << endl; }Description是语音令牌的友好名称,比如 “Microsoft Huihui Desktop”。用这个名称配合SpFindBestToken的Name=属性可以精确定位。如果你的程序只跑在固定机器上,直接把Description写死可以节省枚举开销;否则建议做成配置项。
4.2 语速、音量的参考区间
不同语种下Rate的感知差异很大。英文引擎的默认语速本来就比中文快,所以中文播报推荐Rate=0到2,英文播报可以到4。实际测试中,Rate=6以上时中文会出现明显爆破音和吞字,这时候不要盲目提Rate,而是换用更清晰的语音引擎,比如Microsoft Xiaoxiao Online(如果你愿意走在线神经网络)或者Microsoft Huihui Desktop(离线)。
Volume的调整要注意 SAPI 的最终输出是数字信号混合,设置Volume=100时如果系统主音量为 100,可能出现削波失真。建议应用内音量限制在90以下,留出动态余量。
4.3 处理中文发音词典与特殊词
SpVoice对中文长句有自动分词能力,但遇到“桔子”读“jié”这类多音字、专业术语或英文缩写时,需要强制指定发音。最简单的方式是在文本里插入 SSML 标记:
cpVoice->Speak( L"<speak version='1.0' xml:lang='zh-CN'>" L"<w>长沙</w>是一个好地方," L"<phoneme alphabet='sapi' ph='zhang1 san1'>张三</phoneme>" L"参加了 GB28181 会议</speak>", SPF_IS_XML, nullptr);SPF_IS_XML标志告诉 SAPI 按 SSML 解析。<w>用于强制分词,<phoneme>指定拼音。ph属性中的数字表示声调,zhang1 san1对应“张三”。但注意 SAPI 的 SSML 解析器相对老,不支持<audio>标签,也不支持prosody的pitch大量调节。如果发现某些 SSML 片段被原样朗读,说明引擎不支持该标签,需要用<break time='200ms'/>替代段落停顿。
5. 进阶:把 TTS 塞进更复杂的场景
5.1 后台线程合成与回调
SpVoice默认同步模式会阻塞线程,如果你在 Windows 服务或消息循环中直接调用,轻则界面无响应,重则导致音频设备冲突。常见做法是创建一个独立线程,每次合成任务都投递到该线程执行。示例:
DWORD WINAPI TTSTask(LPVOID param) { CoInitializeEx(nullptr, COINIT_MULTITHREADED); CComPtr<ISpVoice> cpVoice; cpVoice.CoCreateInstance(CLSID_SpVoice); // 任务队列里取出字符串,这里简化为直接合成 cpVoice->Speak(static_cast<wchar_t*>(param), SPF_DEFAULT, nullptr); cpVoice.Release(); CoUninitialize(); return 0; }注意线程退出前必须CoUninitialize,否则下一个线程复用 COM 时可能报RPC_E_CHANGED_MODE。更严谨的做法是给每个工作线程分配独立的SpVoice实例,不要跨线程共享,因为 SAPI 的语音对象内部有线程局部的渲染上下文。
5.2 用 Azure TTS 替代本地 SAPI 的迁移要点
如果你需要神经网络音色,比如“晓晓”“云希”,可从 SAPI 迁移到 Azure TTS。迁移时重点改三处:把SpVoice换成SpeechSynthesizer(微软 C++ SDK 或 REST API);将SetVoice改成SetSpeechSynthesisVoiceName("zh-CN-XiaoxiaoNeural");把同步Speak换成SpeakTextAsync,并处理音频流回调。Azure 的格式选择不是 WAV 枚举,而是Riff16Khz16BitMonoPcm这类字符串。如果还在用TTS.cpp的老代码,换到 Azure 后要特别注意文本长度限制:单次合成不能超过 10 分钟音频,否则需要分片。一个可行的验证技巧是:先本地用 SAPI 做功能走通,再在关键播报节点封装一个TTSProvider接口,这样后续切引擎只改实现,不动业务代码。
本文还有配套的精品资源,点击获取