核心课程 / 自然语言交互应用 / EXP-03
EXP-03 · 第 1 章 · 设计性 · 4 学时

TTS 语音合成:让机器人开口说话

机器人听懂了,还得用自然的人声回答。本实验拆解 Matcha-TTS 声学模型 + Vocos 声码器两级工厂:文本正则化与 rule_fsts 规则(日期/数字/电话读法)、梅尔谱预测、波形重建,再到混音、音量 dB 换算与 RTF 实时率调优;过程中会踩到"单声道却两倍速播放"这类经典硬件坑。

难度 ⭐⭐ · 4 学时 Matcha-TTSVocos 声码器 梅尔谱 · RTFamixer 混音

01问题引入:只会"听"不会"说"的机器人

前两个实验让机器人能听、能转成文字。但如果它回答时屏幕上打印一行字——用户就得盯着一台会说话的机器看字幕,交互感瞬间归零。语音输出的三道坎:

1读不对:文本里"2026年9月24日"该念成"二〇二六……"还是"两千零二十六……"?数字、日期、英文缩写都要先"正读"
2合成慢:一句 10 秒的话算了 30 秒,对话根本没法进行——速度必须跟上语速
3播不对:好不容易合成的语音,从阵列喇叭放出来变成"快放 2 倍 + 音量爆破"——采样率/声道与硬件不匹配
💡 解法总览课程用 Matcha-TTS(文本→梅尔谱)与 Vocos(梅尔谱→波形)两级神经网络接力,配合 rule_fsts 文本正则、RTF 性能指标与 ALSA 混音链,把"文字变声音"做成一条能实时通车的生产线。

02原理通俗讲解:把文字变成声音的两级工厂

生活比喻:做饭。文字是菜单,声音是菜品。一级工厂把菜单翻译成菜谱(先放什么后放什么、几分钟火候),二级工厂照着菜谱把菜炒出来。TTS 管线正是这样两级结构:

1文本正规化 = 翻译菜单:rule_fsts 规则文件把 "2026"→"二零二六"、"+86 10…"→逐位读法、日期/电话各自套读法,让神经网络只面对"可读的字"
2Matcha 声学模型 = 出菜谱:把规范化文本编码成音素序列,按扩散式流程"画"出一张梅尔谱图——横轴时间、纵轴频率的"声音热力图",规定了每个时刻各频率该有多响
3Vocos 声码器 = 照谱炒菜:读入梅尔谱,一步式生成 16kHz 原始波形采样点——决定声音的"音色质感"(像不同厨师炒同一张菜谱,风味仍有差别)
4后处理 = 装盘上桌:float→int16 量化、单声道复制成双声道、tanh 软削幅防爆音、按 dB 目标调音量,送 ALSA 播放
概念生活类比本实验对应
文本正则化把"3碗"念成"三碗"而不是"三点"rule_fsts:number-zh / date-zh / phone-zh
梅尔谱声音的"乐高设计图"Matcha 输出:时间 × 梅尔频带的特征矩阵
声码器按设计图 3D 打印Vocos:梅尔谱 → 16kHz 波形
RTF 实时率炒菜用时 ÷ 菜品时长合成耗时/音频时长,0.25~0.40 达标(<1 才能实时)
增益(dB)拧音量旋钮的角度pct_to_db = 20·log10(pct/100),-96dB 为静音下限
🔍 为什么分两级梅尔谱只有几十个频带,是"压缩过的声音图纸"——直接由文本一步到位生成波形,模型要学的东西太多、又慢又糊。先画图、再填色的两段式让每一步都可单独换更强的模型,这是现代 TTS 的通用骨架。

03关联知识点

模型
Matcha-TTS + Vocos

model-steps-3.onnx(声学模型,扩散 3 步加速)+ vocos-16khz-univ.onnx(万用声码器,16kHz 单域)。

音频
采样率与声道

管线内 16kHz 单声道 float;阵列 I2S 播放常要求立体声——np.column_stack([mono,mono]).ravel() 复制声道,少复制会 2 倍速快放(s9 详解)。

音量
百分比 ↔ dB 与混音链

软件增益 20·log10(pct/100)(-96dB 截底);硬件经 amixer 分控 Output 1 / Output 2 / PCM / Speaker 多级,播放前对照确认目标通道。

性能
RTF 与流式分句

RTF=合成耗时÷音频时长,目标 0.25~0.40;配合 EXP-04 的按标点分句,句子级流式合成掩盖延迟。

