# 语音合成与声音

*语音合成（Text-to-Speech Synthesis）逆向执行 ASR 流水线，从书面文本生成自然听感的音频。本文涵盖 TTS 流水线（文本规范化、G2P、声学模型、声码器）、Tacotron、WaveNet、HiFi-GAN、声音克隆、声音转换以及语音活动检测（VAD）。*

- 在文件 01 中，我们构建了信号处理工具包：波形、语谱图、梅尔滤波器组和 MFCC。在文件 02 中，我们将语音转换为文本。现在我们反方向操作：给定文本，合成自然听感的语音。这就是**语音合成（TTS）**，一个同样通向声音转换、声音克隆和语音活动检测的问题。

- 将 TTS 想象成一场舞台表演。剧本就是文本输入。导演（声学模型）决定每句台词应该如何发音——音高、时长、重音。管弦乐队（声码器）随后演奏乐谱，产生听众实际听到的声波。现代神经 TTS 用媲美人类说话者的演绎，取代了基于规则系统那种僵硬、机械的发音。

![TTS 流水线：文本被规范化、转换为音素、由声学模型处理生成梅尔语谱图，然后通过声码器生成最终波形](../images/tts_pipeline.svg)

- **语音合成流水线** 标准 TTS 流水线包含四个阶段：(1) 文本规范化，(2) 音素转换，(3) 声学模型，(4) 声码器。一些现代系统将阶段 3 和 4 合并为一个端到端模型，但这种概念分解仍然有用。

- **文本规范化** 将原始文本转换为可发音的形式。缩写展开（"Dr."变为"Doctor"）、数字变为词语（"1984"变为"nineteen eighty-four"）、货币符号被口头发音（"$5"变为"five dollars"），以及处理 URL 或特殊字符。这一阶段通常基于规则和语言特定文法，不过也存在神经规范化模型。此处的错误会传播到所有下游阶段：如果"St."被读作"saint"而不是"street"，整个发音就错了。

- **字素到音素（G2P）转换** 将规范化文本映射为音素序列。英语尤其不规则（"though"、"through"、"tough"中的"ough"发音各不相同），因此词典查找（CMU 发音词典）处理常见词语，而神经序列到序列模型（第 06 章的编码器-解码器或第 07 章的 Transformer）处理词汇表外的词语。浅层正字法语言（西班牙语、芬兰语）需要更简单的 G2P。输出通常是 IPA（国际音标）序列或等效的内部音素集合。

- **声学模型** 接收音素序列并产生中间声学表示，几乎总是**梅尔语谱图**（文件 01）。梅尔语谱图捕获每个时间帧的频谱包络，编码了声码器重构波形所需的感知相关信息。声学模型必须决定时长（每个音素持续多久）、音高（基频 $F_0$）和能量（响度）。

- **声码器** 接收梅尔语谱图并产生原始音频波形。这是一个不适定的反演问题：由于相位信息已被丢弃，许多波形可以产生相同的语谱图。经典声码器（Griffin-Lim、WORLD）使用迭代或信号模型方法，但神经声码器现在在质量上占主导地位。

- **声码器：WaveNet**（van den Oord 等人，2016）是第一个生成几乎与人类录音无法区分的语音的神经声码器。它自回归地对波形建模，预测每个样本 $x_t$ 的条件概率依赖于所有先前样本：

$$P(x) = \prod_{t=1}^{T} P(x_t \mid x_1, \ldots, x_{t-1}, c)$$

- 其中 $c$ 是条件信号（梅尔语谱图）。每个样本是 16 位，因此对 65536 个值进行朴素 softmax 是不切实际的。WaveNet 使用 **μ-law 压扩** 减少到 256 个量化级别，或者后来的变体使用 logistics 混合分布。

- WaveNet 的核心构建模块是**扩张因果卷积**。因果意味着滤波器权重只看过去样本（无未来泄露）。扩张意味着滤波器以指数增长的间隔跳过样本：扩张因子 $1, 2, 4, 8, \ldots, 512$。这提供了指数级大的感受野，同时保持参数量线性增长。

- 每层的门控激活函数为：

$$z = \tanh(W_{f} \ast x) \odot \sigma(W_{g} \ast x)$$

- 其中 $W_f$ 和 $W_g$ 是滤波器和门控卷积权重，$\ast$ 表示扩张因果卷积，$\odot$ 是逐元素乘法。这种门控机制（来自第 06 章的 LSTM）允许网络控制信息流。

- WaveNet 产生卓越的质量，但推理速度极慢：生成一秒 24 kHz 音频需要 24000 次顺序前向传播。这推动了所有后续声码器研究。

- **WaveRNN**（Kalchbrenner 等人，2018）用单层循环网络取代了 WaveNet 的深层卷积堆叠。它将每个 16 位样本拆分为粗（高 8 位）和细（低 8 位）分量，使用 GRU（第 06 章）预测每个分量。这种双 softmax 方法显著减少了计算量，同时保持了高质量。经过精心内核优化后，WaveRNN 在移动 CPU 上足以实现实时运行。

- **WaveGlow**（Prenger 等人，2019）是一种基于**流**的声码器，完全避免了自回归生成。它使用一系列可逆变换（仿射耦合层，第 06 章的正则化流）将简单高斯分布映射到波形分布。训练使用变量变换公式最大化精确对数似然：

$$\log P(x) = \log P(z) + \sum_{i} \log \left| \det \frac{\partial f_i}{\partial f_{i-1}} \right|$$

- 其中 $z = f(x)$ 是通过将 $x$ 传递经流得到的潜在变量。推理时，抽取样本 $z \sim \mathcal{N}(0, I)$ 并通过逆流以单次并行前向传播推出。WaveGlow 用模型大小（耦合层的大网络）换取生成速度。

- **HiFi-GAN**（Kong 等人，2020）使用**生成对抗网络**从梅尔语谱图合成波形。生成器通过一系列转置卷积对梅尔语谱图进行上采样，每个卷积后跟一个**多感受野融合（MRF）**模块。MRF 模块并行应用多个具有不同核大小和扩张率的残差块，然后将它们的输出求和。这使得生成器能够同时捕获多个时间尺度的模式。

