从 HiFi-GAN 到 NSF-HiFi-GAN:声码器学习笔记

本文基于 RVC(Retrieval-based Voice Conversion)项目的实际代码,从零开始梳理 HiFi-GAN 声码器的原理,再过渡到 RVC 中真正使用的 NSF-HiFi-GAN 变体。 代码位置:infer/lib/infer_pack/models.pyinfer/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。

有两件事值得注意:

  1. 通道数逐级减半:512 → 256 → 128 → 64 → 32。分辨率在增加,通道数在减少,这跟图像领域的解码器思路一致。
  2. 每个上采样阶段之后都跟着一组 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 默认用的是 ResBlock1modules.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,生成质量更稳定。

还有一种更轻量的 ResBlock2modules.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 代码里,这两者合并成了 MultiPeriodDiscriminatormodels.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] │
    └─────────────────────────────────┘

核心逻辑:

  1. 把 F0(单位 Hz)除以采样率,得到每个采样点的相位增量
  2. 通过 cumsum(累积求和)得到连续的相位序列
  3. 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×10T×1010×2×2 = 40源信号每 40 个点取 1 个
1×10T×1002×2 = 4源信号每 4 个点取 1 个
2×2T×2002源信号每 2 个点取 1 个
3×2T×4001 (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 风格的框架中。以 SynthesizerTrnMs256NSFsidmodels.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解码器
SynthesizerTrnMs256NSFsid256GeneratorNSF
SynthesizerTrnMs768NSFsid768GeneratorNSF
SynthesizerTrnMs256NSFsid_nono256Generator(标准)
SynthesizerTrnMs768NSFsid_nono768Generator(标准)

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-127get_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 文件,推理过程还依赖另外两个预训练模型:

  • HuBERTassets/hubert/hubert_base.pt):Facebook 的自监督语音表示模型,负责从原始音频中提取"说了什么"的内容特征(但不包含音色信息)
  • RMVPEassets/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.weightdec.conv_post.weight:学会了输入和输出的通道映射

所有这些权重是在训练过程中通过 GAN 对抗和重建损失共同优化出来的。不同的训练数据(不同的说话人)会得到不同的权重,所以同一个声码器结构可以生成完全不同的声音——关键就在于 .pth 文件里的这些数字。


六、不同采样率下的配置对比(补充)

RVC 支持 32kHz、40kHz、48kHz 三种采样率,对应不同的声码器配置:

参数32kHz40kHz48kHz
hop_length320400480
upsample_rates[10,4,2,2,2][10,10,2,2][10,6,2,2,2]
上采样总倍率320400480
upsample_kernel_sizes[16,16,4,4,4][16,16,4,4][16,16,4,4,4]
n_mel_channels80125128
上采样阶段数545
upsample_initial_channel512512512

有一个规律:上采样总倍率 = 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:关键类速查表

类名文件位置作用
SineGenmodels.py:312根据 F0 生成正弦波
SourceModuleHnNSFmodels.py:391将多个谐波正弦波合并为单通道激励信号
Generatormodels.py:204标准 HiFi-GAN 生成器(无 F0 输入)
GeneratorNSFmodels.py:448NSF 增强的 HiFi-GAN 生成器(有 F0 输入)
ResBlock1modules.py:2523 轮膨胀残差块(dilation=1,3,5)
ResBlock2modules.py:3672 轮膨胀残差块(dilation=1,3)
MultiPeriodDiscriminatormodels.py:1052训练用判别器(MPD + MSD)
SynthesizerTrnMs256NSFsidmodels.py:602完整合成器(256维,带 F0)
SynthesizerTrnMs768NSFsidmodels.py:779完整合成器(768维,带 F0)
SynthesizerTrnMs256NSFsid_nonomodels.py:836完整合成器(256维,不带 F0)
SynthesizerTrnMs768NSFsid_nonomodels.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)