参数
speed / sid / gain

--speed 语速(时间轴伸缩)、--sid 发音人 ID、--playback-volume 播放音量、--gain 合成内增益。

自测
audio_test.py

先用它单独验证"播放链"是否正常,把故障域从 TTS 模型与声卡播放中切开,二分定位问题。

04实验目标

  • 能默写 TTS 五步管线(文本正则 → 编码 → 梅尔谱 → 声码器 → 后处理播放)并说明每步输入输出
  • 用 python3 tts_test.py --text "…" 合成并播放第一句机器人语音,听感清晰无爆音
  • 验证 rule_fsts:同一句里混入日期与数字,确认按中文习惯读出(而非逐位蹦)
  • 调整 --speed / --sid / --playback-volume,听辨语速、发音人与响度变化
  • 实测 RTF:记录 5 句长短语料的合成耗时,判断是否落在 0.25~0.40 达标区间
  • 复现并修复"单声道两倍速播放"故障:解释声道不匹配的原理并写出修复三行

05实验环境与准备

# ① 模型就位(声学模型 + 声码器 + 规则文件)
ls models/tts/
model-steps-3.onnx   vocos-16khz-univ.onnx   rule_fsts/  lexicon.txt

# ② 播放链自检:放一段已知正常的测试音(不经过 TTS 模型)
python3 audio_test.py --play ref_16k.wav
[audio] playing ref_16k.wav on plughw:4,0 ... OK

# ③ 查看当前 ALSA 各通道音量(对比实验前后)
amixer sget Master | head -5
amixer sget Output          # 阵列侧混音控件名以 amixer 管单为准

# ④ 依赖
pip3 install onnxruntime numpy soundfile sounddevice
💡 准备清单录一段你自己的朗读做"听感对照组";准备三类测试文本:纯短句、含数字日期、含英文缩写——分别暴露正则、节奏与发音三类问题。先跑 audio_test.py 把"喇叭会不会响"与"TTS 好不好"解耦。

06动画讲解:文本 → 音素 → 梅尔谱 → 波形

看一句"今天是2026年9月24日"的旅程:数字先被 rule_fsts 规范成中文读法 → 音素序列进入 Matcha 逐帧"画"出梅尔谱 → Vocos 把热力图逐列翻译为振荡波形 → 打包成 16kHz 音频流出喇叭。

① 文本正则 "今天是 2026年9月" 24日" rule_fsts → "二零二六年九月二十四日" ② Matcha · 梅尔谱 时间 × 梅尔频带 · 每步生成一帧 ③ Vocos · 16kHz 波形 梅尔谱逐列 → 振荡采样 · float→int16 ④ 后处理与播放 单→双声道复制 tanh 软削幅防爆音 音量 dB 校准 ALSA → 阵列喇叭 🔊 RTF = 合成耗时 / 音频时长 达标线 ≤ 0.40 ✓
动画 3-1|数字经 rule_fsts 变中文读法 → Matcha 逐帧点亮梅尔谱 → Vocos 波形逐步描出 → 音量/声道后处理后从阵列喇叭播出(4 秒循环)

07互动讲解:语速 · 音量 · 发音人调节台

选一组参数,观察合成命令、预期听感与度量指标(时间轴/响度为讲义实测的典型行为示意):

TTS 参数调节台 · Synth Control
// 合成链: text → rule_fsts → Matcha(steps-3) → Vocos → int16 → play 就绪:点选上方任一参数开始合成(时长/RTF/响度按讲义四句均值示意)
🔬 动手算一算pct_to_db(50) = 20·log₁₀(0.5) ≈ -6dB;pct_to_db(10) ≈ -20dB。把播放音量从 100% 调到 50%,响度不是"减半"而是降 6dB——分贝是对数刻度,直觉会骗人。

08实操步骤:合成第一句机器人语音

1先跑 audio_test.py 播放参考音频,确认声卡/喇叭链路正常(隔离播放故障)
2短句冒烟:python3 tts_test.py --text "你好,我是自然语言交互机器人",听一遍是否清晰不快放
3正则测试句(日期+数字+英文)、--speed / --sid / --playback-volume 扫参数,逐条记录听感
4测量 RTF(合成耗时 ÷ 音频时长),不达标按 s9 坑表排查
# —— 合成主链(tts_test.py 核心,节选)——
import numpy as np, onnxruntime as ort
text_raw = "今天是2026年9月24日,气温26度"
text = run_fsts(text_raw, ["number-zh","date-zh","phone-zh"])  # 数字/日期/电话正读
tokens, durs = matcha_encoder(text)        # 音素 + 每音时长
mel  = matcha.run(tokens, durs, steps=3)   # 梅尔谱 [T, n_mels]
wav  = vocos.run(mel)                      # float 波形 [-1,1] @16kHz
wav  = np.tanh(wav)                        # 软削幅,防 >1.0 削顶爆音