![HiFi-GAN 生成器架构：梅尔语谱图输入经过转置卷积上采样层，每层后跟多感受野融合块，这些融合块组合了具有不同扩张模式的并行残差堆叠](../images/hifi_gan_generator.svg)

- HiFi-GAN 使用两种鉴别器类型。**多周期鉴别器（MPD）**通过以不同周期（2、3、5、7、11）折叠一维波形，将其重塑为二维，然后应用二维卷积。这捕获了不同基频下的周期结构。**多尺度鉴别器（MSD）**在原始波形、2 倍降采样和 4 倍降采样版本上操作，捕获不同时间分辨率下的模式。

- 训练目标结合了对抗损失、**梅尔语谱图重构损失**（合成音频与真实音频的梅尔语谱图之间的 L1 距离）和**特征匹配损失**（中间鉴别器特征之间的 L1 距离）：

$$\mathcal{L}_G = \mathcal{L}_{\text{adv}}(G) + \lambda_{\text{mel}} \mathcal{L}_{\text{mel}}(G) + \lambda_{\text{fm}} \mathcal{L}_{\text{fm}}(G)$$

- HiFi-GAN 实现了与 WaveNet 相当的合成质量，同时速度提升超过 1000 倍，可在单个 GPU 上实现实时生成。

- **神经源-滤波器（NSF）模型**将传统信号处理与神经网络相结合。在经典源-滤波器模型中，浊音由声源激励（基频 $F_0$ 处的周期脉冲序列）通过声道滤波器（频谱包络）产生。NSF 模型用神经网络替代手工设计的滤波器，同时保留显式源信号。输入的 $F_0$ 轮廓提供了纯数据驱动声码器有时难以处理的精细音高控制。

- **声学模型：Tacotron**（Wang 等人，2017）是第一个直接将字符序列转换为梅尔语谱图的端到端神经 TTS 系统。它使用带注意力机制的编码器-解码器架构（第 07 章）。编码器使用卷积库、高速网络和双向 GRU 处理字符/音素序列。解码器是一个自回归 GRU，逐个预测梅尔帧，使用前一帧和注意力上下文作为输入。

- **Tacotron 2**（Shen 等人，2018）显著改进了架构。编码器是一个 3 层一维卷积堆叠后跟双向 LSTM（第 06 章）。解码器是一个 2 层 LSTM，带**位置敏感注意力**，该注意力机制不仅基于编码器输出和解码器状态，还基于先前步骤累积的注意力权重来条件化。这防止了注意力跳过或重复词语的常见失败模式。

![Tacotron 2 架构：字符/音素编码器包含卷积层和 BiLSTM，位置敏感注意力对齐到梅尔语谱图帧，自回归解码器包含停止标记预测](../images/tacotron2_architecture.svg)

- 解码器步骤 $i$ 下编码器位置 $j$ 的位置敏感注意力能量为：

$$e_{i,j} = w^T \tanh(W_s s_{i-1} + W_h h_j + W_f f_{i,j} + b)$$

- 其中 $s_{i-1}$ 是前一个解码器状态，$h_j$ 是位置 $j$ 处的编码器输出，$f_{i,j}$ 是通过将累积注意力权重 $\sum_{k<i} \alpha_{k,j}$ 与一维卷积滤波器卷积得到的位置特征。注意力权重为 $\alpha_{i,j} = \text{softmax}(e_{i,j})$。

- Tacotron 2 的解码器还在每个步骤预测一个**停止标记**概率，指示梅尔语谱图何时完成。输出的梅尔语谱图随后传递给声码器（最初是 WaveNet，后来被 HiFi-GAN 或类似模型取代）。

- Tacotron 2 的自回归特性意味着合成速度受限于梅尔帧的数量。对于典型的每秒 80 帧的梅尔语谱图，一个 5 秒的发音需要 400 个顺序解码步骤。

- **FastSpeech**（Ren 等人，2019）使用**非自回归**声学模型解决了速度问题。FastSpeech 不是顺序生成梅尔帧，而是并行生成所有帧。关键挑战在于确定每个音素应该产生多少梅尔帧，FastSpeech 通过**时长预测器**来处理。

- 时长预测器是一个小型卷积网络，预测每个音素的整数时长（梅尔帧数）。训练期间，真实时长使用其注意力对齐从预训练的自回归教师模型（Tacotron 2）中提取。推理期间，使用预测时长通过**长度调节器**将音素级隐藏序列扩展到帧级，该调节器简单地将每个音素的隐藏表示重复预测的帧数。

- **FastSpeech 2**（Ren 等人，2021）通过移除教师-学生蒸馏改进了 FastSpeech。它直接使用强制对齐（来自文件 02 的声学模型框架）提取真实时长，并在时长之外添加了显式的音高（$F_0$）和能量**方差适配器**。每个适配器是一个小型卷积预测器，其输出条件化解码器：

```math
\begin{aligned}
\hat{d}_i &= \text{DurationPredictor}(h_i) \\
\hat{p}_i &= \text{PitchPredictor}(h_i) \\
\hat{e}_i &= \text{EnergyPredictor}(h_i)
\end{aligned}
```

- 其中 $h_i$ 是音素 $i$ 的编码器隐藏状态。训练时使用真实值；推理时，预测值提供对韵律的显式控制。这种可控性是 FastSpeech 2 的主要优势：调整音高、速度或能量就像缩放预测器输出一样简单。

- FastSpeech 2 在推理时通常比 Tacotron 2 快 10-20 倍，并避免了常见的自回归失败模式，如词语跳过、重复和注意力崩塌。

- **VITS**（Kim 等人，2021）是一个**端到端** TTS 模型，直接从文本生成波形，消除了独立的声码器阶段。VITS 结合了条件变分自编码器（第 06 章）、正则化流和对抗训练。后验编码器将真实梅尔语谱图映射到潜在空间，先验编码器将音素（通过基于 Transformer 的文本编码器和时长预测器）映射到同一潜在空间，解码器（基于 HiFi-GAN）从潜在样本生成波形。

