全部文章

· Casimiro Ferreira· 2 分钟阅读

清理糟糕的音频:audiosronnx 中的降噪与带宽扩展

  • ONNX
  • denoising
  • bandwidth extension
  • speech

一段录音会以两种不同的方式变糟糕,而修复它们的方法并不重叠。

第一种方式:背景噪声叠加在语音之上——车流声、风扇声、房间的嗡嗡声。要紧的信号是存在的;只是混进了别的东西。去除这个,叫 降噪

第二种方式:录音一开始就从未捕捉到完整的信号。电话音频每秒采样 8,000 次(8 kHz);一段全质量的录音通常是 48 kHz。采样率 决定了一个数字信号能表示的最高频率,因此一通 8 kHz 的通话在 4 kHz 以上完全没有内容——不是被静音了,也不是被滤掉了,只是从来没被录下来过。让这段音频重新听起来完整,意味着要凭空构造出那些从未被捕捉到、但听起来合理的高频内容。这叫 带宽扩展

audiosronnx 把这看作两个不同的问题,有两个不同的入口,因为用错了工具就会做错事。对一个带噪信号运行一个带宽扩展器,它只会忠实地重建出噪声的高频版本。降噪必须先做。

from audiosronnx import load_denoise, load_sr

clean, rate = load_denoise("dpdfnet").denoise("noisy_call.wav")   # remove noise
wide, _     = load_sr("lavasr").upscale(clean, rate)              # extend to 48 kHz

load_denoise()load_sr() 会拒绝对方的引擎——向 load_denoise 请求一个带宽扩展器,会抛出错误,而不是悄悄做错事。

为什么这要放在识别之前

语音识别和说话人识别通常是在相对干净的音频上训练出来的。给一个识别器喂 8 kHz 的电话语音,或是底噪有风扇声的语音,词错误率就会上升——不是因为模型不好,而是因为输入已经不再像它训练时见过的样子了。用于身份识别或说话人分离的说话人嵌入也是同样的道理:噪声和缺失的带宽,会扭曲这些嵌入所依赖的那些精确声学细节。

这就使得清理成为识别 之前 的一个流水线阶段,而不是识别的替代方案。一条真实的、面向嘈杂 8 kHz 电话通话的流水线是这样的:先降噪,再扩展到 48 kHz,然后对结果运行识别或说话人识别。不先修复输入就换上一个更好的识别器,是把力气用错了地方——无论模型多好,都会在同一个受损的信号上退化。

引擎注册表

audiosronnx 提供十个降噪器和七个带宽扩展器,全部可以通过 load_denoise() / load_sr() 按名字加载,全部是纯 ONNX,运行时不需要 torch。

降噪器:

引擎采样率大小许可证
dpdfnet(默认)8/16/48 kHz8.7–14.9 MBApache-2.0
mossformer248 kHz229 MBApache-2.0
frcrn16 kHz57.5 MBApache-2.0
mpsenet16 kHz9.7 MBMIT
gtcrn16 kHz0.54 MBMIT
cmgan16 kHz7.8 MBMIT
metadenoiser16 kHz19–34 MBCC-BY-NC-4.0
mossformergan16 kHz17.7 MBApache-2.0
voicefixer44.1 kHz415 MBMIT
deepfilternet48 kHz~2 MBMIT

带宽扩展器:

引擎输入大小许可证
lavasr(默认)8–48 kHz~52 MBApache-2.0
novasr16 kHz~0.2 MBApache-2.0
flowhigh任意~200 MBMIT
hifiganbwe任意~4 MBMIT
apbwe任意(12 kHz 频带)~120 MBMIT
sidon16 kHz~410 MBMIT
callenhancer8–16 kHz~3 GB / ~1.3 GB int8CC-BY-NC-4.0

库中最小的模型 gtcrn 只有 0.54 MB。最大的 voicefixer 是 415 MB——大了将近 800 倍,而且它做的是一件不同的事:它是一个 修复 模型,把噪声、混响、削波和缺失带宽合在一起处理,而不是一次只解决一个问题。

