我使用SVC让我最喜欢的虚拟角色翻唱流行歌曲
普拉娜(プラナ)是手机游戏Blue Archive(ブルアカ)里的角色,也是我最喜欢的角色。这个项目 的目的从一开始就很直接:让她唱流行歌,尤其是《優しさの記憶》这一首。这首歌是最终篇的ED, 和普拉娜本身的关系也很近,我想听她自己唱一遍。这件事最后花掉了上百个小时,绝大部分时间在试错。
成品是下面这段视频。
《優しさの記憶》SVC翻唱
用来训练这副嗓子的素材全部是说话录音,训练数据里没有任何人唱过任何歌。这是so-vits-svc的基本 性质:唱歌时的音高、咬字和颤音全部来自推理时输入的那段源人声,模型只负责把音色换掉。这一点我在 相当长的时间里没有真正理解,因此走了很多弯路。
十个月前开始做这件事的时候我还没有显卡。最早是在Kaggle上训练,算力不够,环境本身也一直在出 问题挡路;后来改成在Colab上按月订阅,调A100来跑。训练是跑完了,效果很一般,声音沙哑, 高音部分几乎全是气声。
问题不在参数上。我对这个模型在做什么的理解从一开始就是错的,而这一点我过了很久才想明白。 最近在Bilibili上刷到一批AI翻唱的视频,才让我下决心把整个项目重做一遍。本文先给出当时写的 失败复盘,逐条说明每个症状的根因和证据,然后说明纠正认知之后整条链路被改成了什么样子。
一个错误的心智模型
第一版的做法是这样的:从视频里拉下说话录音,用Demucs去掉背景音,用Silero VAD切成单句, 筛掉质量差的,然后只拿普拉娜一个人的语料去训练。推理的时候把原曲丢给Demucs扒出人声, 把这段人声送进模型换音色。
当时凑到的可用语音大约七分钟,而且全是说话,音域集中在中低区,高音那一段等于完全没有。 现在回头看,这是后面一连串问题的起点,但当时我没觉得它有什么不对。
出来的结果有四个毛病:整体沙哑,高音发虚,副歌位置掉八度,部分段落完全不出声。四个毛病都不是偶发的, 每次推理都在。
我当时认定是训练没做够,于是训练侧和推理侧一起改。训练侧重新筛过数据集,把阿罗娜的语料也加了 进来,调过batch和步数,试过几组学习率;推理侧把阿罗娜的混音权重、高F0阈值、noise scale、 Demucs的shifts轮数挨个试。前后几十轮,那四个症状一个都没消失。
问题出在我对这个模型的理解上。我默认唱得好不好是训练决定的,所以每一轮都在训练和参数里找原因。
我当时整理过一份失败复盘,给每个症状都找了原因:沙哑来自训练数据里烘焙进去的旧分离杂讯; 高音发虚和掉八度是因为解码器没见过这把嗓子在高F0的谐波结构,普拉娜说话F0中位225Hz, 而歌曲在312Hz以上;不出声是推理的静音阈值定得太高,把弱声段整段切掉了;损失曲线早就平了 却还是不好听,是因为七分钟数据配上当时的batch和步数,训练早已变成纯粹的过拟合。推理时混入 一半阿罗娜也没有改善,因为混音只是在稀释音色,补不了训练里缺的高音。复盘里最要命的一条不是 故障:我从来没有建立过固定评估集,所以上面每一条其实都验证不了,我调了几十轮参数, 手里却没有一把尺子。
SoVITS到底在做什么
转折点不是我自己想通的。我把手上所有音频的时长和质量整理出来,让Claude过了一遍, 拿回来两个结论,都不在我原来找问题的方向上。
第一个结论是数据太少。七分钟这个量级配上我当时用的batch和步数,模型早就把训练集背下来了, 再练下去只是把过拟合练得更深。
第二个结论是分离环节用错了工具。我一直在用的Demucs,连同同一代的MDX系模型,在人声分离上 已经落后,当下效果最好的是roformer这一类。这一条比第一条更关键,因为它同时影响两端: 训练数据里烘焙进去的杂讯来自它,推理时喂进模型的那段引导人声也来自它。roformer能把多声部 人声的流行歌干净地拆开,而以前那些F0预测出问题的段落,有相当一部分其实是分离没做干净造成的。
顺着第二条往下想,我才看明白so-vits-svc实际在做什么。推理的时候,源人声先被内容编码器 提成与音色无关的内容特征,音高由F0提取器单独算出来另走一路,speaker embedding只决定用哪把 嗓子把这些重新合成出来。唱什么、唱多高、怎么咬字,全部由那段源人声决定,模型负责的只有音色。
这件事说清楚之后,我原先的几个假设一起塌了。训练数据不需要有人唱过歌,说话录音本来就够用, 因为模型从来没有被要求学会唱歌。所谓教她唱歌这个说法本身不成立,实际要做的是两件互不相干的事: 训练侧给她一副覆盖足够音域的嗓子,推理侧给一段干净、音高正确的引导人声。这两件事都不需要 推翻旧方案,旧方案是可修的。
训练侧:补上缺的那半副嗓子
高音从哪来
症结在F0。普拉娜的说话F0中位在225Hz,而这首歌大部分时间在312Hz以上。解码器从来没有见过 这把嗓子在那个音区的谐波结构,到了高音它只能猜,猜出来的结果就是发虚和掉八度。
最直接的解法是在推理时转调,把源人声降下来推完再升回去。我试过,结果很难听:有明显的机械音
和杂音,声音会撕裂,听起来根本不像同一个角色,更像是用了变声器,或者吸了氮气之后在说话。
原因不难理解,转调是在用推理技巧掩盖训练数据的缺失,代价是音色本身被拉变形。
所以后来定下的规矩是禁止转调,也禁止auto-F0,transpose固定为0。
高音必须来自真实的高音数据。运气好的地方在于这个数据是存在的:普拉娜和阿罗娜是同一位声优 小原好美配的,一个人配了两个角色。两把嗓子的音色特征本来就接近,区别主要在音高和发声方式上, 一个更饱满,一个更偏气声。我把两边的语音特征拿去比对了一次,结论是阿罗娜基本上就是普拉娜的 高音版,她的说话F0中位在381Hz,正好落在普拉娜缺的那一段。把两个角色放在一起训练, 既保得住普拉娜的音色特征,又能把高音域直接补出来。
做法是联合训练,两个角色各自保留独立的speaker label。so-vits-svc的decoder和flow是所有
speaker共享的,只有speaker embedding不同,所以高F0的谐波结构会被共享的解码器学到,
而两个角色的音色仍然分得开。配置里的n_speakers保持200不动,这是为了和预训练权重的
embedding形状兼容。
两个角色混在同一批视频里
素材来自一个同时有两个角色的频道,VAD切出来的句子两个角色是混在一起的,两千多条, 全部人工听一遍不现实。我最先做的是音频特征分析:先识别咳嗽、哼声、笑声、喊叫这类非正常发声 把它们筛掉,再按声学特征给剩下的归类,每一类都设计了各自的判据,比如咳嗽和气声笑看浊音比和 谱平坦度,单调哼声看音高标准差,笑声看短时间内的音高抖动。正确率很低。这类非语义发声和正常 说话的声学特征重叠得比我预想的多,阈值紧到能抓住咳嗽就会误伤正常句子,放松了又漏掉一半。
最后回到了最简单的做法:直接按基频中位数分类。两个角色的音高分布本身分得很开,中间留一条 模棱两可的带,只有落进这条带里的才需要人来判断,正确率远高于前面那套特征分析。基频提取很慢, 完整跑一遍要等很久,所以我把第一遍算出来的特征全写进CSV,后面调阈值只从CSV里重算标签, 几秒钟就出结果。调一次阈值几乎不花时间,我才试得起那么多轮,也才试出简单的做法更好。
数据量
普拉娜的训练数据从七分钟扩到大约三十分钟,阿罗娜扩到五十分钟出头。旧版本那七分钟配上当时的 batch和步数,相当于同一批素材反复过了四千多遍。所以问题一直不是训练不足,是练过头了。
分离管线换代
旧管线是Demucs的htdemucs --two-stems vocals --shifts 2,新管线换成两级roformer级联。
第一级用三个人声模型各跑一遍,输出用avg_fft融合,得到全部人声和伴奏两路:
bs_roformer_voc_hyperacev2
model_bs_roformer_ep_368_sdr_12.9628
mel_band_roformer_vocals_gabox第二级把融合后的全部人声再送进三个karaoke模型,同样用avg_fft融合,拆出主唱和和声:
mel_band_karaoke_fusion_standard
mel_band_karaoke_fusion_aggressive
mel_band_roformer_karaoke_gabox_v2这里有一个很容易搞反的地方。karaoke模型输出的两路里,Vocals那一路在已经预先提取过全部
人声的输入上对应的是主唱,而互补的Instrumental那一路是和声和其他人声残留,不是原曲伴奏。
新管线出来的静音段比旧管线干净得多,而这个差距会原样烘焙进训练数据,再以沙哑的形式出现在 推理结果里,所以分离这一步值得跑三个模型再融合。推理时输入的引导人声也走同一条级联, 和训练数据同源,减少域差。入库统一成44.1k单声道,不做响度归一化,不做pitch或formant shift。
浅扩散和它的深度
训练侧还额外训了一个浅扩散模型,作用是在主模型出完mel之后再重新渲染一遍,沙哑主要靠它压下去。
推理的时候它有一个深度参数K,我在这个参数上花的时间比预想的多。K往低调,音色相似度更高, 但音质会损失;K往高调,音质更连贯饱满,音色相似度反而往下掉。两头都试过之后,比较好用的区间 是30到50。再往上就不是线性的了,K超过100会出现逆反,音质反过来变差。
除了K值,我还试过两种提高音色相似度的做法,特征检索和k-means聚类,各按一个固定强度配不同的 K值交叉跑了一轮。两种最后都没有留下来,交付时的推理只开浅扩散,聚类那一路的比例是0。
推理侧:引导人声决定一切
SVC不需要原唱的音色,它需要的是一段干净、单声部、发音清楚、F0正确的引导人声。音色由模型提供, 剩下的全部由这段人声提供。
按这个原则,最直觉的那条路径恰好是最差的一条。把原曲丢给Demucs扒出人声再送进SVC, 拿到的东西带着和声残留、混响尾巴、气声和伴奏残留,F0提取会跳音,换完音色之后声音发抖、 咬字不准。第一版那几个毛病里有一部分就出在这里,而我当时把它们全都记到了训练的账上。
想明白引导人声不需要原唱音色之后,我换了个方向:既然音色是模型给的,那这段人声根本不必来自 原曲,我可以自己造一段。我先在vsqx.top上找到了这首歌的工程文件,但那一份质量不好,音不准, 字对不上,节拍也有错,全都得手动改,改起来非常费时间。后来在BowlRoll上又找到一份配布, 它更贴近原曲,音高和节拍都更正确,而且是日本人做的,日文歌词本身就不会错,最后用的是这一份。
把工程转成ustx和midi,做了去颤音和F0重调几个变体,再用NEUTRINO的一个音源把日语发音重新唱 一遍,导出来就是一段没有和声、没有混响、咬字清楚的干声。它本身不好听,但这不重要。 同一段时间我还试过VOCALOID,做出来的结果也一般。
这条路最后没有留下来,原因有两个。直接的原因是新的roformer级联把从原曲拆人声这件事做成了, 拆出来的主唱已经足够干净,F0跳音基本消失,绕开原曲的必要性就没有了。更重要的原因是合成干声 没有感情。机器唱出来的引导人声强弱一样、气口一样、断句一样,音高精确到没有起伏,换完音色 之后听起来还是机器在唱。原唱那些细微的强弱变化和气声恰恰是SVC会原样保留下来的东西, 它们决定了成品像不像一个人在唱歌。
所以最终交付走的是这条:原曲先过roformer级联拆出主唱和和声,两路都送进模型做音色转换, 最后连同伴奏一起交付。
还有几条不用重训就能改的推理设置。静音阈值slice_db原先是-35,弱声段被整段当成静音切掉,
这是部分段落不出声的直接原因,放宽之后问题就没了。auto_predict_f0关闭,歌声推理不要打开。
transpose固定为0。speaker以普拉娜为主,只在检测到的高音段叠入阿罗娜,默认权重0.3。
最后这条有个陷阱:so-vits-svc的speaker mix会把权重归一化,所以写下的数字不是它在输出里
实际占的比例。旧配置里写的是1.0和0.5,实际生效的是67和33,我一度以为混进去的是一半。
怎么选出最后那个模型
复盘里最要命的那条,固定评估集从来没有建立过,到现在也没有被真正解决。test_set/fixed_eval/
这个目录至今是空的。
实际用的是一把临时的尺子:三四段参考句,加上几首流行歌的推理结果,在TensorBoard里挨个听, 交叉对比的样本另外堆在一个目录里。这比什么都没有强,但和我计划里那把尺子不是一回事, 每次比较的条件也不完全一致。
训练步数并不是越多越好,这一点和直觉相反。过拟合的模型在训练集上会越来越像,在没见过的歌上 却未必更稳,早期权重经常反而更好用。所以候选里要覆盖早、中、晚三段的权重,而不是只测最新的那个。
最后形成了三个可选的世代,都从同一次联合训练分叉出来。PA1是最早那次阿罗娜和普拉娜的联合训练, 练到G_29824。从它的末尾接着微调出了PA2a,起点是G_30000;另外从它中途的G_12000分叉出了PA2b, 只多练了两千步。
三个里最后被定为默认的是PA2b,也就是种子取得最早、总步数最少的那一支,它的音色最接近普拉娜。 从末尾那个起点微调出来的PA2a,音色反而偏向阿罗娜,被标记为弃用,只留着做对照。
PA1没有被删掉。联合训练的那一版在高音区确实更稳,只是日常音色不如PA2b像,所以它作为高音歌的 回退保留了下来。三个模型里没有哪一个是全面胜出的。
主模型和浅扩散模型必须来自同一个世代。扩散这一步是针对特定主模型的输出分布训练的,拿PA2b的 主模型去配PA1的扩散,出来会是错的音色。我没有真的搞混过,但这个错太容易犯,所以代码里的世代 做成了一个同时绑定主模型目录和扩散模型目录的结构,两个路径不给人分开填。
最后这些做成了一个命令行操作台,人声分离、单条推理和整曲翻唱三个操作,整曲翻唱会把主唱和和声 分别做音色转换,连同伴奏一起交付,并写一份manifest记下用的是哪个世代、哪个权重和多深的扩散。
剩下的问题
现在这套流程能稳定跑通了,还剩一个取舍我没解决。高音段的发音偏向阿罗娜偏得有点多, 听得出来那不完全是普拉娜的咬字。想把普拉娜的成分提上去是可以做到的,代价是音质会下降, 两者目前只能二选一。我选的是保音质。