- VITS 的训练目标结合了：
    - **重构损失**：VAE 迫使潜在分布编码声学信息
    - **KL 散度**：对齐文本条件化的先验与音频条件化的后验
    - **对抗损失**：鉴别器确保波形质量
    - **时长损失**：训练随机时长预测器

- VITS 比两阶段系统（FastSpeech 2 + HiFi-GAN）产生更高质量，因为声学模型和声码器被联合优化，避免了预测梅尔语谱图与真实梅尔语谱图之间的不匹配，这种不匹配会降低两阶段系统的性能。

- **VALL-E**（Wang 等人，2023）从根本上将 TTS 重构为离散音频令牌上的**语言建模问题**。它使用神经音频编解码器（EnCodec）将语音表示为来自多个码本级的一系列离散码。给定文本提示和一个 3 秒的注册话语（也编码为离散令牌），VALL-E 使用 Transformer 语言模型自回归地预测音频令牌。

- VALL-E 使用两个模型：一个**自回归（AR）模型**逐个令牌地生成第一个码本级，以及一个**非自回归（NAR）模型**并行预测剩余的码本级，以第一个级别和彼此为条件。这种编解码器语言模型方法实现了卓越的零样本声音克隆：3 秒样本足以重现说话人的声音、音色，甚至情感基调。

- **StyleTTS**（Li 等人，2022）和 **StyleTTS 2** 将语音解耦为内容和风格组件。风格编码器从参考音频中提取风格向量，捕获说话人身份、韵律和录音条件。推理时，风格可以从学习的先验分布中采样，或从参考话语中迁移。StyleTTS 2 使用扩散模型（第 08 章）作为风格先验，生成多样化且自然的韵律。

- **Kokoro**（2024）是一个轻量级、高质量的开放源码 TTS 模型，以其小巧的规模（约 82M 参数）和令人印象深刻的自热度而著称。它采用受 StyleTTS 2 启发的架构，包含基于扩散的风格先验和微调的 ISTFTNet 声码器，该声码器直接预测 STFT 系数（来自文件 01）而不是原始波形样本。尽管模型大小仅为 VALL-E 等模型的一小部分，Kokoro 在英语、日语、法语、韩语和中文上实现了接近人类的自然度，证明了精心策划的训练数据和高效架构设计可以与暴力规模相抗衡。Kokoro 的小体积使其非常适合本地和边缘部署。

- **Orpheus**（Canopy Labs，2025）是一个开放源码 TTS 模型系列（1B 和 3B 参数），构建在 VALL-E 开创的**编解码器语言模型**范式之上。Orpheus 更进一步，使用 LLM 骨干网络（微调的 Llama 3）直接生成 SNAC 音频编解码器令牌。其突出特点是类似人类的情感表达能力：它能够以卓越的自然度处理笑声、叹息、犹豫和情感韵律。Orpheus 可以通过在输入文本中使用 `[laugh]` 或 `[sigh]` 等标签进行提示，从而对副语言表达进行细粒度控制。

- **Dia**（Nari Labs，2025）是一个开放源码对话 TTS 模型，从单个文本转录生成逼真的多说话人对话。Dia 构建在 1.6B 参数的编码器-解码器 Transformer 之上，处理对话中的话轮转换、说话人特定声音和非语言线索（笑声、停顿）。它还支持从简短音频提示进行声音克隆，从而在对话上下文中实现零样本说话人生成。

- **Sesame CSM**（会话语音模型，2025）专注于自然的多人轮换会话语音。Sesame 不是为了优化朗读式 TTS，而是对真实对话的动态进行建模：反馈词（"嗯哼"）、打断、说话人之间的节奏变化和情感响应。该模型使用以对话上下文（文本和音频历史）为条件的 Transformer 骨干网络，生成的语音风格能适应对话的流程。

- **Fish Speech**（Fish Audio，2024）是一个开放源码 TTS 系统，使用双自回归架构：一个大语言模型从文本生成语义令牌，一个较小模型将这些转换为 VQGAN 声学令牌，再由声码器解码为波形。Fish Speech 支持从 10-15 秒参考音频进行零样本声音克隆，并实现适合实时应用的低延迟。其模块化设计允许独立替换组件（例如，不同的声码器）。

- **ChatTTS**（2024）是一个开放源码会话 TTS 模型，专为聊天机器人和虚拟助手等对话应用设计。它通过在文本输入中嵌入特殊令牌，生成自然、会话风格的语音，并对韵律特征（笑声、停顿、填充词）进行细粒度控制。ChatTTS 支持中英混合合成和多说话人生成。

- **Bark**（Suno，2023）是一个基于 Transformer 的开放源码模型，从文本提示生成语音、音乐和音效。它使用三个阶段的 Transformer 模型流水线（文本 → 语义令牌 → 粗声学令牌 → 细声学令牌），并支持声音克隆、多语言合成以及音乐和环境音等非语音音频。Bark 的通用性以可控性为代价——它不如专用 TTS 系统精确，但更灵活。

- **Parler-TTS**（Hugging Face，2024）采用**自然语言描述**方式进行声音控制：用户无需提供参考音频片段来控制风格，而是提供文本描述，例如"一位女性说话者，声音温暖、富有表现力，在安静的房间中。"Parler-TTS 在带注释的语音数据上训练，其中每个话语都配有一个描述说话风格的自然语言描述，从而无需任何参考音频即可实现直观控制。

- **Neuphonic** 是一个基于 API 的 TTS 平台，针对超低延迟语音合成进行了优化，面向实时语音代理和会话 AI 应用。它通过流式架构实现低于 100 毫秒的首音时间，在完整输入文本可用之前就开始生成音频。Neuphonic 专注于部署和延迟优化层面，而不是新颖的模型架构，围绕现代神经 TTS 提供生产级基础设施。

- **KittenTTS** 是一个紧凑、快速的 TTS 模型，专为效率低资源部署设计。它优先考虑最小延迟和小模型大小，适用于边缘和嵌入式应用，以牺牲一定自然度换取在 CPU 和移动设备上的实时性能。

