音频质量评估与自动化测试

音频质量既要有客观指标也要有主观验证。本文讲解 SNR、THD+N、IMD 等客观指标的计算、PESQ 与 ViSQOL 等感知模型、MUSHRA 与 ABX 主观测试的设计、null test 与金样回归、响度与真峰值合规检查、实时链路的欠载与延迟测试,以及把音频回归接入 CI 的完整实践。

引言

音频质量评估是工程中最容易被敷衍的环节:主观听感难以量化,客观指标又常常与听感不一致。结果是很多音频项目只做"人耳抽查",上线后才发现批量产物有系统性缺陷。

实际上,音频测试可以做得相当扎实。客观指标能覆盖电平、失真、噪声、频响等确定性属性;感知模型能在一定程度上预测主观听感;主观测试(MUSHRA、ABX)在方法学成熟的情况下能给出统计显著的结果;回归测试能保证每次改动不引入退化。四者组合,就能构建一条可信的音频质量保障链路。

本文按"客观 → 感知 → 主观 → 回归"的顺序组织,最后落到 CI 集成与金样管理。核心观点是:音频测试的难点不在算法,而在测试用例设计、金样管理与容差设定。

目录

  1. 质量评估的两个维度:客观与主观
  2. 客观指标:SNR、THD+N 与 IMD
  3. 感知模型指标:PESQ、POLQA、ViSQOL、PEAQ
  4. 主观测试:MUSHRA、ABX 与 MOS
  5. null test 与 bit-exact 回归
  6. 响度、峰值与削波合规测试
  7. 实时链路的性能测试
  8. CI 中的音频回归实践
  9. 测试用例设计与金样管理

1. 质量评估的两个维度:客观与主观

1.1 客观指标

可计算、可重复、无人工参与。缺点是与听感的相关性有限:两个 SNR 相同的信号,听感可能差异巨大(失真类型不同)。

指标测量对象与听感相关性
SNR噪声底中(对低电平噪声敏感)
THD谐波失真中(对削波敏感)
THD+N失真 + 噪声中高
IMD互调失真高(对非线性敏感)
频响偏差频率响应高
响度(LUFS)感知响度高

1.2 感知模型

用心理声学模型把物理差异映射到感知差异。代表是 PESQ、POLQA、ViSQOL、PEAQ。它们需要"参考信号 + 处理信号"配对,输出预测的 MOS 分数。

1.3 主观测试

让真人听并打分。最可信但最昂贵。方法学(受试者数量、锚点、统计检验)决定结果是否可用。

1.4 三者的关系

客观指标  → 快速筛查、CI 回归(毫秒级)
感知模型  → 版本对比、编解码选型(秒级)
主观测试  → 最终验收、关键决策(人天级)

工程实践中,客观指标进 CI,感知模型做发版前的批量评估,主观测试只在重大决策时做。

2. 客观指标:SNR、THD+N 与 IMD

2.1 SNR

SNR = 10·log10(P_signal / P_noise)

测法:输入满量程正弦,输出端用陷波器滤掉基频,测剩余能量。实现上用 FFT 把 f0 附近 ±20 Hz 的 bin 置零,IFFT 回时域得到残余,再算两者的功率比即可。注意窗长要足够(Δf < 10 Hz),否则陷波会连带削掉信号本身。

2.2 THD+N

总谐波失真加噪声。用同一个陷波思路,但保留谐波在残余中:

THD+N = 10·log10(P_residual / P_total)

注意 THD+N 与 SNR 的差别:THD+N 的残余包含谐波(失真),SNR 的残余只算噪声。对削波严重的系统,THD+N 会明显变差而 SNR 可能不变。

2.3 IMD(互调失真)

输入两个频率(如 19 kHz + 20 kHz),测量差频(1 kHz)与和频成分。IMD 对非线性非常敏感,是评估非线性处理最灵敏的指标。

输入:19 kHz + 20 kHz(各 -6 dBFS)
输出:除 19/20 kHz 外还出现 1 kHz、39 kHz 等成分
IMD = 10·log10(杂散功率 / 主音功率)

2.4 频响测量

用对数扫频(20 Hz~20 kHz),录音后与原始扫频做反卷积得到脉冲响应,再 FFT 得到频响曲线:

# 生成对数扫频(10 秒,20 Hz ~ 20 kHz)
ffmpeg -f lavfi -i "sine=frequency=20:duration=10" -af "asetrate=48000" sweep_raw.wav
# 实际工程中多用 sox 或专用工具生成精确的对数扫频
sox -n sweep.wav synth 10 sine 20-20000

