Skip to content

Latest commit

 

History

History
308 lines (230 loc) · 18.9 KB

File metadata and controls

308 lines (230 loc) · 18.9 KB

参考对消实现

简体中文 · English

问题定义

设待处理的双声道录音为 $\mathbf{y}(t)$,歌曲音源为 $\mathbf{x}(t)$,现场人声、讲话和环境声等音源中 不存在的内容为 $\mathbf{v}(t)$。参考对消在 STFT 域估计随时间和频率变化的复数 $2\times2$ 传递矩阵:

$$ \mathbf{y}(t) \approx \mathbf{H}(t)\mathbf{x}(t) + \mathbf{v}(t) $$

对消分两级,结构与声学回声消除一致:先用复数减法把预测出的垫音成分从混音中去掉, 再用软掩码抑制线性级未能干净描述的残余。记预测为 $\mathbf{d}=\mathbf{H}\mathbf{x}$,第一级是

$$ \mathbf{e}=\mathbf{y}-\gamma,\mathbf{d},\qquad \lvert\mathbf{e}\rvert\leftarrow\min\bigl(\lvert\mathbf{e}\rvert,\lvert\mathbf{y}\rvert\bigr) $$

第二式把线性级约束为只能减少能量,不会凭空增加。残余里仍然留有窗外混响、残余失配这类传递函数 描述不了的垫音,其大小由残余功率对已减去功率的回归给出(见“泄漏的估计”),再交给联动软掩码去除:

$$ \widehat P_v=\max(P_e-\rho P_d,,0.05^2P_e),\qquad M=\sqrt{\frac{\widehat P_v}{P_e+\varepsilon}} $$

强度 $\alpha\in[0,1]$ 在复频谱上线性插值:

$$ \mathbf{s}=\mathbf{y}+\alpha,(M\mathbf{e}-\mathbf{y}) $$

不执行相减时 $\mathbf{e}=\mathbf{y}$,上式化为 $M_\alpha=1-\alpha(1-M)$,也就是原来的纯掩码路径; 因此 $\alpha=0$ 仍然表示完全不做处理,$\alpha$ 仍然单调。

相减之所以安全,是因为逐频点的最小二乘预测本身就是 $\mathbf{y}$$\mathrm{span}(\mathbf{x})$ 上的投影。参考里有而混音里没有的内容,例如改过的歌词或音源独有的和声, 满足 $\mathbb{E}[y\overline{x_j}]\approx 0$,于是 $h$$d$ 都趋近于零,这类内容根本不会被预测出来, 也就无从注入。$\gamma$ 在这些位置同样趋于零,再加上上面的不放大约束,这三重保护彼此独立。

参考对消成立的前提,是舞台 / 现场音频中包含与歌曲音源对应的垫音内容。编曲不同、升降调、明显的动态 处理或额外乐器,都难以仅靠时间和增益校正解决。

总体流程

flowchart LR
    mix["待处理录音"] --> read["读取并双声道化"]
    ref["歌曲音源"] --> read
    read --> rate["统一采样率"]
    rate --> tempo["整体倍速估计<br/>WSOLA 保音高拉伸"]
    tempo --> enabled{"自动对齐?"}
    enabled -->|是| coarse["谱通量 / GCC-PHAT<br/>粗对齐"]
    coarse --> drift["局部漂移跟踪"]
    drift --> warp["Lanczos 时间扭曲"]
    enabled -->|否| mask["复数传递估计<br/>相干对消 + 残差掩码"]
    warp --> mask
    mask --> protect["峰值保护"]
    protect --> output["WAV<br/>沿用歌曲规格"]
Loading

时间对齐

整体倍速

舞台录音可能被整体变速后重新上传,音高不变而速度偏移几个百分点。局部漂移跟踪每 0.1 秒最多修正 2 毫秒,也就是每秒 20 毫秒,3% 的偏差它整段都追不上,粗对齐也因此找不到峰。

