歌声转换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 定义为:
RMS跟DDSP-SVC里用的音量包络虽然都衡量声音能量,即响度,但二者不同:
- 计算公式首先就不同,从下面的公式可以看出DDSP的音量包络本质是窗口内的标准差。RMS 始终 ≥ 标准差(RMS² = 方差 + 均值²)。
- RMS易受DC(直流)偏置影响,例如输入音频
[0.5, 0.5, 0.5, ..., 0.5](纯直流),有\(RMS\approx 0.5,\; Volume = 0\) - 人耳对平均电平不敏感,但对波动(即声音内容)敏感,显然音量包络Volume更贴合人类感知。
音量包络/标准差(Volume_Extractor提取算法):
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支持两种模式:
- uniform:均匀采样,从\([0,1]\)里随机挑选
size个t - 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 问题不少:
- 需要额外训练。必须训练一个强大的分类器。
- 性能瓶颈。生成效果受限于分类器本身。如果分类器有偏见或识别不准,生成效果也会受影响。
- 不灵活。很难处理复杂的、描述性的文本提示,因为分类器通常只针对简单的类别。
以文生图为例,CFG在训练时需要两种输入:
- 条件训练:图像-文本提示对。模型学习如何根据文本提示去噪图像。
- 无条件训练:以一定概率(例如 20%),将文本提示替换成一个空标记(如 "")。此时模型只能学习如何从纯噪声中恢复出一张“看起来合理”的、通用的图像。
推理时也需要两次生成:
- 有条件预测:将当前的噪声图像和提示词(如 "a photograph of an astronaut riding a rabbit")输入模型,得到一个预测的噪声 \(\epsilon_{cond}\)。
- 无条件预测:将同一个噪声图像和空提示词("")输入模型,得到另一个预测的噪声 \(\epsilon_{uncond}\)。
用一个公式将这两个预测结果结合起来,得到最终的、用于去噪的噪声 \(\epsilon_{guide}\):
其中\(\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_strength 用 repeat_interleave 复制多次 cvec 并拼接上 bad_cvec 一次性输入,然后再从输出拆解出所需的预测结果/速度/变化率/dy/dt xx_pred,然后按照CFG的公式调整预测的速度\(pred\):
最后的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_cfg,std_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就好了

浙公网安备 33010602011771号