- 现代 TTS 格局正在分化为两种范式：(1) **编解码器语言模型**（VALL-E、Orpheus、Fish Speech），将语音生成视为离散音频码上的下一个令牌预测，利用 LLM 的扩展规律；以及 (2) **流/扩散模型**（VITS、StyleTTS 2、Kokoro），通过迭代细化生成连续梅尔语谱图或波形。编解码器语言模型在零样本克隆和表现力方面表现出色；流/扩散模型通常更小、更快。两者都在快速向人类级别的自然度收敛。

- **韵律建模**控制语音的"音乐"：音高、时长、能量、节奏和语调。没有良好的韵律，即使单个音素清晰，合成语音听起来也平淡且机械。可以把韵律想象成单调的 GPS 语音与富有表现力的有声读物旁白之间的区别。

- **音高**（基频 $F_0$）是语音感知的高低程度。它在问句末尾上升，在陈述句末尾下降，并在情感性语音中连续变化。$F_0$ 使用 CREPE（一种神经音高追踪器）或 YIN（基于自相关，来自文件 01）等算法从音频中提取。在 TTS 中，音高由声学模型预测（FastSpeech 2 的音高预测器）或隐式学习（Tacotron 2）。

- **时长**决定了语速和节奏。重读音节更长，功能词缩短，停顿标记短语边界。时长建模在非自回归模型（FastSpeech）中是显式的，在自回归模型（Tacotron 的注意力对齐决定时长）中是隐式的。

- **能量**（响度）承载着重音。"我没说他**偷**了" vs "我没说他**偷**了"具有完全不同的含义，完全通过能量模式传达。

- **风格嵌入**捕获更高级的韵律模式。**全局风格令牌（GST）**框架（Wang 等人，2018）学习一个风格令牌库（对一组学习到的嵌入进行软注意力），捕获"兴奋"、"悲伤"或"低语"等说话风格。风格嵌入从参考话语中提取并添加到编码器输出中，允许在推理时进行风格迁移。

- **声音转换（VC）**改变话语的说话人身份，同时保留语言内容。想象一下录下自己的声音，然后让输出听起来像某个特定的目标说话人。VC 需要将说话人身份与内容解耦。

![声音转换流水线：源语音被分解为内容表示和说话人嵌入，目标说话人嵌入替换源说话人嵌入，解码器以目标声音重构语音](../images/voice_conversion_pipeline.svg)

- **说话人嵌入**（在文件 04 中进一步详述）将说话人身份编码为固定维度的向量。这些可以来自预训练的说话人验证模型（x-vectors、ECAPA-TDNN）。在 VC 中，源语音被编码为与说话人无关的内容表示，然后使用目标说话人嵌入进行解码。

- **解耦表示**将语音分离为独立因素：内容（音素）、说话人身份、音高和节奏。方法包括：
    - **信息瓶颈**：压缩内容表示，使其紧密到丢失说话人信息（AutoVC）
    - **对抗训练**：在内容表示上训练说话人分类器，并使用梯度反转去除说话人信息
    - **向量量化**：VQ-VAE 迫使内容通过离散瓶颈，这自然剥离了说话人身份（因为码本条目表示音素类别，而非说话人特征）

- **声音克隆**以目标说话人的声音合成语音。**多说话人 TTS**在来自许多说话人的数据上训练，以说话人嵌入条件化模型。推理时，从注册音频中提取新说话人的嵌入，并用于条件化生成。

- **少样本声音克隆**使用少量数据（几分钟）适应新说话人。说话人编码器从注册音频中提取嵌入，TTS 模型以此嵌入为条件生成语音。这是 SV2TTS（Jia 等人，2018）中使用的方法：一个单独训练的说话人编码器、一个以说话人嵌入为条件的 Tacotron 2 合成器，以及一个 WaveRNN 声码器。

- **零样本声音克隆**完全不需要适应：一个简短的话语（3-30 秒）就足够了。VALL-E 通过将注册音频作为语言模型的提示来实现这一点。该模型学会以相同的声音继续生成，因为它是在大规模多说话人数据上训练的，其中话语内声音一致性是统计上的常态。

- **语音活动检测（VAD）**在每个时间帧回答一个简单的二值问题：是否有人在说话？尽管简单，VAD 是 ASR（文件 02）、说话人日志（文件 04）和降噪（文件 05）的关键预处理步骤。好的 VAD 通过跳过静音减少计算量，并通过防止噪声被作为语音处理来提高准确性。

- 经典 VAD 使用能量阈值法（语音比静音响亮）、过零率（语音具有特征性的过零模式）和频谱特征。这些在信噪比较低的嘈杂环境中会失效。

- **神经 VAD**模型将问题视为帧级二分类。小型 RNN 或 CNN 接收声学特征（来自文件 01 的对数梅尔能量）并预测语音/非语音概率。

- **WebRTC VAD**（Google）是一个经典轻量级 VAD，使用基于 GMM 的分类器对简单的频谱特征进行分类。它以四个激进级别（0-3）运行，速度极快，但在音乐、非语音发声和低 SNR 环境中表现不佳。由于其零依赖的简单性，它仍然被广泛用作基线。

- **Silero VAD**（Silero Team，2021）是生产环境中的事实标准神经 VAD。其架构是一个小型深度可分离一维卷积堆叠（第 08 章的 MobileNet 思路应用于音频），后跟一个用于时间上下文的单层 LSTM，最后是一个线性头产生每帧的语音概率。整个模型小于 2MB（约 1M 参数），以 30-100 ms 块处理音频。
    - **输入**：原始 16 kHz 音频（无需手动特征提取——卷积前端直接从波形中学习自己的特征）。
    - **窗口化有状态推理**：LSTM 隐藏状态在块之间传递，因此模型处理流式音频而无需重新处理完整历史。每次调用处理一个 30、60 或 100 ms 的块，并返回 $[0, 1]$ 范围内的语音概率。
    - **自适应阈值**：Silero VAD 使用独立的开始和结束阈值，而不是单个固定阈值，并设有最小语音/静音持续时间，防止在噪声边界上快速切换。语音段必须超过开始阈值并持续最小时长才被确认，静音必须低于结束阈值持续一段时间后段才关闭。
    - **性能**：Silero VAD 在 CPU 上以 1-2% 的实时因子运行（处理 1 秒音频约需 10-20 ms），使其适用于边缘设备、手机和实时流水线。它在嘈杂和音乐丰富的音频上显著优于 WebRTC VAD，同时保持足够小以便于设备端部署。
    - Silero VAD 通常用作 Whisper（文件 02）的前端，将长音频在转录前分割成话语级块，也用于说话人日志流水线（文件 04），在提取说话人嵌入之前识别语音区域。