实现先用 4 kHz 单声道代理做一次全局标尺搜索:把音源的起音特征按候选倍速在时间轴上线性插值,用与下面 粗对齐同一种谱通量特征与舞台代理的前两分钟做归一化互相关,取整个滞后范围内的峰值。倍速对每个音源是 一个常数,更长的素材实测给出相同结果,两分钟之后多出来的部分只会拉长重采样与互相关。插值的是特征而 不是音频——两者 给出的峰值在实测中只差一个网格,而按音频重采样再重算特征要贵十倍,一个音源的搜索因此从 2.4 秒降到 0.23 秒。候选先在 0.88~1.15 之间按 0.004 步长粗扫,再在峰值附近按 0.0005 精修。

只有峰值同时满足以下条件才采纳:得分不低于 0.25、比峰值邻域之外的次优高出 10%、比“倍速为 1”的得分 高出 0.04,且倍速变化不小于 0.2%。最后一条尤其重要——它保证本来就对齐的素材仍走原来的路径,不会因为 一次长互相关的偶然峰被拉偏。音源比舞台还长时,每个候选都裁到舞台长度再比,否则“不变”这个假设根本 无从度量。

采纳后按倍速做保音高时间拉伸,用波形相似叠加(WSOLA):每一输出帧在标称位置 ±7.5 毫秒内搜索与上一帧 波形最连续的输入段,加 64 毫秒汉宁窗按 50% 叠加,立体声各声道共用同一个偏移以保住声道间相干。这里 不能用重采样代替:重采样会把 3% 的速度差变成 51 音分的音高差,逐频点的复数传递再也对不上。也不能用 朴素相位声码器:在同一段真实素材上端到端测量,不拉伸时对消深度 −0.05 dB,相位声码器 −1.46 dB, WSOLA −2.99 dB,与 ffmpeg atempo 的 −3.15 dB 基本持平;相位声码器抹掉的正是承载相干能量的瞬态。

拉伸后的参考再进入下面的粗对齐与局部漂移跟踪,此时剩下的只是微小残余漂移。shared/dsp/rate.pyshared/dsp/stretch.py 只用 numpy 与 scipy,因为网页版在 Pyodide 里跑同一条管线,没有外部二进制。

粗对齐

现场相机音轨与歌曲音源可能波形相关性很低,但音符起音位置仍相近。因此实现优先提取多频带谱通量特征 (shared.dsp.log_flux_bands(),与音源匹配共用),在最多约 60 秒的公共范围内寻找显著相关峰。 若某个声道的频带过于平坦,整个估计会被放弃;若特征峰不可靠,再依次尝试 GCC-PHAT 与普通互相关。

GCC-PHAT 对互功率谱只保留相位:

$$ R_{\mathrm{PHAT}}(\tau) =\mathcal{F}^{-1}!\left( \frac{Y(f),\overline{X(f)}} {\lvert Y(f),\overline{X(f)}\rvert+\varepsilon} \right) $$

相关峰对应粗略时延。默认先在较小范围搜索,峰落在边界附近时再扩大到最多约 20 秒,降低无意义远距离 匹配的概率。

局部漂移

不同录音设备的时钟误差会使起点正确、结尾逐渐错位。实现把音频重采样为 16 kHz 的抗混叠代理信号, 每 0.1 秒估计一次局部时延,并限制相邻估计的最大变化。低相关窗口沿用预测值,最后使用三点中值滤波抑制 跳变。

决定局部时延精度的是代理信号的带宽而不是它的采样格。在一段带漂移的宽带参考上端到端测量,代理 采样率从 2 kHz 提高到 16 kHz 使对消深度上升约 10 dB,而对齐耗时基本不变——跟踪循环仍然每 0.1 秒 推进一格,每格的相关只是变长。

时延曲线保持在代理采样格上,不做亚采样精化:恒定的小数延迟已经被逐频点的复数传递当作相位斜坡吸收。

得到时延曲线 $d(t)$ 后,参考信号按

$$ x_{\mathrm{aligned}}(t)=x\bigl(t-d(t)\bigr) $$

重采样。非整数位置由半径为 3 的 Lanczos 核插值,过程按块写入映射文件。核只依赖源位置的小数部分, 因此改为查 8192 个相位的预计算表,避免对每个输出样本重复求 sinc;表的量化误差约在 −80 dBFS。 局部跟踪窗口的滑动能量用前缀和计算,从 O(N·M) 降为 O(N)。

