ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

PESQ语音质量评估实战:从mos-pesq.zip编译到MOS打分

2026/9/26 16:47:42 拓冰建站 浏览量
PESQ语音质量评估实战:从mos-pesq.zip编译到MOS打分 简介mos-pesq 是一个面向音频编解码与通信领域的语音质量评估工具基于 PESQ 算法可对 PCM 编解码前后的样本进行 MOS 打分帮助开发者在项目中客观衡量音质损失适合具备 C 语言基础的音视频或通信方向开发者使用。压缩包共 12 个文件、约 207KB以 C 源码为主包含核心算法文件与 DSP 处理模块并配有头文件、Makefile、shell 评分脚本、README 和测试音频结构紧凑便于直接编译验证。源码按模块划分可跟踪 PESQ 的滤波、对齐和听觉映射流程同时自带测试样本与运行示例便于迁移到自有 PCM 文件上对比编码前后的 MOS 变化是一款实用的小型音质评估参考。目前已有 962 人学习使用适合正在调试编码器或评测实时语音传输质量的工程师快速上手。1. mos-pesq.zip 是什么一个能离线算 MOS 分的语音质量评估工具做 VoIP、音视频 RTC 或语音编解码的工程师迟早会收到一个叫 mos-pesq.zip 的压缩包里面装的是 ITU-T P.862 标准定义的 PESQ 算法。PESQ 输入一段无损参考语音和一段处理后的退化语音输出和主观听感强相关的 MOS 分1~5自动回答“这条链路有没有把声音搞坏”。它适合验证 VoIP 线路改造、做降噪和编解码器选型、给录音批量定级的工程师。要提醒的是这类 zip 里的可执行文件通常只为 Linux 编译Windows 直接跑大概率翻车网传版本多是老源码需要自己编译解压前要检查 zip 完整性我遇到过伪加密包解压到一半报 CRC 错误。这篇笔记覆盖编译、调用、批处理和踩坑照着做半小时能出一张 MOS 打分表。2. PESQ 打分原理为什么客观分能逼近人耳它到底在算什么2.1 MOS 的主观听测与客观模型为什么我们敢用算法打分MOS 的起点不是算法是主观测试。ITU-T P.800 规定了完整的听测流程找几十个听力正常的人在声学处理过的听音室里用统一耳机听一段 10 到 20 秒的语音按 1 到 5 打分取平均。1 分是极差2 分是差3 分是一般4 分是好5 分是优秀。主观分的问题非常实际一场听测要预约场地、招募受试者、支付劳务费整套下来预算轻松过万更麻烦的是在算法迭代过程中人员状态和听音环境的细微变化就能带来 0.1 分上下的波动正好吃掉你一次优化改版的成果。这逼着工程界想办法用机器模拟人耳。PESQ 的思路不是拟合某个人的口味而是拟合一组听测者在平均意义上的打分习惯。它输出单调、可复现、随时能跑的分数让“改了一版降噪参数到底变好没有”从主观印象变成数字对比。客观模型不是要替代主观听测而是把主观听测从每个迭代周期里解放出来只在关键节点做真人对齐。这个定位从 PESQ 诞生到现在二十年没有变过工程上围绕它建的回归工具链也最成熟。2.2 PESQ 内部的三层链路对齐、感知变换、扰动累积PESQ 拿到两个 WAV 后第一步是逐层对齐。电平对齐把参考和退化的整体响度压到同一水平避免把音量差算成质量差时间对齐用包络互相关估计两路信号之间的时延再把语音按能量包络切成短时片段逐步对齐。这段逻辑决定了 PESQ 对网络抖动和缓冲延迟有一定容忍度但容忍上限不到 2 秒超出后退化信号会被后续感知模型当成噪声分数直接跳水。第二步是感知变换。信号分帧做 FFT帧长 32 毫秒帧移 16 毫秒加 Hann 窗频谱映射到 40 多个 Bark 临界频带再做响度密度变换。这一步模拟的是人耳基底膜对频率的选择性掩蔽和非线性响度感知。第三步是扰动计算在时频网格上逐点比较参考和退化的响度密度差值就是扰动密度。PESQ 对“该有的声音被切掉”和“不该有的声音被插进来”处罚不同前者按掩蔽效应宽容后者按暴露噪声重罚。这也是它比纯 SNR 更贴主观听感的根本原因——降噪算法把噪声压过头导致语音损伤时分数会立刻显示出问题。2.3 MOS-LQO 版本差异P.862、P.862.1、P.862.2 不能混用拿到 mos-pesq.zip 直接跑看到数字就记下来换台机器再跑发现结果不一样第一反应往往是环境问题实际上版本问题更常见。纯 P.862 原始分范围在 -0.5 到 4.5 之间不是我们习惯的 1 到 5P.862.1 把原始分映射成窄带 MOS-LQO范围压缩到 1 到 4.5 左右P.862.2 是宽带扩展支持 16kHz 采样率原始分算法和映射函数都变了。记录结果时必须写清三件事用了哪个实现、采样率声明是 8000 还是 16000、输出字段是 Raw MOS 还是 MOS-LQO。不同版本分差 0.5 是常态不算 bug但会把回归对比带偏。我在测试计划里固定一条规矩所有回归跑同一个可执行文件版本信息写进结果文件表头换版本就重跑基线。这条规矩省下来的排查时间远比跑一次基线的时间多。2.4 为什么选 PESQ与 SSNR、POLQA、ViSQOL 的取舍方法来源适用采样率时延敏感常见用途PESQITU-T P.8628k/16k容差有限VoIP 基线回归POLQAITU-T P.8638k~48k支持大时延终验、超宽带ViSQOLGoogle 开源8k~48k流式处理流媒体监控SSNR自实现任意不敏感冒烟测试SSNR 实现不超过 20 行按帧算信噪比取平均适合当冒烟测试。但它只对能量敏感对时延几乎无感一段延迟 500 毫秒的清晰语音在 SSNR 眼里仍然优秀它也没有模拟掩蔽效应一个高频噪声在 SSNR 里被重罚人耳可能根本听不到。POLQA 是 PESQ 的官方换代品主观相关性更高支持超宽带和空间音频但授权费和运行环境把很多团队挡在门外一般只在项目终验时买几套。ViSQOL 适合流式质量监控但语音场景的历史积累比 PESQ 少一个量级。在 VoIP 的老场景里PESQ 的工程经验沉淀最深网上能找到的坑和参数经验也最多。zip 里的源码版适合内部评估和二次开发社区封装的 pip 版适合自动化测试两者同源但输出细节有差异选一个固定下来比纠结“哪个更准”重要得多。我一般以命令行源码版为基准pip 版只用来做快速验证绝不混用两边的结果做对比。3. 把 mos-pesq.zip 跑通验证 zip、编译、调用与参数说明3.1 解压前先验证 zip 完整性unzip -t 与 7z t 双保险网上流传的 mos-pesq.zip 很多是从 Linux 服务器或网盘转存出来的链路长、工具杂zip 被改坏的概率不小。最常见的两个状况一是文件头的加密标志位被某些压缩工具改过出现 zip 伪加密打开就要密码且密码永远不对二是转存过程中字节丢失解压到一半报 CRC 错误。这两种问题都发生在目录建好、准备编译之后才暴露非常费时间。所以拿到包的第一件事不是解压是验证。unzip -t 会把 zip 里每个文件解码并做 CRC 校验7z t 也能做类似检查而且对伪加密的处理更宽容。两条命令都跑一遍先确认没有文件缺失再确认密码位是不是被异常置位# 列出内容但不解压确认平台和版本 unzip -l mos-pesq.zip # 测试完整性和 CRC输出 OK 才算过 unzip -t mos-pesq.zip # 如果报错要求密码先怀疑伪加密用 7z 列目录确认 7z l mos-pesq.zipunzip -l 显示每个文件的原始大小和压缩后大小从文件名就能判断这是纯源码版还是带二进制版。看到 Makefile 加一堆 .c 文件是需要自己编译的源码包看到 pesq 可执行文件就是预编译版。7z x -y mos-pesq.zip 在伪加密场景下通常能直接解开如果 7z 也提示需要密码才是真加密只能回源头要密码。提示zip 伪加密和真加密的区别在于真加密没有密码拿不到内容伪加密只是文件头置了加密位内容本身没有加密。7-Zip 这类工具对标志位处理更宽松常能直接打开。3.2 解压后的目录结构源码版与预编译版怎么分辨解压后先看目录。源码版 mos-pesq 常见结构如下路径内容用途pesq/C 源码与 Makefile需要编译生成可执行文件pesq/readme.txt版本说明与命令示例写明 P.862 还是 P.862.2pesq/test/参考与退化测试音频编译后验证分数一致pesq/pesq预编译 Linux 可执行文件有则无需编译tools/ 或 pypesq/Python 包装脚本批处理时更顺手拿到包先看 readme.txt 里有没有写版本号。写 P.862 是老窄带实现写 P.862.2 或 WB 是宽带实现两者命令参数和输出格式都不同。test 目录是验证工具链的标尺编译完拿 test 音频跑一遍分数和 readme 参考值一致说明编译正确后续打分才可信。3.3 Linux 下编译与最小命令make 出可执行文件源码版的基本流程是进源码目录make 编译。老 PESQ 源码依赖很少C 标准库就够不需要额外装库这在生产环境里是优势cd mos-pesq/pesq make clean make ./pesq 16000 ../test/ref.wav ../test/deg.wavmake clean 清掉打包者留在其他平台上的 .o 文件避免编译选项串台。编译成功后生成 pesq 可执行文件./pesq 后跟采样率参数和两个 WAV 路径这是 PESQ 最标准的调用形式。16000 按 16kHz 宽带模式处理8000 是 8kHz 窄带模式参数必须和文件实际采样率一致否则会报 Invalid sample rate。输出格式随版本差异很大老版本常见PESQ Score (MOS-LQO): 3.412新一点的版本可能长这样P.862 Prediction (Raw MOS, MOS-LQO): 3.051 4.002字段顺序是原始分在前、映射后 MOS 在后。批处理解析时不要按字段名硬编码取每行最后一个数字最稳第 4 章会详细讲。3.4 用 Python 包装命令行批处理绕不开的子进程调用单条打分用命令行足够几十条上百条的回归就得包进脚本。最稳的做法是 subprocess 逐条调用不碰 C 代码import subprocess result subprocess.run( [./pesq, 16000, ref.wav, deg.wav], capture_outputTrue, textTrue, ) print(result.stdout) print(result.returncode)subprocess.run 默认不抛异常必须检查 returncode。PESQ 遇到输入异常会以非零码退出stdout 可能什么都没有此时盲目解析分数会得到空值。脚本里先判断 returncode不为 0 就把 stderr 写进日志再单独复核这对文件比结果表里出现空行好排查得多。3.5 pip 版 pesq 库的取舍float 数组输入是最大差异如果 zip 里没有源码或者不想碰编译另一个常见路径是 pip 安装社区封装的 pesq 包。它带编译好的二进制和 Python 绑定调用更 Pythonicimport soundfile as sf from pesq import pesq ref, sr sf.read(ref.wav) deg, _ sf.read(deg.wav) score pesq(sr, ref, deg, wb) # nb 窄带, wb 宽带 print(score)这个库的输入是 float 数组幅度范围落在 [-1, 1]。很多人把 int16 数组直接喂进去分数离谱地高或低这不是算法问题是幅度域不对。soundfile 读出来本身是 float但要确认文件没有经过自定义归一化缩放。另外这个包返回的是 MOS-LQO 分数不是原始分和命令行版混用前要确认版本统一。提示命令行版本适合 Linux 服务器pip 版适合测试脚本。原则是整套回归只认一种调用方式交替使用会把版本差异混进结果。4. 批处理打分与 MOS 映射从单条 WAV 到一张回归表4.1 文件配对与重采样统一到 16kHz 单声道批处理的第一步是统一输入规格。PESQ 对采样率很挑剔窄带只接受 8kHz宽带只接受 16kHz文件实际采样率和命令行声明不匹配会直接报错。我统一把素材重采样到 16kHz 单声道再跑宽带模式原因是 16kHz 保留语音频段的绝大部分信息单声道则消除声道间相位差造成的干扰。sox ref.wav -r 16000 -c 1 ref_16k.wav sox deg.wav -r 16000 -c 1 deg_16k.wavsox 的 -r 指定输出采样率-c 指定声道数。重采样后用 soxi 抽查确认 Sample Rate 和 Channels 字段与预期一致。一个隐蔽坑是如果原始文件是 A-law 等压缩编码必须先解码成 PCM否则 PESQ 会把这一层编码损伤算进结果分数比真实链路差。sox 在读这类文件时会自动解码但如果你手动改后缀或者用某些工具直接转就可能在文件头留下错误的编码标记。4.2 批处理脚本按文件名一一配对日志与分数分离文件配对最怕错位。参考目录和退化目录文件名对不上时脚本必须显式跳过并留痕而不是让 PESQ 拿着不存在的路径报错中断。我一般用下面的模板#!/bin/bash set -u REF_DIRrefs DEG_DIRdegs OUTresults.txt LOGrun.log : $OUT : $LOG for ref in $REF_DIR/*.wav; do name$(basename $ref) deg$DEG_DIR/$name if [ ! -f $deg ]; then echo MISSING $name $LOG continue fi echo $name $OUT ./pesq 16000 $ref $deg $OUT 21 echo $name done $LOG done脚本先清空输出文件再遍历参考目录用 basename 取文件名去退化目录找同名文件。找不到就写 MISSING 跳过绝不静默失败。找到就追加 PESQ 输出stdout 和 stderr 都进结果文件最后写一条 done。跑完后看 LOG 里有多少 MISSING 和 done就知道回归覆盖了几条、漏了几条。如果文件名不一致但目录顺序能对上也可以按行号配对那条路坑更多。生产环境里我宁可拷贝时花十分钟把名字对齐也不在脚本里赌顺序。顺序错位的后果是每一条分数都算错结果表却看起来一切正常这是批处理里最隐蔽的问题。4.3 解析结果字段取每行最后一个数字最稳妥PESQ 不同版本的输出格式不稳定有的打印 PESQ Score有的打印 Prediction有的多打印一版 MOS-LQO 标签。最通用的解析是取含关键字行的最后一个字段grep -E PESQ|Prediction|MOS results.txt | awk {print $NF}awk 的 $NF 表示最后一个字段。如果行是 PESQ Score (MOS-LQO): 3.412$NF 就是 3.412如果行是 Prediction (Raw MOS, MOS-LQO): 3.051 4.002$NF 是 4.002正好是映射后的 MOS。两种格式都能得到想要的最终分数。要小心错误信息也被 grep 捞进来先剔除再取字段grep -E PESQ|Prediction|MOS results.txt | grep -v Error | awk {print $NF}解析完的分数继续汇总平均值、95 置信区间或者按目录分组对比。到这里手工一条条打分已经变成可重复的回归流程了。4.4 原始分到 MOS-LQO 的映射两个公式不要张冠李戴老版本 PESQ 输出原始分范围 -0.5 到 4.5和口语里的“3.5 分不错”对不上。P.862.1 窄带映射曲线是awk BEGIN{ a1.4945; b4.6607 } { raw$1 mos0.999 0.999/(1exp(-a*rawb)) print raw, mos }raw 是 PESQ 原始分mos 是映射后的 MOS-LQO。这个 S 形曲线把原始分两端压到 1 和 4.5 左右中间保持近似线性。宽带 P.862.2 的映射系数不同a 换 1.3669b 换 3.8224。很多人拿窄带公式硬套宽带分数结果整体偏高约 0.3回归结论直接反转。更常见的陷阱是把手动映射套在新版输出上。新版 PESQ 输出已经标注 MOS-LQOpip 版更是直接返回 MOS-LQO再映射一次等于把分数压两遍数字失去意义。动手前花一分钟确认输出字段是 Raw MOS 还是 MOS-LQO这个确认比调任何参数都重要。5. PESQ 避坑指南5 个让分数翻车的常见问题与排查PESQ 这套工具源码老、版本杂、输入要求严格跑通不难跑对很难。下面五条是我在回归测试里反复遇到的坑每一条都按现象、原因、解决三个层面写排查时可以直接对照。5.1 解压要密码7z 能看到内容zip 伪加密现象unzip mos-pesq.zip 提示需要密码输入网盘页面给的解压码又报错文件卡在解压环节压缩包大小和网页描述完全一致。原因zip 文件头通用位里的加密标志被置位但文件内容并没有真正加密这就是 zip 伪加密。老压缩工具或转发时有人改过标志位内容本身完好。解决先 7z l 列目录能看到文件名说明内容可读直接 7z x -y 强制解压。如果 7z 也坚持要密码才是真加密只能回源头核对解压码。以后拿到存有可执行文件的 zip先跑一遍 unzip -t再决定要不要花时间解压编译。5.2 分数大面积 1.0WAV 编码不是 16-bit PCM现象一批音频跑完分数全部集中在 1.0 到 1.2像集体判了死刑和实际听感完全对不上。原因PESQ 内部按 16-bit PCM 解析 WAV。输入是 32-bit float 或 24-bit 编码时样本被错误解析成垃圾数据算法算不出有效扰动只能给出最低分。解决用 ffprobe 或 soxi 查编码格式非 16-bit PCM 先转码再喂ffmpeg -i deg.wav -sample_fmt s16 -ar 16000 -ac 1 deg_pcm.wav转码后分数回到合理区间。这个问题每次换数据源都可能复发我的习惯是批处理前先随机抽两个文件查编码确认无误再开跑。5.3 报 Invalid sample rate声明和实际采样率不一致现象命令执行后立刻输出 Invalid sample rate不给分数。或者一部分文件正常另一部分报错。原因命令行 8000 或 16000 和 WAV 文件头里的采样率对不上。文件实际是 48kHz声明 16kPESQ 直接拒绝。解决soxi 查实际采样率按实际值选 8000 或 16000。PESQ 只支持 8k 和 16k 两档44.1k、48k 必须先重采样。批处理脚本里统一先重采样到 16k 再跑 16000这个报错就不会再出现。5.4 同一音频两个版本差 0.5P.862 与 P.862.2 混用现象同样的 WAVA 机器跑 3.4B 机器跑 3.9查遍环境没有异常分数像玄学一样飘。原因A 用老 P.862 窄带实现 8000B 用 P.862.2 宽带实现 16000。两者的感知模型和映射函数不同分数不在同一标尺上。解决归档结果时把可执行文件的 MD5、版本号、采样率参数写进表头。回归测试只认一个版本换版本就重跑基线新旧分数严禁混着比较。我见过有人拿两版分数做均值对比得出“优化有效”的结论实际只是版本漂移。5.5 大时延场景分数失真PESQ 的时间对齐有上限现象端到端链路延迟超过 2 秒语音本身很清楚PESQ 却打出 1.5 分。原因PESQ 的时间对齐模块按短时包络做互相关超过约 2 秒的帧级错位超出对齐能力退化信号被当成无法匹配的噪声惩罚。解决先做前置对齐。用 ffmpeg 的 adelay 或 Python 互相关把两路信号粗对齐到 50ms 以内再喂 PESQ。PESQ 适合评估编码、降噪、丢包这类短时损伤不适合评估端到端大缓冲这类时延主导的问题。遇到这种场景我直接换 POLQA 或者人工听测不硬拿 PESQ 给结论。6. 进阶验证用 PESQ 回归集把每个改动变成可量化的 MOS 差6.1 搭一套固定回归集回归集不必大但要稳定。我固定 10 条语音男女声各半、中英文各半覆盖安静、车内、街道三类背景噪声每条 10 到 20 秒。这 10 条在每次算法改动后都跑一遍 PESQ得到一张基线表。如果改动后分数普遍上涨方向对某一条掉分就回头听那一条往往能发现一个只在特定噪声下触发的边界问题。这套回归集跑一次不到五分钟是性价比最高的质量闸门。6.2 用交叉验证校准 PESQ 的可信度PESQ 不是真理需要定期和主观听感对齐。我在每个里程碑节点挑 5 条差异最大的样本自己听一遍按直觉排序再和 PESQ 排序对比。如果出现明显反转先不急着下结论检查那几条是否触发了 5.2 的编码问题或 5.5 的时延问题。客观打分加人工复核的流程比单信任何一个数字都可靠。6.3 记住 PESQ 的边界再动手PESQ 对短时损伤敏感对长时延不敏感对 8k/16k 语音有效对 48k 超宽带没有标准定义对降噪、编解码、丢包很灵敏对 AGC 引起的动态起伏会误判。遇到这些场景我会换 POLQA 或 ViSQOL 交叉验证而不是硬让 PESQ 给结论。以前我接过一个回声消除项目追着 PESQ 分数查了一周最后发现是 AGC 动态范围变化触发了感知模型误判改成固定增益后分数立刻正常。那次之后我的评测方案里永远先写“适用边界”再列指标。这个习惯少走了很多弯路希望也能帮到你。本文还有配套的精品资源点击获取