Qt + FFmpeg 音频工具:降噪功能实现

Qt + FFmpeg 音频工具:降噪功能实现 前言录音素材中经常混入各种背景噪声——电流底噪、空调嗡声、键盘敲击、环境杂音等严重影响听感和后续处理质量。AudioTools 的降噪功能提供三种互补的降噪算法和可选的高通/低通滤波链覆盖从稳定底噪到复杂环境噪声的主要场景并以先预览对比、再满意导出的非破坏性工作流让用户反复调参而不丢失原始数据。算法全称适用噪声类型afftdnFFT Spectral Denoising稳定背景底噪、白噪声电流声、空调声anlmdnNon-Local Means Denoising周期性噪声、脉冲噪声爆音、咔嗒声arnndnRecurrent Neural Network Denoising复杂环境噪声、人声增强键盘声、街噪三种算法在同一时刻只能选择一种二选一策略避免叠加使用带来的不可预测副作用。功能流程用户导入文件 → 解码 PCM → 选择算法 设置参数 → 点击「降噪预览」 ↓ ↓ └── 异步处理QtConcurrent │ ↓ │ packed PCM → Swr(转 FLTP) → abuffer │ → [highpass] → afftdn/anlmdn/arnndn │ → [lowpass] → aformat → abuffersink │ → Swr(转回 packed) │ ↓ │ denoisedPcmData 缓存 ←──────────────────────────────┘ ↓ QML 波形对比原始 vs 降噪后 ↓ 满意 → 导出 WAVsavePcmAsWav 不满意 → 调整参数重新预览非破坏性关键特点非破坏性导出不会修改当前 PCM 数据用户可以反复用不同参数导出并对比。异步调度预览和导出都通过runAsync()在工作线程执行不阻塞 QML/UI 线程。独立预览缓存降噪结果存入denoisedPcmData/denoisedPcmInfo不占用其它功能的 PCM 字段。核心算法FFT 频谱降噪afftdn在频域中估计噪声功率谱底然后从每一帧的频谱中减去噪声分量。适合处理频谱特征稳定的背景底噪。nrNoise Reduction降噪强度单位 dB实际取值 0.01~97.0UI 滑块范围 -80~-20 dB取绝对值后传给 FFmpeg。越大的值降噪力度越强。nfNoise Floor噪声底限估计单位 dB取值 -80~-20 dB。控制噪声估计的灵敏度较低值意味着更保守地估计噪声底。tn1启用时间噪声整形Temporal Noise Shaping让降噪后的残余噪声在时间上更平滑减少水下感的听觉伪影。滤镜字符串示例afftdnnr30.0:nf-50.0:tn1非局部均值降噪anlmdn在时域中搜索相似的音频片段并做加权平均利用信号自身的冗余性来抑制噪声。对周期性噪声和脉冲噪声如爆音、咔嗒声效果较好。sStrength降噪强度参数取值 0.00001~0.1。UI 滑块范围 0~100通过对数映射转换s 0.00001 × 10^(uiValue / 25.0)。越右越强。p0.002patch 半径控制相似片段匹配的局部窗口大小。r0.006搜索半径控制相似片段搜索的时间范围。oo输出模式为原始信号而非噪声信号。对数映射函数functionuiToAnlmdnStrength(uiValue){vars0.00001*Math.pow(10,uiValue/25.0);returnMath.max(0.00001,Math.min(0.1,s));}滤镜字符串示例anlmdns0.001:p0.002:r0.006:oo神经网络降噪arnndn基于 RNNoise 递归神经网络架构加载预训练的.rnnn模型文件进行逐帧推理。模型被训练为识别人声特征并抑制非人声信号对复杂环境噪声效果最佳。modelPath.rnnn模型文件路径。内置模型存放在Dist/bin/models/目录下包括cb.rnnn偏均衡的语音降噪适合会议和轻中度环境底噪抑制力度温和。mp.rnnn偏保真适合含背景音乐、直播录屏对音乐质感保留更好。sh.rnnn偏强抑制适合键盘声、风扇声、空调声等明显噪声场景。lq.rnnn专门针对人声/语音 vs 一般噪声来自 GregorR/rnnoise-models。mix混合系数范围 -1~1。1.0表示纯降噪输出0.0表示原始信号-1.0表示只听被去除的噪声分量。模型列表通过MediaAnalyzer::availableDenoiseModels()在启动时动态扫描models/目录加载QML 页面通过ToolComboBox展示供用户选择。Windows 路径转义模型路径中的\会被 FFmpeg filter 解析器当作转义符盘符:会与参数分隔符冲突因此统一转换为/并转义冒号QString safePathQDir::toNativeSeparators(modelPath);safePath.replace(QLatin1Char(\\),QLatin1Char(/));safePath.replace(QLatin1Char(:),QStringLiteral(\\:));// mix 值可能为负用单引号保护避免被 FFmpeg option parser 误解析filters.append(QStringLiteral(arnndnm%1:mix%2).arg(safePath).arg(QString::number(mix,f,2)));高通 / 低通滤波作为降噪滤镜链的前置和后置辅助环节高通highpass截止频率 0~500 Hz去除低频嗡声、风声和桌面震动。设为 0 表示禁用。低通lowpass截止频率 0~20000 Hz去除嘶声、齿音和高频电磁干扰。设为 0 表示禁用。滤镜链拼接逻辑QStringList filters;if(highpassHz0.0)filters.append(QStringLiteral(highpassf%1).arg(highpassHz));// 插入主降噪滤镜afftdn / anlmdn / arnndn 三选一filters.append(mainFilter);if(lowpassHz0.0)filters.append(QStringLiteral(lowpassf%1).arg(lowpassHz));// 锁定输出格式filters.append(QStringLiteral(aformatsample_fmtsfltp:sample_rates%1:channel_layouts%2).arg(sampleRate).arg(channelLayout));主处理函数AudioToolProcessor::denoisePcm核心处理函数位于src/audio/audiotoolprocessor.cpp无状态、无 UI 依赖只负责纯 PCM 数据转换。处理流程7 步boolAudioToolProcessor::denoisePcm(constQByteArrayinputPcm,constQVariantMapinputInfo,constQStringalgorithm,doublenrDb,doublenoiseFloorDb,doubleanlmdnStrength,doublehighpassHz,doublelowpassHz,constQStringmodelPath,doublemix,QByteArray*outputPcm,QVariantMap*outputInfo,QVariantMap*denoiseInfo,QString*errorText)constStep 1 — 输入校验与格式提取从inputInfo中提取采样率、声道数和采样格式校验 PCM 大小是否与格式匹配。同时做参数范围保护constdoubleafftdnNoiseReductionDbqBound(0.01,std::abs(nrDb),97.0);noiseFloorDbqBound(-80.0,noiseFloorDb,-20.0);anlmdnStrengthqBound(0.00001,anlmdnStrength,0.1);highpassHzqBound(0.0,highpassHz,500.0);lowpassHzqBound(0.0,lowpassHz,20000.0);mixqBound(-1.0,mix,1.0);Step 2 — packed PCM → FLTP 平面浮点转换FFmpeg 滤镜链要求 FLTP 格式输入因此先通过SwrContext将 packedinterleavedPCM 转为平面浮点SwrContext*swrInnullptr;swr_alloc_set_opts2(swrIn,layout,AV_SAMPLE_FMT_FLTP,sampleRate,// 输出layout,sampleSpec.format,sampleRate,// 输入0,nullptr);swr_init(swrIn);swr_convert(swrIn,fltpPlanes,inputFrameCount,inData,inputFrameCount);Step 3 — 构建 filter graph动态拼接滤镜链abuffer → [highpass] → 主降噪 → [lowpass] → aformat → abuffersink通过avfilter_graph_parse_ptr解析constQString filterDescfilters.join(QStringLiteral(,));retavfilter_graph_parse_ptr(graph,filterDesc.toUtf8().constData(),inputs,outputs,nullptr);retavfilter_graph_config(graph,nullptr);Step 4 — 推入 FLTP 帧将转换后的 FLTP 平面数据封装为AVFrame推入abuffer再发送 NULL 帧通知输入结束inFrame-formatAV_SAMPLE_FMT_FLTP;inFrame-sample_ratesampleRate;inFrame-nb_samplesconvertedIn;av_buffersrc_add_frame_flags(abufferCtx,inFrame,AV_BUFFERSRC_FLAG_PUSH);av_buffersrc_add_frame_flags(abufferCtx,nullptr,AV_BUFFERSRC_FLAG_PUSH);Step 5 — 拉取处理后的帧循环从abuffersink拉取输出帧按声道收集 FLTP 平面数据while(true){AVFrame*outFrameav_frame_alloc();retav_buffersink_get_frame(abuffersinkCtx,outFrame);if(retAVERROR(EAGAIN)||retAVERROR_EOF)break;// 按声道追加平面数据for(intch0;chchannels;ch)outputFltpPlanes[ch].append(reinterpret_castconstchar*(outFrame-extended_data[ch]),outFrame-nb_samples*sizeof(float));outputFrameCountoutFrame-nb_samples;av_frame_free(outFrame);}Step 6 — FLTP → packed PCM 反向转换通过第二个SwrContext将输出 FLTP 平面数据转回原始 packed 格式SwrContext*swrOutnullptr;swr_alloc_set_opts2(swrOut,layout,sampleSpec.format,sampleRate,// 输出原始 packed 格式layout,AV_SAMPLE_FMT_FLTP,sampleRate,// 输入FLTP0,nullptr);swr_convert(swrOut,outData,outputFrameCount,inFltpPlanes.data(),outputFrameCount);packedOutput.truncate(convertedOut*frameSize);Step 7 — 组装输出信息返回处理结果信息包括算法名称、各参数值、输入/输出帧数、时长和文件大小等。导出封装MediaAnalyzer::exportDenoisedWavQML 调用入口负责参数校验、路径处理、异步调度和状态更新boolMediaAnalyzer::exportDenoisedWav(constQStringalgorithm,doublenrDb,doublenoiseFloorDb,doubleanlmdnStrength,doublehighpassHz,doublelowpassHz,constQStringmodelPath,doublemix,constQStringfilePath){if(m_pcmData.isEmpty()){/* 提示用户先解码 PCM */returnfalse;}// 路径兼容file:// URL 和本地路径都支持缺省用 defaultDenoisedWavPath()QString outputPathfilePath.trimmed();if(outputPath.startsWith(file:,Qt::CaseInsensitive))outputPathQUrl(outputPath).toLocalFile();if(outputPath.isEmpty())outputPathdefaultDenoisedWavPath();if(!outputPath.endsWith(.wav,Qt::CaseInsensitive))outputPath.wav;clearDenoisePreview();setBusy(true);// 值捕获所有必要数据worker 线程不访问 MediaAnalyzer 成员constQByteArray pcmCopym_pcmData;constQVariantMap pcmInfoCopym_mediaInfo.value(pcm).toMap();runAsync([]()-AsyncResult{// ── 工作线程 ──AsyncResult r;QVariantMap denoiseInfo;r.okprocessor.denoisePcm(pcmCopy,pcmInfoCopy,algorithm,nrDb,noiseFloorDb,anlmdnStrength,highpassHz,lowpassHz,modelPath,mix,r.pcmData,r.pcmInfo,denoiseInfo,r.errorText);if(r.ok)r.okprocessor.savePcmAsWav(outputPath,r.pcmData,r.pcmInfo,r.errorText);r.infodenoiseInfo;r.outputPathoutputPath;returnr;},[this](constAsyncResultr){// ── 主线程 ──if(r.ok){setDenoisedPcm(r.pcmData,r.pcmInfo);// 同时更新波形预览setDenoiseInfo(info);setStatus(降噪 WAV 导出完成r.outputPath);}else{setDenoiseInfo(QVariantMap());setStatus(降噪导出失败r.errorText);}});returntrue;}MediaAnalyzer::previewDenoise预览与导出共享同一条处理管线区别在于预览不写文件只把降噪后的 PCM 缓存到denoisedPcmData属性供 QML 波形对比voidMediaAnalyzer::previewDenoise(constQStringalgorithm,doublenrDb,doublenoiseFloorDb,doubleanlmdnStrength,doublehighpassHz,doublelowpassHz,constQStringmodelPath,doublemix){if(m_pcmData.isEmpty()){setStatus(请先解码 PCM);return;}clearDenoisePreview();setBusy(true);runAsync([]()-AsyncResult{// 工作线程只调用 denoisePcm不写文件AsyncResult r;r.okprocessor.denoisePcm(...,r.pcmData,r.pcmInfo,...);returnr;},[this](constAsyncResultr){// 主线程更新降噪预览缓存if(r.ok){setDenoisedPcm(r.pcmData,r.pcmInfo);setDenoiseInfo(r.info);setStatus(降噪预览完成可以对比波形);}});}默认输出路径defaultDenoisedWavPath()按项目规范生成源文件目录 源文件名 _denoised.wav。arnndn 模型列表availableDenoiseModels()在应用启动时扫描Dist/bin/models/目录下所有.rnnn文件返回[{name, file}, ...]供 QML 下拉框使用QVariantListMediaAnalyzer::availableDenoiseModels()const{QVariantList models;constQString modelsDirQCoreApplication::applicationDirPath()/models;QDirdir(modelsDir);constQFileInfoList entriesdir.entryInfoList({*.rnnn},QDir::Files,QDir::Name);for(constQFileInfoentry:entries){QVariantMap m;m.insert(name,entry.baseName());m.insert(file,entry.absoluteFilePath());models.append(m);}returnmodels;}状态清理降噪功能的状态清理在三个时机触发时机清理内容调用方式切换文件 / 导入新文件denoiseInfo、denoisedPcmDatasetPcmData()内部调用setDenoiseInfo(QVariantMap())PCM 重新解码denoiseInfo、denoisedPcmDatasetPcmData()→clearDenoisePreview()页面主动清空denoisedPcmData、denoisedPcmInfoclearDenoisePreview()clearDenoisePreview()实现voidMediaAnalyzer::clearDenoisePreview(){m_denoisedPcmData.clear();m_denoisedPcmInfo.clear();emitdenoisedPcmChanged();}QML 侧DenoisePage通过mediaAnalyzer.denoisedPcmSize 0判断是否有预览数据自动控制降噪后波形区域的可见性。小结音频降噪功能通过三种互补算法afftdn 频谱减噪、anlmdn 非局部均值、arnndn 神经网络和可选的高通/低通辅助滤波覆盖了从稳定底噪到复杂环境噪声的主要降噪场景。实现遵循项目架构规范算法层AudioToolProcessor::denoisePcm无状态、无 UI 依赖基于 FFmpeg filter graph 管线处理 FLTP 格式数据。状态层MediaAnalyzer负责异步调度、路径生成、PCM 缓存和 QML 属性绑定维护独立的denoisedPcmData预览缓存。UI 层DenoisePage提供算法选择、参数调节、预设模板和原始/降噪后双波形对比复用WaveformPlayer、ToolComboBox、ActionButton等通用组件。非破坏性工作流导出和预览都不修改当前 PCM用户可以反复调参试听满意后再保存 WAV。