相干对消

STFT 窗口目标约 46 ms,并按采样率取最接近的 2 次幂,限制在 512~4096 点;帧移为窗口的四分之一。 每个频点根据平滑的参考协方差和混音与参考的互功率估计复数传递矩阵。协方差加入对角正则, 每个输出声道的总传递增益限制为 2 倍。

正规方程为 $R,h=c$,其中 $R_{jk}=\mathbb{E}[x_k\overline{x_j}]$$c_j=\mathbb{E}[y\overline{x_j}]$下标顺序不能写反:把 $R_{jk}$ 填成 $\mathbb{E}[x_j\overline{x_k}]$ 会转置整个方程组并解出另一个 传递,在左右声道强相关且带小的声道间时延时(真实立体声垫音的常态)实测损失约 3.4 dB 对消深度。实现 只构造下三角,并用逐频谱向量化的 $LDL^{H}$ 分解求解;把每个时频单元堆成张量交给 numpy.linalg.solve 会让 LAPACK 逐单元调用,实测慢约 7 倍。

传递估计的可靠性

相减多少取决于传递估计有多可信,所以这个可信度最好不是事后另算的量。在最小二乘最优点上 $\mathbb{E}[|Hx|^2]=h^{H}c$,参考究竟解释了混音多少,可以直接从求解结果里读出来:

$$ \gamma^2=\frac{\mathrm{Re},(h^{H}c)}{P_y+\varepsilon} $$

这个多重相干在解方程时顺带就有了,不必在混音与预测之间另算一次互谱相干。

有限窗口上的最小二乘总会碰巧解释掉一部分混音,期望大小恰好是 $p/N_{\text{eff}}$,其中 $p$ 是阶数。 把它扣掉就得到调整后的相干:

$$ N_{\text{eff}}=\max\Bigl(W\cdot\tfrac{\text{hop}}{n_{\text{fft}}}\cdot 1.5,;p+2\Bigr),\qquad \gamma^2_{\text{adj}}=1-(1-\gamma^2)\frac{N_{\text{eff}}}{N_{\text{eff}}-p} $$

其中 $W$ 是平滑窗的帧数,1/8 是相邻帧的等效独立度,1.5 是频率轴 $\sigma=1$ 高斯的等效独立频点数。 帧独立度是实测定下来的,不是推导出来的:75% 重叠按 $\text{hop}/n_{\text{fft}}$ 直接读会得到 1/4, 但 Hann 窗在重叠帧之间本身就有相关性,频率高斯在四频点主瓣下的等效独立频点数也比标称的少,两者都让 这个读法过于乐观。这项修正带来的性质可以直接测出来:面对一段完全无关的参考,程序会精确地保持原样 (相关系数 1.000000、RMSE 为 0)。“无关的音源不该被减掉”正来自它。

代入之前,$W$ 还要先按本块实际拥有的帧数截断。盒滤波在块边界是反射而不是凭空生成样本, 比整块还宽的统计窗只值这个块所拥有的帧数;照标称宽度读会高估观测数,而且恰好高估在过拟合最严重的 短块上。

传递的估计误差方差约为未解释功率的 $p/N_{\text{eff}}$ 倍,使均方误差最小的收缩量也就随之确定:

$$ \gamma=\frac{\gamma^2_{\text{adj}}}{\gamma^2_{\text{adj}}+\frac{p}{N_{\text{eff}}}\bigl(1-\gamma^2_{\text{adj}}\bigr)+\varepsilon} $$

参考什么都解释不了时 $\gamma$ 为零,完全不减;解释得越充分,越接近把预测全额减掉。

对角正则同样需要一个下限。只按局部参考功率按比例添加时,正则项会随着功率一起衰减:当某个频点的 参考瞬时静音而混音没有静音时,方程几乎无据可解,传递会一路放大,直到增益上限才被拦住。因此正则 不会低于该频点按自身中位数电平计算出来的量。

判断离群值所用的尺度也不能被离群值自己抬起来。平滑后的算术均值会被它本该抑制的人声瞬态拉高,改用 几何均值就没有这个问题,仍然是同一个 O(N) 平滑器,只是在对数域上读。

