概述
均衡器(Equalizer)是音频处理中最常用的工具之一,用于对不同频率区间的信号进行增益或衰减调整。本文介绍如何在 Qt/C++ 音频工具中,使用 Peaking EQ 双二阶(biquad)数字滤波器 直接在 PCM 浮点数据上实现 5 频段均衡器,完全绕过 FFmpeg 滤镜图,避免了滤镜解析的兼容性问题。
效果图:

技术选型
为什么不用 FFmpeg firequalizer?
最初方案是使用 FFmpeg 的 firequalizer 滤镜,但在实际开发中遇到了一系列问题:
avfilter_graph_parse_ptr字符串解析器 按冒号:拆分滤镜参数,gain='f=60:g=0.0;f=250:g=3.0;...'中的冒号被错误拆分- 即使改用程序化 API(
avfilter_graph_alloc_filter+av_opt_set),firequalizer 的gain参数在不同 FFmpeg 版本中行为不一致 - 最终决定彻底移除 FFmpeg 滤镜图,改用纯 C++ biquad 滤波器直接在 PCM 数据上处理
Peaking EQ Biquad 滤波器
采用 Robert Bristow-Johnson 的 Audio Cookbook 中的 Peaking EQ 公式:
A=10gainDb/40 A = 10^{\text{gainDb}/40} A=10gainDb/40
ω=2πf0/fs \omega = 2\pi f_0 / f_s ω=2πf0/fs
α=sin(ω)/(2Q) \alpha = \sin(\omega) / (2Q) α=sin(ω)/(2Q)
滤波器系数:
| 分子 | 分母 |
|---|---|
| b0=1+αAb_0 = 1 + \alpha Ab0=1+αA | a0=1+α/Aa_0 = 1 + \alpha / Aa0=1+α/A |
| b1=−2cos(ω)b_1 = -2\cos(\omega)b1=−2cos(ω) | a1=−2cos(ω)a_1 = -2\cos(\omega)a1=−2cos(ω) |
| b2=1−αAb_2 = 1 - \alpha Ab2=1−αA | a2=1−α/Aa_2 = 1 - \alpha / Aa2=1−α/A |
差分方程:
yn=b0xn+b1xn−1+b2xn−2−a1yn−1−a2yn−2a0 yn = \frac{b_0 xn + b_1 xn-1 + b_2 xn-2 - a_1 yn-1 - a_2 yn-2}{a_0} yn=a0b0xn+b1xn−1+b2xn−2−a1yn−1−a2yn−2
整体架构
┌─────────┐ ┌────────────┐ ┌─────────────┐ ┌────────────┐ ┌─────────┐
│ packed │───>│ SwrContext │───>│ FLTP 浮点 │───>│ SwrContext │───>│ packed │
│ PCM │ │ (转FLTP) │ │ biquad EQ │ │ (转回packed)│ │ PCM │
└─────────┘ └────────────┘ │ 级联处理 │ └────────────┘ └─────────┘
└─────────────┘
处理流程:
- 格式转换:将输入 packed PCM 转为 FLTP 平面浮点格式
- EQ 滤波:直接在 FLTP 浮点数据上逐频段应用 biquad 滤波器
- 安全防护:清洗 NaN/Inf,防止数值发散
- 格式还原:将处理后的 FLTP 转回原始 packed 格式
关键代码
5 频段参数定义
qml
// EqPage.qml - 频段参数列表
property ListModel bands: ListModel {
ListElement { frequency: 60; gain: 0.0; width: 1.0 } // 低音(Bass)
ListElement { frequency: 250; gain: 0.0; width: 1.0 } // 中低(Low Mid)
ListElement { frequency: 1000; gain: 0.0; width: 1.0 } // 中音(Mid)
ListElement { frequency: 4000; gain: 0.0; width: 1.0 } // 中高(High Mid)
ListElement { frequency: 12000; gain: 0.0; width: 1.0 } // 高音(Treble)
}
Packed PCM → FLTP 格式转换
cpp
// Step 1: 准备 SwrContext(packed → FLTP)
SwrContext *swrIn = nullptr;
int ret = swr_alloc_set_opts2(&swrIn,
&layout, AV_SAMPLE_FMT_FLTP, sampleRate,
&layout, sampleSpec.format, sampleRate,
0, nullptr);
swr_init(swrIn);
// Step 2: 执行格式转换
const int convertedIn = swr_convert(swrIn, fltpPlanes,
static_cast<int>(inputFrameCount),
inData, static_cast<int>(inputFrameCount));
Peaking EQ Biquad 核心实现
cpp
// Step 3: 直接在 FLTP 浮点数据上应用 Peaking EQ 双二阶滤波器
for (int b = 0; b < bands.size(); ++b) {
const double frequency = band.value("frequency").toDouble();
const double gainDb = qBound(-24.0, band.value("gain").toDouble(), 24.0);
const double Q = qMax(0.1, band.value("width", 1.0).toDouble());
// ★ 关键:奈奎斯特频率保护
// biquad 滤波器要求中心频率 < sampleRate/2,否则系数发散
const double nyquist = sampleRate / 2.0;
if (frequency <= 0.0 || frequency >= nyquist || qFuzzyIsNull(gainDb))
continue;
// 计算 biquad 系数(Audio Cookbook)
const double A = std::pow(10.0, gainDb / 40.0);
const double omega = 2.0 * M_PI * frequency / sampleRate;
const double alpha = std::sin(omega) / (2.0 * Q);
const double a0 = 1.0 + alpha / A;
const double a1 = -2.0 * std::cos(omega);
const double a2 = 1.0 - alpha / A;
const double b0 = 1.0 + alpha * A;
const double b1 = -2.0 * std::cos(omega);
const double b2 = 1.0 - alpha * A;
// 归一化系数
const double nb0 = b0 / a0, nb1 = b1 / a0, nb2 = b2 / a0;
const double na1 = a1 / a0, na2 = a2 / a0;
// 对每个声道独立应用 Direct Form I 滤波
for (int ch = 0; ch < channels; ++ch) {
float *data = reinterpret_cast<float *>(fltpPlanes[ch]);
double x1 = 0.0, x2 = 0.0, y1 = 0.0, y2 = 0.0;
for (int n = 0; n < convertedIn; ++n) {
const double x0 = static_cast<double>(data[n]);
double y0 = nb0 * x0 + nb1 * x1 + nb2 * x2
- na1 * y1 - na2 * y2;
// NaN/Inf 保护:若滤波器发散则重置为原始输入
if (std::isnan(y0) || std::isinf(y0))
y0 = x0;
// 防止级联滤波导致数值溢出
y0 = qBound(-2.0, y0, 2.0);
data[n] = static_cast<float>(y0);
// 更新延迟线
x2 = x1; x1 = x0;
y2 = y1; y1 = y0;
}
}
}
数值安全防护
cpp
// Step 3.5: 清洗 FLTP 数据,确保无 NaN/Inf(防止 resampler 失败)
for (int ch = 0; ch < channels; ++ch) {
float *data = reinterpret_cast<float *>(fltpPlanes[ch]);
for (int n = 0; n < convertedIn; ++n) {
if (std::isnan(data[n]) || std::isinf(data[n]))
data[n] = 0.0f;
}
}
开发踩坑记录
踩坑一:firequalizer 滤镜图解析失败
FFmpeg 的 avfilter_graph_parse_ptr 按冒号拆分参数,gain 字符串内部的冒号被误认为参数分隔符:
firequalizer=gain='f=60:g=0.0;f=250:g=3.0;...'
^ 这里的冒号被当成参数分隔符
教训:对于包含复杂参数的 FFmpeg 滤镜,优先考虑程序化构建或直接数值计算。
踩坑二:超奈奎斯特频率导致输出畸变
当音频采样率为 16000 Hz 时,奈奎斯特频率为 8000 Hz。EQ 的高频段(12000 Hz)超过了奈奎斯特频率,biquad 滤波器系数发散(绝对值 > 1),输出指数级增长并被钳位为方波畸变。
解决方案:添加频率上限检查,自动跳过超出奈奎斯特频率的频段:
cpp
const double nyquist = sampleRate / 2.0;
if (frequency <= 0.0 || frequency >= nyquist || qFuzzyIsNull(gainDb))
continue; // 跳过无效频段
踩坑三:级联滤波的数值累积
5 个频段依次滤波时,前一级的数值误差会被后续级放大。需要同时采取:
- 系数安全检查:跳过 NaN/Inf 系数
- 输出值钳位 :
qBound(-2.0, y0, 2.0)防止级联溢出 - 最终 NaN 清洗:确保输出数据干净
预设模板
项目内置 5 种经典预设,涵盖常见音频风格需求:
| 预设名称 | 60Hz | 250Hz | 1kHz | 4kHz | 12kHz | 适用场景 |
|---|---|---|---|---|---|---|
| 平坦 | 0 dB | 0 dB | 0 dB | 0 dB | 0 dB | 参考基准 |
| 流行 | +2 dB | +1 dB | +3 dB | +2 dB | +1 dB | 增强中高频,人声清晰 |
| 摇滚 | +5 dB | +3 dB | -1 dB | +3 dB | +5 dB | 增强低频和高频 |
| 古典 | -1 dB | +1 dB | +2 dB | +1 dB | +1 dB | 增强动态范围 |
| 爵士 | +2 dB | +3 dB | +1 dB | +2 dB | +3 dB | 温暖饱满 |
总结
相比 FFmpeg firequalizer 滤镜方案,纯 C++ biquad 滤波器实现具有以下优势:
| 对比维度 | firequalizer 滤镜 | Peaking EQ biquad |
|---|---|---|
| 兼容性 | 依赖 FFmpeg 版本 | 纯数学计算,无依赖 |
| 数值稳定性 | 由滤镜内部处理 | 完全可控,含防护机制 |
| 性能 | 滤镜图开销 | 直接内存操作,更高效 |
| 灵活性 | 受限于滤镜参数 | 完全自由定制 |
| 可调试性 | 黑盒 | 每一步都可追踪 |