智能外呼机器人的音频处理中的隐形杀手:IIR 滤波器状态重置引发的“滋滋”声

智能外呼机器人的音频处理中的隐形杀手:IIR 滤波器状态重置引发的“滋滋”声 音频处理中的隐形杀手IIR 滤波器状态重置引发的“滋滋”声一、问题现象机器人说话带“滋滋”杂音某次用户反馈电话接通后云雀语音智能体播报的 TTS 人声中夹杂着密集的“滋滋”声像是电流声又像蜂鸣录音文件听起来完全正常只有手机端下行能听到。我们截取了通话的 PCM 录音PCMA/8kHz用频谱分析工具扫描发现在正常的 TTS 语音段之间每隔 20ms 左右出现一次高频脉冲。3.5kHz 以上的能量峰值频繁出现密度约50 次/秒这与通话的打包时长ptime20ms完全吻合。脉冲频率集中在 2600~3600Hz紧贴着后续分析发现的低通滤波器截止频率。听感上每秒 50 次的“咔嗒”声连贯起来就是人耳敏感的中高频“滋滋”声这不是噪声而是规律的 click 流。二、根因定位Butterworth IIR 的“冷启动”问题问题的代码路径是TTS 引擎输出 24kHz 的单声道 PCM需要转换成 8kHz 窄带音频发送给运营商。在resample_pcm16le函数中采样率转换分两步先用一个低通滤波器滤掉 3.6kHz 以上的频率防止混叠。再用audioop.ratecv进行整数倍下采样。这个低通滤波器是手动实现的 2 阶 Butterworth IIR 滤波器简化后的核心运算如下def_lowpass_butter_2nd_pcm16le(pcm,sr,cutoff_hz):# 计算系数 ...x1x2y1y20# ← 问题在这里forsampleinpcm:x0sample y0b0*x0b1*x1b2*x2-a1*y1-a2*y2# 更新延迟单元x2,x1x1,x0 y2,y1y1,y0yieldy0注意x1 x2 y1 y2 0这一行。因为音频数据是分chunk送入的每 20ms 一段每次调用该函数时滤波器的历史状态都被重置为零。也就是说无论上一段音频的波形如何新的 chunk 都要面对一个“从未见过信号”的滤波器就像冷启动一样。对于 IIR 滤波器状态重置会引入一个从 0 到稳态的瞬态响应ringing。如果输入是纯 1kHz 正弦波滤波器稳态下几乎不会输出高频但每个 chunk 开头都会产生一簇 3.6kHz 附近的振荡这正是我们看到的 50 次/秒的高频脉冲。实验复现完美证实了这一点用纯净 1kHz 正弦波作为输入24kHz→8kHz 强制窄带转换。统计 5ms 帧中 3.5kHz 以上峰值 500 的次数结果为 49 次/199 帧即49 clicks/秒与真实录音完全一致。故障链总结TTS 24kHz chunk →_lowpass_butter_2nd状态归零 → 每个 chunk 开头产生瞬态 → 50 clicks/秒 → 人耳听到“滋滋”声。三、修复让滤波器状态跨 chunk 持续流动解决方案极其简单把 filter state 从局部变量改为外部传入的持久状态每次处理 chunk 后返回更新后的 state供下一次调用使用。修改后的函数签名变为def_lowpass_butter_2nd_pcm16le(pcm,sr,cutoff_hz,stateNone):ifstateisNone:x1x2y1y20else:x1,x2,y1,y2state# ... 处理循环 ...returnlist_output,(x1,x2,y1,y2)在resample_pcm16le中力窄带路径的 state 被持久化为一个 tuple随会话一直传递# 伪代码state_lpNoneforchunkinaudio_chunks:filtered,state_lp_lowpass_butter_2nd_pcm16le(chunk,sr,3400,state_lp)# 继续后续处理 ...同时为了获得更好的阻带衰减滤波器升级为 4 阶级联两个 biquad截止频率微调到 3.4kHz。修复后的效果立竿见影指标修复前修复后1kHz 纯音输出的 hi3.5k RMS1340303.5k 峰值 500 的帧数49 次/秒0 次/秒主观听感滋滋声干净无杂音滋滋声彻底消失。四、进阶8 阶 50Hz 高通抹平宽带噪声实际部署后用户反馈“电流声”虽然大幅减弱但仍有轻微残留。频谱分析发现4 阶 Butterworth 在 4~5kHz 的滚降不够陡-24dB/oct残留的能量被 OPUS 编码器放大形成类似白噪声的听感。于是我们将滤波器升级为8 阶 Butterworth 3.0kHz并增加一个50Hz 高通去除直流和工频干扰。最终在 3.5~5kHz 实现了超过 30dB 的衰减宽带噪声也被消除。整个过程只修改了一个核心函数没有改动任何流媒体框架CPU 开销增加不到 1%。五、云雀语音智能体的启示这个 bug 的狡猾之处在于它只出现在分块流式处理且状态未保持的场景中。传统的离线转码或短文件测试根本触发不了因为一次性处理整个文件时状态自然连续。云雀语音智能体在升级后所有机器人的下行语音都恢复了干净、清晰的人声。如果你也在开发类似的实时语音对话系统务必警惕那些“每次调用都重置”的滤波器、编码器或重采样器——一个看似无害的初始化可能就是杂音的源头。搞定。就这样一个小点足以吃掉你半天的调试时间。希望这篇文章能帮你省下这半天。