- **声学活动检测（AAD）**将 VAD 泛化为检测任何声学活动，而不仅仅是语音。这在智能家居设备、安防系统和野生动物监测中很有用。AAD 模型检测诸如玻璃破碎、狗叫或警报等事件，通常使用文件 04 中描述的音频分类框架。

- **TTS 评估指标**衡量客观质量和主观自然度：
    - **平均意见得分（MOS）**：人类听者在 1-5 量表上对自然度进行评分。黄金标准，但昂贵且缓慢。
    - **梅尔倒谱失真（MCD）**：测量合成与参考梅尔倒谱之间的距离。越低越好，但并不总是与感知相关。
    - **PESQ / POLQA**：最初为电话语音设计的标准化感知评估指标。
    - **说话人相似度**：合成与参考音频的说话人嵌入之间的余弦相似度（与声音克隆相关）。
    - **可懂度**：将合成音频输入 ASR 系统（文件 02）并计算词错误率（WER）来衡量。

## 编程任务（使用 CoLab 或 notebook）

- **任务 1：基于梅尔语谱图的 Griffin-Lim 声码器。** 实现 Griffin-Lim 迭代相位重构算法，将梅尔语谱图转换回波形。这演示了声码器问题以及为何需要神经声码器。

```python
import jax
import jax.numpy as jnp
import matplotlib.pyplot as plt

# 生成合成波形（模拟元音的谐波之和）
sr = 16000
duration = 1.0
t = jnp.linspace(0, duration, int(sr * duration))
f0 = 220.0  # 基频
waveform = (
    0.6 * jnp.sin(2 * jnp.pi * f0 * t) +
    0.3 * jnp.sin(2 * jnp.pi * 2 * f0 * t) +
    0.1 * jnp.sin(2 * jnp.pi * 3 * f0 * t)
)

# 计算 STFT
n_fft = 1024
hop_length = 256
window = jnp.hanning(n_fft)

def stft(signal, n_fft, hop_length, window):
    """计算短时傅里叶变换。"""
    n_frames = 1 + (len(signal) - n_fft) // hop_length
    frames = jnp.stack([
        signal[i * hop_length : i * hop_length + n_fft] * window
        for i in range(n_frames)
    ])
    return jnp.fft.rfft(frames, n=n_fft)

def istft(stft_matrix, hop_length, window, length):
    """使用重叠相加法计算逆 STFT。"""
    n_fft = (stft_matrix.shape[1] - 1) * 2
    n_frames = stft_matrix.shape[0]
    frames = jnp.fft.irfft(stft_matrix, n=n_fft)
    frames = frames * window[None, :]
    output = jnp.zeros(length)
    for i in range(n_frames):
        start = i * hop_length
        end = start + n_fft
        if end <= length:
            output = output.at[start:end].add(frames[i])
    return output

# 正向 STFT
S = stft(waveform, n_fft, hop_length, window)
magnitude = jnp.abs(S)

# 梅尔滤波器组
n_mels = 80
mel_low = 0.0
mel_high = 2595 * jnp.log10(1 + (sr / 2) / 700)
mel_points = jnp.linspace(mel_low, mel_high, n_mels + 2)
hz_points = 700 * (10 ** (mel_points / 2595) - 1)
freq_bins = jnp.floor((n_fft + 1) * hz_points / sr).astype(int)

mel_filterbank = jnp.zeros((n_mels, n_fft // 2 + 1))
for m in range(n_mels):
    f_left = freq_bins[m]
    f_center = freq_bins[m + 1]
    f_right = freq_bins[m + 2]
    for k in range(f_left, f_center):
        mel_filterbank = mel_filterbank.at[m, k].set(
            (k - f_left) / max(f_center - f_left, 1)
        )
    for k in range(f_center, f_right):
        mel_filterbank = mel_filterbank.at[m, k].set(
            (f_right - k) / max(f_right - f_center, 1)
        )

# 转到梅尔并返回（伪逆）
mel_spec = magnitude @ mel_filterbank.T
magnitude_reconstructed = mel_spec @ jnp.linalg.pinv(mel_filterbank.T)
magnitude_reconstructed = jnp.maximum(magnitude_reconstructed, 1e-7)

# Griffin-Lim 算法
def griffin_lim(magnitude, n_iter, hop_length, window, signal_length):
    """迭代相位重构。"""
    n_fft = (magnitude.shape[1] - 1) * 2
    key = jax.random.PRNGKey(42)
    phase = jax.random.uniform(key, magnitude.shape, minval=-jnp.pi, maxval=jnp.pi)

    for _ in range(n_iter):
        complex_spec = magnitude * jnp.exp(1j * phase)
        signal = istft(complex_spec, hop_length, window, signal_length)
        reanalysis = stft(signal, n_fft, hop_length, window)
        phase = jnp.angle(reanalysis)

    complex_spec = magnitude * jnp.exp(1j * phase)
    return istft(complex_spec, hop_length, window, signal_length)

reconstructed = griffin_lim(magnitude_reconstructed, n_iter=60, hop_length=hop_length,
                            window=window, signal_length=len(waveform))

# 绘制对比图
fig, axes = plt.subplots(3, 1, figsize=(12, 8))

axes[0].plot(t[:1000], waveform[:1000], color='#3498db', linewidth=0.8)
axes[0].set_title('原始波形')
axes[0].set_ylabel('振幅')

axes[1].imshow(jnp.log1p(mel_spec.T), aspect='auto', origin='lower', cmap='magma')
axes[1].set_title('梅尔语谱图（中间表示）')
axes[1].set_ylabel('梅尔频带')

axes[2].plot(t[:1000], reconstructed[:1000], color='#e74c3c', linewidth=0.8)
axes[2].set_title('Griffin-Lim 重构波形（60 次迭代）')
axes[2].set_xlabel('时间 (秒)')
axes[2].set_ylabel('振幅')

plt.tight_layout()
plt.show()

# 测量重构误差
mse = jnp.mean((waveform[:len(reconstructed)] - reconstructed[:len(waveform)]) ** 2)
print(f"原始与重构之间的 MSE：{mse:.6f}")
print("注意：通过梅尔反演导致的相位信息丢失会引起伪影。")
```

