概述
高低通滤波是音频修复和声音整理中非常常用的一类处理。高通滤波(Highpass)用于削减截止频率以下的低频内容,例如麦克风低频隆隆声、风噪、手持震动;低通滤波(Lowpass)用于削减截止频率以上的高频内容,例如底噪嘶声、电磁干扰、过亮齿音等。
当前项目中的"高低通滤波"功能采用 FFmpeg highpass / lowpass 滤镜实现,整体设计保持非破坏式:处理结果会写入独立的预览 PCM 和导出 WAV,不直接覆盖当前解码后的原始 PCM。这样用户可以反复调整参数、预览对比、再决定是否导出。
效果图:

功能入口
后端向 QML 暴露了三类能力:
cpp
Q_INVOKABLE bool exportFilteredWav(double highpassHz,
double lowpassHz,
int poles,
const QString &filePath);
Q_INVOKABLE void previewFilter(double highpassHz,
double lowpassHz,
int poles);
Q_INVOKABLE void clearFilterPreview();
同时通过属性把处理结果返回给界面:
cpp
Q_PROPERTY(QVariantMap filterInfo READ filterInfo NOTIFY filterInfoChanged)
Q_PROPERTY(int filteredPcmSize READ filteredPcmSize NOTIFY filteredPcmChanged)
Q_PROPERTY(QByteArray filteredPcmData READ filteredPcmData NOTIFY filteredPcmChanged)
Q_PROPERTY(QVariantMap filteredPcmInfo READ filteredPcmInfo NOTIFY filteredPcmChanged)
QML 页面只负责收集参数和展示结果。真正的音频处理放在 AudioToolProcessor::filterPcm,MediaAnalyzer 负责异步调度、状态更新、结果缓存和 WAV 导出。
参数设计
当前滤波功能提供三个核心参数:
| 参数 | 含义 | 处理方式 |
|---|---|---|
highpassHz |
高通截止频率,单位 Hz | 0 表示禁用高通 |
lowpassHz |
低通截止频率,单位 Hz | 0 表示禁用低通 |
poles |
滤波器阶数 | 只允许 1 或 2,非法值回退到 2 |
底层会做参数保护:
cpp
if (highpassHz <= 0.0 && lowpassHz <= 0.0) {
*errorText = QStringLiteral("至少需要启用一个滤波器(高通或低通)。");
return false;
}
highpassHz = qBound(0.0, highpassHz,
static_cast<double>(sampleRate) / 2.0 - 1.0);
lowpassHz = qBound(0.0, lowpassHz,
static_cast<double>(sampleRate) / 2.0 - 1.0);
poles = (poles < 1 || poles > 2) ? 2 : poles;
这里最重要的是截止频率不能超过奈奎斯特频率,也就是采样率的一半。项目中保留了 sampleRate / 2 - 1 的余量,避免 FFmpeg 滤镜收到无效频率。
整体处理链路
项目中解码后的 PCM 是 packed / interleaved 格式,例如双声道 16-bit PCM 的内存排列是:
text
L0 R0 L1 R1 L2 R2 ...
而 FFmpeg 音频滤镜更适合处理平面浮点格式 FLTP。因此滤波管线分为五步:
text
packed PCM
│
▼
SwrContext:原始 PCM → FLTP
│
▼
FFmpeg filter graph:
abuffer → highpass? → lowpass? → aformat → abuffersink
│
▼
SwrContext:FLTP → 原始 PCM 格式
│
▼
packed PCM 输出
这个设计有两个好处:
- 输入和输出保持原始采样率、通道数和位深,调用方不用感知中间格式变化。
- 滤镜内部统一使用
FLTP,可以减少不同整数位深下的处理分支。
格式检查与 frame 对齐
滤波前会先从 inputInfo 解析采样率、通道数和采样格式:
cpp
const int sampleRate = inputInfo.value(QStringLiteral("sampleRate")).toInt();
const int channels = inputInfo.value(QStringLiteral("channels")).toInt();
const SampleSpec sampleSpec = sampleSpecFromInfo(inputInfo);
然后计算一个 frame 的字节数:
cpp
const int frameSize = channels * sampleSpec.bytesPerSample;
if (frameSize <= 0 || inputPcm.size() % frameSize != 0) {
*errorText = QStringLiteral("PCM size does not match its format.");
return false;
}
这里的 frame 是"同一时间点的所有通道采样"。所有切分和转换都必须按 frame 对齐,否则会破坏多声道排列,造成声道错位或杂音。
packed PCM 转 FLTP
底层使用 SwrContext 把输入 PCM 转成 AV_SAMPLE_FMT_FLTP:
cpp
swr_alloc_set_opts2(&swrIn,
&layout, AV_SAMPLE_FMT_FLTP, sampleRate,
&layout, sampleSpec.format, sampleRate,
0, nullptr);
转换后的数据通过 av_samples_alloc_array_and_samples 分配为多声道平面数据:
text
channel 0: L0 L1 L2 ...
channel 1: R0 R1 R2 ...
这一步把项目自己的 packed PCM 数据变成 FFmpeg filter graph 可直接消费的浮点平面帧。
动态构建滤镜链
高通和低通都可以单独启用,也可以组合启用,因此滤镜描述字符串是动态拼接的:
cpp
QStringList filters;
if (highpassHz > 0.0) {
filters.append(QStringLiteral("highpass=f=%1:p=%2")
.arg(QString::number(highpassHz, 'f', 1))
.arg(poles));
}
if (lowpassHz > 0.0) {
filters.append(QStringLiteral("lowpass=f=%1:p=%2")
.arg(QString::number(lowpassHz, 'f', 0))
.arg(poles));
}
filters.append(QStringLiteral(
"aformat=sample_fmts=fltp:sample_rates=%1:channel_layouts=%2")
.arg(sampleRate).arg(channelLayout));
const QString filterDesc = filters.join(QStringLiteral(","));
几个典型结果:
text
highpass=f=80.0:p=2,aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo
lowpass=f=8000:p=2,aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo
highpass=f=300.0:p=2,lowpass=f=3400:p=2,aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo
最后追加 aformat 是为了约束滤镜输出格式,保证后续 swrOut 能按预期把 FLTP 转回原始 PCM 格式。
Filter Graph 执行
项目手动构建了一个最小的 FFmpeg filter graph:
text
abuffer 作为输入节点
abuffersink 作为输出节点
中间通过 avfilter_graph_parse_ptr 按 filterDesc 自动连接
输入帧创建时指定格式、采样率、样本数和通道布局:
cpp
inFrame->format = AV_SAMPLE_FMT_FLTP;
inFrame->sample_rate = sampleRate;
inFrame->nb_samples = convertedIn;
av_channel_layout_copy(&inFrame->ch_layout, &layout);
for (int ch = 0; ch < channels; ++ch)
inFrame->data[ch] = fltpPlanes[ch];
随后把整段 PCM 推入 abuffer:
cpp
av_buffersrc_add_frame_flags(abufferCtx, inFrame, AV_BUFFERSRC_FLAG_PUSH);
av_buffersrc_add_frame_flags(abufferCtx, nullptr, AV_BUFFERSRC_FLAG_PUSH);
第二次推入 nullptr 表示输入结束,让滤镜内部可以 flush 尾部缓存。
输出侧循环从 abuffersink 拉取处理后的帧:
cpp
while (true) {
ret = av_buffersink_get_frame(abuffersinkCtx, outFrame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
break;
for (int ch = 0; ch < channels; ++ch)
outputFltpPlanes[ch].append(
reinterpret_cast<const char *>(outFrame->extended_data[ch]),
outFrame->nb_samples * sizeof(float));
}
这里把每次拉出的平面浮点数据追加到 QVector<QByteArray>,最后再统一转回 packed PCM。
FLTP 转回原始 PCM
输出转换使用第二个 SwrContext:
cpp
swr_alloc_set_opts2(&swrOut,
&layout, sampleSpec.format, sampleRate,
&layout, AV_SAMPLE_FMT_FLTP, sampleRate,
0, nullptr);
转换完成后,输出 QByteArray 会按原始 frame size 截断到真实转换样本数:
cpp
packedOut.resize(convertedOut * frameSize);
并生成新的 pcmInfo:
cpp
QVariantMap pcmInfo = inputInfo;
pcmInfo.insert(QStringLiteral("size"), packedOut.size());
pcmInfo.insert(QStringLiteral("sizeText"),
FFmpegUtils::formatBytes(packedOut.size()));
这意味着滤波结果仍然是当前项目内部通用的 PCM 表示,可以继续用于波形预览、播放和 WAV 封装。
预览与导出
MediaAnalyzer::previewFilter 会复制当前 PCM 和格式信息,然后异步执行 filterPcm:
cpp
const QByteArray pcmCopy = m_pcmData;
const QVariantMap pcmInfoCopy = m_mediaInfo.value(QStringLiteral("pcm")).toMap();
const AudioToolProcessor processor;
成功后只更新预览缓存:
cpp
setFilteredPcm(r.pcmData, r.pcmInfo);
setFilterInfo(r.info);
setStatus(QStringLiteral("滤波预览完成,可以对比波形"));
exportFilteredWav 则在处理成功后多做一步 WAV 封装:
cpp
if (r.ok)
r.ok = processor.savePcmAsWav(outputPath,
r.pcmData,
r.pcmInfo,
&r.errorText);
导出默认路径采用当前文件名追加 _filtered.wav:
cpp
return fileInfo.absolutePath()
+ QLatin1Char('/')
+ fileInfo.completeBaseName()
+ QStringLiteral("_filtered.wav");
QML 页面交互
FilterPage.qml 提供了完整的参数界面:
- 高通开关 + 20Hz 到 500Hz 滑块,默认 80Hz
- 低通开关 + 1000Hz 到 20000Hz 滑块,默认 12000Hz
- 滤波器阶数下拉框:
1 pole或2 poles - 常用预设:去除低频隆隆声、去除高频嘶声、电话频段、人声频段、广播频段
- 处理前后两个
WaveformPlayer,用于波形对比和试听
页面点击"滤波预览"时只调用:
js
mediaAnalyzer.previewFilter(hp, lp, p)
点击"导出 WAV"时调用:
js
mediaAnalyzer.exportFilteredWav(hp, lp, p, outputPath)
前端不参与任何采样数据处理,职责边界比较清晰。
结果信息
滤波完成后,filterInfo 会记录处理摘要:
cpp
info.insert(QStringLiteral("highpassHz"), ...);
info.insert(QStringLiteral("lowpassHz"), ...);
info.insert(QStringLiteral("poles"), poles);
info.insert(QStringLiteral("inputDurationText"), ...);
info.insert(QStringLiteral("outputDurationText"), ...);
info.insert(QStringLiteral("inputSizeText"), ...);
info.insert(QStringLiteral("outputSizeText"), ...);
info.insert(QStringLiteral("sampleRate"), sampleRate);
info.insert(QStringLiteral("channels"), channels);
info.insert(QStringLiteral("bitsPerSample"), sampleSpec.bitsPerSample);
导出成功时还会补充:
cpp
info.insert(QStringLiteral("outputPath"), r.outputPath);
info.insert(QStringLiteral("outputSizeText"),
FFmpegUtils::formatBytes(r.outputSize));
这些字段最终由 QML 的 InfoPanel 展示给用户。
实现特点
- 非破坏式处理 :滤波预览和导出都不覆盖
m_pcmData。 - 格式兼容性好 :输入支持项目当前常用的 8-bit、16-bit、32-bit PCM,内部统一转
FLTP。 - 滤镜链可组合:高通、低通可以独立启用,也可以串联形成带通效果。
- 异步执行 :
MediaAnalyzer使用runAsync调度,避免界面阻塞。 - 结果可视化 :前端用两个
WaveformPlayer展示处理前后波形,支持试听和缩放对比。
适用场景
常用参数可以这样理解:
| 场景 | 高通 | 低通 |
|---|---|---|
| 去除低频隆隆声 | 80Hz | 禁用 |
| 去除高频嘶声 | 禁用 | 8000Hz |
| 电话频段模拟 | 300Hz | 3400Hz |
| 人声清理 | 80Hz | 12000Hz |
| 广播频段整理 | 50Hz | 15000Hz |
高低通滤波看似简单,但项目里的实现重点不是某一个滤镜参数,而是把 FFmpeg filter graph、PCM 格式转换、QML 预览、WAV 导出和状态展示串成了一条稳定的工具链。这个管线后续也可以继续扩展到更多基于 FFmpeg 滤镜的音频处理功能。