一段录音会以两种不同的方式变糟糕,而修复它们的方法并不重叠。
第一种方式:背景噪声叠加在语音之上——车流声、风扇声、房间的嗡嗡声。要紧的信号是存在的;只是混进了别的东西。去除这个,叫 降噪。
第二种方式:录音一开始就从未捕捉到完整的信号。电话音频每秒采样 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 kHz | 8.7–14.9 MB | Apache-2.0 |
| mossformer2 | 48 kHz | 229 MB | Apache-2.0 |
| frcrn | 16 kHz | 57.5 MB | Apache-2.0 |
| mpsenet | 16 kHz | 9.7 MB | MIT |
| gtcrn | 16 kHz | 0.54 MB | MIT |
| cmgan | 16 kHz | 7.8 MB | MIT |
| metadenoiser | 16 kHz | 19–34 MB | CC-BY-NC-4.0 |
| mossformergan | 16 kHz | 17.7 MB | Apache-2.0 |
| voicefixer | 44.1 kHz | 415 MB | MIT |
| deepfilternet | 48 kHz | ~2 MB | MIT |
带宽扩展器:
| 引擎 | 输入 | 大小 | 许可证 |
|---|---|---|---|
| lavasr(默认) | 8–48 kHz | ~52 MB | Apache-2.0 |
| novasr | 16 kHz | ~0.2 MB | Apache-2.0 |
| flowhigh | 任意 | ~200 MB | MIT |
| hifiganbwe | 任意 | ~4 MB | MIT |
| apbwe | 任意(12 kHz 频带) | ~120 MB | MIT |
| sidon | 16 kHz | ~410 MB | MIT |
| callenhancer | 8–16 kHz | ~3 GB / ~1.3 GB int8 | CC-BY-NC-4.0 |
库中最小的模型 gtcrn 只有 0.54 MB。最大的 voicefixer 是 415 MB——大了将近 800 倍,而且它做的是一件不同的事:它是一个 修复 模型,把噪声、混响、削波和缺失带宽合在一起处理,而不是一次只解决一个问题。
大多数权重是 MIT 或 Apache-2.0 许可。有两个不是:metadenoiser 和 callenhancer 采用 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 构建的已发布结果就能保持可复现,而一个不同的架构也依然可供比较。
在带宽扩展这一侧,sidon 和 callenhancer 做的是与 lavasr 或 novasr 不同的工作:它们不是在现有信号之上添加一个听起来合理的高频段,而是通过一个神经声码器从零重新合成语音,这能修复带宽扩展器碰不了的编解码损伤——代价是计算成本高得多。callenhancer 是专门针对电话音频训练的,这正是它的权重带有非商用许可证的原因。
哪些没能收录进来
只有当一个引擎能导出为单一的静态 ONNX 计算图、能通过 onnxruntime 在 CPU 上运行、携带明确的许可证,并且已经端到端地对照原始实现验证过——而不仅仅是对照原始模型,因为一张与网络本身匹配、但与其周边归一化处理不匹配的计算图,会产出听起来还不错、但实际上悄悄出错的音频——audiosronnx 才会收录它。
这个项目的 docs/not-shipped.md 记录了它评估过并拒绝的每一个候选模型及具体原因,这让它成为这个仓库里最有用的文档之一,因为它展示的是”纯 ONNX、仅 CPU”如今实际能做到什么的真实边界,而不是空口断言。
迭代式采样器没有静态计算图可导出。 扩散模型和流匹配模型在每句话上都要多次运行一个网络,循环长度在导出时并不固定。AudioSR(一个约 6 GB 的潜在扩散流水线,带有独立的 VAE、LDM 和声码器)和 SGMSE 都属于这一类——SGMSE 自己在 2025 年推出的流式后续版本,也只能在消费级 GPU 上达到实时速度,更不用说 CPU 了。
位置可变卷积看起来会让模型出局,但大多不会。 resemble-enhance 长期以来因为其声码器中的位置可变卷积 LVCNet 而在这份文档中被拒收,理由是那些通过 unfold 和 einsum 逐位置预测出来的卷积核,无法折叠进一张静态计算图。直接测试后发现这个判断是错的——这两个算子都有 ONNX 对应实现。真正的失败是另一个、已被充分理解的追踪错误(“针对未知形状卷积核的 ONNX 导出”),这个问题已经在代码库别处、针对 BigVGAN 的重采样器解决过了。真正让 resemble-enhance 出局的是规模:四个网络,包括一个 CFM ODE 采样器和一个自编码器,运行在 44.1 kHz——这是一个取舍决定,而非做不到。
有些模型根本没有可导出的训练产物。 RNNoise 是手写的 C 代码发布的,而不是某个可训练框架里的一张计算图——移植它意味着从零重新训练一个等效的网络。Fast-ULCNet 只公开了架构代码,完全没有检查点。
限制性许可证是一个标注决定,而非自动出局的理由——这正是 callenhancer 和 metadenoiser 依然被收录的原因。真正会导致出局的是完全没有许可证发布的权重:mdctGAN 正是因此被拒收的,再加上它基于 torch.fft 的前端本来就无法可靠导出。
在模型内部调用 torch.stft 是一个真正的结构性障碍。 NU-Wave2 的采样循环本身不是问题——那可以像其他每个引擎的 STFT 一样在计算图之外用 numpy 运行。真正挡住它的是它的 forward 方法内部调用了 torch.stft 和 torch.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 语音库。
在音频抵达一个识别器、一个说话人识别系统,或一位人类听众之前把它清理好,本身就是一项独立的工程工作,有自己的一套权衡取舍注册表,也有自己的一份”尝试过但没能在真实信号面前存活下来”的方法清单。
关于把这套方法应用到某条具体流水线上的问题:联系我们。