- **任务 2：时长预测器（FastSpeech 风格）。** 训练一个小型卷积时长预测器，将音素嵌入映射到时长。这是实现非自回归 TTS 的核心组件。

```python
import jax
import jax.numpy as jnp
import jax.random as jr
import matplotlib.pyplot as plt

# 模拟带真实时长的音素序列
# 在真实 TTS 中，时长来自强制对齐或教师注意力
def generate_synthetic_data(key, n_samples=200, max_phonemes=30, embed_dim=64):
    """生成合成音素嵌入和时长。"""
    keys = jr.split(key, 4)
    lengths = jr.randint(keys[0], (n_samples,), 5, max_phonemes)

    all_embeddings = []
    all_durations = []
    all_masks = []

    for i in range(n_samples):
        L = int(lengths[i])
        emb = jr.normal(keys[1], (max_phonemes, embed_dim))
        # 时长：元音（偶数索引）较长，辅音较短
        base_dur = jnp.where(jnp.arange(max_phonemes) % 2 == 0, 8.0, 4.0)
        noise = jr.normal(jr.fold_in(keys[2], i), (max_phonemes,)) * 1.5
        dur = jnp.clip(base_dur + noise, 1.0, 20.0).astype(jnp.float32)
        mask = (jnp.arange(max_phonemes) < L).astype(jnp.float32)

        all_embeddings.append(emb)
        all_durations.append(dur * mask)
        all_masks.append(mask)

    return (jnp.stack(all_embeddings), jnp.stack(all_durations),
            jnp.stack(all_masks))

key = jr.PRNGKey(42)
embeddings, durations, masks = generate_synthetic_data(key)

# 时长预测器：2 层一维卷积 + 线性投影
def init_duration_predictor(key, embed_dim=64, hidden_dim=128, kernel_size=3):
    """初始化时长预测器权重。"""
    keys = jr.split(key, 4)
    scale1 = jnp.sqrt(2.0 / (embed_dim * kernel_size))
    scale2 = jnp.sqrt(2.0 / (hidden_dim * kernel_size))
    params = {
        'conv1_w': jr.normal(keys[0], (kernel_size, embed_dim, hidden_dim)) * scale1,
        'conv1_b': jnp.zeros(hidden_dim),
        'conv2_w': jr.normal(keys[1], (kernel_size, hidden_dim, hidden_dim)) * scale2,
        'conv2_b': jnp.zeros(hidden_dim),
        'linear_w': jr.normal(keys[2], (hidden_dim, 1)) * jnp.sqrt(2.0 / hidden_dim),
        'linear_b': jnp.zeros(1),
    }
    return params

def duration_predictor(params, x):
    """从音素嵌入预测对数时长。x: (batch, seq, embed)。"""
    # 卷积层 1 加 ReLU
    h = jax.lax.conv_general_dilated(
        x.transpose(0, 2, 1),  # (batch, embed, seq)
        params['conv1_w'].transpose(2, 1, 0),  # (out, in, kernel)
        window_strides=(1,), padding='SAME'
    ).transpose(0, 2, 1) + params['conv1_b']  # 回到 (batch, seq, hidden)
    h = jax.nn.relu(h)

    # 卷积层 2 加 ReLU
    h = jax.lax.conv_general_dilated(
        h.transpose(0, 2, 1),
        params['conv2_w'].transpose(2, 1, 0),
        window_strides=(1,), padding='SAME'
    ).transpose(0, 2, 1) + params['conv2_b']
    h = jax.nn.relu(h)

    # 线性投影到标量
    log_dur = (h @ params['linear_w'] + params['linear_b']).squeeze(-1)
    return log_dur

# 损失：对数时长的 MSE（FastSpeech 中的标准做法）
def loss_fn(params, embeddings, durations, masks):
    log_dur_pred = duration_predictor(params, embeddings)
    log_dur_true = jnp.log(jnp.clip(durations, 1.0, None))
    sq_err = (log_dur_pred - log_dur_true) ** 2 * masks
    return jnp.sum(sq_err) / jnp.sum(masks)

grad_fn = jax.jit(jax.value_and_grad(loss_fn))

# 训练循环
params = init_duration_predictor(jr.PRNGKey(0))
lr = 1e-3
losses = []

for epoch in range(300):
    loss_val, grads = grad_fn(params, embeddings, durations, masks)
    params = jax.tree.map(lambda p, g: p - lr * g, params, grads)
    losses.append(float(loss_val))

# 在一个样本上评估
log_dur_pred = duration_predictor(params, embeddings[:1])
dur_pred = jnp.exp(log_dur_pred[0])
dur_true = durations[0]
mask = masks[0]
valid_len = int(jnp.sum(mask))

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

axes[0].plot(losses, color='#3498db', linewidth=1.5)
axes[0].set_xlabel('轮次')
axes[0].set_ylabel('MSE 损失（对数时长）')
axes[0].set_title('时长预测器训练')
axes[0].set_yscale('log')

x_pos = jnp.arange(valid_len)
width = 0.35
axes[1].bar(x_pos - width/2, dur_true[:valid_len], width, color='#27ae60',
            label='真实值', alpha=0.8)
axes[1].bar(x_pos + width/2, dur_pred[:valid_len], width, color='#e74c3c',
            label='预测值', alpha=0.8)
axes[1].set_xlabel('音素索引')
axes[1].set_ylabel('时长（帧）')
axes[1].set_title('时长预测与真实值对比')
axes[1].legend()

plt.tight_layout()
plt.show()
```

- **任务 3：使用上采样卷积的简单神经声码器。** 构建一个最小化的 HiFi-GAN 风格生成器，使用转置卷积和残差块将梅尔语谱图上采样为波形。

