TTS 语音合成:让机器人开口说话
机器人听懂了,还得用自然的人声回答。本实验拆解 Matcha-TTS 声学模型 + Vocos 声码器两级工厂:文本正则化与 rule_fsts 规则(日期/数字/电话读法)、梅尔谱预测、波形重建,再到混音、音量 dB 换算与 RTF 实时率调优;过程中会踩到"单声道却两倍速播放"这类经典硬件坑。
01问题引入:只会"听"不会"说"的机器人
前两个实验让机器人能听、能转成文字。但如果它回答时屏幕上打印一行字——用户就得盯着一台会说话的机器看字幕,交互感瞬间归零。语音输出的三道坎:
02原理通俗讲解:把文字变成声音的两级工厂
生活比喻:做饭。文字是菜单,声音是菜品。一级工厂把菜单翻译成菜谱(先放什么后放什么、几分钟火候),二级工厂照着菜谱把菜炒出来。TTS 管线正是这样两级结构:
| 概念 | 生活类比 | 本实验对应 |
|---|---|---|
| 文本正则化 | 把"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 为静音下限 |
03关联知识点
model-steps-3.onnx(声学模型,扩散 3 步加速)+ vocos-16khz-univ.onnx(万用声码器,16kHz 单域)。
管线内 16kHz 单声道 float;阵列 I2S 播放常要求立体声——np.column_stack([mono,mono]).ravel() 复制声道,少复制会 2 倍速快放(s9 详解)。
软件增益 20·log10(pct/100)(-96dB 截底);硬件经 amixer 分控 Output 1 / Output 2 / PCM / Speaker 多级,播放前对照确认目标通道。
RTF=合成耗时÷音频时长,目标 0.25~0.40;配合 EXP-04 的按标点分句,句子级流式合成掩盖延迟。
--speed 语速(时间轴伸缩)、--sid 发音人 ID、--playback-volume 播放音量、--gain 合成内增益。
先用它单独验证"播放链"是否正常,把故障域从 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
06动画讲解:文本 → 音素 → 梅尔谱 → 波形
看一句"今天是2026年9月24日"的旅程:数字先被 rule_fsts 规范成中文读法 → 音素序列进入 Matcha 逐帧"画"出梅尔谱 → Vocos 把热力图逐列翻译为振荡波形 → 打包成 16kHz 音频流出喇叭。
07互动讲解:语速 · 音量 · 发音人调节台
选一组参数,观察合成命令、预期听感与度量指标(时间轴/响度为讲义实测的典型行为示意):
pct_to_db(50) = 20·log₁₀(0.5) ≈ -6dB;pct_to_db(10) ≈ -20dB。把播放音量从 100% 调到 50%,响度不是"减半"而是降 6dB——分贝是对数刻度,直觉会骗人。08实操步骤:合成第一句机器人语音
audio_test.py 播放参考音频,确认声卡/喇叭链路正常(隔离播放故障)python3 tts_test.py --text "你好,我是自然语言交互机器人",听一遍是否清晰不快放--speed / --sid / --playback-volume 扫参数,逐条记录听感# —— 合成主链(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
09重点与易错点
| ⚠️ 常见坑 | 现象 → 解法 |
|---|---|
| 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 → 播放,全链出声且参数可控。
梅尔谱作为中间表示的意义、RTF 语义、dB 对数刻度与增益链层级。
播放链二分自检、声道/采样率 mismatch 定位、听感与指标双轨验收。
会听也 会说了,下一步让两者在一个状态机里你来我往。