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

在这里插入图片描述

一、问题现象:机器人说话带“滋滋”杂音

某次用户反馈:电话接通后,云雀语音智能体播报的 TTS 人声中夹杂着密集的“滋滋”声,像是电流声又像蜂鸣,录音文件听起来完全正常,只有手机端下行能听到。

我们截取了通话的 PCM 录音(PCMA/8kHz),用频谱分析工具扫描,发现:

  • 在正常的 TTS 语音段之间,每隔 20ms 左右出现一次高频脉冲。
  • 3.5kHz 以上的能量峰值频繁出现,密度约 50 次/秒,这与通话的打包时长(ptime=20ms)完全吻合。
  • 脉冲频率集中在 2600~3600Hz,紧贴着后续分析发现的低通滤波器截止频率。

听感上,每秒 50 次的“咔嗒”声连贯起来就是人耳敏感的中高频“滋滋”声,这不是噪声,而是规律的 click 流


二、根因定位:Butterworth IIR 的“冷启动”问题

问题的代码路径是:TTS 引擎输出 24kHz 的单声道 PCM,需要转换成 8kHz 窄带音频发送给运营商。在 resample_pcm16le 函数中,采样率转换分两步:

  1. 先用一个 低通滤波器 滤掉 3.6kHz 以上的频率(防止混叠)。
  2. 再用 audioop.ratecv 进行整数倍下采样。

这个低通滤波器是手动实现的 2 阶 Butterworth IIR 滤波器,简化后的核心运算如下:

def _lowpass_butter_2nd_pcm16le(pcm, sr, cutoff_hz):
    # 计算系数 ...
    x1 = x2 = y1 = y2 = 0   # ← 问题在这里
    for sample in pcm:
        x0 = sample
        y0 = b0*x0 + b1*x1 + b2*x2 - a1*y1 - a2*y2
        # 更新延迟单元
        x2, x1 = x1, x0
        y2, y1 = y1, y0
        yield y0

注意 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, state=None):
    if state is None:
        x1 = x2 = y1 = y2 = 0
    else:
        x1, x2, y1, y2 = state
    # ... 处理循环 ...
    return list_output, (x1, x2, y1, y2)

resample_pcm16le 中,力窄带路径的 state 被持久化为一个 tuple,随会话一直传递:

# 伪代码
state_lp = None
for chunk in audio_chunks:
    filtered, state_lp = _lowpass_butter_2nd_pcm16le(chunk, sr, 3400, state_lp)
    # 继续后续处理 ...

同时为了获得更好的阻带衰减,滤波器升级为 4 阶(级联两个 biquad),截止频率微调到 3.4kHz。

修复后的效果立竿见影:

指标 修复前 修复后
1kHz 纯音输出的 hi3.5k RMS 1340 30
3.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 的狡猾之处在于:它只出现在分块流式处理状态未保持的场景中。传统的离线转码或短文件测试根本触发不了,因为一次性处理整个文件时状态自然连续。

云雀语音智能体在升级后,所有机器人的下行语音都恢复了干净、清晰的人声。如果你也在开发类似的实时语音对话系统,务必警惕那些“每次调用都重置”的滤波器、编码器或重采样器——一个看似无害的初始化,可能就是杂音的源头。


搞定。就这样一个小点,足以吃掉你半天的调试时间。希望这篇文章能帮你省下这半天。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