引言
音频质量评估是工程中最容易被敷衍的环节:主观听感难以量化,客观指标又常常与听感不一致。结果是很多音频项目只做"人耳抽查",上线后才发现批量产物有系统性缺陷。
实际上,音频测试可以做得相当扎实。客观指标能覆盖电平、失真、噪声、频响等确定性属性;感知模型能在一定程度上预测主观听感;主观测试(MUSHRA、ABX)在方法学成熟的情况下能给出统计显著的结果;回归测试能保证每次改动不引入退化。四者组合,就能构建一条可信的音频质量保障链路。
本文按"客观 → 感知 → 主观 → 回归"的顺序组织,最后落到 CI 集成与金样管理。核心观点是:音频测试的难点不在算法,而在测试用例设计、金样管理与容差设定。
目录
- 质量评估的两个维度:客观与主观
- 客观指标:SNR、THD+N 与 IMD
- 感知模型指标:PESQ、POLQA、ViSQOL、PEAQ
- 主观测试:MUSHRA、ABX 与 MOS
- null test 与 bit-exact 回归
- 响度、峰值与削波合规测试
- 实时链路的性能测试
- CI 中的音频回归实践
- 测试用例设计与金样管理
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 秒 |
| 断言严格度 | 严格(易误报) | 宽松(漏检) | 按属性分级,数值严格、性能宽松 |
常见坑清单
- 只做主观抽查:人耳疲劳后判断力下降,批量产物必须有客观断言。
- null test 不做时间对齐:延迟未补偿导致残差虚高,得出错误结论。
- 跨平台断言 bit-exact:浮点累加顺序不同必然失败,应改容差比较。
- 金样无环境记录:跨环境差异无法归因,必须记录平台与编译选项。
- 自动覆盖金样:掩盖真实退化,金样变更必须人工审查。
- 不做 NaN/Inf 检查:一个除零 bug 会污染整条链路,这是最廉价的检查。
- 只测性能平均值:平均值达标但最坏值超标仍会爆音,必须测最坏块处理时间。
小结
音频质量评估的可信度取决于方法学而非工具。客观指标覆盖确定性属性,感知模型预测主观听感,主观测试给出最终验收,回归测试保证不退化。四层各司其职,缺一层就有盲区。
工程落地的三条建议:把 NaN/Inf 检查与响度合规放进最快的 CI 层(成本极低、收益极高);金样必须带环境清单(否则跨环境差异无法归因);断言按属性分级(数值严格、性能宽松、延迟用范围)。
继续深入建议读 audio-sampling-quantization 理解量化噪声与抖动的测量原理,读 audio-codec-opus-aac 掌握编解码场景下的感知质量评估,读 audio-mixing-mastering 了解响度与真峰值合规的完整要求。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。