3. 感知模型指标

3.1 PESQ

PESQ(ITU-T P.862)是最广泛使用的语音质量模型。输入参考与处理信号,输出 MOS-LQO(1.0~4.5)。

适用:窄带(8 kHz)与宽带(16 kHz)语音
要求:两信号时间对齐(PESQ 内部会做粗对齐,但大延迟差会失败)
限制:不适用于音乐,对非线性失真不敏感
# 使用 pesq 库
from pesq import pesq
score = pesq(16000, ref_audio, deg_audio, 'wb')   # 'wb' 宽带
print(f"PESQ-WB: {score:.3f}")

3.2 POLQA

PESQ 的继任者(ITU-T P.863),支持超宽带与 48 kHz,对现代编解码(Opus、EVS)更准确。商业许可,需付费。

3.3 ViSQOL

基于频谱相似度(NSIM),开源(Apache 2.0),适合音乐与通用音频。

ViSQOL 输出 MOS-LQO 与各频带的 NSIM 值
各频带值能定位问题出现在哪个频段

3.4 PEAQ

ITU-R BS.1387,面向音乐质量的感知模型。计算复杂(需多次 FFT 与心理声学建模),实现较少。

3.5 选型建议

内容推荐模型
窄带语音PESQ-NB
宽带/超宽带语音PESQ-WB、POLQA
音乐ViSQOL、PEAQ
通用(无许可预算)ViSQOL

4. 主观测试:MUSHRA、ABX 与 MOS

4.1 MUSHRA

ITU-R BS.1534,音频编解码评估的事实标准。

1. 受试者听到一个隐藏的参考信号(Reference)与多个候选
2. 按 0~100 打分(0=极差,100=与参考无差异)
3. 必须包含一个 3.5 kHz 低通的"锚点"(低锚)用于校准
4. 至少 15~20 名受试者
5. 结果用中位数 + 四分位距表示

关键设计点:

  • 隐藏参考:把参考信号混在候选中(标记为 “Reference”),若受试者给它打低分,说明测试无效。
  • 锚点:低锚(3.5 kHz 低通)应该稳定得到 20~40 分,用于检测评分尺度漂移。
  • 随机化:候选顺序随机,避免顺序效应。

4.2 ABX

判断"能否区分 A 与 B"。受试者听 A、B 与随机给出的 X,判断 X 是 A 还是 B。统计上需要至少 10~20 次试验才能达到显著(p < 0.05)。

ABX 回答的是"有没有差异",MUSHRA 回答的是"差多少"。两者用途不同。

4.3 MOS

MOS(Mean Opinion Score)是 1~5 分的平均主观评分。传统 MOS 用于电话质量,ACR(Absolute Category Rating)是最常用的采集方法。

MOS 与 PESQ 的映射(近似):
PESQ 1.0 → MOS 1.0
PESQ 3.0 → MOS 3.5
PESQ 4.5 → MOS 4.5

4.4 样本量与统计

主观测试的样本量必须足够才能得出显著结论:

检测 0.5 MOS 的差异(标准差 1.0):
n = 2 × (1.96 + 0.84)² × 1.0² / 0.5² ≈ 63 个评分

样本不足的测试结论不可信,这是最常见的实验设计错误。

5. null test 与 bit-exact 回归

5.1 null test

把处理后的信号与参考反相相加,理想结果为零。

# 用 ffmpeg 做 null test
ffmpeg -i processed.wav -i reference.wav \
  -filter_complex "[0:a][1:a]amix=inputs=2:weights=1 -1,volumedetect" \
  -f null - 2>&1 | grep max_volume

残差的 max_volume 反映失真程度:

残差电平含义
-∞(全零)完全一致(bit-exact)
< -90 dB可忽略的浮点差异
-60 ~ -90 dB有轻微失真,需确认是否可接受
> -60 dB明显失真,必须排查

5.2 时间对齐是前提

null test 要求两信号逐样本对齐。若处理链路有延迟,必须先补偿:用互相关(scipy.signal.correlate)求峰值位置得到 lag,把较长的信号截去 |lag| 个样本后再相减。未对齐的 null test 会得到"看似很大"的残差,得出错误结论。

