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

从 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 ×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 风格的框架中。以 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 解码器
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                   │
└──────┴────────────────────────────┴─────────────────────────┘

其中索引 914 直接决定了声码器 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 三种采样率,对应不同的声码器配置:

参数 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)
相关推荐
袁小皮皮不皮2 小时前
1.HCIP BFD 学习笔记(优化版)
服务器·网络·笔记·网络协议·学习·智能路由器·ip
装不满的克莱因瓶2 小时前
【自动驾驶领域】学习 Cityscapes 数据集——城市街景语义理解的标准基准
人工智能·pytorch·python·深度学习·学习·机器学习·自动驾驶
清辞8533 小时前
产品经理需求推进流程
大数据·深度学习·学习·产品经理
YM52e4 小时前
鸿蒙PC ArkTS 声明合并问题深度解析与最佳实践
学习·华为·harmonyos·鸿蒙·鸿蒙系统
海兰4 小时前
【实用程序】电商销售分析仪表盘 — 从零搭建一个AI参与的全栈数据洞察系统
人工智能·学习·算法
ken22325 小时前
在 Libreoffice Calc中输入自定义表情字符时,需要保存之后,才能正常显示
学习
zwenqiyu5 小时前
P5283 [十二省联考 2019] 异或粽子题解
c++·学习·算法
编程圈子5 小时前
电机驱动开发学习2. 直流无刷电机工作原理
驱动开发·学习
MartinYeung55 小时前
[论文学习]大型语言模型(LLM)安全与隐私-基于善、恶、丑的深度分析
学习·安全·语言模型
什仙6 小时前
Mathcad Prime 的教程资料
学习·工具