电话线路那头送来的通常是 8 kHz 采样、单声道、G.711 μ-law 或 A-law 编码的窄带音频,而实时模型侧一般吃固定长度的线性 PCM 小块。中间这层差距不该靠猜:先用命令把电话侧的真实采样率、位深和编码量出来,再把重采样和编码转换固定在一个层里只做一次,最后用回放对比确认没有引入明显失真。效果忽好忽坏,多数时候是格式假设与实际不符、或者链路两端各转了一次。
对齐的重点不是“把 8k 改成 16k”这一步,而是三件事:确认真实格式、选定唯一的转换点、把分片长度换算成稳定的毫秒数。建议在网关侧一次性完成解码、重采样和 PCM 分片,采集端只保留必要的降噪与增益。判断是否对齐成功,不看配置写得对不对,看三点:ffprobe 输出与配置是否一致、分片长度日志是否稳定、回放对比有没有可听出的变调或块边界杂音。
先量清电话侧音频的真实格式
不要按对接文档里写的“8k 单声道”直接开工,先对一段真实录音或一次真实通话录音执行下面的命令,把结果贴进接入记录。
# 看容器与编码层面的关键字段
ffprobe -hide_banner -select_streams a:0 -show_entries stream=codec_name,sample_rate,channels,sample_fmt,bits_per_sample,bits_per_raw_sample,duration -of default=noprint_wrappers=1 phone_sample.wav
# WAV 场景下 soxi 的输出更好读
soxi phone_sample.wav
# 不看头部、直接确认容器类型
file phone_sample.wav
读输出时注意几个容易误判的地方:codec_name=pcm_mulaw 说明存储形态是每样本 1 字节的压扩编码,而 sample_fmt=s16 是解码之后的形态,两者不是一回事,按 sample_fmt 去算字节数会算错一倍;sample_rate 若为 8000 而配置里写的是 16000,模型侧就会听到语速和音调都变慢的结果;channels=1 与 channels=2 若弄混,左右声道交错会被当成单声道数据送入。
如果链路是 SIP/RTP 而不是录音文件,先在 SDP 里看 a=rtpmap 行确认编码名与时钟频率,再用抓包工具核对实际负载类型:
tshark -r call.pcap -Y rtp -T fields -e rtp.p_type -e rtp.seq -e rtp.timestamp | head
把测量结论写成一行记录,例如“编码 pcm_mulaw / 采样率 8000 / 声道 1 / 每样本 1 字节 / 来源:通话录音 30s”,后续所有换算都以这一行为准,不要以文档描述为准。
选重采样的位置:采集端还是网关端
重采样和编码转换只应发生一次。两种常见放法如下,差别主要在于“有多少环节会顺手再转一次”。
方案 A(采集端转,网关透传)
电话线路 8k mu-law --> 采集端[解码+重采样] --> PCM16 16k --> 网关(只转发) --> 模型
转换次数:解码 1 次、重采样 1 次
方案 B(网关端转,采集端只收原始流)
电话线路 8k mu-law --> 采集端[只封装/降噪] --> 8k mu-law --> 网关[解码+重采样] --> PCM16 16k --> 模型
转换次数:解码 1 次、重采样 1 次
两者转换次数相同,取舍在运维面:方案 A 把压力放在话机或采集进程上,网关只需透传,但每台采集端都要维护一份重采样配置,版本容易漂;方案 B 配置集中,改一次全网生效,代价是网关要承担解码与重采样开销,并发高时需要观察 CPU。真正要避免的是第三种情况——采集端转了一次、网关以为还是 8k 又转一次,音频经历两轮滤波,高频被削或出现混叠。
对比听测的组织方式:取同一段 30 到 60 秒素材(含男声、女声、连续数字串、爆破音),分别走方案 A 和方案 B 产出两个 16 kHz PCM16 单声道文件;文件重命名为无意义编号并打乱顺序,用同一个播放器、同一音量盲听,不要在文件名里标注方案。
编码转换与分片大小
模型侧希望收到稳定长度的小块,分片大小要按字节数与毫秒数双向换算,而不是随手写个 1024。
每毫秒字节数 = 采样率 x 每样本字节数 / 1000
分片字节数 = 采样率 x 每样本字节数 x 分片毫秒数 / 1000
8k mu-law 1 字节/样本 -> 8 字节/ms -> 20ms = 160 字节
8k PCM16 2 字节/样本 -> 16 字节/ms -> 20ms = 320 字节
16k PCM16 2 字节/样本 -> 32 字节/ms -> 20ms = 640 字节
分片毫秒数建议先按 20ms 试,太小会让请求变密,太大则首字延迟上升;无论选多少,都要保证每个分片字节数与该毫秒数严格对应,否则解码端会听到周期性咔哒声。
# 占位骨架:参数以 ffprobe 测量结果和模型侧要求为准
IN_RATE = 8000 # 以测量结果为准,勿按文档假设
IN_CODEC = pcm_mulaw # 每样本 1 字节
IN_FRAME_MS = 20
IN_FRAME_BYTES = IN_RATE * 1 * IN_FRAME_MS / 1000 # 160
OUT_RATE = 16000 # 目标值待确认后写死
OUT_FMT = s16le
OUT_FRAME_MS = 20
OUT_FRAME_BYTES = OUT_RATE * 2 * OUT_FRAME_MS / 1000 # 640
离线转换可用 ffmpeg 验证一遍,注意输入侧要按原始编码声明,不要先解成 WAV 再转:
ffmpeg -hide_banner -f mulaw -ar 8000 -ac 1 -i phone.raw \
-af aresample=16000 \
-ac 1 -f s16le -acodec pcm_s16le out_16k_s16le.raw
若 ffmpeg 使用了 soxr 重采样器,与默认 swr 的听感可能略有差别,切换时同样要做一次回放对比。发送侧的分片请求骨架(字段名以实际对接文档为准,这里只体现参数位置):
{
"type": "input_audio_chunk",
"audio_base64": "BASE64_OF_FRAME",
"sample_rate": 16000,
"format": "pcm_s16le",
"channels": 1,
"frame_ms": 20
}
用回放对比判断是否失真
配置写对不等于听感没问题。先把裸 PCM 转回可播放容器,再做 A/B 回放:
ffmpeg -f s16le -ar 16000 -ac 1 -i out_16k_s16le.raw out_16k.wav
ffprobe -hide_banner -show_entries stream=sample_rate,channels,duration -of default=noprint_wrappers=1 out_16k.wav
ffmpeg -hide_banner -i out_16k.wav -af astats -f null - 2>&1 | tail -n 20
回放时依次确认:原始 8k 录音与转换后 16k 文件时长是否一致(时长对不上通常是采样率标错或分片丢帧);整体音调是否偏低或偏高(变调几乎一定是采样率与实际数据不符);块边界处是否有规律性咔哒;底噪是否明显抬升(两次重采样或增益叠加会放大底噪)。听感差异记录建议按“时间点 / 现象 / 怀疑原因”三列写,例如“0:12 段尾咔哒 / 分片字节数与 20ms 不匹配 / 检查分片换算”。
需要记录的听感差异项至少包括:音调偏移、齿音发闷或过亮、底噪抬升、块边界杂音、断续丢字、爆破音失真。若只听到轻微差异且不影响识别,可以先上线再按真实通话样本复核;若出现变调或规律性杂音,先停下排查格式,不要用增益或降噪去掩盖。
把格式对齐写进接入检查清单
把下面几项做成接入单里的固定栏目,每次新增线路照着填,能减少重复返工。
- 采样率:记录 ffprobe 或 soxi 读出的实际值,与发送侧声明的
sample_rate逐字比对;不一致即判为未通过。 - 位深与编码:写明存储形态(mu-law 1 字节)与传输形态(pcm_s16le 2 字节),避免混用造成字节数算错一倍。
- 声道:确认单声道;立体声交错数据被当作单声道送入时,语速会表现为异常。
- 分片长度:写出目标毫秒数与换算后的字节数(如 20ms / 640 字节),并在日志里打印实际发送字节数,观察是否恒定。
- 转换位置:明确写在采集端还是网关端,并在另一端禁止二次重采样;两端都改过一次是最常见的失真来源。
- 验证方式:一段真实通话回放对比加一次分片长度日志检查,两项都留记录,缺一项不算完成。
清单填完之后,把测量结论和转换位置写成一行注释放进配置文件顶部。后续换线路、换网关版本时先读这一行,再决定是否需要重新测量,避免又回到按文档假设格式的老路上。