from scipy.signal import correlate
corr = correlate(a[:len(a)//2], b[:len(b)//2], mode='full')
lag = np.argmax(np.abs(corr)) - (len(a[:len(a)//2]) - 1)
a, b = (a[lag:], b) if lag > 0 else (a, b[-lag:]) if lag < 0 else (a, b)
residual = a[:min(len(a), len(b))] - b[:min(len(a), len(b))]

5.3 bit-exact 回归

同一份代码在同一平台上应当产出 bit-exact 的结果。可用于检测非预期的改动:

def assert_bitexact(path_ref, path_out):
    ref = np.fromfile(path_ref, dtype=np.float32)
    out = np.fromfile(path_out, dtype=np.float32)
    assert len(ref) == len(out), f"length mismatch: {len(ref)} vs {len(out)}"
    diff = np.abs(ref - out)
    if diff.max() > 0:
        idx = int(diff.argmax())
        raise AssertionError(f"not bit-exact: max diff {diff.max()} at {idx}")

注意:跨平台或跨 SIMD 宽度时 bit-exact 不成立(浮点累加顺序不同)。此时应改用容差比较,见 audio-sampling-quantization 中关于累加顺序的讨论。

6. 响度、峰值与削波合规测试

6.1 响度合规

# 测量整轨响度与真峰值
ffmpeg -i out.wav -af loudnorm=print_format=json -f null - 2>&1 | tail -20

断言逻辑:

def check_loudness(path, target_lufs=-16.0, max_tp=-1.0, tol=0.5):
    m = measure(path)          # 解析 loudnorm 的 JSON 输出
    assert abs(m['input_i'] - target_lufs) <= tol, \
        f"loudness {m['input_i']} not within {tol} of {target_lufs}"
    assert m['input_tp'] <= max_tp, \
        f"true peak {m['input_tp']} exceeds {max_tp}"

6.2 削波检测

削波表现为连续相同值的样本(对定点)或绝对值达到满量程(对浮点)。检测方法是扫描样本、统计连续满足 |x| >= 0.999 的游程长度。经验规则:连续 3 个以上样本达到满量程即为削波(1~2 个样本可能是合法的瞬态)。检测到的削波总数应为 0。

6.3 DC 偏移

DC 偏移会浪费动态范围并导致功放发热:

def check_dc_offset(x, max_dc=1e-4):
    dc = np.mean(x)
    assert abs(dc) < max_dc, f"DC offset {dc} exceeds {max_dc}"

6.4 NaN / Inf 检测

DSP 代码的 bug(除零、溢出、未初始化状态)会产生 NaN 或 Inf,一旦出现会"污染"整条链路:

def check_finite(x):
    if not np.all(np.isfinite(x)):
        n_nan = np.sum(np.isnan(x))
        n_inf = np.sum(np.isinf(x))
        raise AssertionError(f"non-finite samples: {n_nan} NaN, {n_inf} Inf")

这是最廉价也最有效的检查,应当放在每个音频测试的第一步。

7. 实时链路的性能测试

7.1 欠载(Underrun)测试

1. 以最小缓冲运行长时间(如 30 分钟)
2. 记录每次回调的实际可用样本数
3. 统计欠载次数与最大处理时间

断言:欠载次数为 0,且最坏块处理时间 < 块时长 × 0.8(留 20% 余量)。

// 在音频回调中测量
auto start = std::chrono::steady_clock::now();
processBlock(buffer);
auto elapsed = std::chrono::steady_clock::now() - start;
double ms = std::chrono::duration<double, std::milli>(elapsed).count();
double budgetMs = numSamples / sampleRate * 1000.0;
worstMs = std::max(worstMs, ms);      // 记录最坏值

7.2 延迟测试

用脉冲响应法或环路法测量端到端延迟,详见 audio-streaming-latency 。断言延迟不超过预算值。

7.3 内存与 CPU 增长

长时间运行后若内存持续增长,说明有泄漏(常见于未释放的 AudioBuffer、未 close 的 AudioData):

# 监控进程内存(macOS)
while true; do ps -o rss= -p $PID; sleep 60; done

CPU 占用应当稳定;若随时间上升,通常是反规格化数或状态漂移导致,见 audio-plugin-vst 中的相关讨论。

7.4 极端参数压力测试

随机(但可复现)地改变参数,运行数小时:

import random
random.seed(42)          # 可复现
for _ in range(10000):
    param.set(random.uniform(param.min, param.max))
    process_block()

断言:无 NaN/Inf、无自激(输出电平有界)、无崩溃。

8. CI 中的音频回归实践

8.1 分层策略

快速层(每次提交,< 30 秒):
  - 短素材(1~5 秒)的 bit-exact / 容差比较
  - NaN/Inf 检查
  - 响度与真峰值合规

中等层(每次 PR,< 5 分钟):
  - 多组参数的输出比对
  - 极端参数压力测试
  - 频谱指标(THD+N、频响偏差)

慢速层(每晚,数十分钟):
  - 长素材的感知模型评估(ViSQOL)
  - 长时间稳定性测试
  - 跨平台一致性

8.2 GitHub Actions 示例

name: audio-regression
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: sudo apt-get install -y ffmpeg sox python3-numpy
      - run: python3 tests/run_audio_tests.py --tolerance 1e-5
      - uses: actions/upload-artifact@v4
        if: failure()
        with: { name: audio-diffs, path: "tests/output/*.wav" }

失败时上传差异音频,便于人工复核。

8.3 金样管理

金样(golden reference)是回归的基准。管理原则:

  • 版本化:金样随代码一起提交,或用 Git LFS 管理大文件。
  • 明确的更新流程:金样变更必须由人审查(说明为什么输出变了),不能自动覆盖。
  • 记录环境:金样必须在固定平台、固定编译选项下生成,否则会因浮点差异导致误报。
golden/
├── MANIFEST.json      (平台、编译选项、生成时间、git commit)
├── gain_0.5.wav
└── filter_lp_1k.wav

MANIFEST.json 是关键:没有它,跨环境的差异无法归因。

9. 测试用例设计与金样管理

9.1 素材选择

测试素材必须覆盖边界情况:

素材覆盖的场景
1 kHz 满量程正弦基本频响、THD
20 Hz / 20 kHz 正弦频响边界
对数扫频全频段频响
白噪声 / 粉噪声宽带稳定性
单样本脉冲瞬态响应、群延迟
静音噪声底、DC 偏移
满量程方波非线性、过采样
真实音乐/语音片段综合听感

9.2 断言的选择

属性断言方式
数值正确性容差比较(如 max diff < 1e-5)
无异常值NaN/Inf 检查
电平合规响度、真峰值、削波
频响与参考频响的偏差 < 0.5 dB
失真THD+N 不超过阈值
性能最坏处理时间 < 预算

9.3 避免脆弱测试

  • 不要用 bit-exact 做跨平台断言:浮点累加顺序不同必然失败。
  • 不要断言精确的延迟值:不同版本的操作系统与驱动可能不同,用范围断言。
  • 不要依赖绝对时间:用相对指标(如处理时间 / 块时长)。
  • 容差要留余量:1e-6 的容差在 FMA 与普通乘加混用时会失败,用 1e-5 更稳。

权衡取舍

决策点选择 A选择 B建议
客观指标bit-exact容差比较同平台 bit-exact,跨平台用容差
感知模型PESQ(快)ViSQOL(通用)语音 PESQ,音乐 ViSQOL
主观测试ABX(是否可辨)MUSHRA(差多少)按问题选,验收用 MUSHRA
测试素材合成信号真实素材两者都要,合成覆盖边界
CI 频率每次提交全量分层(快/中/慢)分层,快速层 < 30 秒
断言严格度严格(易误报)宽松(漏检)按属性分级,数值严格、性能宽松

常见坑清单

  1. 只做主观抽查:人耳疲劳后判断力下降,批量产物必须有客观断言。
  2. null test 不做时间对齐:延迟未补偿导致残差虚高,得出错误结论。
  3. 跨平台断言 bit-exact:浮点累加顺序不同必然失败,应改容差比较。
  4. 金样无环境记录:跨环境差异无法归因,必须记录平台与编译选项。
  5. 自动覆盖金样:掩盖真实退化,金样变更必须人工审查。
  6. 不做 NaN/Inf 检查:一个除零 bug 会污染整条链路,这是最廉价的检查。
  7. 只测性能平均值:平均值达标但最坏值超标仍会爆音,必须测最坏块处理时间。

小结

音频质量评估的可信度取决于方法学而非工具。客观指标覆盖确定性属性,感知模型预测主观听感,主观测试给出最终验收,回归测试保证不退化。四层各司其职,缺一层就有盲区。

工程落地的三条建议:把 NaN/Inf 检查与响度合规放进最快的 CI 层(成本极低、收益极高);金样必须带环境清单(否则跨环境差异无法归因);断言按属性分级(数值严格、性能宽松、延迟用范围)。

继续深入建议读 audio-sampling-quantization 理解量化噪声与抖动的测量原理,读 audio-codec-opus-aac 掌握编解码场景下的感知质量评估,读 audio-mixing-mastering 了解响度与真峰值合规的完整要求。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「音频工程」更多文章

  1. 音视频同步与时间码
  2. 音频硬件接口与驱动栈
  3. 响度标准化与交付规范