引言
滤波器与效果器是音频处理的基本构件。均衡、去齿音、混响、延迟、压缩、失真——这些看似不同的功能,底层都是同一套工具:延迟线、乘加、反馈。理解这套工具的组合方式,就能自己实现几乎任何效果器。
工程上的难点不在数学,而在数值稳定性与参数变化的连续性。一个理论上正确的滤波器,在 48 kHz 下、参数被 UI 每秒更新 60 次时,可能因为系数突变而产生爆音;一个 IIR 滤波器在极端参数下可能极点跑出单位圆而自激;一个压缩器的包络检测器用错时间常数会产生可听的"抽吸"感。这些都不是教科书公式能解决的问题。
本文按"基础 → 滤波器 → 延迟类效果 → 动态处理 → 非线性 → 工程细节"的顺序组织。每部分都给出可直接使用的实现片段与参数取值,并在最后讨论实时实现中的稳定性、平滑与精度问题。
目录
- 滤波器基础:传递函数与频响
- Biquad 与 RBJ Cookbook
- FIR 与 IIR 的工程取舍
- 延迟线:延迟、回声与梳状滤波
- 混响:从 Schroeder 到 FDN
- 动态处理:压缩器与限幅器
- 失真与波形整形
- 参数平滑与拉链噪声
- 实时实现的数值稳定性
1. 滤波器基础:传递函数与频响
数字滤波器由差分方程描述。以最简单的一阶低通为例:
y[n] = b0·x[n] + b1·x[n-1] - a1·y[n-1]
对它做 z 变换,得到传递函数:
b0 + b1·z⁻¹
H(z) = ─────────────
1 + a1·z⁻¹
频响就是令 z = e^(jω) 代入 H(z) 得到的复函数:模长是幅度响应,辐角是相位响应。
1.1 极零点与稳定性
分母的根是极点,分子的根是零点。IIR 滤波器稳定的充要条件是所有极点位于单位圆内(|p| < 1)。
低通:极点在正实轴靠近 1 处
高通:极点在负实轴靠近 -1 处
带通:一对共轭极点靠近单位圆
极点越靠近单位圆,谐振越尖锐(Q 值越高),但也越接近不稳定边界。定点实现时系数量化误差可能把极点推出单位圆,导致自激——这是嵌入式音频最常见的故障。
1.2 相位与延迟
滤波器的相位响应决定不同频率成分的延迟差异(群延迟)。IIR 滤波器的群延迟随频率变化,会把瞬态"抹开";FIR 可以做到严格线性相位(群延迟恒定),代价是延迟与阶数成正比。
FIR 线性相位:群延迟 = (N-1)/2 个样本
48 kHz 下 511 抽头 FIR → 延迟 = 255 样本 ≈ 5.3 ms
对音乐制作,这个延迟通常可接受;对实时监听,5 ms 已经接近可感知边界。
2. Biquad 与 RBJ Cookbook
**Biquad(双二阶)**是音频中使用最广的滤波器结构,因为它用最少的计算实现了二阶 IIR,而二阶足以覆盖绝大多数均衡需求。
b0 + b1·z⁻¹ + b2·z⁻²
H(z) = ───────────────────────
1 + a1·z⁻¹ + a2·z⁻²
2.1 RBJ Cookbook 系数
Robert Bristow-Johnson 的 Audio EQ Cookbook 给出了各类 biquad 的闭式系数,是事实标准。以低通为例,给定 f0(截止频率)、Q(品质因数)、fs:
import math
def lowpass_coeffs(f0, Q, fs):
w0 = 2 * math.pi * f0 / fs
cos_w0, sin_w0 = math.cos(w0), math.sin(w0)
alpha = sin_w0 / (2 * Q)
b0 = (1 - cos_w0) / 2
b1 = 1 - cos_w0
b2 = (1 - cos_w0) / 2
a0 = 1 + alpha
a1 = -2 * cos_w0
a2 = 1 - alpha
# 归一化:全部除以 a0
return (b0/a0, b1/a0, b2/a0, a1/a0, a2/a0)
峰值均衡(peaking EQ)只需把上式中的 alpha 替换为 sin(w0)/(2Q),并引入 A = 10^(gain_db/40):b0 = 1 + alpha·A、b2 = 1 - alpha·A、a0 = 1 + alpha/A、a2 = 1 - alpha/A,其余项不变。低架(low shelf)与高架(high shelf)形式类似,只是 b 与 a 的构造不同。
2.2 直接 I 型 vs 转置直接 II 型
直接 I 型的差分方程为 y = b0·x + b1·x1 + b2·x2 - a1·y1 - a2·y2,需要 4 个状态变量(x1、x2、y1、y2),每次 5 次乘加。**转置直接 II 型(TDF-II)**只需 2 个状态、数值特性更好,是工程首选:
struct BiquadTDF2 {
float b0, b1, b2, a1, a2;
float s1 = 0, s2 = 0;
inline float process(float x) {
float y = b0 * x + s1;
s1 = b1 * x - a1 * y + s2;
s2 = b2 * x - a2 * y;
return y;
}
};
TDF-II 在浮点下对系数量化的敏感度更低,且状态变量就是滤波器的"内部能量",便于做稳定性监控。
2.3 常用滤波器类型
| 类型 | 用途 | 典型 Q |
|---|---|---|
| Lowpass | 去高频、抗混叠 | 0.707(Butterworth) |
| Highpass | 去低频、去直流 | 0.707 |
| Peaking | 均衡某频段 | 0.5~4 |
| Low Shelf | 提升/衰减低频 | 0.707 |
| High Shelf | 提升/衰减高频 | 0.707 |
| Notch | 去特定频率(工频哼声) | 5~30 |
| Allpass | 只改相位不改幅度 | 任意 |
3. FIR 与 IIR 的工程取舍
| 特性 | FIR | IIR(biquad) |
|---|---|---|
| 相位 | 可严格线性 | 非线性 |
| 阶数 | 数十~数千抽头 | 2 阶(可级联) |
| 算力 | 高(N 次乘加/样本) | 低(5 次乘加/样本) |
| 稳定性 | 无条件稳定 | 需关注极点 |
| 参数变化 | 需重算全部系数 | 只改 5 个系数 |
| 瞬态响应 | 干净 | 有振铃 |
3.1 何时用 FIR
- 需要线性相位:母带均衡、相位敏感的多轨叠加。
- 需要陡峭过渡带:抗混叠、采样率转换。
- 需要精确的频率响应:房间校正(数百抽头)。
- 需要卷积:混响(用 FFT 分块卷积)。
3.2 何时用 IIR
- 实时参数变化:均衡器旋钮要平滑跟随,FIR 重算代价太大。
- 算力受限:嵌入式设备。
- 只需要幅度控制:不需要严格线性相位。
3.3 级联 biquad 实现高阶滤波
需要四阶或更高阶时,级联多个 biquad,每个承担一对极点。顺序很重要:Q 值高的放前面可以避免中间级过载(定点实现尤其关键),浮点实现影响较小。级联的实现只是把上一步的输出喂给下一级,用 for (auto& s : stages) x = s.process(x); 即可。
滤波器系数设计的更多数学细节与频域验证方法见 audio-spectral-analysis-fft 。
4. 延迟线:延迟、回声与梳状滤波
延迟线(Delay Line)是效果器的基础构件:一块环形缓冲加一个读写指针。
class DelayLine {
std::vector<float> buf;
size_t write = 0;
public:
explicit DelayLine(size_t maxSamples) : buf(maxSamples, 0.0f) {}
void writeSample(float x) {
buf[write] = x;
write = (write + 1) % buf.size();
}
float readDelayed(size_t delaySamples) const {
size_t idx = (write + buf.size() - delaySamples) % buf.size();
return buf[idx];
}
// 分数延迟:线性插值
float readFractional(float delaySamples) const {
size_t d0 = (size_t)delaySamples;
float frac = delaySamples - d0;
float a = readDelayed(d0);
float b = readDelayed(d0 + 1);
return a + frac * (b - a);
}
};
4.1 分数延迟与插值质量
延迟时间通常不是整数样本(例如 3.7 ms @ 48 kHz = 177.6 样本),必须插值。插值方法决定质量:
| 方法 | 质量 | 算力 |
|---|---|---|
| 无插值(截断) | 调制时明显抖动 | 最低 |
| 线性插值 | 高频衰减明显 | 低 |
| 三次(Hermite) | 良好 | 中 |
| 全通插值 | 幅度平坦,相位有偏 | 中 |
| sinc 插值 | 最佳 | 高 |
调制型效果(合唱、镶边)必须用至少三次插值,否则调制时会听到"阶梯"感。
4.2 梳状滤波
把延迟信号与原始信号相加,得到梳状滤波器(Comb Filter):频响上出现等间隔的峰谷。
y[n] = x[n] + g·x[n-D]
峰出现在 f = k/D(k = 0, 1, 2, ...)
谷出现在 f = (k + 0.5)/D
- 前馈梳状(Feedforward Comb):只有前向路径,用于镶边(flanger)。
- 反馈梳状(Feedback Comb):
y[n] = x[n] + g·y[n-D],用于回声与混响。g < 1才稳定。
// 反馈梳状:回声
float feedbackComb(float x, DelayLine& dl, size_t D, float g) {
float delayed = dl.readDelayed(D);
float y = x + g * delayed;
dl.writeSample(y);
return y;
}
5. 混响:从 Schroeder 到 FDN
混响要模拟房间的密集晚期反射。三条技术路线:
5.1 Schroeder 混响
最简单的结构:4 个并联反馈梳状滤波器 + 2 个串联全通滤波器。
输入 ──┬─▶ Comb(D1,g1) ──┐
├─▶ Comb(D2,g2) ──┤
├─▶ Comb(D3,g3) ──┼─▶ Allpass ─▶ Allpass ─▶ 输出
└─▶ Comb(D4,g4) ──┘
延迟时间取互质数(例如 1116、1188、1277、1356 样本 @ 44.1 kHz)避免峰谷重合。Schroeder 混响的密度不足,听感上"金属味"明显,但算力极低(约 20 次乘加/样本),适合嵌入式。
5.2 Freeverb
Freeverb 用 8 个并联梳状(每个带低通阻尼)+ 4 个串联全通,立体声用 23 样本的偏移量错开左右。阻尼系数模拟高频在空气中更快衰减的物理现象。
comb 延迟(@ 44.1 kHz):1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617
allpass 延迟:556, 441, 341, 225
stereo spread:23 样本
Freeverb 是开源混响的经典实现,代码量小、听感可用,是很多游戏与轻量应用的默认选择。
5.3 FDN(Feedback Delay Network)
FDN 用 N 个延迟线 + 一个正交反馈矩阵,能在同等延迟线数量下获得更高的回声密度。
s[n+1] = A · (衰减 × 延迟(s[n])) + b·x[n]
y[n] = c · s[n]
A 通常取 Hadamard 矩阵(元素 ±1/√N),它正交且无乘法(只有加减),算力友好。N 取 8 或 16 时,回声密度已接近真实房间。FDN 是现代高质量算法混响的主流结构。
5.4 卷积混响
直接与真实房间的脉冲响应(IR)卷积,质量最高。短 IR(< 100 ms)用直接卷积,长 IR 必须用FFT 分块卷积(uniform partitioned convolution):把 IR 分成 N/B 块(B 为分块大小),每块做一次 FFT 乘法并累加,延迟为 B 个样本,每样本算力约 O(log B)。分块越小延迟越低但算力越高,典型取 B = 128 或 256。
6. 动态处理:压缩器与限幅器
6.1 压缩器的四个参数
| 参数 | 含义 | 典型值 |
|---|---|---|
| Threshold | 起控电平 | -20 dBFS |
| Ratio | 压缩比 | 4:1 |
| Attack | 起控时间 | 10 ms |
| Release | 释放时间 | 100 ms |
| Knee | 拐点软硬 | 6 dB(软拐点) |
6.2 增益计算
若 level_dB > threshold:
gain_reduction_dB = (level_dB - threshold) × (1 - 1/ratio)
软拐点(soft knee)在阈值附近平滑过渡,避免"突然开始压缩"的听感。
6.3 包络检测器
增益的平滑用一阶低通,时间常数由 attack/release 换算:
class Compressor {
float thresholdDb = -20.0f, ratio = 4.0f;
float attackMs = 10.0f, releaseMs = 100.0f;
float envDb = 0.0f;
float sr = 48000.0f;
float process(float x) {
float levelDb = 20.0f * std::log10(std::fabs(x) + 1e-9f);
float coeff = (levelDb > envDb)
? std::exp(-1.0f / (attackMs * 0.001f * sr))
: std::exp(-1.0f / (releaseMs * 0.001f * sr));
envDb = levelDb + coeff * (envDb - levelDb);
float over = envDb - thresholdDb;
float grDb = (over > 0.0f) ? over * (1.0f - 1.0f / ratio) : 0.0f;
return x * std::pow(10.0f, -grDb / 20.0f);
}
};
注意 log10 与 pow 是昂贵的超越函数,每样本调用会成为热点。工程优化是在块级别计算增益(k-rate),或查表 + 插值。
6.4 限幅器
限幅器是 ratio 无穷大的压缩器,但真正的"砖墙限幅"需要前瞻(look-ahead):提前看 N 毫秒的信号,预先把增益降下来,避免瞬态穿过。前瞻引入了 N 毫秒的延迟,这是母带链路上主要的延迟来源。
look-ahead 1.5 ms @ 48 kHz = 72 样本的延迟
6.5 侧链(Sidechain)
侧链让压缩器的检测信号来自另一个源(例如让贝斯给底鼓让路)。实现上只是把检测输入与处理输入分开:
float process(float x, float detectInput) {
// 用 detectInput 算包络,用 x 做处理
}
7. 失真与波形整形
失真本质是非线性函数,会产生新的谐波。
// 软削波(tanh),奇次谐波为主
inline float softClip(float x) { return std::tanh(x); }
// 硬削波,产生大量奇次谐波
inline float hardClip(float x) { return std::clamp(x, -1.0f, 1.0f); }
// 非对称(产生偶次谐波,听感更"温暖")
inline float asymmetric(float x) { return x > 0 ? std::tanh(x) : std::tanh(0.7f * x); }
7.1 过采样是必须的
非线性产生的谐波会超出奈奎斯特频率并折回。任何失真都必须过采样,这是 audio-sampling-quantization 中反复强调的原则:
2× 过采样:混叠减少约 12 dB
4× 过采样:混叠减少约 24 dB
8× 过采样:混叠减少约 36 dB
过采样的代价是算力线性增长,通常 4× 是性价比拐点。
7.2 波形整形查表
tanh 每样本调用太慢,工程上预计算一张查找表(例如 1024 点)加线性插值:构造期把 [-1, 1] 上的 tanh(x·drive) 填入表,处理时把输入映射到表索引、取相邻两点做线性插值即可。这与 Web Audio 的 WaveShaperNode 是同一思路,见 audio-web-audio-api
。
8. 参数平滑与拉链噪声
参数被 UI 直接写入时,每块之间的突变会产生"拉链噪声"(zipper noise)。
8.1 一阶平滑
class SmoothedParam {
float current = 0.0f, target = 0.0f, coeff = 0.0f;
public:
void setup(float sampleRate, float timeMs) {
coeff = 1.0f - std::exp(-1.0f / (timeMs * 0.001f * sampleRate));
}
void setTarget(float t) { target = t; }
inline float next() {
current += (target - current) * coeff;
return current;
}
};
时间常数取 5~20 ms 通常听不出但能有效抑制拉链噪声。
8.2 逐样本 vs 逐块
- 增益、声像、滤波截止频率:逐样本平滑(a-rate)。
- 压缩器阈值、混响大小:逐块平滑(k-rate)足够。
- 采样率、滤波器阶数:不可变,需重建。
8.3 滤波器系数插值
滤波器系数不能直接线性插值(会经过非稳定区域),正确做法是插值参数(频率、Q、增益)再重算系数,或对系数做"极点保形插值"。工程上通常每 16~64 个样本重算一次系数,把 sin/cos 的调用频率降到 1/32 左右,听感上完全无差。
9. 实时实现的数值稳定性
9.1 反规格化数(Denormals)
浮点运算结果趋近于 0 时(例如混响尾巴衰减到极小),CPU 会进入**反规格化(denormal)**模式,速度可能下降 10~100 倍。这在混响尾音上极其常见,表现为"CPU 突然飙高"。
解决方案:
// 在反馈路径上加一个极小的直流偏置
y += 1e-20f; // 防止 denormal
// 或设置 CPU 的 FTZ/DAZ 标志(x86)
#include <xmmintrin.h>
_mm_setcsr(_mm_getcsr() | 0x8040); // FTZ + DAZ
9.2 定点实现的注意点
- 累加器要足够宽(Q15 输入用 32 bit 累加器)。
- 每级之后做饱和(
__SSAT)而非回绕。 - 系数用 Q 格式表示时注意精度损失,二阶极点在低频(
f0/fs很小)时对系数量化极敏感,必要时改用一阶或提高系数位宽。
9.3 浮点累加顺序
不同 SIMD 宽度下加法顺序不同,结果会有 LSB 级差异。这对听感无影响,但会破坏 null test 的零残差,做自动化回归时需要注意,见 audio-quality-testing 。
权衡取舍
| 需求 | 方案 A | 方案 B | 建议 |
|---|---|---|---|
| 均衡 | FIR 线性相位 | IIR biquad | 实时参数变化用 IIR,母带用 FIR |
| 混响 | 算法(Freeverb/FDN) | 卷积 | 算力紧用算法,质量优先用卷积 |
| 失真 | 直接非线性 | 过采样 | 一律 4× 过采样,否则混叠刺耳 |
| 参数更新 | 逐样本 | 逐块 | 增益/声像逐样本,阈值/大小逐块 |
| 延迟插值 | 线性 | 三次/sinc | 静态延迟可线性,调制必须三次以上 |
| 滤波结构 | 直接 I 型 | TDF-II | 一律 TDF-II,状态少、数值好 |
| 超越函数 | 每样本调用 | 查表+插值 | 热点路径一律查表 |
常见坑清单
- 反馈增益 ≥ 1:梳状或混响自激,输出指数增长直到削波。所有反馈路径必须
|g| < 1。 - denormal 拖慢 CPU:混响尾音衰减到极小值时触发反规格化,CPU 飙升 10 倍以上,需加偏置或开 FTZ。
- 滤波器系数直接线性插值:插值路径可能穿过不稳定区域,应插值参数再重算系数。
- 失真不过采样:高次谐波折回产生刺耳混叠,这是"数字失真难听"的主因。
- 延迟线读指针越界:
write - delay为负时未加模数,读到垃圾数据,必须用(write + size - d) % size。 - 每样本调用 log/pow:压缩器与失真的热点,应改为逐块计算或查表。
- 级联 biquad 顺序随意:定点实现下高 Q 级放后面会过载,应从高 Q 到低 Q。
- 忘记清零输出缓冲:累加语义的效果器会残留上一块数据,产生周期性噪声。
- 调制延迟不做分数插值:合唱/镶边会听到明显的阶梯感,至少用三次插值。
- 静音时不重置状态:长时间运行后状态漂移,恢复播放时出现瞬态"噗"声。
小结
音频效果器的实现可以归结为三种基本构件:滤波器(biquad/FIR)、延迟线(含反馈)、非线性函数。均衡、混响、延迟、压缩、失真都是它们的组合。掌握 TDF-II biquad 与环形延迟线的实现,就掌握了 80% 的效果器开发。
工程上最容易翻车的三处:反馈路径的稳定性、反规格化数导致的 CPU 飙升、以及参数突变产生的拉链噪声。这三者都不会在教科书的公式里出现,但会在真实产品里频繁触发。
继续深入建议读 audio-worklet-realtime 把这些效果器放进浏览器的实时线程,读 audio-spectral-analysis-fft 用频谱验证滤波器的实际频响,读 audio-mixing-mastering 理解这些构件如何组成完整的混音与母带链路。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。