这个尺度所依据的预测按单个参考声道分别拟合,而不是用完整的双声道系统。尺度只需要知道残余在哪里起伏, 对角拟合实测端到端便宜 13%,在相位相关的立体声场景上反而略深(对消深度 +0.04 dB、保真度 +0.0002);真正做减法的仍然是完整系统,而那个专门用来惩罚声道交叉方向搞错的场景没有变化。

泄漏的估计

线性级之后剩下的相关成分主要是窗外的混响尾巴和残余失配。要问的是残余里有多少还是垫音,而这个问题 无法靠“只看参考主导的帧”回避:现场人声整段都在响时,根本不存在只有参考在响的帧,于是 $\rho=P_e/P_d$ 这样的无截距比值会把人声本身算成泄漏,掩码随后就把人声一起去掉。

泄漏因此由一次带截距的功率回归给出,形式与下面的非相干通道完全相同:

$$ P_e \approx \rho(f,n),P_d(f,n) + c(f,n) $$

$\rho P_d$ 随当时确实存在的垫音一起起伏,所以它是泄漏;缓变的 $c$ 不随之起伏,所以它是现场内容, 只有前一项允许被去除。$\rho$ 同样按调整后的相关做 smoothstep 门限,并约束为不超过它所在的残余, 弱证据的单格因此不会把掩码压到地板。

非相干功率通道

在真实素材上,失效模式并不是混响,而是相位关系整体消失。以一段 KWDA 舞台录音为例:对齐本身是 亚采样准确的(残余时延 0.00~0.04 ms),结构和速度也吻合(帧能量包络相关 0.896),但复数多重相干 在 0~100 Hz 尚有 0.68,500 Hz 以上就只剩 0.02~0.05。把分析窗从 43 ms 一路加长到 1365 ms,足足 32 倍,可减深度也只从 2.30 dB 抬到 4.24 dB,说明这不是“窗比混响短”能解释的。

不过同一段素材的幅度仍然相关,对数幅度谱逐带相关在 0.45~0.72 之间。因此在复数通道之外还并行做 一路功率域回归:

$$ P_y \approx g(f,n),P_x + c(f,n) $$

回归保留截距是有意的:$gP_x$ 是跟随音源变化的部分,缓变的 $c$ 是现场内容,只有前者允许被去除。 斜率约束为非负——音源里的伴奏更多,舞台上的量不可能反而更少——再按同样的自由度修正得到调整后的 相关,并用 smoothstep 设置阈值。无关素材的响度包络本身就有一定相关, 这道阈值就是“无关的音源不该被压制”的保障。

掩码最终去除的是两条通道中解释得更多的那一个,并扣除相干级已经减掉的部分。在这段舞台录音上,去除量 实测为 6.85 dB。

这条通道去除的是功率上跟随音源的内容,而不是波形上确实来自音源的内容,判断依据比相干通道弱得多。 让它直接驱动掩码会使掩码随弱证据逐格开合,在强相位失配的素材上听得出音乐噪声,因此有两道约束:

  • 单个时频格里非相干扣除最多只能吃掉一部分残余功率(_INCOHERENT_MAX_SHARE),弱证据再吵也 不能把掩码压到地板;
  • 掩码比值改为先对分子、分母做时间域功率平滑(_MASK_POWER_SMOOTH)再相除,把逐格起伏在进入 掩码之前摊平,最后仍保留原有的窄高斯平滑。

$\gamma^2$ 在 500 Hz 以上只有 0.02~0.05 时,用来判断“这一格究竟是不是垫音”的信息本身就不够, 因此该路径在低置信区间仍然会被 smoothstep 门槛关闭;其余可调参数为 _INCOHERENT_OVERSUBTRACTION_MASK_FLOOR_MASK_SMOOTHING