# —— 音量:百分比 → dB(pct_to_db)——
def pct_to_db(pct):
    if pct <= 0: return -96.0              # 数字音频静音下限
    return 20 * np.log10(pct / 100)

# —— 播放:单声道必须复制成双声道(I2S 立体声出)——
pcm = (wav * 32767).astype(np.int16)
stereo = np.column_stack([pcm, pcm]).ravel()   # 关键三行之一:防 2 倍速
sd.play(stereo, samplerate=16000, device="plughw:4,0")

# —— 命令行 ——
python3 tts_test.py --text "你好,小飞已就位"
[TTS] syn 0.62s / audio 1.8s → RTF=0.34 ✓ → play plughw:4,0
python3 tts_test.py --text "3pm meeting, 2026-09-24" --speed 1.2 --sid 1 --playback-volume 70
⚠️ 先听感,后指标RTF 达标但声音"电音、吞字"同样是失败——听感问题优先查词典 lexicon 缺词与 rule_fsts 覆盖,其次才是模型步数(steps 增大:质量升、速度降)。

09重点与易错点

🎯 本实验三重点① 五步管线与两级模型的职责切分;② "单声道直接播 = 2 倍速"的声道坑;③ 用 RTF + 听感双指标验收,而不是"响了就算通"。
⚠️ 常见坑现象 → 解法
2 倍速快放声音像按了快进 → 播放缓冲按立体声排布,单声道被两帧并一帧:np.column_stack([pcm,pcm]).ravel()
完全无声合成日志正常但无声音 → 先 audio_test.py 验证喇叭,再查 device 是否 plughw:N,0、amixer 是否静音
音量假生效改了 --playback-volume 没变化 → 确认操作的是哪个混音控件:Master / Output / PCM / Speaker 通道可能不同
爆音刺耳大音量时"咔咔"削顶 → 检查 float>1.0 是否先过 tanh;软件增益与 amixer 硬件增益只动一个
数字乱读"2026"被逐位念 → 确认 rule_fsts 文件路径与加载顺序(数量词/日期/电话的优先级)
变调变快调 --speed 后音调跟着变 → 语速靠时长缩放实现,极端值(>1.5 或 <0.6)会失真,走中间值
RTF 不达标>1.0 无法实时 → 降 steps、确认 onnxruntime 的 NNAPI/NPU EP 生效、缩短单句长度
  • audio_test.py 与 tts_test.py 两级自检均通过,能说清两者分别验证什么
  • 正则测试句中数字、日期按中文习惯读出,无逐位蹦读
  • --speed / --sid / --playback-volume 三参数的听感变化可被清晰描述并截图日志
  • 5 句语料 RTF 全部 ≤ 0.4,且声音无爆音、无快放
  • 能白板默写"声道不匹配导致 2 倍速"的三行修复代码

10总结与思考

✅ 完成成果
机器人会说话了

文本正则 → Matcha → Vocos → 播放,全链出声且参数可控。

🧠 原理收获
两级 TTS 骨架

梅尔谱作为中间表示的意义、RTF 语义、dB 对数刻度与增益链层级。

🛠 技能收获
音频工程排障

播放链二分自检、声道/采样率 mismatch 定位、听感与指标双轨验收。

➡ 去向
EXP-04 串成对话

会听也 会说了,下一步让两者在一个状态机里你来我往。

📝 思考题 1RTF=0.35 表示"1 秒音频花 0.35 秒合成"。若大模型每生成 15 个字就送来一个分句(约 4 秒音频),整轮回复 100 字 —— 合成算力总体够用吗?为什么还要分句流式?(提示:延迟公式 = 等句时间 + 合成时间;与 EXP-04 的 SENTENCE_RE 分句呼应。)
📝 思考题 2如果要把回复音色从"标准播音腔"改成"亲切的客服语气",你会动哪几层?(词典风格 / 发音人 sid / 语速节奏 / 后接的韵律控制,结合 EXP-06 情绪感知想想——语气能不能作为情绪标签的输出?)