跳到正文
原文
Hacker News· epestr·· 2 小时前精选AI 评分26

如何逆向工程一款商业空间音频效果

How I reverse engineered a commercial spatial audio effect

AI 导读

作者为 Linux 写空间音频程序时,针对一款商业效果器设计了一套测量逆向流程:通过 PipeWire 在朋友笔记本上播放含脉冲、正弦对数扫频、多频音和分频段噪声的测试文件并录制输出,再用正则化谱除法提取四通道 HRTF 类响应,最后加载进 Saq 复现出目标中的离头感。前期作者曾用 Gemini 作为思路讨论的辅助工具,但得到的描述无法落地,因此转向以测量驱动的方法。

推荐理由

作者给出针对黑盒音频效果的测量与重建流程,从激励信号设计到滤波器提取都列出步骤,方便迁移到其他效果器复现。

正文 · AI 翻译

早在我使用另一个操作系统的时候,我用过一款程序,它可以实时处理正在播放的音频,让声音听起来好得多。它让声音感觉更远,听起来也更轻盈,同时保留了相关信息。我一直想要一个同样易于使用、能在 Linux 上运行的类似程序,于是决定自己写一个。

0:00 / 0:10

请用耳机收听。片段来自Bacao Rhythm & Steel Band 的 P.I.M.P.[0]。

处理后的版本听起来离耳朵更远,不再那么"贴着耳朵",因此听一段时间音乐后也不会觉得那么"闷"。

首次尝试

由于是为 Linux 写的,我的基础技术栈是PipeWire[1]。我之前没有任何音频方面的经验,所以开始时我把大语言模型当作讨论伙伴,一边尝试标准的音频滤波器和心理声学技术。

在这个过程中,我了解了 HRTF,它描述的是来自特定方向的声音如何到达耳朵,这取决于听者的朝向和头部几何。我先试了MIT KEMAR 数据库[2],觉得听起来很糟,因为它是在消声室中录制的,没有房间反射声——而在这里,房间反射声正是我想要的心理声学效果。结果听起来既廉价又过于"贴耳"。后来我找到了SADIE II 数据库[3],它附带房间响应,听起来就更接近我记忆中的样子了。

但这条路很快暴露出了局限。大语言模型开始大谈"感知包络感"、"后期场去相关"、"频谱扩散"和"弥散空间能量"等概念。[1][1] Gemini 在这方面尤其出色,也是唯一一个有免费学生计划的模型。虽然我能听出问题出在哪里,但这些描述并没有指向任何我可以理解、可以用来改进效果的具体东西。

我把这个项目搁置了一段时间,因为没有明显的方向可以推进。

再次尝试

很久以后,我有了一个不同的想法。与其试图弄清楚程序内部使用的具体参数、技术和算法,不如去研究滤波器本身的行为。

事后看来这是显而易见的,但我手头并没有那个程序。我只能在朋友的笔记本上做实验,所以没法随时修改自己的实现并和参考效果做对比。

我开始阅读Signalsmith[4]的文章以及一些音频工程师的博客,逐步建立起足够的直觉,知道应该测量什么;很快我就意识到,我不必一直开着那个程序去捕捉它的脉冲响应和其他可测量特征。我在 Linux 上生成一个音频文件,借了朋友的笔记本一段时间,把生成的音频通过那个程序播放,再录制输出。

测试的组合

问题变成了:我应该往那个文件里放什么内容,才能一次性捕捉到那个程序区别于其他方案的相关细节,而不需要反复录制各种特征或滤波器。

我找到的第一件有用的事是脉冲响应测量。

如果我让一个只含单个非零采样的文件通过滤波器,那么录音中出现在它之后的所有内容都是滤波器添加的。诸如衰减随时间延伸的程度、衰减的形状以及与之相关的相位延迟等特征都会保留在输出中。在右声道播放该脉冲,可以提取两路输出对右声道声音的响应,左声道同理。

如果对单个脉冲的响应是 $h$,那么对位于位置 $k$ 的脉冲的响应,就是把同一 $h$ 平移到 $k$ 处再按 $x[k]$ 缩放的结果。把所有这些响应相加得到

$$ y[n]=\sum_k x[k]h[n-k]=(x*h)[n]. $$

这当然取决于滤波器是否为线性时不变滤波器[5]。

更响的脉冲应当产生按比例更响的相同响应:

$$ F(ax)=aF(x). $$

两个输入同时播放,应当产生各自输出之和:

$$ F(x_1+x_2)=F(x_1)+F(x_2). $$

而将同一输入延后播放,其响应也应相应延后。

由于是立体声,完整的等式应为:

$$ \begin{bmatrix} y_L\\ y_R \end{bmatrix} {}={} \begin{bmatrix} h_{LL} & h_{LR}\\ h_{RL} & h_{RR} \end{bmatrix} * \begin{bmatrix} x_L\\ x_R \end{bmatrix}. $$

