跳到正文
Hacker News· epestr·· 6 天前AI 评分34

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

How I reverse engineered a commercial spatial audio effect

AI 导读

作者在 Linux 上用 PipeWire 复刻一款商业空间音频效果器,通过录制脉冲响应和正弦扫频来测量其滤波器行为,而非猜测内部算法。他先用 MIT KEMAR 和 SADIE II 的 HRTF 数据库尝试,发现无响室录音效果差,SADIE II 带房间响应更接近目标。测试文件包含不同幅度的脉冲、左右声道及反相组合,并用正则化公式从扫频结果中估计传递函数。

正文 · AI 翻译

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

0:00 / 0:10

戴上耳机聆听。片段来自 Bacao Rhythm & Steel Band 的 P.I.M.P.[0]。

处理后的版本听起来离耳朵更远,感觉不那么“贴在耳朵上”,因此在听了一段时间音乐后,感觉不那么“堵塞”。

第一次尝试

由于我是为 Linux 编写的,我的基础技术栈是 PipeWire[1]。我之前没有音频方面的经验,所以我开始用 LLM 作为探讨对象,同时试验标准音频滤波器和心理声学技术。

在此过程中,我了解到了 HRTF,它描述了来自特定方向的声音如何到达耳朵,取决于听者的方向和几何形状。我先尝试了 MIT KEMAR 数据库[2],觉得听起来很糟糕,因为它是在无响室中录制的,所以没有房间回声,而房间回声在这里是一种理想的心理声学效果。结果听起来廉价,而且更向耳朵内部。后来我找到了 SADIE II 数据库[3],它也带有房间响应,听起来更接近我记忆中的效果。

但这种方法很快显露出局限。LLM 开始谈论诸如“感知包围感”、“晚期场去相关”、“频谱扩散”和“漫射空间能量”之类的东西。[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