大多数权重是 MIT 或 Apache-2.0 许可。有两个不是:metadenoisercallenhancer 采用 CC-BY-NC-4.0 许可,非商用。这个许可证约束的是权重本身,而非用它处理过的音频,库在每一个使用点都会说明这一点——audiosronnx list 会按引擎报告许可证信息。没有什么能阻止调用方因为其时域架构而选择 metadenoiser,但这个选择必须是知情做出的。

之所以维护这样一个注册表,是因为没有哪个单一模型能在所有录音上都赢。dpdfnet 是默认选项,因为它不需要额外依赖,一个模型就能覆盖 8、16 和 48 kHz。mossformer2 是在全频带输入上测得的最佳选择。mossformergan 在已收录的降噪器中,公开的 PESQ 分数最高(3.47)。当约束条件是体积时,gtcrn 是首选,只有 0.54 MB。在一段带有宽带高斯噪声的测试片段上,各降噪器在 19 dB 输入信噪比下恢复了 3.5 到 5.9 dB 的信噪比,在更困难的 5 dB 输入下升至 7.5–13.7 dB。这是一个合成的、恶劣的噪声场景:它能一致地给各引擎排序,但对咿呀噪声或编解码伪影说明不了太多——这正是这个注册表保留十个模型、而不是只发布赢家的原因。

cmgan 是”明知落后仍刻意保留”这一点最清楚的例子:在 PESQ 和信噪比两项上,它都被体积只有它十四分之一的 gtcrn 全面超越,但它依然留在那里——这样一来,基于 cmgan 构建的已发布结果就能保持可复现,而一个不同的架构也依然可供比较。

在带宽扩展这一侧,sidoncallenhancer 做的是与 lavasrnovasr 不同的工作:它们不是在现有信号之上添加一个听起来合理的高频段,而是通过一个神经声码器从零重新合成语音,这能修复带宽扩展器碰不了的编解码损伤——代价是计算成本高得多。callenhancer 是专门针对电话音频训练的,这正是它的权重带有非商用许可证的原因。

哪些没能收录进来

只有当一个引擎能导出为单一的静态 ONNX 计算图、能通过 onnxruntime 在 CPU 上运行、携带明确的许可证,并且已经端到端地对照原始实现验证过——而不仅仅是对照原始模型,因为一张与网络本身匹配、但与其周边归一化处理不匹配的计算图,会产出听起来还不错、但实际上悄悄出错的音频——audiosronnx 才会收录它。

这个项目的 docs/not-shipped.md 记录了它评估过并拒绝的每一个候选模型及具体原因,这让它成为这个仓库里最有用的文档之一,因为它展示的是”纯 ONNX、仅 CPU”如今实际能做到什么的真实边界,而不是空口断言。

迭代式采样器没有静态计算图可导出。 扩散模型和流匹配模型在每句话上都要多次运行一个网络,循环长度在导出时并不固定。AudioSR(一个约 6 GB 的潜在扩散流水线,带有独立的 VAE、LDM 和声码器)和 SGMSE 都属于这一类——SGMSE 自己在 2025 年推出的流式后续版本,也只能在消费级 GPU 上达到实时速度,更不用说 CPU 了。

位置可变卷积看起来会让模型出局,但大多不会。 resemble-enhance 长期以来因为其声码器中的位置可变卷积 LVCNet 而在这份文档中被拒收,理由是那些通过 unfoldeinsum 逐位置预测出来的卷积核,无法折叠进一张静态计算图。直接测试后发现这个判断是错的——这两个算子都有 ONNX 对应实现。真正的失败是另一个、已被充分理解的追踪错误(“针对未知形状卷积核的 ONNX 导出”),这个问题已经在代码库别处、针对 BigVGAN 的重采样器解决过了。真正让 resemble-enhance 出局的是规模:四个网络,包括一个 CFM ODE 采样器和一个自编码器,运行在 44.1 kHz——这是一个取舍决定,而非做不到。