```python
import jax
import jax.numpy as jnp
import jax.random as jr
import matplotlib.pyplot as plt

def init_residual_block(key, channels, kernel_size, dilation):
    """初始化扩张残差卷积块。"""
    k1, k2 = jr.split(key)
    scale = jnp.sqrt(2.0 / (channels * kernel_size))
    return {
        'conv1_w': jr.normal(k1, (kernel_size, channels, channels)) * scale,
        'conv1_b': jnp.zeros(channels),
        'conv2_w': jr.normal(k2, (kernel_size, channels, channels)) * scale,
        'conv2_b': jnp.zeros(channels),
        'dilation': dilation
    }

def residual_block(params, x):
    """x: (batch, time, channels)。带 LeakyReLU 的扩张卷积残差块。"""
    h = jax.nn.leaky_relu(x, negative_slope=0.1)
    # 简化：使用标准卷积（扩张在概念上处理）
    h = jax.lax.conv_general_dilated(
        h.transpose(0, 2, 1),
        params['conv1_w'].transpose(2, 1, 0),
        window_strides=(1,),
        padding='SAME',
        rhs_dilation=(params['dilation'],)
    ).transpose(0, 2, 1) + params['conv1_b']
    h = jax.nn.leaky_relu(h, negative_slope=0.1)
    h = jax.lax.conv_general_dilated(
        h.transpose(0, 2, 1),
        params['conv2_w'].transpose(2, 1, 0),
        window_strides=(1,),
        padding='SAME'
    ).transpose(0, 2, 1) + params['conv2_b']
    return x + h

def init_generator(key, n_mels=80, upsample_rates=(8, 8, 4),
                   channels=128):
    """初始化最小化的 HiFi-GAN 风格生成器。"""
    keys = jr.split(key, 10)
    params = {}

    # 输入投影：梅尔频带 -> 通道
    params['input_w'] = jr.normal(keys[0], (7, n_mels, channels)) * 0.02
    params['input_b'] = jnp.zeros(channels)

    # 上采样块（转置卷积）
    in_ch = channels
    for i, rate in enumerate(upsample_rates):
        k_size = rate * 2
        scale = jnp.sqrt(2.0 / (in_ch * k_size))
        out_ch = in_ch // 2
        params[f'up{i}_w'] = jr.normal(keys[i+1], (k_size, in_ch, out_ch)) * scale
        params[f'up{i}_b'] = jnp.zeros(out_ch)
        # 每个尺度下的残差块
        params[f'res{i}_0'] = init_residual_block(jr.fold_in(keys[i+4], 0),
                                                    out_ch, 3, 1)
        params[f'res{i}_1'] = init_residual_block(jr.fold_in(keys[i+4], 1),
                                                    out_ch, 3, 3)
        in_ch = out_ch

    # 输出投影到单声道波形
    params['output_w'] = jr.normal(keys[8], (7, in_ch, 1)) * 0.02
    params['output_b'] = jnp.zeros(1)
    params['upsample_rates'] = upsample_rates

    return params

def generator_forward(params, mel):
    """mel: (batch, time, n_mels) -> waveform: (batch, time * prod(rates), 1)。"""
    # 输入投影
    h = jax.lax.conv_general_dilated(
        mel.transpose(0, 2, 1),
        params['input_w'].transpose(2, 1, 0),
        window_strides=(1,), padding='SAME'
    ).transpose(0, 2, 1) + params['input_b']

    for i, rate in enumerate(params['upsample_rates']):
        h = jax.nn.leaky_relu(h, negative_slope=0.1)
        # 通过转置卷积上采样
        k_size = rate * 2
        h = jax.lax.conv_transpose(
            h.transpose(0, 2, 1),
            params[f'up{i}_w'].transpose(2, 1, 0),
            strides=(rate,),
            padding='SAME'
        ).transpose(0, 2, 1) + params[f'up{i}_b']
        # 残差块
        h = residual_block(params[f'res{i}_0'], h)
        h = residual_block(params[f'res{i}_1'], h)

    h = jax.nn.leaky_relu(h, negative_slope=0.1)
    out = jax.lax.conv_general_dilated(
        h.transpose(0, 2, 1),
        params['output_w'].transpose(2, 1, 0),
        window_strides=(1,), padding='SAME'
    ).transpose(0, 2, 1) + params['output_b']

    return jnp.tanh(out)

# 创建一个合成梅尔语谱图（模拟元音）
n_mels = 80
n_frames = 50
mel = jnp.zeros((1, n_frames, n_mels))
# 在低频梅尔频带中添加能量（模拟共振峰）
mel = mel.at[:, :, 5:15].set(1.0)
mel = mel.at[:, :, 20:25].set(0.6)

# 初始化并运行生成器
key = jr.PRNGKey(42)
params = init_generator(key, n_mels=n_mels, upsample_rates=(8, 8, 4),
                         channels=128)
waveform = generator_forward(params, mel)

print(f"输入梅尔形状：{mel.shape}")
print(f"输出波形形状：{waveform.shape}")
print(f"上采样因子：{8 * 8 * 4} = {8*8*4}x")

fig, axes = plt.subplots(2, 1, figsize=(12, 6))

axes[0].imshow(mel[0].T, aspect='auto', origin='lower', cmap='magma')
axes[0].set_title('输入梅尔语谱图')
axes[0].set_ylabel('梅尔频带')
axes[0].set_xlabel('帧')

waveform_np = waveform[0, :, 0]
axes[1].plot(waveform_np[:2000], color='#9b59b6', linewidth=0.5)
axes[1].set_title('生成器输出波形（未经训练 - 随机噪声）')
axes[1].set_ylabel('振幅')
axes[1].set_xlabel('样本')

plt.tight_layout()
plt.show()
print("注意：输出是噪声，因为生成器未经训练。")
print("在实践中，对抗损失 + 梅尔损失训练会将其塑造成语音。")
```

- **任务 4：使用简单 RNN 的语音活动检测。** 在合成音频特征上训练一个基于小型 GRU 的 VAD 模型，对帧进行语音或静音分类。

