资讯详情

PESQ语音质量评估实战:MOS-LQO、采样率与可复现评测

📅 2026/10/27 12:37:27 | 华诺云谱 👁 阅读
PESQ语音质量评估实战:MOS-LQO、采样率与可复现评测
简介mos-pesq.zip 是面向音频编解码与语音质量评估开发者的 PESQ/MOS 测试工具包适合在项目开发中验证 PCM 编解码前后的音质差异也可用于算法调试阶段的客观评分对比。压缩包共 12 个文件以 5 个 C 源码和 3 个头文件为主体辅以 Makefile、运行脚本、说明文档、测试音频压缩包及外层 zip 结构整体约 207KB体积轻便但保留了完整编译与运行链路。代码按 PESQ 主流程、DSP 信号处理、音频读写等模块组织附带测试音频与评分脚本编译后可按采样率对干净语音和加噪语音进行对比测试便于理解 PESQ 评分实现、替换自有 PCM 数据做回归验证并据此排查编解码导致的音质劣化。已有 963 人学习适合需要快速搭建音频质量客观评测环境、积累排错与调参思路的音频与语音算法开发者参考。1. mos-pesq.zip 不是“打分神器”它把两段波形折成一个 1~4.5 的数拿到 mos-pesq.zip 的人第一反应基本一致解压、找入口、扔两个 wav 进去看它吐出一个 3.82 还是 2.14。这个包的内核是 PESQPerceptual Evaluation of Speech Quality一种拿“干净参考语音”和“过完链路之后的语音”做逐帧比对、最后折算成 MOS 分的客观评测方法。它解决的是一个很具体的工程问题你改了降噪、换了编码器、动了增益曲线上线前总得知道语音“听起来”损失了多少而组织一轮几十人的主观 MOS 打分要排期、要花钱、还不可复现。适合上手的人做语音降噪、回声消除、编解码、TTS 后处理、录音链路人手上有成对的干净参考和对应处理输出。不适合的人只有处理后音频、没有参考的想评含丢包、抖动、回声、时变延时的实时通话的——那些不是 PESQ 的靶心硬套会得到一个看起来很专业、实际完全误导的小数点。2. 从波形到 MOS-LQOPESQ 内部那条四步评分链路把 mos-pesq.zip 当黑匣子用的人最常见的困惑是“我明明只改了增益分数怎么没动”和“我只过载了一点点分数怎么掉了 0.6”。这两个现象都能从评分链路里找到解释。整条链路大致四步电平规整、时间对齐、听觉域变换按 Bark 带划分子带、算响度、坏区间加权与分数映射。你改动的东西落在哪一步决定了它会不会体现在最终那个数字上。2.1 电平规整与削顶为什么“整体调小 3dB”几乎不掉分“过载一点点”却掉得厉害PESQ 在比对前会把参考和被测都规整到一个固定的等效听音电平也就是说它对“整体增益”这件事是基本免疫的。我做过一组很朴素的对照同一段 16k 干净语音被测侧整体乘 0.7、乘 1.4MOS-LQO 的抖动在 0.05 以内肉眼看着像是随机噪声。这也解释了不少人拿它去评“自动增益控制”效果时为什么分数纹丝不动——AGC 做得再激进只要没削顶、没把底噪抬上来PESQ 就不认为你损失了什么。真正致命的两个东西是削顶和底噪随增益抬升。削顶产生的是宽带谐波失真落在每个 Bark 带里都是不可忽略的感知误差底噪被抬升则直接改变了信噪比而 PESQ 的听觉模型对“语音间隙里的稳态噪声”特别敏感。所以评测前先看一眼峰值和底噪比反复调参有用得多# 同时看均值电平与峰值电平max_volume 接近 0 dB 就要怀疑削顶 ffmpeg -hide_banner -i deg_16k.wav -af volumedetect -f null - 21 \ | grep -E mean_volume|max_volumemean_volume是被测语音的平均电平max_volume是峰值。峰值贴着 0 dB比如 -0.1 dB说明大概率已经削过这种情况下算出来的分数低不代表算法差代表素材废了。我一般要求峰值留 3 dB 以上余量均值电平落在 -26 dB 到 -20 dB 之间参考和被测的差值控制在 1 dB 内——不是因为 PESQ 在乎而是为了让人耳复核时听感一致避免自己骗自己。2.2 时间对齐与坏区间剔除PESQ 的容错边界在哪PESQ 会先估计参考和被测之间的整体延时把两段信号拉到同一时间轴上再按约 32 ms 一帧、50% 重叠切块逐帧算扰动。对齐这一步是它最脆弱的地方它假设延时是缓变的、近似全局一致的。所以纯线性滤波EQ、编码器、固定延时几十毫秒的算法延迟它都能扛住而丢包导致的整块缺失、抖动导致的时变延时、变速播放导致的时长不一致会让对齐直接失效这时候报出来的分数通常低得离谱且不可解释。链路里还有一步叫坏区间识别某些帧的失真严重到“人耳会说这段完全糊了”这些区间会被单独标记并在聚合时加重惩罚。这带来一个反直觉的后果——少量极端坏的片段比均匀的中等失真更能拉低总分。做降噪时如果算法只在少数强噪帧上崩掉PESQ 会把这几个帧放大成整体评价跟你“大部分时候都挺好”的直觉不符。想看清楚是哪些帧出了问题就把长音频按 8~12 秒切段分别打分看分数分布而不是只盯着一个全局限平均。注意PESQ 的对齐能力有明确边界不要指望它容忍变速、丢帧、时间轴拼接错误。凡是“两段音频内容对不上”的问题都是预处理问题不是打分问题。2.3 原始分与 MOS-LQO同一段音频为什么能报出 3.1 和 3.9mos-pesq.zip 这类包最容易被忽略的一点是它内部有两套量纲。原始分raw score大致落在 -0.5 到 4.5 之间是算法直接输出的扰动聚合值MOS-LQO 是经过一条 logistic 曲线压出来的映射分窄带和宽带各有一条曲线取值范围大约 1.0 到 4.6。同一段音频报 3.1 还是 3.9往往不是算法不同而是输出的量纲不同、或者窄带/宽带模式不同。输出量纲大致范围含义用的时候注意raw score-0.5 ~ 4.5算法直接输出的扰动聚合值只能同模式内部比别跨版本MOS-LQO窄带约 1.0 ~ 4.68 kHz 模式映射分与主观 MOS 的线性关系稍好MOS-LQO宽带约 1.0 ~ 4.616 kHz 模式映射分同一条链路通常高于窄带主观 MOS1 ~ 5人打分样本量小时置信区间很宽我吃过一次亏拿 A 方案窄带模式的 MOS-LQO 去对比 B 方案宽带模式的 MOS-LQO得出“B 明显更好”的结论后来统一到 16k/wb 重跑差距只剩 0.1。写报告的时候一定要把这四个字带上——量纲、模式、采样率、语料集缺一个数字就不可复现。3. 本地跑通最小评测解压、编译、一条命令出分从零到跑出第一个分数其实只有三件事把音频规整成 PESQ 能吃的格式、让工具链跑起来、拿一对样本验证输出是否合理。别一上来就写批量脚本先用一对样本把链路打通确认数字在合理区间再谈规模化。3.1 环境与依赖三件套加上一条采样率硬约束需要的就三样能编译 C 的编译环境解压出来的参考实现通常是 C 写的make一下就有可执行文件如果里面已经有预编译产物跳过这步、一个 Python 环境用来写批处理、以及 ffmpeg 或 sox 做格式规整。检查一遍gcc --version | head -n 1 # 有输出就说明编译器在 python3 -c import numpy, soundfile; print(ok) ffmpeg -version | head -n 1硬约束只有一条但踩的人最多PESQ 只在 8 kHz 和 16 kHz 两个采样率下工作8k 对应窄带模式16k 对应宽带模式。喂 44.1k 直接报错喂 16k 却选了窄带模式也会报错。常见做法是统一走 16k/单声道/16 bit PCM除非你评的就是电话带宽链路。位深和声道其实不是算法硬要求但混着来会让你在排查时多花两小时所以我在流水线入口就一律强制统一。3.2 单文件跑分先从 8k/16k 规整开始先规整格式。注意重采样只做一次别在链路里来回 16k→48k→16k 折腾每次重采样都会引入一点点滤波失真攒起来就体现在分数上# 统一成 16 kHz / 单声道 / 16 bit PCM-y 覆盖旧文件避免残留 ffmpeg -y -i ref_raw.wav -ac 1 -ar 16000 -sample_fmt s16 ref_16k.wav ffmpeg -y -i deg_raw.wav -ac 1 -ar 16000 -sample_fmt s16 deg_16k.wav # 顺手确认两段时长一致差超过几十毫秒基本就是内容没对齐 ffprobe -v error -show_entries formatduration -of csvp0 ref_16k.wav ffprobe -v error -show_entries formatduration -of csvp0 deg_16k.wav参考实现的命令行入口一般是“采样率 参考 被测”三个位置参数16k 用160008k 用8000# 若解压目录里是 C 源码先编译已有可执行文件则跳过 make make # 输出里通常会同时打印原始分和 MOS-LQO注意看清是哪一列 ./pesq 16000 ref_16k.wav deg_16k.wavPython 侧更省事一个函数调用就够适合后面写批处理import soundfile as sf from pesq import pesq # dtypefloat32 是为了匹配底层 C 实现的入参类型省一次隐式转换 ref, sr sf.read(ref_16k.wav, dtypefloat32) deg, _ sf.read(deg_16k.wav, dtypefloat32) print(sr, len(ref), len(deg)) # 先确认采样率与长度 # 16k 配 wb8k 配 nb喂错组合会直接抛异常而不是给你个错分 print(MOS-LQO:, pesq(sr, ref, deg, wb))第一对样本一定要自己造把参考复制一份当被测分数应该在 4.5 附近宽带模式把被测加一点白噪声到 10 dB 信噪比分数应该明显掉下来。如果复制品都跑不到 4.4 以上说明格式规整那一步有问题别急着往下走。3.3 批量评测脚本两个目录配对成一张 CSV批量脚本的核心不是“循环调用”而是“把异常显式记下来”。真实的评测集里总有几对文件采样率不一致、长度不一致、内容压根不是同一句这些必须落盘成错误行而不是悄悄跳过——跳过就意味着你的均值只统计了“能跑通的那部分”系统性偏高。import os import csv from concurrent.futures import ProcessPoolExecutor import soundfile as sf from pesq import pesq, PesqError REF_DIR, DEG_DIR data/ref, data/deg # 两侧文件名必须一一对应 OUT_CSV, SR, MODE scores.csv, 16000, wb def score_one(name): ref, sr1 sf.read(os.path.join(REF_DIR, name), dtypefloat32) deg, sr2 sf.read(os.path.join(DEG_DIR, name), dtypefloat32) # 采样率不一致直接判失败先重采样再进来不在打分环节做隐式转换 if sr1 ! SR or sr2 ! SR: return name, None, fsr {sr1}/{sr2} # 长度不一致通常是内容错位或裁剪不一致这种分数没有意义 if abs(len(ref) - len(deg)) 160: # 160 点约 10 ms 容差 return name, None, flen {len(ref)}/{len(deg)} try: s pesq(SR, ref, deg, MODE) except PesqError as e: # 太短、全静音等都会走到这里 return name, None, str(e) return name, round(float(s), 3), def main(): names sorted(set(os.listdir(REF_DIR)) set(os.listdir(DEG_DIR))) with ProcessPoolExecutor(max_workersos.cpu_count()) as ex, \ open(OUT_CSV, w, newline, encodingutf-8) as f: w csv.writer(f) w.writerow([file, mos_lqo, error]) for name, s, err in ex.map(score_one, names): w.writerow([name, s, err]) if __name__ __main__: main()几个参数值得单独说。SR16000和MODEwb必须成对出现改一个就要改另一个max_workers用 CPU 核数PESQ 是纯计算密集型进程池比线程池合适abs(len(ref)-len(deg)) 160这个容差不是标准里给的是我按经验定的——超过 10 ms 的长度差多半意味着两条链路里有一条做了额外裁剪这种样本的分数不可信。跑完之后先看error列有多少非空行再对mos_lqo求均值和中位数两者差得多说明分布有长尾得去看最差的那几条。4. 让分数可复现的六个设置采样率、电平、句长与参考对齐同一个算法两个人跑出 0.4 的差距九成不是工具问题是语料准备不一致。这一章列的六件事做到位了你的分数才具备“别人照着重跑能得到同样结论”的资格。4.1 采样率、位深、声道16k/mono/s16 是硬通货采样率决定模式和上限这件事没有折中空间。评电话链路8k 源就用 8k 窄带别升采样到 16k 再走宽带模式那等于给原始信号补了一段本不存在的带宽分数会虚高。位深统一 16 bit虽然内部计算是浮点但输入如果是 8 bit 或者做了 μ 律压缩没解开失真会直接算进你的分数里。声道必须单声道双声道文件读进来会变成二维数组长度校验这关就过不去。重采样工具链也要统一。我见过同一个项目里参考侧用 A 工具重采样、被测侧用 B 工具两者群延时差了半个采样点累积起来在部分句子上就体现为分数偏低。定一条规矩全流程只用一个重采样器、只重采样一次、参数写死在配置里。4.2 语料切分与静音裁剪句长 8~12 秒别让静音拉高分数PESQ 是全段平均的静音段在没有噪声时几乎不贡献惩罚所以一段 60 秒里如果 40 秒是静音分数会被“稀释”得偏高。我一般把语料切成 8~12 秒的句子逐句打分再取中位数这样既能看出方差也不至于让静音主导结果。裁剪时留 0.2~0.3 秒前后静音是必要的因为算法需要一点上下文来做对齐但别留 2 秒。另外如果被测信号在静音段有底噪降噪算法常见的残留噪声、编码器的舒适噪声这些段恰恰是最能暴露问题的全裁掉就等于把缺陷删了。所以裁剪策略要在参考和被测上完全一致用参考的 VAD 结果去裁两段而不是各裁各的。4.3 参考与被测必须“同句同轴”内容错位比噪声更致命最隐蔽的一类错误是文件名配对上了、内容其实错位了。比如参考是 001.wav 对应“第三句话”被测的 001.wav 因为前面多了一段提示音实际是“第二句话”。这时候 PESQ 会给出一个 1.2 之类的极低分你可能会以为是算法彻底崩了回头查两天代码。防御手段很土但有效先算两段信号的互相关看峰值位置对应的延时是否稳定再抽 10 对做时长比对正常链路里两侧时长差应该在几十毫秒内。还有一个更省事的办法——把参考复制一份当被测跑一遍全量如果某一对跑出来不是 4.4 以上那一对必然有问题这步自检只要几分钟能省掉后面几小时的怀疑人生。4.4 一张可以直接抄的参数表参数推荐值为什么这么定写错的后果采样率16 kHz宽带/ 8 kHz窄带只有两档被支持直接报错或模式错配模式与采样率严格配对映射曲线不同分数不可跨方案比较位深/声道16 bit PCM / 单声道避免隐式转换失真长度校验失败、分数偏低句长8 ~ 12 秒兼顾对齐上下文与静音稀释长句掩盖局部缺陷峰值余量≥ 3 dB防削顶谐波失真导致假低分参考被测电平差≤ 1 dB便于人耳复核对齐不影响分数但影响判断每句打分聚合中位数 分位数抗长尾均值被极端句带偏提示这张表建议直接写进项目的评测 README而不是留在某个人的脚本注释里。参数一旦口口相传第三次复现就一定会跑偏。5. 分数不对劲时的排查清单五条踩坑记录与定位方法分数异常分两类虚高和虚低。虚高更危险因为它不会触发任何报警你会带着一个错误的结论去做决策。下面五条是我这几年反复遇到的每条都按“现象—原因—解决”写遇到了可以直接对号入座。5.1 采样率与重采样链路的三次翻车现象一某批样本稳定报错提示模式与采样率不匹配但文件头显示是 16k。原因是文件头写了 16k实际内容是被某个工具“声明式”改了头、没有真正重采样送给算法时的样点数与采样率对不上。解决不要信文件头用ffprobe读出实际采样率再用解码后的样点数除以时长交叉验证批量入口处加一道断言采样率不等于目标值就拒绝进入打分环节。现象二同一批音频换台机器跑均分差了 0.3。原因是重采样器版本不同滤波器群延时和通带特性有差异在宽带模式下这段差异会落在高频子带里被计入。解决把重采样这一步固化成流水线里的一个独立阶段产物落盘、带哈希打分阶段只读不转。这样任何人重跑输入都是同一批样点。现象三8k 语料升采样到 16k 后走宽带模式分数比预期高一大截。现象看着像“算法变好了”实际是升采样补齐了带宽PESQ 认为高频没有损失。原因是模式选错。解决模式由源语料采样率决定不由“我想评宽带”决定在配置里把语料采样率和模式绑成一个枚举不给手工改的机会。5.2 内容与电平不一致造成的假低分现象四个别句子分数只有 1.5 左右其余句子都在 3.8 以上分布呈明显双峰。原因是这些句子的参考和被测内容错位或者被测侧多了一段前导提示音导致时间轴偏移。双峰分布是内容错位的典型指纹——如果是算法在某些噪声类型上失效分布通常是单峰长尾而不是干净的双峰。解决对低分句子做互相关峰值检查确认延时是否恒定确认后在预处理阶段用参考的 VAD 统一裁剪两侧。现象五整批分数普遍偏低但人耳听着都还行。原因是电平或削顶问题最常见的是被测侧经过 AGC 后峰值贴到 0 dB削顶引入的宽带失真被听觉模型捕捉到。解决先跑一遍峰值与均值统计把峰值余量小于 3 dB 的样本单独拎出来用限幅器压回去再重跑一遍对比两次分数——如果差距超过 0.3说明这批数据本身就不该直接拿来评。5.3 把 PESQ 用在不该用的场景现象六评一条带丢包模拟的实时通话链路分数波动巨大且和主观听感完全不相关。原因是丢包造成的时间轴跳变破坏了 PESQ 的对齐假设它给出的低分反映的是“对齐失败”不是“听感差”。这类场景 PESQ 本来就不适用。解决换成对流式损伤更鲁棒的评测方式或者先把丢包位置对齐、按“丢包前/丢包后”分段评测同时必须补主观抽检否则你无法知道那个数字到底在说什么。现象七评 TTS 合成语音用真人录音当参考分数低得离谱。原因是文本相同但发音人、语速、韵律完全不同逐帧比对的意义被削弱。这种“跨说话人”的评测PESQ 只能作为弱参考。解决要么用同一个说话人的重录版本做参考要么改用不依赖帧对齐的指标做交叉验证别拿一个数字下结论。6. 单指标不可信用 STOI 与 SI-SDR 交叉验证再抽 30 条做人耳复核6.1 三个指标各自看得见什么PESQ 高不代表可懂度高——这是最容易被忽略的一条。降噪算法把噪声压得很干净、但吃掉了辅音PESQ 可能只掉 0.3而人耳已经听不清“s”和“f”的区别。反过来SI-SDR 很高也不代表好听因为它是波形级的相似度对相位和轻微延时极其敏感人耳却未必在意。所以我的习惯是三个指标一起看任何两个方向相反就必须人工介入。指标主要反映对什么敏感对什么不敏感PESQ / MOS-LQO整体感知质量噪声、编码失真、削顶时变延时、丢包、语义可懂度STOI语音可懂度时频掩蔽类失真整体音色、稳态噪声SI-SDR波形保真度相位误差、残留噪声人耳感知加权import numpy as np import soundfile as sf from pystoi import stoi from pesq import pesq def si_sdr(ref, deg): # 先去掉直流再最小二乘求最优缩放避免整体增益干扰相似度 ref, deg ref - ref.mean(), deg - deg.mean() alpha np.dot(deg, ref) / np.dot(ref, ref) target alpha * ref noise deg - target return 10 * np.log10(np.sum(target ** 2) / np.sum(noise ** 2)) ref, sr sf.read(ref_16k.wav, dtypefloat32) deg, _ sf.read(deg_16k.wav, dtypefloat32) print(PESQ :, round(pesq(sr, ref, deg, wb), 3)) print(STOI :, round(stoi(ref, deg, sr, extendedFalse), 3)) print(SI-SDR:, round(si_sdr(ref, deg), 2), dB)si_sdr里那个alpha是关键不做最优缩放的话被测侧只要整体音量差一点SI-SDR 就会断崖式下跌你会误判成算法有问题。stoi的extendedFalse给的是标准可懂度分0~1打开扩展版会同时返回更多子指标日常排查用标准版够用。三个数一起打印看方向一致性PESQ 涨、STOI 平、SI-SDR 涨是典型的“去噪去了但没伤到语音”PESQ 涨、STOI 跌那就要警惕了多半是过度压制把弱辅音一起抹了。6.2 一个 30 条的抽检流程批量跑完几百条之后我一定会抽 30 条做人工复核抽样规则固定下来分数最高的 10 条、最低的 10 条、中间随机 10 条。听的时候只问三个问题——有没有残留的噪声起伏、字与字之间有没有被“切断”的感觉、整体音量是否明显小于参考。这三个问题分别对应稳态噪声、过度压制、增益失配正好覆盖了指标最容易骗人的三类情况。抽检结果我习惯记成三列机器分、主观印象好/中/差、是否愿意上线。如果出现“机器分最高但主观印象差”的样本那一条就是最有价值的样本通常能挖出评测集本身的缺陷——比如参考录音本身带底噪算法把底噪压掉了PESQ 反而认为你和参考不像了。这种发现只靠跑脚本永远得不到。我自己的教训是早期太相信一个数字把一次 MOS-LQO 从 3.1 提到 3.9 当作重大改进写进汇报结果上线后用户反馈“声音发闷”。回头查算法确实提升了信噪比但同时把 4 kHz 以上压掉了 6 dBPESQ 的宽带加权对这段损失的惩罚有限人耳却很明显。从那以后我的规矩是任何指标提升超过 0.3必须有对应的主观抽检记录没有就不算数。评测这件事慢一点比返工便宜。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