歌声转换SVC主流方法原理剖析2 — RIFT-SVC

pre

本文SVC指的是歌声转换(Singing Voice Conversion (SVC)),例如常见且开源的 So-VITS-SVC, RVC, DDSP-SVC
关键词:歌声转换、声音克隆、音色

DDSP-SVC训练快,但总是有音色泄漏,瞎改了几下似乎也没啥帮助。于是试试RIFT-SVC,并不复杂,相比ddsp显得有些大力出奇迹

观前提示:本文聚焦于算法流程和模型细节,虽然也会涉及预处理、训练推理等步骤,但并非使用手册。

code

https://github.com/Pur1zumu/RIFT-SVC

latest commit: 5d4c95ffa9addd1b019c3a64a77ae2dfe1f8987d

数据

预处理

此项目不需要提前切片,会在训练时随机截取,不过实测还是切了比价好,至少不能太长,否则在数据预处理时容易爆显存。

首先是数据组织,这部分项目原文档写得挺好:

data/
    finetune/
        speaker1/
            audio1.wav
            audio2.wav
            ...
        speaker2/
            audio1.wav
            audio2.wav
            ...

data/finetune是微调的默认数据目录,里面音频文件应重采样为 44100 Hz 并对响度进行标准化至 -18 LUFS

然后会往数据目录下写一个meta_info.json文件,以文件名形式记录了数据集的划分,(个人感觉不是很优雅),然后就是用各个scripts/prepare_xx.py脚本提取特征。

这个过程中如果用到神秘的flac,并且将其切片似乎会丢失音频的时长,可尝试以下命令:

$indir = '文件目录'
$filename = '文件名'
$duration = 180  # 切片时长:3分钟
# m4a转flac
ffmpeg -i "$indir/${filename}.m4a" "$indir/${filename}.flac"
# flac切片
ffmpeg -i "$indir/${filename}.flac" -f segment -segment_time $duration -reset_timestamps 1 -write_header 1 -c:a flac -ar 44100 -ac 2 "$indir/${filename}_%d.flac"

# 修复flac分段长度 法1
Get-ChildItem "$indir\${filename}_*.flac" | ForEach-Object {
    $tmp = "$_.tmp.flac"
    ffmpeg -i $_ -c:a flac -compression_level 5 -y $tmp
    if (Test-Path $tmp) {
        Move-Item $tmp $_ -Force
    }
}
# 修复flac分段长度 法2
$inputFile = Join-Path $indir "${filename}.flac"
$ffprobe = ffprobe -v quiet -of csv=p=0 -show_entries format=duration $inputFile
$totalDuration = [Math]::Ceiling([double]$ffprobe)
$segmentCount = [Math]::Ceiling($totalDuration / $duration)
for ($i = 0; $i -lt $segmentCount; $i++) {
    $start = $i * $duration
    $outputFile = Join-Path $indir "${filename}_$i.flac"
    ffmpeg -y -ss $start -i $inputFile -t $duration -c:a flac -ar 44100 -ac 2 -map_metadata 0 -compression_level 5 $outputFile
    if ($LASTEXITCODE -ne 0) {
        break
    }
}

一共有四个特征:基频(F0)、梅尔频谱(mel spectrograms)、语义/内容向量(content vectors, cvec)、均方根能量(RMS energy)。前面三个跟DDSP-SVC用的一样,第四个:

均方根能量(RMS energy) 是音频能量的度量,反映响度。RMS 越大,说明这段音频越“响”;越小则越“安静”。它常用于语音活动检测(VAD)、动态范围压缩、音频归一化等任务。对于一段离散音频信号 ( x[n] )(比如一个短窗口内的采样点),其 RMS 定义为:

\[\text{RMS} = \sqrt{ \frac{1}{N} \sum_{n=0}^{N-1} x[n]^2 } \]

RMS跟DDSP-SVC里用的音量包络虽然都衡量声音能量,即响度,但二者不同:

  1. 计算公式首先就不同,从下面的公式可以看出DDSP的音量包络本质是窗口内的标准差。RMS 始终 ≥ 标准差(RMS² = 方差 + 均值²)。
  2. RMS易受DC(直流)偏置影响,例如输入音频[0.5, 0.5, 0.5, ..., 0.5](纯直流),有\(RMS\approx 0.5,\; Volume = 0\)
  3. 人耳对平均电平不敏感,但对波动(即声音内容)敏感,显然音量包络Volume更贴合人类感知。

音量包络/标准差(Volume_Extractor提取算法):

\[\text{Var} = \mathbb{E}[x^2] - (\mathbb{E}[x])^2 \\ \text{Volume} = \sqrt{ \text{Var}(x) } \]

batch