由于我只发送一个文件,让它以所有模式进行录音,因此我加入了不同幅度的脉冲以检测与电平相关的处理,并在两声道同时播放同一脉冲,然后将右声道反相。第一种情况下输出应等于各孤立响应之和,第二种情况下应等于各孤立响应之差,从而验证上述条件。我还在脉冲之间留出了足够的静音,使响应在下一个脉冲之前结束。

三个电平的脉冲,分别在左声道、右声道、双声道同相以及双声道反相下播放。

在查阅过程中我发现,相关测量论文更常使用正弦扫频而非脉冲[6]。[2][2] 脉冲虽然已经包含所有频率,但由于只有一个采样,每个频率上的能量很少,因此较长的扫频信号能让响应更容易从录音噪声中区分出来。

一个缩短的对数扫频信号。

$$ Y(f)=H(f)X(f), \qquad H(f)=\frac{Y(f)}{X(f)}. $$

在大多数频段这都能奏效,但如果 $X(f)$ 接近零,用它去除录音噪声会得到一个看似不错、却与程序几乎无关的滤波器。这可以通过正则化版本来避免。

$$ \hat H(f)=\frac{Y(f)X^*(f)}{|X(f)|^2+\lambda}. $$

当输入能量足够时,这近似于原来的除法,而 $\lambda$ 会在其他位置限制结果。复数值还保留了相位,考虑到送入一个声道的声音可能以一定延迟出现在另一声道,这是必需的。

扫频提供了另一种可与脉冲对比的测量方式,但其播放时间要长得多,期间程序可能改变其行为。我加入了不同频率和幅度的稳态音,以及由轻到响再回到轻的较长音调,以观察增益是否变化以及变化所需的时间。

稳态音,缩短后通过两个声道播放。
在两个电平之间交替的 1000 Hz 音。

还有一个多音,用来检查是否有新频率出现:

八频率多音。

还有重复信号,用来检验时不变性:

多音的三份相同副本。

我留出了一段噪声,用来测试提取出的滤波器之后能否重现其录音输出。

依次覆盖低、中、高频段的噪声。

恢复滤波器

由于录音和播放是手动开始的,录音首先需要一个偏移量。我在开头附近放了均匀间隔的点击声,最初尝试通过将录音与原始点击声进行相关来找到它。由于滤波器会把每个点击声在时间上展开,我改为对一个窗口 $W$ 内的样本平方求和,以估计其周围的能量,

八个同步点击声。

$$ e[n]=\sum_{m\in W}\left(y_L[n+m]^2+y_R[n+m]^2\right), $$

并通过以下方式找到了偏移量

$$ \hat\tau=\arg\max_\tau\sum_n e_{\text{probe}}[n]e_{\text{recording}}[n+\tau]. $$

我对两个通道应用了相同的偏移量,以保留它们之间的延迟。

输出

对于主要的环绕声设置,缩放后的脉冲几乎相同,合并后的输入与它们各自响应的总和一致,多频信号几乎不产生新的频率。为了将重建的输出与录音进行比较,我使用了

$$ \varepsilon=\frac{\lVert y-\hat y\rVert_2}{\lVert y\rVert_2}, $$

其中 $y$ 是录音输出,$\hat y$ 是提取的滤波器产生的输出。鉴于相同的重复段落已经略有不同,我将模型的误差与该差异进行了比较,

$$ \varepsilon_{\text{model}}\approx\varepsilon_{\text{repeat}}\ll1. $$

降低音量的模式会随幅度改变其增益并需要时间恢复,而另一种空间模式在重复之间会发生变化,因此两者都无法用固定的卷积滤波器重现。

在每个输入通道施加脉冲并录制两个输出通道,得到了四个滤波器。

声音也出现在对面通道中,更安静且略有延迟,随后跟着一个长长的衰减。这些共同包含了我之前一直想做出的脱头效应。

为了决定保留多少响应,我针对噪声段尝试了越来越长的滤波器,

$$ \varepsilon_N= \frac{\lVert y_{\text{noise}}-h^{(N)}*x_{\text{noise}}\rVert_2} {\lVert y_{\text{noise}}\rVert_2}, $$

并保留了完整的响应,它比较短的版本更准确地预测了噪声。从扫频信号中提取的响应在具有足够输入能量的范围内也保持一致。

将四个响应加载到 Saq[7] 中,让我得到了我之前一直试图手工重现的定位效果。

你可以在这里试试这个效果。选择你自己的本地音频文件或捕获一个标签页(当不在 Firefox 浏览器[8] 中时),然后切换环绕声以进行比较。

参考文献

来源:Hacker News · epestr.com