Shift as Restore: Problem & Attempt

本文AI辅助内容极少,仅部分公式的LaTeX代码由AI生成,请放心阅读😂 当然,代码实现部分的AI率已经飙升到接近100%

Demo网页

https://vococo.vercel.app/

实际效果比WORLD较好,但是听起来并没有完美无瑕,肯定是比不上Melodyne的,还需更多研究…

目标

可以先了解一下Melodyne这个软件。Melodyne 是 Celemony 出的一款单声部人声/单音乐器的音高与时长编辑器,核心交互是把一段音频在钢琴卷帘上以“音符块”的形式可视化:每个音符可以单独被拖动中心音高、重绘 F0 曲线、压缩或拉伸时长、调整颤音深度,也可以在 attack 处加滑入滑出。本文要做的事情,可以理解为:尝试用神经网络去替换 Melodyne 内部把“被你拖过的音符”渲染回音频的那个引擎。

我主要是因为开源的软件的功能完全没有能够匹配Melodyne那种生成质量(更不必说功能,但是我打算先从生成质量说起)。开源的不基于神经网络所工作的结果基本上没有办法听;基于神经网络的工作的结果也有没有达到可以用于录音制作的质量。

目标:给一个人声音频片段,往往是唱得略不够好,没有到音准的歌声,然后对其中的某些音符进行音高微调;或者是对其F0曲线进行重绘。

为什么

个人审美问题。

我个人对全自动音高修正没有兴趣,主要原因是

1.我自己也唱歌 2.神经网络不可能识别人的所有意图,有的音没唱准是艺术处理,有的音没唱准是真的没唱准,而我想要尽可能的自由度 3.我觉得歌声音乐的消费主要还是以人为中心的,全自动音高修正就已经往虚拟歌手方向狂飙了

主要还是因为我喜欢。

问题

这个任务虽然说可以以一种非常显然的方式建模为监督学习的任务,但是从配对的角度上来看,这种数据根本就不存在:理想中的配对数据应当是 “略走音的演唱 ->同一句话唱准音的版本”,但是不会有人专门为了同一段演唱录两版(一版故意走音、一版照着编辑结果重新唱回去);就算录了,每次发声的呼吸、共振峰、瞬态噪声都不一样,本质上是两段不同的演唱,而不是同一段音频的“修音前后”。公开数据集(OpenCpop、PJS、ItakoSing、OpenSinger、CCMusic 等)都是单版本(不论是否唱准)的录音,没有标注告诉你“这一段应当被改成什么样子”。

而且从现在的深度学习方式来看,大家都希望利用海量的质量较高,但是没有监督信号的信息,这样就不需要对数据进行标注,而只需要进行清洗。

因此从推理角度出发,我的想法是先经过一个传统算法的DSP(比如WORLD,rubberband等等)对音高微调之后的那一段音频进行粗略估计,然后再用神经网络进行修复。

数据就可以如此构造:对变调的部分应用两次相反方向的DSP(比如一次是升调,一次是把升的部分降回去)为x,原始音频为y,这样就构造出了配对数据。

实际上这个任务还有一个非常核心的问题,就是:人对于一段音频,音色是否一致?是否符合自然的人声?是否好听?目前是没有一个好的公式去量化它的。也正是因此,我选择了生成式模型的方式

输入

根据对音频深度学习的文献的调研,音频深度学习一般不会在时域上进行,而是会在时频谱上进行。调研大量的开源文献之后,由于个人审美的缘故,不愿意引入人为设计的先验特征,最终选取了complex stft作为输入输出的表示域。

表示形式

文献中广泛使用的STFT的表示形式就有多种,包括 ,等等。

首先排除的是 ,由于具有phase wrapping的问题,导致不适合深度学习。phase wrapping 是说相位 的取值在 ,本质上是一个圆周而不是一条直线:当真实相位是 而预测是 时,二者在圆上其实只差 ,但是直接拿欧氏 loss 算 会得到 ,瞬间变成一个巨大的惩罚;同样,相邻 bin 的相位本来在物理上是连续的,但只要有一处跨过 边界,数值上就会发生一次 的跳变。把这种圆周拓扑硬塞到 MSE/L1 这种平直空间上的回归 loss 里,梯度方向是错的(驱使模型绕远路而不是走最短弧),优化也不稳定,所以这种方法不行。

通过实验发现, 表示学习其实也挺困难的,经常会发生,因为相位出错。导致被loss惩罚,但是最后惩罚的结果却不是旋转(改变相位),而是把负数的模惩罚的越变越小等问题,不太稳定。