dataloader准备预处理阶段保存的那些特征,每个batch中有以下数据:

  • spk_id 说话人id
  • mel(\(x_{spec}\)) 梅尔频谱
  • rms 均方根能量
  • f0 基频
  • cvec 内容特征
  • frame_len 帧长度,通常等于max_frame_len(截取片段)

同样, mel, rms, f0, cvec 这四个均是一个小时间段的切片,时间段起止每次读取时随机,由参数max_frame_len控制。

模型架构

模型类名为 RF ,是一个 Rectified Flow。里面用的ODE/流模型/\(v_\theta\)velocity_fn)采用的是DiT,通过MSE(Flow matching loss)进行优化。

RF

即ReFlow模型,RF.forward 接收输入 mel, spk_id, f0, rms, cvec ,其中mel归一化到\([-1,1]\)后作为\(x_1\),同样从标准高斯采样噪声作为 \(x_0\)。调用 sample_time 函数采样时间t

sample_time支持两种模式:

  1. uniform:均匀采样,从\([0,1]\)里随机挑选size个t
  2. lognorm:对数正态分布采样,先将\([0,1]\)分层采样(stratified sampling),每个小区间采样一个随机数,然后将每个数变换为正态分布再借助sigmoid压缩回\([0,1]\)区间。如此采样的t大多集中于\(0.5\)附近。

ps. DDSP-SVC里面同样更关注\(0.5\)附近的t,但那是均匀采样t然后通过调整最终不同样本的损失权重实现的(重要性采样)。而RIFT-SVC这里则是从源头上更偏向于采样出\(0.5\)左右的t。

之后线性插值得到 \(x_t = x_0 + t (x_1 - x_0)\),带着 t, spk_id, f0, rms, cvec 扔给DiT预测速度 \(v_{pred}\),然后计算与 \(x_1 - x_0\) 之间的MSE作为损失

DiT

Diffusion Transformer,将 Transformer 架构应用于扩散模型(Diffusion Models)的生成模型,负责根据输入的特征预测速度 \(v_{pred}\)

Classifier-Free Guidance

无分类器引导(Classifier-Free Guidance, CFG)是一种在不使用额外分类器的情况下,增强生成模型对文本提示词遵循能力的技术。

既然有“无”分类器引导,也就有分类器引导。在CFG之前,为了引导模型生成特定类别的图像(比如“猫”),通常需要训练一个独立的图像分类器。在生成过程中,分类器会判断当前生成的图像“像猫”的程度,并以此来指导生成方向。这种方法被称为 Classifier Guidance。

但 Classifier Guidance 问题不少:

  1. 需要额外训练。必须训练一个强大的分类器。
  2. 性能瓶颈。生成效果受限于分类器本身。如果分类器有偏见或识别不准,生成效果也会受影响。
  3. 不灵活。很难处理复杂的、描述性的文本提示,因为分类器通常只针对简单的类别。

以文生图为例,CFG在训练时需要两种输入:

  1. 条件训练:图像-文本提示对。模型学习如何根据文本提示去噪图像。
  2. 无条件训练:以一定概率(例如 20%),将文本提示替换成一个空标记(如 "")。此时模型只能学习如何从纯噪声中恢复出一张“看起来合理”的、通用的图像。

推理时也需要两次生成:

  1. 有条件预测:将当前的噪声图像和提示词(如 "a photograph of an astronaut riding a rabbit")输入模型,得到一个预测的噪声 \(\epsilon_{cond}\)
  2. 无条件预测:将同一个噪声图像和空提示词("")输入模型,得到另一个预测的噪声 \(\epsilon_{uncond}\)

用一个公式将这两个预测结果结合起来,得到最终的、用于去噪的噪声 \(\epsilon_{guide}\)

\[\epsilon_{guide} = \epsilon_{cond} + \omega_{guide} * (\epsilon_{cond} - \epsilon_{uncond}) \]

其中\(\epsilon_{cond} - \epsilon_{uncond}\)代表了“遵循提示词”这个行为本身所产生的方向向量。它告诉模型,与“自由创作”相比,为了“遵循提示词”需要朝哪个方向调整。\omega_{guide}是一个超参数越大模型生成越遵循提示词。

采样

RF.sample 接收输入 src_mel, spk_id, f0, rms, cvec 以及 bad_cvec, ds_cfg_strength, spk_cfg_strength, skip_cfg_strength, cfg_skip_layers, cfg_rescale

其中 bad_cvec 是内容特征 cvec 相对的无/差条件输入,实际上是下采样的内容特征cvec_ds,后面则是用于cfg的参数。

