从 HiFi-GAN 到 NSF-HiFi-GAN:声码器学习笔记
本文基于 RVC(Retrieval-based Voice Conversion)项目的实际代码,从零开始梳理 HiFi-GAN 声码器的原理,再过渡到 RVC 中真正使用的 NSF-HiFi-GAN 变体。 代码位置:
infer/lib/infer_pack/models.py和infer/lib/infer_pack/modules.py
一、先搞清楚声码器在干什么
在语音合成或语音转换的流程里,声码器处在最后一环。它的上游会输出某种"中间表示"——可能是 mel 频谱图,也可能是某个隐空间的向量。声码器要做的事情就一件:把这个中间表示变回可以听的音频波形。
说得直白点:频谱图是一张"图",声码器要把这张图"念"出来。
传统做法(Griffin-Lim 之类的)靠数学迭代来恢复相位信息,结果通常比较糊。HiFi-GAN 走的是神经网络的路线——用一个生成器直接输出波形采样点,同时用判别器来监督生成质量。
二、HiFi-GAN 的基本结构
HiFi-GAN 的论文是 2020 年发的(Jungil Kong 等人),核心思路可以用一句话概括:
转置卷积做上采样,残差块做波形精炼,多尺度判别器做质量监督。
2.1 生成器的整体流程
在 RVC 的代码里,标准 HiFi-GAN 生成器对应的是 Generator 类(models.py:204)。它的结构其实很规整:
HiFi-GAN 生成器结构(40kHz 配置为例)
┌─────────────────────────────────────────────────────────────┐
│ 输入:隐变量 z │
│ [batch, 192, T帧] │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌────────────────────┐
│ 预处理卷积层 │
│ Conv1d(192→512) │ ← 把输入投射到高维空间
│ kernel_size=7 │
└────────┬───────────┘
│
┌────────────┴────────────┐
│ 上采样 Stage 1 (×10) │
│ ConvTranspose1d(512→256)│ ← 时间轴拉长到 T×10
└────────┬─────────────────┘
│
┌────────┴──────────────────────────────────┐
│ MRF (多感受野融合) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐│
│ │ResBlock │ │ResBlock │ │ResBlock ││
│ │kernel=3 │ │kernel=7 │ │kernel=11 ││
│ └──────────┘ └──────────┘ └──────────┘│
│ 输出取平均 │
└────────┬──────────────────────────────────┘
│
┌────────┴────────────┐
│ 上采样 Stage 2 (×10)│
│ ConvTranspose1d(256→128)│ ← 时间轴拉长到 T×100
└────────┬─────────────┘
│
┌────────┴──────────────────────────────────┐
│ MRF 融合 │
│ ResBlock×3 (kernel=3/7/11) → 平均 │
└────────┬──────────────────────────────────┘
│
┌────────┴────────────┐
│ 上采样 Stage 3 (×2) │
│ ConvTranspose1d(128→64)│ ← 时间轴拉长到 T×200
└────────┬─────────────┘
│
┌────────┴──────────────────────────────────┐
│ MRF 融合 │
│ ResBlock×3 (kernel=3/7/11) → 平均 │
└────────┬──────────────────────────────────┘
│
┌────────┴────────────┐
│ 上采样 Stage 4 (×2) │
│ ConvTranspose1d(64→32) │ ← 时间轴拉长到 T×400
└────────┬─────────────┘
│
┌────────┴──────────────────────────────────┐
│ MRF 融合 │
│ ResBlock×3 (kernel=3/7/11) → 平均 │
└────────┬──────────────────────────────────┘
│
┌────┴────────────┐
│ 后处理卷积层 │
│ Conv1d(32→1) │ ← 压缩到单声道
│ kernel_size=7 │
└────┬─────────────┘
│
▼
┌────────────┐
│ Tanh │ ← 限幅到 [-1, 1]
└─────┬──────┘
│
▼
┌──────────────────┐
│ 输出波形 │
│ [batch, 1, T×400]│
└──────────────────┘
上采样的总倍率 = 10 × 10 × 2 × 2 = 400。这个数字等于 hop_length(帧移),含义是:输入的每一帧对应输出的 400 个采样点。对于 40kHz 采样率来说,一帧就是 10ms。
有两件事值得注意:
- 通道数逐级减半:512 → 256 → 128 → 64 → 32。分辨率在增加,通道数在减少,这跟图像领域的解码器思路一致。
- 每个上采样阶段之后都跟着一组 ResBlock,而不是只放一个。这组 ResBlock 就是 HiFi-GAN 最核心的设计之一——MRF。
2.2 MRF:多感受野融合
MRF 的全称是 Multi-Receptive Field Fusion。思路也很朴素:用不同大小的卷积核去观察不同范围的上下文,然后把结果加起来取平均。
在 RVC 的配置(configs/v1/40k.json)里,resblock_kernel_sizes 是 [3, 7, 11],所以每个上采样阶段后面并排放了 3 个 ResBlock,分别用 kernel_size=3、7、11。
MRF 多感受野融合示意图
上采样后的特征 x
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ResBlock1 │ │ResBlock2 │ │ResBlock3 │
│kernel=3 │ │kernel=7 │ │kernel=11 │
│(看近处) │ │(看中距) │ │(看远处) │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└───────────────┴───────────────┘
│
三路相加求平均
│
▼
融合后的输出
小 kernel(3)看局部细节,大 kernel(11)看更长的上下文。融合后既保留了细节纹理,也保持了长距离的连贯性。
2.3 ResBlock:膨胀卷积的堆叠
RVC 默认用的是 ResBlock1(modules.py:252),它的内部结构是:
ResBlock1 内部结构(三轮串行处理)
输入 x
│
├──────────────────────────┐
│ │
│ ┌────────────────────┐ │
│ │ LeakyReLU │ │
│ │ DilatedConv(d=1) │ │ 第一轮:膨胀率 1
│ │ LeakyReLU │ │ 感受野:3
│ │ Conv(d=1) │ │
│ └──────────┬─────────┘ │
│ │ │
└─────────────┼────────────┘
│ (残差相加)
▼
┌──────────────────────────┐
│ │
│ ┌────────────────────┐ │
│ │ LeakyReLU │ │
│ │ DilatedConv(d=3) │ │ 第二轮:膨胀率 3
│ │ LeakyReLU │ │ 感受野:7
│ │ Conv(d=1) │ │
│ └──────────┬─────────┘ │
│ │ │
└─────────────┼────────────┘
│ (残差相加)
▼
┌──────────────────────────┐
│ │
│ ┌────────────────────┐ │
│ │ LeakyReLU │ │
│ │ DilatedConv(d=5) │ │ 第三轮:膨胀率 5
│ │ LeakyReLU │ │ 感受野:11
│ │ Conv(d=1) │ │
│ └──────────┬─────────┘ │
│ │ │
└─────────────┼────────────┘
│ (残差相加)
▼
输出
膨胀卷积原理
- dilation=1 时感受野是 3(标准卷积)
- dilation=3 时感受野是 7(跳着看,覆盖更远)
- dilation=5 时感受野是 11(跳得更远,看得更宽)
这样一个 ResBlock 内部就覆盖了多种尺度。再加上 MRF 层面的多 kernel 并行,HiFi-GAN 等于在"感受野"这件事上下了双重功夫。
每一层卷积都用了 weight_norm——这是 HiFi-GAN 的另一个特点,用权重归一化代替 BatchNorm,生成质量更稳定。
还有一种更轻量的 ResBlock2(modules.py:367),只有两轮、每轮只有一层膨胀卷积。配置文件里 "resblock": "1" 表示用 ResBlock1,设成 "2" 就用 ResBlock2。RVC 默认用的是 1。
2.4 判别器(训练用)
生成器只管"生成",判别器负责"挑毛病"。HiFi-GAN 用了两种判别器组合:
Multi-Period Discriminator(MPD):把一维波形按不同周期(2, 3, 5, 7, 11, 17)折叠成二维,然后用 2D 卷积判别。不同的周期能捕获不同频率成分的规律性。
Multi-Scale Discriminator(MSD):在原始波形和降采样后的波形上分别做判别,关注不同时间尺度的真实感。
在 RVC 代码里,这两者合并成了 MultiPeriodDiscriminator(models.py:1052),里面同时包含了一个 DiscriminatorS(MSD 的角色)和多个 DiscriminatorP(MPD 的角色)。
判别器在推理阶段不参与,但训练阶段是不可缺的。
2.5 小结:标准 HiFi-GAN 的特点
- 生成速度快(全卷积,没有自回归)
- 音质好(GAN 训练 + 多尺度判别)
- 结构简洁(上采样 + ResBlock + 判别器,没有过于复杂的组件)
但它有一个问题:对音高(F0)的控制是隐式的。生成器只接收中间表示,音高信息完全靠网络自己从数据中学习。对于语音转换这种需要精确控制音高的任务来说,这还不够。
三、NSF-HiFi-GAN:加入显式的音高控制
NSF 的全称是 Neural Source-Filter。这个名字来自语音学中经典的"源-滤波器"模型:
人类的发声可以分解为两个部分:声带振动产生的源信号(周期性的脉冲),和声道共振形成的滤波器(塑造音色)。
NSF-HiFi-GAN 做的事情就是把这个物理直觉引入神经网络:用 F0 生成一个显式的周期性源信号,注入到 HiFi-GAN 的生成过程中。这样网络就不用"猜"音高了,音高由输入直接决定。
RVC 中的 NSF-HiFi-GAN 对应 GeneratorNSF 类(models.py:448)。
3.1 生成源信号:SineGen 和 SourceModuleHnNSF
源信号的生成分两步。
第一步:SineGen(正弦波生成器,models.py:312)
SineGen 的输入是 F0 序列,输出是对应频率的正弦波。
SineGen 工作流程
┌──────────────────────────────────────────────────────┐
│ 输入:F0 序列 [batch, T] │
│ (每个时刻的基频,单位 Hz) │
└────────────────┬─────────────────────────────────────┘
│
▼
┌────────────────────────────┐
│ 1. F0 → 相位增量 │
│ phase_inc = F0 / sr │ (除以采样率)
└──────────┬─────────────────┘
│
▼
┌────────────────────────────┐
│ 2. 累积相位 (cumsum) │
│ 保证相位连续不跳变 │ ← 避免"咔哒"声
└──────────┬─────────────────┘
│
▼
┌────────────────────────────┐
│ 3. sin(2π × phase) │
│ 生成正弦波 │
└──────────┬─────────────────┘
│
▼
┌────────────────────────────────────┐
│ 4. 浊音/清音判断 │
│ ┌────────────┬────────────┐ │
│ │ F0 > 0 │ F0 = 0 │ │
│ │ (浊音) │ (清音) │ │
│ ├────────────┼────────────┤ │
│ │ 正弦波 │ 纯噪声 │ │
│ │ + 微噪声 │ │ │
│ └────────────┴────────────┘ │
└────────────────┬───────────────────┘
│
▼
┌─────────────────────────────────┐
│ 输出:sine_waves, uv, noise │
│ [batch, T×upp, harmonic_num+1] │
└─────────────────────────────────┘
核心逻辑:
- 把 F0(单位 Hz)除以采样率,得到每个采样点的相位增量
- 通过
cumsum(累积求和)得到连续的相位序列 - 取
sin(2π × phase)得到正弦波
为什么要用 cumsum 而不是直接算?因为 F0 在时间轴上是变化的。如果每帧独立计算,帧与帧之间的相位会不连续,产生"咔哒"声。cumsum 保证了相位的连续累积。
浊音vs清音的物理含义
- 浊音段(F0 > 0):输出正弦波 + 微量噪声(std=0.003)
- 对应声带周期性振动产生的元音、鼻音等
- 清音段(F0 = 0):输出纯噪声(幅度 = sine_amp / 3)
- 对应气流湍流产生的摩擦音、清音等
SineGen 还支持泛音(harmonics),不过 RVC 里 harmonic_num=0,所以只生成基频,没有泛音。
第二步:SourceModuleHnNSF(源模块,models.py:391)
这一层包装了 SineGen,做了两件事:
SourceModuleHnNSF 结构
┌──────────────────────────────────────┐
│ 输入:F0 [batch, T], upp=400 │
└──────────────┬───────────────────────┘
│
▼
┌──────────────────────┐
│ SineGen │
│ 生成多个谐波正弦波 │ ← 可能有多个谐波通道
└──────────┬───────────┘
│
▼
┌──────────────────────────────┐
│ Linear(harmonic_num+1, 1) │ ← 把多个谐波混合成单通道
│ (可学习的权重) │
└──────────┬───────────────────┘
│
▼
┌──────────────────────┐
│ Tanh │ ← 限幅到 [-1, 1]
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ 输出:har_source │
│ [batch, T×400, 1] │ ← 单通道激励信号
└──────────────────────┘
在 RVC 的默认配置下只有基频(harmonic_num=0),所以这个 Linear 层实际上就是一个标量缩放。但设计上保留了扩展性——如果以后想加泛音,只需要改一个参数。
3.2 谐波注入:GeneratorNSF 的关键改动
GeneratorNSF 和标准 Generator 的骨架几乎一样——相同的上采样层,相同的 ResBlock,相同的 conv_pre/conv_post。关键区别在于多了一套 noise_convs,用于在每个上采样阶段注入源信号。
GeneratorNSF 前向过程概览
步骤1:生成源信号
┌──────────────────────────────────┐
│ F0 → SourceModuleHnNSF │
│ 输出:har_source [B, 1, T×400] │ ← 保持最终输出的分辨率
└──────────────┬───────────────────┘
│
│ (全程保持这个分辨率)
│
步骤2:隐变量处理 │
┌───────────────┐ │
│ z [B,192,T] │ │
└──────┬────────┘ │
│ │
▼ │
conv_pre(192→512) │
│ │
│ │
步骤3:逐级上采样+谐波注入 │
│ │
▼ ▼
┌─────────────────────────────────┐
│ Stage 0: 上采样 ×10 │
│ ups[0]: 512→256 │
│ + noise_conv[0](stride=40) ← 从 har_source 下采样
│ = x + x_source │ 每40个点取1个匹配当前分辨率
│ → MRF(ResBlock×3) │
└──────────┬──────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Stage 1: 上采样 ×10 │
│ ups[1]: 256→128 │
│ + noise_conv[1](stride=4) ← 从 har_source 下采样
│ = x + x_source │ 每4个点取1个
│ → MRF(ResBlock×3) │
└──────────┬──────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Stage 2: 上采样 ×2 │
│ ups[2]: 128→64 │
│ + noise_conv[2](stride=2) ← 从 har_source 下采样
│ = x + x_source │ 每2个点取1个
│ → MRF(ResBlock×3) │
└──────────┬──────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Stage 3: 上采样 ×2 │
│ ups[3]: 64→32 │
│ + noise_conv[3](stride=1) ← 从 har_source 直接用
│ = x + x_source │ 不再下采样
│ → MRF(ResBlock×3) │
└──────────┬──────────────────────┘
│
▼
┌─────────────────────┐
│ conv_post(32→1) │
│ Tanh │
└──────────┬──────────┘
│
▼
┌──────────────────┐
│ 输出波形 │
│ [B, 1, T×400] │
└──────────────────┘
这里有个细节很关键:源信号(har_source)始终保持在最终输出的采样率上(比如 40000 个采样点/秒),而上采样过程中的 x 的时间分辨率是逐级增加的。所以每个阶段的 noise_convs 需要把全分辨率的源信号下采样到当前阶段的分辨率。
noise_conv 的 stride 计算逻辑
以 40kHz 配置(upsample_rates = [10, 10, 2, 2])为例:
| 上采样阶段 | 上采样倍率 | x 的时间分辨率 | noise_conv 的 stride | 含义 |
|---|---|---|---|---|
| 0 | ×10 | T×10 | 10×2×2 = 40 | 源信号每 40 个点取 1 个 |
| 1 | ×10 | T×100 | 2×2 = 4 | 源信号每 4 个点取 1 个 |
| 2 | ×2 | T×200 | 2 | 源信号每 2 个点取 1 个 |
| 3 | ×2 | T×400 | 1 (kernel=1) | 源信号直接用 |
注意:noise_conv 不只是简单的下采样,它同时把 1 通道扩展到了 c_cur 通道(256 / 128 / 64 / 32),这样才能跟 x 直接相加。这个卷积的参数是可学习的,网络可以自己决定怎么利用源信号的信息。
3.3 用一张图看全貌
把上面的内容组合起来,GeneratorNSF 在 40kHz 配置下的完整数据流是:
F0 [batch, T]
│
├── Upsample(×400) ──→ SourceModuleHnNSF ──→ har_source [batch, 1, T×400]
│ │
│ ┌───────────────────────────┤(全程保持最高分辨率)
│ │ │
z [batch, 192, T] │ │
│ │ │
▼ │ │
conv_pre(192→512) │ │
│ │ │
▼ ▼ │
Stage 0: ×10上采样 noise_conv(stride=40) │
[512→256] [1→256] │
x = x + x_source ◄──┘ │
→ MRF(3 ResBlocks, k=3/7/11) │
│ │
▼ ▼
Stage 1: ×10上采样 noise_conv(stride=4)
[256→128] [1→128]
x = x + x_source ◄────────────────────┘
→ MRF(3 ResBlocks, k=3/7/11)
│
▼
Stage 2: ×2上采样 noise_conv(stride=2)
[128→64] [1→64]
x = x + x_source ◄────────────────────┘
→ MRF(3 ResBlocks, k=3/7/11)
│
▼
Stage 3: ×2上采样 noise_conv(stride=1)
[64→32] [1→32]
x = x + x_source ◄────────────────────┘
→ MRF(3 ResBlocks, k=3/7/11)
│
▼
conv_post(32→1) → tanh → 输出波形 [batch, 1, T×400]
3.4 说话人条件(gin_channels)
RVC 还支持多说话人。GeneratorNSF 里有一个可选的说话人条件注入层。
说话人条件注入示意
┌────────────────────────────┐
│ 说话人嵌入 g │
│ [batch, gin_channels, 1] │ ← 从 emb_g 查表得到
└──────────┬─────────────────┘
│
▼
┌──────────────────┐
│ 1×1 卷积映射 │
│ gin_ch → 512 │ ← 映射到与 conv_pre 输出相同的维度
└──────────┬───────┘
│
│
隐变量 z ────┼────→ conv_pre(192→512)
│ │
└──────────────┤
▼
x + cond(g) ← 直接相加
│
▼
后续所有上采样和ResBlock处理
都在"带有说话人特征"的基础上进行
这意味着说话人信息在最开始就注入了,后续所有上采样和 ResBlock 处理都是在"带有说话人特征"的基础上进行的。
四、GeneratorNSF 在 RVC 系统中的位置
声码器不是孤立存在的。在 RVC 里,它被嵌入到一个完整的 VITS 风格的框架中。以 SynthesizerTrnMs256NSFsid(models.py:602)为例:
完整的推理流程:
phone(语音特征,256 维) pitch(音高序号) sid(说话人 ID)
│ │ │
▼ ▼ ▼
TextEncoder ──────────────────────────→ (μ, σ) emb_g → g
│ │
▼ │
采样 z_p ~ N(μ, σ) │
│ │
▼ │
ResidualCouplingBlock (flow, reverse=True) ◄────── g
│
▼
z(隐变量)
│
▼ F0(连续的基频值 nsff0)
GeneratorNSF(z, f0, g) ◄─────────────────┘
│
▼
输出波形
- TextEncoder:把 phone 特征(+ pitch 嵌入)编码成高斯分布的均值和方差
- ResidualCouplingBlock:一个 normalizing flow 模块,在推理时做逆变换,把先验分布的采样转换到隐空间
- GeneratorNSF:我们分析的声码器,接收 flow 输出的隐变量
z和连续的 F0 值nsff0,生成最终波形
训练时还有一个 PosteriorEncoder,它直接从 mel 频谱编码出后验分布,用于计算 KL 散度 loss,推理时不用。
RVC 提供了四种合成器变体:
| 类名 | phone 维度 | 是否使用 F0 | 解码器 |
|---|---|---|---|
SynthesizerTrnMs256NSFsid | 256 | 是 | GeneratorNSF |
SynthesizerTrnMs768NSFsid | 768 | 是 | GeneratorNSF |
SynthesizerTrnMs256NSFsid_nono | 256 | 否 | Generator(标准) |
SynthesizerTrnMs768NSFsid_nono | 768 | 否 | Generator(标准) |
256 和 768 的区别在于上游特征提取器的输出维度(256 对应 HuBERT base,768 对应 HuBERT large 等)。
带 “nono” 后缀的版本不使用 F0,也就不需要 NSF——它们直接用标准的 HiFi-GAN Generator。这种模式音高控制差一些,但不需要提取 F0,适合对音高没有严格要求的场景。
五、.pth 权重文件:声码器的"灵魂"从哪来
到目前为止我们一直在看网络结构——但结构只是骨架。一个 GeneratorNSF 刚创建出来的时候,所有卷积核的权重都是随机初始化的,它什么都不会。真正让声码器能生成逼真语音的,是训练好的权重参数,保存在 .pth 文件里。
理解 .pth 文件的结构和加载流程,对于理解声码器的实际运行至关重要。
5.1 .pth 文件里到底有什么
RVC 的语音模型文件(比如用户训练出来的 xxx_e200_s8000.pth)并不是只存了声码器的权重——它存的是整个合成器的完整快照。
.pth 文件结构
┌─────────────────────────────────────────────┐
│ .pth 文件 (PyTorch 检查点) │
├─────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────┐ │
│ │ "weight" (state_dict) │ │
│ │ ├─ enc_p.* ← TextEncoder │ │
│ │ ├─ dec.* ← GeneratorNSF │ │ ★ 声码器权重
│ │ ├─ flow.* ← Flow模块 │ │
│ │ └─ emb_g.weight ← 说话人嵌入 │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ "config" (列表, 18个元素) │ │
│ │ [0] spec_channels = 1025 │ │
│ │ [1] segment_size = 12800 │ │
│ │ [2] inter_channels = 192 │ │
│ │ ... │ │
│ │ [9] resblock = "1" │ │
│ │ [10] resblock_kernel_sizes = [3,7,11] │
│ │ [11] resblock_dilation_sizes │ │
│ │ [12] upsample_rates = [10,10,2,2]│ │ ★ 声码器结构参数
│ │ [13] upsample_initial_channel=512 │ │
│ │ [14] upsample_kernel_sizes │ │
│ │ [15] spk_embed_dim = 109 │ │
│ │ [16] gin_channels = 256 │ │
│ │ [17] sr = 40000 │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ "f0" = 1 (是否使用F0) │ │
│ │ "version" = "v1" (模型版本) │ │
│ │ "info" = "epoch_200_step_8000" │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────┘
weight 字典中的声码器权重(dec. 前缀)*
┌─────────────────────────────────────────────────────┐
│ dec.conv_pre.weight ← 预卷积层 │
│ dec.conv_pre.bias │
├─────────────────────────────────────────────────────┤
│ dec.ups.0.weight ← 第0级上采样 │
│ dec.ups.0.bias │
│ dec.ups.1.weight ← 第1级上采样 │
│ dec.ups.1.bias │
│ dec.ups.2.weight ← 第2级上采样 │
│ dec.ups.2.bias │
│ dec.ups.3.weight ← 第3级上采样 │
│ dec.ups.3.bias │
├─────────────────────────────────────────────────────┤
│ dec.noise_convs.0.weight ← 第0级谐波注入 │
│ dec.noise_convs.0.bias │
│ dec.noise_convs.1.weight ← 第1级谐波注入 │
│ dec.noise_convs.1.bias │
│ dec.noise_convs.2.weight ← 第2级谐波注入 │
│ dec.noise_convs.2.bias │
│ dec.noise_convs.3.weight ← 第3级谐波注入 │
│ dec.noise_convs.3.bias │
├─────────────────────────────────────────────────────┤
│ dec.resblocks.0.convs1.0.weight ← ResBlock膨胀卷积│
│ dec.resblocks.0.convs1.0.bias │
│ dec.resblocks.0.convs2.0.weight │
│ dec.resblocks.0.convs2.0.bias │
│ ...(每个上采样阶段后有3个ResBlock) │
├─────────────────────────────────────────────────────┤
│ dec.m_source.l_linear.weight ← 谐波混合层 │
│ dec.m_source.l_linear.bias │
├─────────────────────────────────────────────────────┤
│ dec.conv_post.weight ← 后卷积层 │
│ dec.conv_post.bias │
└─────────────────────────────────────────────────────┘
注意:dec.* 开头的就是声码器 GeneratorNSF 的权重。声码器的上采样卷积、noise_convs、所有 ResBlock、SourceModule 的线性层——全部保存在这个 .pth 文件里。
config 列表(决定网络结构)
┌──────┬────────────────────────────┬─────────────────────────┐
│ 索引 │ 参数名 │ 示例值(40kHz) │
├──────┼────────────────────────────┼─────────────────────────┤
│ [0] │ spec_channels │ 1025 │
│ [1] │ segment_size │ 12800 │
│ [2] │ inter_channels │ 192 │
│ [3] │ hidden_channels │ 192 │
│ [4] │ filter_channels │ 768 │
│ [5] │ n_heads │ 2 │
│ [6] │ n_layers │ 6 │
│ [7] │ kernel_size │ 3 │
│ [8] │ p_dropout │ 0 │
├──────┼────────────────────────────┼─────────────────────────┤
│ [9] │ resblock │ "1" │ ◄─┐
│ [10] │ resblock_kernel_sizes │ [3, 7, 11] │ │
│ [11] │ resblock_dilation_sizes │ [[1,3,5], [1,3,5], ...] │ │ 声码器
│ [12] │ upsample_rates │ [10, 10, 2, 2] │ │ 结构
│ [13] │ upsample_initial_channel │ 512 │ │ 参数
│ [14] │ upsample_kernel_sizes │ [16, 16, 4, 4] │ │
├──────┼────────────────────────────┼─────────────────────────┤ ◄─┘
│ [15] │ spk_embed_dim │ 109 │
│ [16] │ gin_channels │ 256 │
│ [17] │ sr │ 40000 │
└──────┴────────────────────────────┴─────────────────────────┘
其中索引 [9] 到 [14] 直接决定了声码器 GeneratorNSF 的内部结构——用哪种 ResBlock、上采样几级、每级多少倍率、通道数多少。这些参数一旦确定,声码器的网络结构就被完全固定了。
5.2 加载流程:从文件到可用的声码器
加载过程在 infer/modules/vc/modules.py:103-127 的 get_vc() 方法中完成。
四步加载流程
┌─────────────────────────────────────────────┐
│ 步骤1:读取文件,解析元信息 │
├─────────────────────────────────────────────┤
│ torch.load("model.pth") │
│ ├─ 读取 config[-1] → tgt_sr (采样率) │
│ ├─ 读取 emb_g.weight.shape[0] → n_spk │ ★ 直接看嵌入表行数
│ ├─ 读取 f0 → if_f0 (是否用F0) │
│ └─ 读取 version → "v1"或"v2" │
└─────────────────┬───────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 步骤2:根据版本和F0选项,创建合成器空壳 │
├─────────────────────────────────────────────┤
│ 选择合成器类: │
│ ┌────────────────┬─────────────────────┐ │
│ │ v1 + F0=1 │ SynthesizerTrnMs256NSFsid │
│ │ v1 + F0=0 │ SynthesizerTrnMs256NSFsid_nono│
│ │ v2 + F0=1 │ SynthesizerTrnMs768NSFsid │
│ │ v2 + F0=0 │ SynthesizerTrnMs768NSFsid_nono│
│ └────────────────┴─────────────────────┘ │
│ │
│ 用 config 列表参数构建模型 │
│ (此时权重还是随机值) │
└─────────────────┬───────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 步骤3:删掉推理不需要的模块,加载权重 │
├─────────────────────────────────────────────┤
│ del self.net_g.enc_q │ ← 后验编码器只在训练时用
│ self.net_g.load_state_dict( │
│ cpt["weight"], │
│ strict=False │ ← 允许缺失的 enc_q.*
│ ) │
└─────────────────┬───────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 步骤4:设置精度,移到目标设备 │
├─────────────────────────────────────────────┤
│ self.net_g.eval() │ ← 切换到评估模式
│ self.net_g.to(device) │ ← CPU/CUDA/MPS
│ if is_half: │
│ self.net_g.half() # FP16推理 │
│ else: │
│ self.net_g.float() # FP32推理 │
└─────────────────┬───────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 声码器就绪(作为 self.net_g.dec) │
│ 带着训练好的权重,可以开始推理 │
└─────────────────────────────────────────────┘
5.3 推理时权重是怎么流动的
加载完成后,推理走的是这条路径(pipeline.py:186-279):
原始音频 (16kHz)
│
├──→ HuBERT (assets/hubert/hubert_base.pt)
│ 提取语音内容特征 feats [batch, T, 256/768]
│
├──→ RMVPE (assets/rmvpe/rmvpe.pt)
│ 提取 F0 基频曲线 → pitch (量化) + pitchf (连续)
│
├──→ FAISS Index (.index 文件)
│ 用特征检索替换部分 feats,融入目标说话人的音色
│
└──→ net_g.infer(feats, p_len, pitch, pitchf, sid)
│
├── TextEncoder: feats + pitch → z_p(先验隐变量)
├── Flow (reverse): z_p → z(解码用的隐变量)
├── GeneratorNSF: z + pitchf + speaker_emb → 波形
│ │
│ ├── dec.conv_pre: z 投射到高维
│ ├── dec.cond: 注入说话人嵌入
│ ├── dec.m_source: pitchf → 谐波激励源
│ ├── dec.ups + dec.noise_convs: 上采样 + 谐波注入
│ ├── dec.resblocks: MRF 波形精炼
│ └── dec.conv_post + tanh: 输出波形
│
└──→ 输出音频波形
可以看到,除了语音模型自身的 .pth 文件,推理过程还依赖另外两个预训练模型:
- HuBERT(
assets/hubert/hubert_base.pt):Facebook 的自监督语音表示模型,负责从原始音频中提取"说了什么"的内容特征(但不包含音色信息) - RMVPE(
assets/rmvpe/rmvpe.pt):F0 估计模型,提取"唱/说的音高是多少"
这三者的分工很明确:HuBERT 管内容,RMVPE 管音高,语音模型 .pth 管"怎么用目标说话人的声音把这些内容和音高重新合成出来"。
5.4 .index 文件和特征检索
除了 .pth 之外,RVC 推理时还会用到一个 .index 文件。这不是声码器的权重,而是一个 FAISS 向量索引。
它的作用发生在特征空间,在声码器之前:
特征检索流程
┌──────────────────────────────────┐
│ 输入音频 HuBERT 特征 │
│ [batch, T, 256/768] │
└──────────┬───────────────────────┘
│
▼
┌──────────────────────────────────┐
│ FAISS Index 检索 │
│ search(feats, k=8) │ ← 找最近的8个邻居
└──────────┬───────────────────────┘
│
▼
┌──────────────────────────────────┐
│ 加权平均 │
│ weight = 1 / distance² │ ← 距离越近权重越大
│ retrieved = Σ(neighbors × w) │
└──────────┬───────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 混合原始特征和检索特征 │
│ final_feats = retrieved × index_rate │
│ + orig_feats × (1-rate) │
└──────────┬───────────────────────────────┘
│
▼
送入 TextEncoder 和 声码器
index_rate 参数的影响
- index_rate = 0:完全使用原始特征,保持输入音色
- index_rate = 1:完全使用检索特征,最像目标说话人
- index_rate = 0.75(常用):平衡音色转换和细节保留
这个检索过程跟声码器没有直接关系,但它决定了声码器接收到的输入质量。如果检索做得好,送进 GeneratorNSF 的隐变量就已经很接近目标说话人了,声码器只需要忠实地合成就行。
5.5 训练出来的权重对声码器意味着什么
回到声码器本身。.pth 文件中 dec.* 前缀的权重,到底"学"到了什么?
dec.ups.*.weight(上采样转置卷积):学会了怎么把低分辨率特征平滑地拉伸到高分辨率,同时不引入明显的棋盘格伪影dec.resblocks.*.convs*.weight(ResBlock 里的膨胀卷积):学会了在不同尺度上精炼波形的细节纹理——比如摩擦音的高频噪声特征、元音的共振峰结构dec.noise_convs.*.weight(谐波注入卷积):学会了在每个分辨率级别上怎么利用正弦波激励源——不是简单地"加上去",而是选择性地调整和变形dec.m_source.l_linear.weight(谐波混合层):学会了用什么比例把基频正弦波传递给后续网络dec.conv_pre.weight和dec.conv_post.weight:学会了输入和输出的通道映射
所有这些权重是在训练过程中通过 GAN 对抗和重建损失共同优化出来的。不同的训练数据(不同的说话人)会得到不同的权重,所以同一个声码器结构可以生成完全不同的声音——关键就在于 .pth 文件里的这些数字。
六、不同采样率下的配置对比(补充)
RVC 支持 32kHz、40kHz、48kHz 三种采样率,对应不同的声码器配置:
| 参数 | 32kHz | 40kHz | 48kHz |
|---|---|---|---|
| hop_length | 320 | 400 | 480 |
| upsample_rates | [10,4,2,2,2] | [10,10,2,2] | [10,6,2,2,2] |
| 上采样总倍率 | 320 | 400 | 480 |
| upsample_kernel_sizes | [16,16,4,4,4] | [16,16,4,4] | [16,16,4,4,4] |
| n_mel_channels | 80 | 125 | 128 |
| 上采样阶段数 | 5 | 4 | 5 |
| upsample_initial_channel | 512 | 512 | 512 |
有一个规律:上采样总倍率 = hop_length。这是因为声码器的输入是帧级别的(每帧对应 hop_length 个采样点),输出是采样点级别的,正好需要拉长 hop_length 倍。
32kHz 和 48kHz 用了 5 级上采样(多了一级 ×4 或 ×6),而 40kHz 只用 4 级(两个 ×10)。级数多意味着更细粒度的逐步还原,但也意味着更多的参数和计算量。
七、代码中值得注意的实现细节
7.1 weight_norm 而不是 batch_norm
HiFi-GAN 全程使用 weight normalization。相比 batch norm,weight norm 不依赖 batch 统计量,在生成任务中表现更稳定。推理时可以通过 remove_weight_norm() 去掉,减少计算开销。
7.2 LeakyReLU 的斜率
整个项目里 LeakyReLU 的负半轴斜率统一是 0.1(modules.py:17: LRELU_SLOPE = 0.1)。这个值在 HiFi-GAN 原论文里就是这么设的,基本是业界默认。
7.3 转置卷积的 padding 计算
上采样用的 ConvTranspose1d 的 padding 是 (kernel_size - stride) // 2。这个公式保证输出长度恰好是输入长度 × stride,不会多出或少掉采样点。
7.4 SineGen 中的相位连续性处理
SineGen 的 _f02sine() 方法中有一段关键的相位连续性处理逻辑:
帧间相位累积过程
第 1 帧 第 2 帧 第 3 帧
┌────────────┐ ┌────────────┐ ┌────────────┐
│ F0=220Hz │ │ F0=440Hz │ │ F0=330Hz │
│ 生成400个点│ │ 生成400个点│ │ 生成400个点│
└──────┬─────┘ └──────┬─────┘ └──────┬─────┘
│ │ │
│ 取最后一个点 │ 取最后一个点 │
│ 的相位增量 │ 的相位增量 │
▼ ▼ ▼
phase_end₁ phase_end₂ phase_end₃
│ │ │
└─────────────────────┼─────────────────────┘
│
┌───────────▼───────────┐
│ 累积相位修正序列 │
│ rad_acc = cumsum() │
│ 保证帧间相位不跳变 │
└───────────┬───────────┘
│
┌──────────────┴──────────────┐
│ 加到下一帧的起始相位上 │
│ 避免"咔哒"声 │
└─────────────────────────────┘
核心机制:
1. 每帧最后一个采样点的相位增量
2. 通过 fmod 归一化到 [-0.5, 0.5) 防止数值累积溢出
3. cumsum 累积相位差
4. 用 F.pad 填充到下一帧起始位置
这个处理保证了当 F0 在帧与帧之间变化时,正弦波的相位保持连续,不会产生相位突变导致的"咔哒"杂音。归一化到 [-0.5, 0.5) 范围是为了避免浮点数累积误差。
7.5 torch.jit.script 的兼容处理
代码里有不少 __prepare_scriptable__ 方法和类型标注(Optional[torch.Tensor])。这是为了兼容 TorchScript 编译——RVC 支持把模型导出为 TorchScript 格式以提升推理性能。@torch.jit.ignore 标记训练用的 forward,@torch.jit.export 标记推理用的 infer。
八、回顾:为什么 RVC 选择 NSF-HiFi-GAN
把标准 HiFi-GAN 和 NSF 变体放在一起看,关键差异只有一处:有没有把 F0 信息显式注入生成过程。
对于 TTS(文本转语音)来说,标准 HiFi-GAN 就够了——因为 TTS 的上游模型(比如 FastSpeech)已经把音高信息编码进了 mel 频谱,声码器只需要忠实还原。
但 RVC 做的是变声。它需要把一个人的声音转换成另一个人的声音,同时允许用户手动调整音高。如果声码器不知道目标音高是多少,它就只能从隐变量里"猜",结果往往是音高不稳、出现抖动甚至跑调。
NSF-HiFi-GAN 通过正弦波信号把 F0 “告诉"了生成器的每一层。网络不再需要自己发明轮子来建模周期性——正弦波已经提供了准确的周期结构,网络只需要在此基础上"雕刻"出正确的音色和细节。
这种设计的代价很小(多了一个 SineGen + 几个 1D 卷积),但带来的音高稳定性提升是显著的。
附录 A:关键类速查表
| 类名 | 文件位置 | 作用 |
|---|---|---|
SineGen | models.py:312 | 根据 F0 生成正弦波 |
SourceModuleHnNSF | models.py:391 | 将多个谐波正弦波合并为单通道激励信号 |
Generator | models.py:204 | 标准 HiFi-GAN 生成器(无 F0 输入) |
GeneratorNSF | models.py:448 | NSF 增强的 HiFi-GAN 生成器(有 F0 输入) |
ResBlock1 | modules.py:252 | 3 轮膨胀残差块(dilation=1,3,5) |
ResBlock2 | modules.py:367 | 2 轮膨胀残差块(dilation=1,3) |
MultiPeriodDiscriminator | models.py:1052 | 训练用判别器(MPD + MSD) |
SynthesizerTrnMs256NSFsid | models.py:602 | 完整合成器(256维,带 F0) |
SynthesizerTrnMs768NSFsid | models.py:779 | 完整合成器(768维,带 F0) |
SynthesizerTrnMs256NSFsid_nono | models.py:836 | 完整合成器(256维,不带 F0) |
SynthesizerTrnMs768NSFsid_nono | models.py:994 | 完整合成器(768维,不带 F0) |
附录 B:进一步阅读
- HiFi-GAN 原论文:HiFi-GAN: Generative Adversarial Networks for Efficient and High Fidelity Speech Synthesis (Kong et al., 2020)
- NSF 原论文:Neural Source-Filter Waveform Models for Statistical Parametric Speech Synthesis (Wang et al., 2019)
- VITS 论文(RVC 整体框架的基础):Conditional Variational Autoencoder with Adversarial Learning for End-to-End Text-to-Speech (Kim et al., 2021)