后来想到了 这种方式,不强制约束后两个数的平方和为一,而是通过一个loss去惩罚它,让网络自己学到,在二维平面上把这个点从高斯分布的任意采样流动到单位圆上的目标点。

目标点在单位圆上,所有目标点之间的线性插值可能经过的点的集合就是所有目标点的凸包,单位圆的凸包就是单位圆。因此,训练的路径应当尽可能经过单位圆的任何地方,避免发生OOD。

采样分布先确定为二维高斯分布,然后约束为在单位圆内的概率质量为0.95,这样可以确定出标准差大约为0.4左右。由于浮点计算精度的问题,有可能会略微超出单位圆的范围,因此允许采样点的模大于单位圆一些,这里取1/8,就得出了最终分布为一个二维的标准差大约为0.4的高斯分布在模为1.125处截断。实验发现,这样直接比使用二维的标准正态分布的收敛快很多。

DSP

实验中rubberband和WORLD都试过,也都训了相应的网络;

虽然rubberband质量较好但是rubberband的可调节f0的帧率看起来不够;而且无极调节f0也不是它支持很好的功能

WORLD的缺点就是会偶尔出现嘶哑导致神经网络完全恢复不过来

学习范式:Flow Matching + BERT

把任务建模成“在 STFT 序列上做局部填空”。一段时长 T 帧的 clean STFT 序列被切成两类:context 帧(用户没动过、保留 clean)和 masked 帧(用户在 Melodyne 上拖过的那些音符所覆盖的帧)。模型只在 masked 帧上重新生成 STFT,同时可以双向看到两侧 context 帧的真实 clean 频谱——这正是 BERT 风格 masked language modeling 的直接搬运,区别只是把判别式的 token 分类换成对连续向量的生成。

生成部分用 conditional flow matching:

为了让模型学到“输入已经是好的就不要改”,5% 的样本强制 artifact = clean(identity 样本),mask 仍然正常采样。

训练数据据此天然成对:clean 音频 提供 ;对 整段跑 DSP(先变调再变回来)得到 artifact 作为条件;mask 区间从 DSP envelope 的若干段里随机选 2 6 段、再做 ±50ms 中心抖动后合并。

网络结构

这部分其实不是很重要,最主要的几个点就是:

其他

Loss

数据

包括opencpop, 两个日语数据集,ccmusic,vocal92,以及一些自己去5sing、b站爬的片段,以及一些自己唱的片段…但是数据量很小,总共加起来也只有50h左右。

推理

Flow matching通过4~16步的ODE积分,从高斯噪声积分到预测音频。

这种软件在命令行或者编程的方式使用其实很不方便,因此我移植了一份到苹果的GPU上,使用苹果机器学习框架MLX的Rust绑定实现。

这部分完全让LLM参考PyTorch的实现编写,使用的模型是Cursor Composer 2.5。Prompt如下:

## 移植

现在我希望把sar的推理 1:1 用Rust的mlx-rs复刻。
原代码目录:sar/impl/v5
port目录:src/sar , 示例推理写在一个example里面。
参考Python的实现进行推理;但是r3r使用和当前项目相同的库;

包括所有完整的预处理、后处理逻辑和整个深度学习模型的全部内容。
注意:
- 不允许进行任何的简化,这是一个生产上需要使用的,为了抛弃Python运行时的移植;
- 不允许改变任何逻辑,包括数值精度、运算使用的类型等等。同时你应该和原本的PyTorch实现打印日志对齐每一步的精度,因为部分mlx-rs的API即使输入相同,输出也和Python的API不同,可能体现在
- 维度顺序定义的不同;
- 精度的不同;
- 等等...
- mlx实现的输出结果应当和Python的torch实现在精度允许范围内完全对齐
- 不允许“为了通过测试”而无限制的降低精度要求。
- 不允许为了实现方便做任何的妥协。

=== 备注
所有依赖都必须使用cargo add添加,若非必要,不能修改manifest文件。
如果你发现你使用的API无法编译通过,这说明你的训练数据是陈旧版本的库,你应该查阅源码,获取新的API,不允许通过降级依赖库来通过编译。

mlx-rs使用这个版本:
mlx-rs = { git = "https://github.com/blossom-slopware/mlx-rs.git", rev = "b81194a47c2c6ba4ecb0cf370e4f0d941f52dd5d", features = ["accelerate", "metal"] }
每次需要使用mlx-rs的API的时候,你必须要先阅读源码,然后再写代码.直接写代码几乎肯定会出错.
禁止使用程序化的方式修改任何代码,包括heredocs,python脚本,sed,等等一切方式。

然后用Rust egui实现了一个GUI界面。其中F0检测使用的模型是FCPE,歌声转MIDI使用的模型是GAME。