资讯动态

二阶IIR音频滤波器设计:巴特沃斯与双线性变换实战指南

发布时间:2026/10/21 9:05:34 来源:尧图企业网站定制
二阶IIR音频滤波器听起来是个教科书名词可我真正确掌握它的设计是栽了跟头之后才做到的。之前在某项目里调低通滤波我套用网上抄来的巴特沃斯系数模板直接烧进设备实测截止点比预期低了将近10%后来才明白问题出在数字域与模拟域的频率映射上。这篇文章想把“二阶IIR音频滤波器基于巴特沃斯设计”这件事从头到尾讲透为什么巴特沃斯适合做音频、系数怎么从模拟原型推到z域、工程上用什么结构实现最省心以及我在实际调试里踩过的坑和验证手段。适合正在写音频效果器、嵌入式音频代码或者刚接触DSP但不想只看理论公式的开发者。1. 巴特沃斯“最平坦”这句广告语到底意味着什么1.1 为什么是它通带无纹波转折处刚刚好巴特沃斯滤波器最常被引用的特点是“最大平坦幅频响应”意思是通带内幅度响应没有波纹不会有某个频率点莫名被加强或削弱。对于音频来说这一点很关键因为做EQ、分频、降噪时我们希望滤波器做的事情是“该过的过、该减的减”而不是在通带里留下可闻的染色。二阶巴特沃斯的极点分布在s平面左半平面一对共轭极点与虚轴呈45度角。正是这个几何位置决定了它的品质因数Q固定为Q 1 / √2 ≈ 0.7071这个Q值带来的结果是在截止频率fc处幅度精确衰减到-3dB也就是0.707倍没有凸起也没有提前滚降。很多初学者会把Q理解成“越高越尖锐”但在滤波器设计里Q影响的是转折点附近的形状。Q低于0.707会让转折变得更圆缓截止处衰减超过-3dBQ高于0.707则会在截止处出现共振峰比如Q1时会有一个约1.25dB的凸起听感上就是“某个频段有点顶耳朵”。二阶巴特沃斯的滚降斜率固定为每倍频程-12dB。这是一个工程上很舒服的斜率比一阶滤波器的-6dB/oct明显更有效又不像四阶那样产生明显群延迟。在多数音频场景里二阶巴特沃斯已经能把“需要滤掉的部分”压到人耳不太敏感的状态而代价相对可控。1.2 和切比雪夫、贝塞尔的取舍有些场景下巴特沃斯不是唯一选择甚至不是最优选择。我做选型时通常会拿三张牌对比逼近类型通带纹波过渡带陡度相位特性典型用途巴特沃斯无纹波最平坦中等二阶12dB/oct群延迟非线性但无过冲音色塑形、分频、EQ基础切比雪夫I型通带有纹波更陡群延迟波动更大需要更强截止的测量或抗混叠贝塞尔无纹波最缓接近线性相位需要保形的脉冲信号如果目标是“尽快滤掉隔壁频段的能量”切比雪夫确实更猛但通带纹波会在某些频率造成固定染色音乐信号经过它之后容易产生“金属感”。贝塞尔赢在相位代价是过渡带太缓做EQ显得不够利落。巴特沃斯卡在中间听感自然、无过冲、过渡带够用所以音频领域最常见到它。我自己的习惯是分频器、EQ、降噪链路优先用巴特沃斯只有在测量器材校准这类对相位要求极高的地方才考虑贝塞尔切比雪夫基本不碰。2. 从模拟原型到数字系数一张纸推导加工程公式2.1 双线性变换的代价频率压缩与预畸变IIR数字滤波器最稳的设计路线是先写出模拟原型再用双线性变换映射到z域。模拟二阶巴特沃斯低通原型长这样H(s) Ωa² / (s² (Ωa/Q)s Ωa²)其中Ωa是预畸变后的模拟截止角频率Q0.7071。双线性变换的替代关系是s 2Fs · (1 - z⁻¹) / (1 z⁻¹)这个变换有两个好处第一稳定的模拟左半平面极点会映射到z平面单位圆内滤波器保持因果稳定第二它能把整个模拟频率轴压缩到数字的0到π频段不会出现混叠。代价是频率轴发生了非线性压缩。如果直接把想要的目标频率fc当作模拟Ωa代入变换后数字滤波器的实际截止点会偏低频率越高偏差越大。这就是开头我提到的翻车根源没做预畸变低通截止点整体往下跑。解决办法是在代入原型前做预畸变pre-warpingΩa 2Fs · tan(π·fc / Fs)fc是目标数字截止频率单位Hz。这个公式会把希望的数字截止点精确钉在变换后的频率轴上保证最终-3dB点就是你设置的fc。实际使用中如果fc远小于Fstan近似等于线性值偏差很小一旦fc接近Fs/4以上偏差会肉眼可见必须老老实实预畸变。2.2 用业界通用的biquad公式把系数一次算完手动推完整个代入、合并、化简过程会很啰嗦。工程上我直接采用Audio EQ Cookbook里流传最广的那套biquad系数公式它是把预畸变和双线性变换合并后的闭式解省去中间推导。对于二阶巴特沃斯低通只需三个中间量w0 2π·fc / Fscos_w0 cos(w0)sin_w0 sin(w0)alpha sin_w0 / (2Q)其中Q固定取0.7071。然后系数直接写b0 (1 - cos_w0) / 2b1 1 - cos_w0b2 (1 - cos_w0) / 2a0 1 alphaa1 -2·cos_w0a2 1 - alpha最后所有系数除以a0把差分方程变成y[n] b0·x[n] b1·x[n-1] b2·x[n-2] - a1·y[n-1] - a2·y[n-2]分母归一化这步不能省因为实际代码里我们只存b0、b1、b2、a1、a2五个系数默认a0恒等于1。2.3 一个数值例子算给你看设Fs48000Hz目标fc1000HzQ0.7071。代入公式w0 2π×1000/48000 ≈ 0.130900 radcos(w0) ≈ 0.991445sin(w0) ≈ 0.130526alpha 0.130526 / 1.414214 ≈ 0.092297a0 1.092297然后b0 ≈ 0.004278b1 ≈ 0.008555b2 ≈ 0.004278a1 ≈ -1.982890a2 ≈ 0.907703归一化后b0 ≈ 0.003916b1 ≈ 0.007833b2 ≈ 0.003916a1 ≈ -1.815349a2 ≈ 0.831014这组系数做出来的滤波器1000Hz处增益约-3dB10Hz到900Hz接近0dB2000Hz处约-9dB5000Hz处已经衰减到接近-18dB。如果手里有工具可以拿SciPy的butter函数生成同参数系数对照结果几乎一致差异只在小数点后第5位左右来自浮点舍入。3. 系数落地C代码模板与结构选型3.1 TDF2实现与DF1的差别有了系数接下来是差分方程的具体实现结构。最直观的是Direct Form IDF1需要四个状态变量分别存x[n-1]、x[n-2]、y[n-1]、y[n-2]。它逻辑清晰适合教学但内存多、中间变量多。实际音频代码里我更推荐Transposed Direct Form IITDF2只需要两个状态变量代码更短浮点下的噪声特性通常也更好。TDF2的C语言实现如下#include math.h typedef struct { float b0, b1, b2; float a1, a2; float z1, z2; } biquad_t; void biquad_lp_butter_init(biquad_t *f, float fs, float fc) { float w0 2.0f * (float)M_PI * fc / fs; float cw cosf(w0); float sw sinf(w0); float alpha sw * 0.70710678f; // sin(w0) / (2 * Q) float a0 1.0f alpha; f-b0 (1.0f - cw) * 0.5f / a0; f-b1 (1.0f - cw) / a0; f-b2 f-b0; f-a1 (-2.0f * cw) / a0; f-a2 (1.0f - alpha) / a0; f-z1 0.0f; f-z2 0.0f; } float biquad_process(biquad_t *f, float x) { float y f-b0 * x f-z1; f-z1 f-b1 * x - f-a1 * y f-z2; f-z2 f-b2 * x - f-a2 * y; return y; }注意TDF2里状态变量z1、z2的更新顺序先算当前输出y再用y更新z1最后更新z2。这个顺序在绝大多数biquad实现里都是固定写法不能随意换否则输出会错位一个采样点。DF1和TDF2在数学上完全等价输出结果几乎一样。区别主要在数值表现DF1有独立的延时线输入输出历史值都比较“干净”不容易出现内部累加溢出但状态多TDF2把部分累加藏在z1、z2里当滤波器极点接近单位圆时z1的内部数值可能远大于输入信号在定点平台要格外小心。浮点处理器上我默认用TDF2如果目标平台是没有硬件浮点的MCU建议先评估内部定点溢出问题。3.2 实际工程中如何调用与清状态处理音频时按块循环调用就好for (int i 0; i num_samples; i) { out[i] biquad_process(filter, in[i]); }有一点容易被忽略滤波器状态必须随着音频流生命周期正确清零。如果音频流中途断过、切换过场景或者滤波器系数被大改旧状态里残留的电平可能会产生一次瞬态爆音。我的做法是在每次音频设备启动、停止、参数大变时把z1、z2显式置零。更稳妥的做法是配合一个极短的淡入淡出比如5到10毫秒把状态清零造成的咔哒声掩盖掉。4. 一阶变三态从低通衍生出高通和带通4.1 高通把分子翻个面做高通只需要改分子系数分母系数保持和低通完全一样。低通的分子零点在z1处直流被压零高通则把零点放到z-1处让奈奎斯特频率被压零。公式如下类型b0b1b2a0a1a2低通(1-cos_w0)/21-cos_w0(1-cos_w0)/21alpha-2cos_w01-alpha高通(1cos_w0)/2-(1cos_w0)(1cos_w0)/21alpha-2cos_w01-alpha带通alpha0-alpha1alpha-2cos_w01-alpha带通这里的alpha由中心频率和带宽决定Q_bp fc/BWalpha sin(w0)/(2Q_bp)。带通不再严格是巴特沃斯因为巴特沃斯本质上描述的是低通/高通的“最平坦”属性拿通用biquad公式做带通会更灵活日常够用。高通在音频里的典型用法是切除低频隆隆声、去除直流偏置。比如录音里常见的空调低频噪声、麦克风近讲效应带来的低频提升用高通100Hz左右处理效果非常自然。4.2 高阶怎么做级联而不是重算需要更陡峭的滚降时正确的做法不是去推导“四阶巴特沃斯闭式解”而是把两个完全相同的二阶巴特沃斯节级联起来。二级级联后滚降变成-24dB/oct这就是Linkwitz-Riley分频器的核心思路——很多专业音箱的分频网络就是两只相同巴特沃斯二阶串起来。级联实现简单代码里再建一个biquad_t实例按顺序处理即可y1[i] biquad_process(stage1, x[i]);y2[i] biquad_process(stage2, y1[i]);级联时要注意headroom第一级输出可能在某些频段接近满幅第二级再叠加增益整体容易过载。我会在链路里预留至少6dB余量或者在每一级后面加一个纯增益做归一化。四阶巴特沃斯虽然在频域更陡但群延迟明显变大对脉冲型信号会产生更长的“拖尾”做实时监听要留意。5. 数字域里最容易翻车的三件事系数精度、极点贴边、采样率误配5.1 采样率不同步导致截止偏移这是最隐蔽的坑。如果代码里写死了Fs48000但底层音频驱动实际工作在其他采样率所有频率点都会整体偏移。估算方法很简单实际截止频率约等于目标fc乘以两个采样率的比值。假设代码按48000算系数实际跑在44100那么1000Hz的截止会变成1000×44100/48000≈919Hz整体下移近8%。反之代码按44100算、实际跑48000截止会抬高到1088Hz。遇到这个问题的第一反应不是去猜而是先从框架里把真实采样率打出来确认参数一致。很多音频中间件在初始化时会修改时钟配置和外部声卡之间也可能存在取样率转换光看工程配置不可靠。为了自检我习惯在初始化后打印w0或直接打印归一化系数对照预期值一目了然。5.2 小截止频率下float精度不够用当fc特别低、Fs又特别高时比如Fs48000、fc40Hzw0只有0.0052sin(w0)约等于0.0052alpha只有0.0037左右a2≈0.9963。这时极点非常靠近单位圆滤波器内部状态z1和z2在低频信号通过时会累积特别大的值。这套系数用float算理论误差不大但在24bit定点或老平台上是灾难输出底噪会明显抬高甚至出现低频自激的“嗡”声。我踩过这个坑之后总结了两条规矩第一系数计算统一用double算完再转float给运行结构体这能避免初始化阶段的精度损失第二极低截止频率尽量放在高采样率链路的中间别让它在44.1kHz这种本来就紧张的带宽里硬扛。如果一定要在低频段做处理先做一次96kHz过采样让滤波器在更宽的频率间隔里工作系数会更健康。5.3 实时调参爆音系数插值怎么做一边播放音乐一边拖动截止频率参数如果直接给滤波器赋一组新系数输出会瞬间跳变听到的就是“咔哒”一声。原因是z1、z2里的旧状态仍然基于旧系数积累新系数突然接管后差分方程的平衡点被打破。最简单的缓解方案是系数缓动每次只把系数向目标值移动一小步用几十毫秒完成过渡。比如每采样更新f-a1 (target_a1 - f-a1) * 0.0005f; f-a2 (target_a2 - f-a2) * 0.0005f; // b0, b1, b2 同理但系数插值在TDF2结构下有一个隐患状态变量z1、z2不会跟随系数插值连续变化插值过程也可能产生轻微杂音。更彻底的做法是同时保持“旧系数滤波器和新系数滤波器并行”输出在几十毫秒内从旧滤波器渐变到新滤波器这个就叫交叉淡化。工程量大一些但爆音问题彻底消失。我自己的系统里如果参数变化不频繁用系数插值就够频繁自动扫频比如做Wah效果时我会用交叉淡化。6. 验证闭环脉冲、扫频与听感三方对照6.1 脉冲响应验证系数验证系数有没有算错最快的方法是用单位脉冲输入。输入x[1,0,0,0,...]根据已归一化的差分方程前几个输出可以直接手算y[0] b0y[1] b1 - a1·y0y[2] b2 - a1·y1 - a2·y0对于上面的1000Hz低通系数算出来y0≈0.003916y1≈0.015y2≈0.037。把这些预期值写进自动化测试跑代码后对比实际输出能立刻发现系数赋值顺序、符号正负、甚至状态变量未清零的问题。这招比直接听声音可靠得多因为人耳对毫秒级的错误不敏感。6.2 扫频与白噪声实测脉冲验证通过后再看频域。我惯用的方法是扫正弦生成不同频率的单音分别过滤波器测稳态RMS增益。用Python写一个很短的脚本import numpy as np from scipy.signal import lfilter fs 48000 fc 1000 b [0.003916, 0.007833, 0.003916] a [1.0, -1.815349, 0.831014] def gain_at(freq, seconds0.5): t np.arange(0, seconds, 1 / fs) x np.sin(2 * np.pi * freq * t) y lfilter(b, a, x) idx int(fs * 0.2) # 丢弃前0.2秒瞬态 return np.sqrt(np.mean(y[idx:] ** 2)) / np.sqrt(0.5) for f in [100, 500, 1000, 2000, 5000]: print(f, round(gain_at(f), 3))预期输出里100Hz接近1.01000Hz大约0.7072000Hz大概0.355000Hz不会超过0.15。如果1000Hz处的值明显偏离0.707优先检查预畸变或Q值是否写成了1。白噪声验证也类似但白噪声结果方差大更适合看整体频谱形状不适合精确读数。6.3 听感主观检验的量化方法最后是人耳验收但别只凭“感觉”。低通1kHz处理后高频嘶声、齿音应该明显收敛鼓点变得更“闷”更有肌肉感高通100Hz处理后低频隆隆声消失人声变薄但更干净。为了减少主观偏差我会把原始素材和滤波输出做AB切换同时用RMS表观察整体电平变化。滤波器通带增益接近1输出RMS应该和输入接近如果出现明显音量下降说明系数归一化出了问题比如b0算错。做分频点验证时还要看群延迟让一个瞬态鼓点通过低通和高通再混合听感上应该保持时间对齐。如果高音和低音错位后处理链路里一定还有未补偿的延迟。巴特沃斯本身群延迟在截止点附近最大两级级联后更明显这属于物理规律而不是实现错误但调试时要心里有数。最后分享一个我后来一直用的技巧设计完滤波器不要只看幅频响应一定要看阶跃响应。给滤波器输入一列常数值观察输出如何爬升到稳态。巴特沃斯无过冲但低截止时建立时间会变长如果系统快速切换参数时每次都能听到“呼哧”一声尾音多半就是状态没有清理干净或者参数跳变太快。阶跃响应能把这类问题直接暴露在示波器上比反复调系数高效得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