有些模型根本没有可导出的训练产物。 RNNoise 是手写的 C 代码发布的,而不是某个可训练框架里的一张计算图——移植它意味着从零重新训练一个等效的网络。Fast-ULCNet 只公开了架构代码,完全没有检查点。

限制性许可证是一个标注决定,而非自动出局的理由——这正是 callenhancermetadenoiser 依然被收录的原因。真正会导致出局的是完全没有许可证发布的权重:mdctGAN 正是因此被拒收的,再加上它基于 torch.fft 的前端本来就无法可靠导出。

在模型内部调用 torch.stft 是一个真正的结构性障碍。 NU-Wave2 的采样循环本身不是问题——那可以像其他每个引擎的 STFT 一样在计算图之外用 numpy 运行。真正挡住它的是它的 forward 方法内部调用了 torch.stfttorch.istft,这正是这个库在它发布的每一张计算图中都刻意排除的东西,也是总体而言导出可靠性最差的算子。修复它意味着要在变换边界处拆分模型,这是真正的重构,而不是换个算子那么简单。

复现一个模型的架构,不等于复现它的输出。 LiSenNet 只有 5.6 万参数,不到 300 KB——它本会是这个库里最小的引擎。它公开可用的 ONNX 移植版本能运行,也能产出看起来合理的、经过衰减的音频,但端到端测量下来它其实是在破坏信号:11 dB 输入下信噪比为 −10.8 dB。精确复现该移植版本自身的参考实现,得到的是完全相同的负值结果,这说明那份参考实现本身,就与它自己文档所描述的前端不匹配——目前还没有一个正确的目标可供验证。

贯穿这些案例的规律是:真正有意思的失败,很少是”模型太大”或”扩散太慢”这种笼统的理由。它们都很具体:一个没有 ONNX 算子、但有精确替代方案的操作(torch.complex 没有 ONNX 算子,但 atan2(im, re) 计算的是同一个相位角)、一个由输入的运行时形状构造出来、追踪器无法固定下来的张量,或是一个被放在了计算图边界错误一侧的变换。

确认输出真的变好了

一个能运行的 ONNX 文件,并不能证明一段录音变好了。两种不同的失败模式从外部看起来一模一样:一个把语音和噪声一起消音的降噪器,以及一个添加了谐波内容错误的高频段的带宽扩展器,都会产出播放时没有错误、甚至粗听起来更干净的音频。

姊妹库 speechonnxmetrics 存在的意义,就是把这种判断变成可测量的,而不是凭印象。它以两种方式为音频的 MOS(平均意见得分,一个 1 到 5 分的感知质量评分)打分:像 DNSMOS 和 UTMOS 这样的无参考神经预测器,可以在没有干净原始音频可供比较的情况下为一段录音打分;以及像 STOI 和 SI-SDR 这样的侵入式指标,需要一个干净的参考音频,衡量输出与它究竟有多接近。

import speechonnxmetrics as s

s.score("clean.wav", ["utmos"])
# -> {'utmos': 4.41}

s.score("denoised.wav", ["stoi", "si_sdr"], ref="clean.wav")
# -> {'stoi': 0.66, 'si_sdr': -26.9}

在一次降噪或扩展前后各运行一次,一次真正的比较的形态就浮现出来了:用 DNSMOS 或 UTMOS 分别测原始音频和处理后的音频,看感知质量是否真的发生了变化;而在存在干净参考音频的情况下——合成噪声测试中有,但真实电话通话中很少有——用 SI-SDR 或 STOI 看处理后的信号是否真的向参考音频靠拢,而不只是听起来不一样了。这与上面降噪器表格中信噪比数字背后的原则是同一套:一个绑定了具体噪声条件的数字,而不是一个形容词。这套纯 ONNX 库家族更广泛的图景,包括 speechonnxmetrics 本身,参见 一族纯 ONNX 语音库

在音频抵达一个识别器、一个说话人识别系统,或一位人类听众之前把它清理好,本身就是一项独立的工程工作,有自己的一套权衡取舍注册表,也有自己的一份”尝试过但没能在真实信号面前存活下来”的方法清单。

关于把这套方法应用到某条具体流水线上的问题:联系我们