研究依据与边界

  • Gorlow、Ramona 与 Pachet 的现场独奏伴奏消除研究 比较了自适应噪声消除、频谱减法和短时 ERB 子带 Wiener 滤波; 本文实现采用频域复数相减加相干度加权软掩码,不改用时域 LMS,也不保留独立 ERB 投票层。
  • Boll 的经典谱减法论文 说明了幅度谱减和残留噪声问题;本实现保留谱减式功率目标, 但作用在复数相减之后的残差上,并使用逐频点泄漏比、掩码下限和窄时频平滑,避免直接硬减频谱。
  • Avery Lee 的 Center Cut 以及 ADRess 一类方法依赖中心声像、通道电平差或相位差,与本实现无关: 它们只看左右两个声道的空间关系,而参考对消看的是歌曲音源提供的证据。
  • 卷积传递函数(CTF)文献说明了乘性窄带近似在长混响下失效,以及用有限个跨帧抽头替代它的做法, 例如混合 CTF 模型的联合去混响与盲分离。 Schröter 等人的 DeepFilterNet 以及 Tammen 与 Doclo 的深度多帧 MVDR 是同一思想的学习型形态: 为每个时频点估计跨帧复数滤波器,而不是逐点掩码。
  • 结构相同的工程实现可作对照:WebRTC 的 AEC3 同样是“已知参考 + 未知传递 + 双讲干扰”, 其延迟估计、线性回声对消与残余回声抑制逐级对应本文的对齐、复数相减与残差软掩码; 本实现按块批量最小二乘求解,因此不像 AEC3 那样跨块递推,块之间可以并行。

这些论文提供的是算法结构和失败边界,并不保证某一段真实演出一定会改善;最终的判断标准仍是相同片段、 相同响度下的 A/B 试听。

输出保护与验证

处理结束后会把非有限值替换为有限数,并在需要时限制峰值以防越界。结果直接按歌曲自身的采样率与位深写出 PCM WAV:对消不改变时间轴,也不产生新的频谱,把导出规格抬到固定下限只会让文件变大。

算法测试覆盖时间偏移、局部漂移、整体倍速(含不变假设的否决)、反极性、频率相关房间传递、无关参考、 参考独有改词否决、矩阵串扰、 正规方程取向(相位相关的立体声参考)、一直在响的现场源(守护泄漏回归的截距)和块接缝。 tools/eval_cancellation.py 另外提供离线 A/B 评测, 按场景报告对消深度与保真度,供改动前后对照。合成信号指标只用于发现实现回归; 真实素材仍需以相同输入和参数导出对照版本,重点听人声大小、齿音、呼吸、和声、混响尾音与观众声 是否自然。

导出内容

一次对消可以导出两条音轨。消音结果是管线本身的产物;垫音轨不从录音里减,而是把对消自己估计出的 传递套在音源上重新渲染。每个时频单元估一个实数功率增益

$$ g(f,n)=\sqrt{\frac{\widehat P_b(f,n)}{P_x(f,n)}} $$

分子是这场播放带过去的音源功率——相干级减掉的部分,与泄漏回归、非相干通道两条估计中更大的 那个,并约束不超过混音自身的功率;分母是音源自己的平滑功率。两边在同一段统计上下文里平滑后 再开方,得到一个缓变的实增益,乘在音源自身的频谱上。这样做的原因有两条:垫音保留音源的相位、 瞬态和细节,音质等于音源而不是现场采集;而复数传递在相干度低的地方相位本身就是噪声,直接 渲染会把功率通道好不容易恢复的内容抹开。增益上限为 2 倍,因为播放链路可以比母带更响,但拟合 在弱证据上的增益不能失控。

强度 $\alpha$ 同样缩放垫音,所以 $\alpha=0$ 导出的是静音,而不是一份没被减过的垫音。

只导出一条轨时,输出路径就是用户指定的那条;两条都要时,第二条由输出名派生——词尾的 _vocals 换成 _backing,没有这个词尾就直接追加。垫音渲染在自己的整长缓冲区里,因此导出两条轨会多占 一份整长缓冲,不再像相减那样就地覆盖。峰值保护按各自的峰值分别缩放两条轨:合成出来的垫音可以 比人声更响,为了保住一条而削另一条只会拿电平换失真。

因为垫音不再由相减得到,两条轨相加不再等于原始录音。垫音换来的,是音源级别的音质,以及未识别 区间里干净的静音——那里本来就没有做过对消。