类型 条件变体 作用 说明
内容向量引导强度 (ds_cfg_strength) [cvec, bad_cvec] 增强对语义的控制 默认:0.0,可试用0.1
说话人引导强度 (spk_cfg_strength) [spk_embed, null_spk_embed] 增强对说话人身份的控制 默认:0.0,可试用0.2~1
跳层引导强度 (skip_cfg_strength) [None, skip_layers] 增强高层语义 实验性功能,默认:0.0,可试用0.1
要跳过的目标层 (cfg_skip_layers) list[int],指定要跳过的层 实验性功能,默认:None
无分类器引导重缩放因子 (cfg_rescale) 防止引导过饱和 默认:0.7

实际上会根据指定使用的 xx_cfg_strengthrepeat_interleave 复制多次 cvec 并拼接上 bad_cvec 一次性输入,然后再从输出拆解出所需的预测结果/速度/变化率/dy/dt xx_pred,然后按照CFG的公式调整预测的速度\(pred\)

\[pred = pred + (pred - xx\_pred) * xx\_cfg\_strength \]

最后的cfg_rescale实现似乎有问题:

cfg_flag = (ds_cfg_strength > 1e-5) or (skip_cfg_strength > 1e-5) or (spk_cfg_strength > 1e-5)
if cfg_rescale > 1e-5 and cfg_flag:
    std_pred = pred.std()

if skip_cfg_strength > 1e-5:
    skip_pred = self.transformer(...)
    pred = pred + (pred - skip_pred) * skip_cfg_strength

if cfg_rescale > 1e-5 and cfg_flag:
    std_cfg = pred.std()
    pred_rescaled = pred * (std_pred / std_cfg)
    pred = cfg_rescale * pred_rescaled + (1 - cfg_rescale) * pred

按照代码看,似乎只有跳层引导启用的时候引导重缩放能起作用,其余时候 std_cfg == std_pred。这里应该算的都是std_cfgstd_pred得用原始预测来算吧?

而且把下采样的内容特征cvec_ds作为CFG方法种的无条件输入吗?但这玩意只是下采样,应该扔含有信息算不上无条件输入吧?最重要的是cvec_ds似乎只在验证/推理时有用上,这能行吗,会出现训练-验证不一致吧。

总之根据上面描述的ODE步骤,使用 torchdiffeq.odeint 计算得到每个时间步的预测输出trajectory,然后取t=1时刻的trajectory[-1]进行反向归一化等操作得到最后预测的梅尔频谱。

推理

DDSP-SVC中先将参考音频分段,每一小段音频各自抽取特征。然后ddsp部分先通过提取的的梅尔频谱、基频等信息生成一个粗糙的目标梅尔频谱,再由 Rectified Flow 去精炼得到最终的目标梅尔频谱。注意DDSP-SVC的ddsp部分是不需要参考音频梅尔频谱作为输入的,它依据 语义特征, 基频信息, 音量包络, 说话人id 等直接预测目标音频。

而RIFT-SVC这边也是先音频分段,然后每一小段同样是单独提取出梅尔频谱等特征,然后调用 RF.sample 进行采样,它的参数里接收参考音频梅尔频谱,但是并没有直接利用它,只是提取该tensor的shape/device信息。推理部分没有沿用lightning。

RIFT这边的 Rectified Flow 就相当于DDSP那边两个模型的整体,都是接收 语义特征, 基频信息, 音量包络, 说话人id 预测目标梅尔频谱,最终加上基频信息一起扔给声码器解码出目标音频。只是DDSP那边有得选,能利用ddsp的生成梅尔频谱作为推理起点,而RIFT就完全是纯噪声充当x0进行降噪,跟DDSP中\(t_{start} = 0\)情况一致。

碎碎念

整体看是一个ReFlow,相当于DDSP-SVC去掉了ddsp部分并换上了更强大的速度预测器(DiT),但支持更多的超参数调节。

跟ddsp差不多,更贴近参考的内容(唱法)及响度?训练慢一些,音色泄露较轻,音色更明亮?
二者都利用cvec抽取语义信息作为条件,reflow从噪声开始预测mel频谱(只是里面的预测模型用的不一样),再由vocoder合成为audio,音色泄露或许是reflow的通病?

之前用的时候一直吞字,发现与 --slicer-threshold 影响很大,原本值是\(-30db\),调整为\(-60db\)后原本不行的部分正常,但又有新的字被吞掉...似乎不是很稳定。又或许是训练不足的问题?实际上只训练了作者推荐的大概1/10时间,但感觉已经收敛

提供了一个方便开发的框架,但代码还是有点乱,好的是用了lightning,可话又说回来hydra真难受吧,要是LightningCLI+Omegaconf就好了

posted @ 2025-11-09 20:22  NoNoe  阅读(11)  评论(0)    收藏  举报