```python
import jax
import jax.numpy as jnp
import jax.random as jr
import matplotlib.pyplot as plt

# 生成合成对数梅尔能量特征及语音/静音标签
def generate_vad_data(key, n_sequences=100, n_frames=200, n_features=40):
    """模拟对数梅尔特征：语音区域能量更高且具有结构。"""
    keys = jr.split(key, 5)
    all_features = []
    all_labels = []

    for i in range(n_sequences):
        k = jr.fold_in(keys[0], i)
        k1, k2, k3 = jr.split(k, 3)

        # 随机语音/静音模式
        label = jnp.zeros(n_frames)
        n_segments = jr.randint(k1, (), 2, 6)
        for seg in range(int(n_segments)):
            start = jr.randint(jr.fold_in(k2, seg), (), 0, n_frames - 20)
            length = jr.randint(jr.fold_in(k3, seg), (), 10, 50)
            end = jnp.minimum(start + length, n_frames)
            label = label.at[int(start):int(end)].set(1.0)

        # 特征：语音帧具有更高能量 + 频谱结构
        noise = jr.normal(jr.fold_in(keys[1], i), (n_frames, n_features)) * 0.3
        speech_pattern = jnp.outer(label, jnp.exp(-jnp.arange(n_features) / 15.0))
        features = speech_pattern * 2.0 + noise + 0.1

        all_features.append(features)
        all_labels.append(label)

    return jnp.stack(all_features), jnp.stack(all_labels)

key = jr.PRNGKey(123)
features, labels = generate_vad_data(key)
train_features, train_labels = features[:80], labels[:80]
test_features, test_labels = features[80:], labels[80:]

# 基于 GRU 的简单 VAD 模型
def init_vad_model(key, input_dim=40, hidden_dim=64):
    keys = jr.split(key, 6)
    scale_ih = jnp.sqrt(2.0 / input_dim)
    scale_hh = jnp.sqrt(2.0 / hidden_dim)
    return {
        'W_z': jr.normal(keys[0], (input_dim, hidden_dim)) * scale_ih,
        'U_z': jr.normal(keys[1], (hidden_dim, hidden_dim)) * scale_hh,
        'b_z': jnp.zeros(hidden_dim),
        'W_r': jr.normal(keys[2], (input_dim, hidden_dim)) * scale_ih,
        'U_r': jr.normal(keys[3], (hidden_dim, hidden_dim)) * scale_hh,
        'b_r': jnp.zeros(hidden_dim),
        'W_h': jr.normal(keys[4], (input_dim, hidden_dim)) * scale_ih,
        'U_h': jr.normal(keys[5], (hidden_dim, hidden_dim)) * scale_hh,
        'b_h': jnp.zeros(hidden_dim),
        'W_out': jr.normal(jr.fold_in(keys[0], 99), (hidden_dim, 1)) * 0.1,
        'b_out': jnp.zeros(1),
    }

def gru_step(params, h, x):
    """单步 GRU。"""
    z = jax.nn.sigmoid(x @ params['W_z'] + h @ params['U_z'] + params['b_z'])
    r = jax.nn.sigmoid(x @ params['W_r'] + h @ params['U_r'] + params['b_r'])
    h_tilde = jnp.tanh(x @ params['W_h'] + (r * h) @ params['U_h'] + params['b_h'])
    h_new = (1 - z) * h + z * h_tilde
    return h_new

def vad_forward(params, x):
    """x: (batch, time, features) -> logits: (batch, time)。"""
    batch_size, n_frames, _ = x.shape
    hidden_dim = params['W_z'].shape[1]
    h = jnp.zeros((batch_size, hidden_dim))

    outputs = []
    for t in range(n_frames):
        h = gru_step(params, h, x[:, t, :])
        logit = (h @ params['W_out'] + params['b_out']).squeeze(-1)
        outputs.append(logit)

    return jnp.stack(outputs, axis=1)

def bce_loss(params, features, labels):
    """VAD 的二元交叉熵损失。"""
    logits = vad_forward(params, features)
    probs = jax.nn.sigmoid(logits)
    probs = jnp.clip(probs, 1e-7, 1 - 1e-7)
    loss = -(labels * jnp.log(probs) + (1 - labels) * jnp.log(1 - probs))
    return jnp.mean(loss)

grad_fn = jax.jit(jax.value_and_grad(bce_loss))

# 训练
params = init_vad_model(jr.PRNGKey(0))
lr = 5e-3
losses = []

for epoch in range(200):
    loss_val, grads = grad_fn(params, train_features, train_labels)
    params = jax.tree.map(lambda p, g: p - lr * g, params, grads)
    losses.append(float(loss_val))
    if epoch % 50 == 0:
        print(f"轮次 {epoch}：损失 = {loss_val:.4f}")

# 在测试集上评估
test_logits = vad_forward(params, test_features)
test_preds = (jax.nn.sigmoid(test_logits) > 0.5).astype(jnp.float32)
accuracy = jnp.mean(test_preds == test_labels)
print(f"\n测试准确率：{accuracy:.4f}")

# 可视化一个测试示例
idx = 0
fig, axes = plt.subplots(3, 1, figsize=(14, 7))

axes[0].imshow(test_features[idx].T, aspect='auto', origin='lower', cmap='magma')
axes[0].set_title('对数梅尔能量特征')
axes[0].set_ylabel('梅尔频带')

axes[1].fill_between(range(200), test_labels[idx], alpha=0.4, color='#27ae60',
                     label='真实值')
axes[1].plot(jax.nn.sigmoid(test_logits[idx]), color='#e74c3c',
             linewidth=1.5, label='预测概率')
axes[1].axhline(0.5, color='gray', linestyle='--', linewidth=0.8)
axes[1].set_ylabel('语音概率')
axes[1].legend()
axes[1].set_title('VAD 预测')

axes[2].fill_between(range(200), test_labels[idx], alpha=0.4, color='#27ae60',
                     label='真实值')
axes[2].fill_between(range(200), test_preds[idx], alpha=0.4, color='#f39c12',
                     label='预测（阈值=0.5）')
axes[2].set_ylabel('语音 / 静音')
axes[2].set_xlabel('帧')
axes[2].legend()
axes[2].set_title('VAD 二值决策')

plt.tight_layout()
plt.show()
```
