资讯动态

实时降噪模块实战:频域滤波与噪声估计完全解析

发布时间:2026/9/6 9:15:07 来源:尧图企业网站定制
写一个实时降噪模块这事儿我琢磨了挺久。做录音相关项目的人应该都有体会底噪这东西不处理吧听着难受处理不好吧人声也跟着变味。我最初的需求其实很简单就是想把录音里的电流声、环境底噪压下去但要求实时处理不能等录完再离线跑一把降噪毕竟用户录的时候就得听到干净的声音。这个实时降噪模块折腾下来我算是把频域处理、噪声估计、增益控制这些玩意儿摸了一遍这篇文章就把我的完整思路和代码实现摊开来聊聊给同样被底噪折磨的朋友一个参考。1. 先搞清楚要降的是什么噪1.1 底噪从哪来底噪这个东西说白了就是录音信号里那把“背景音”它不像突然的门铃声、键盘敲击声那样是瞬态的而是持续存在、相对平稳的一种噪声。常见的来源大概有这几类设备本底噪声麦克风前置放大器的电子噪声芯片本身的热噪声还有电源纹波串进来的哼声这类噪声在安静环境下特别明显你把增益推高一点沙沙声就出来了。环境噪声空调风声、电脑风扇、房间里的混响底噪这类噪声虽然不那么平稳但在一段较短的时间内看频谱特征相对稳定。量化噪声ADC转换过程产生的底噪这个在低位数采样、低信号电平的时候会更明显。我一开始踩过一个误区以为降噪就是“砍高频”把底噪当成了单纯的高频嘶声来处理。实际上录音底噪往往覆盖整个频段尤其是电源带来的哼声可能集中在50Hz/100Hz附近而设备本底噪声则是全频段的粉噪或白噪特征。所以降噪模块必须做全频段的处理不能只靠一个高通滤波器打天下。1.2 为什么非要实时处理有人可能会问录音完了再用Audacity之类的工具降噪不行吗当然行但场景不对。我这边的需求是给一个实时录音工具加能力用户在录音过程中就要听到降噪后的声音也就是所谓的“监听即所得”。这也是实时降噪模块和离线降噪最大的区别你必须在几毫秒到几十毫秒的时间窗口内完成处理不能丢数据、不能等整段录完、延迟不能高到让用户觉得声音“劈了”。实时处理对算法的复杂度有硬性约束。你不能用那种需要整段音频做统计的全局算法——比如先算整段噪声的平均频谱再去减这在实时场景里根本跑不通因为未来还没到。你必须依赖“过去的估计”来指导“当前的处理”这就需要一整套以帧为单位、以噪声估计为核心的信号处理流程。2. 方案选型为什么我最终选了频域滤波这条路2.1 那些年踩过的方案对比在做这个实时降噪模块之前我大概调研过几条技术路线各有各的坑第一种时域高通/带通滤波。实现极其简单一个IIR滤波器几行代码搞定CPU占用也低。但问题很明显对平稳底噪的抑制能力有限。低切只能滤掉低频部分中高频的底噪它管不了人声的齿音区照样有沙沙声。而且滤波器的相位响应会让人声变“闷”听感不自然。这个方案适合处理特定频段的噪声不适合全频段降噪。第二种自适应滤波比如LMS/NLMS。它需要一个参考信号比如在录音设备里单独放一个采集环境噪声的麦克风然后用自适应算法把主麦克风里的相关噪声去掉。这个方案在语音通话里很常见但问题是大多数录音场景没有第二个麦克风给你做参考而且自适应滤波对参考信号的同步性要求很高稍微有点延迟差异就收敛得乱七八糟。第三种深度学习中用的RNN降噪模型。效果确实好但模型跑起来要么需要NPU加速要么CPU占用率高得吓人而且模型部署体积不小。在资源受限的实时录音链路上动不动几MB的模型文件加几毫秒的推理延迟说实话不太划算。我在项目里也尝试过轻量级模型但最终因为延迟和兼容性放弃了。第四种频域谱减法/维纳滤波。思路是把时域信号变换到频域估计每一帧的噪声频谱然后用减法或者增益的方式把噪声成分压下去。这个方案最大的优势是实时性好FFT一帧几毫秒就能跑完而且能精细控制每个频点的增益不会像高通滤波那样把所有低频一刀切。缺点是要处理好“音乐噪声”的问题相关参数调起来要耐得烦。最终我选了第四种路线具体是“短时傅里叶变换 噪声功率谱估计 频域增益计算维纳滤波风格 逆变换重叠相加”的组合。这套组合在实时性、降噪效果、资源占用之间达到了平衡。2.2 频域降噪的核心思路频域降噪的思路说起来其实不复杂。假设你的带噪语音信号 ( y(n) s(n) d(n) ) 其中 ( s(n) ) 是干净的语音( d(n) ) 是加性噪声。因为噪声和语音在频域上混合在一起如果我能估计出在当前这一帧里噪声在每个频点上的能量有多大就可以根据“信噪比”来决定这个频点该留多少。这里的关键是“估计”。实时场景下你不能等拿到整段信号才去算噪声只能靠噪声功率谱估计器来跟踪噪声底。常见的手段是用最小值统计或者时间递归平均的方法说白了就是当一个频点的能量持续很低、很平稳的时候就认为它是噪声并把它的统计值更新到噪声估计里。而当一个频点能量高起来大概率是语音出现了就维持噪声估计不变避免把语音也算进噪声里。有了噪声估计下一步是算增益。增益的对照关系可以用维纳滤波的形式来写[ G(f, t) \frac{\hat{P}_s(f, t)}{\hat{P}_s(f, t) \hat{P}_d(f, t)} ]其中 ( \hat{P}_s ) 是估计的语音功率谱( \hat{P}_d ) 是估计的噪声功率谱。这个 ( G ) 的值越接近1表示该频点语音成分越多越接近0表示噪声成分越多。然后把带噪频谱乘以这个增益噪声就被压下去了。这套流程在后文会展开包括怎么分帧、怎么估计噪声、怎么避免音乐噪声这些实操细节。先记住一句话实时降噪的核心就是“又快又准地跟踪噪声底”所有代码都是在围绕这句话转。3. 实时降噪模块的完整实现3.1 整体架构与数据流我先说整体架构。这个实时降噪模块我处理的是16kHz采样率、单声道、16bit的PCM音频流。为什么选这个配置16kHz是语音通信和录音工具里最常见的采样率能覆盖大部分人声频率范围语音能量主要分布在300Hz到3400Hz处理起来计算量也适中。如果你要处理44.1kHz的音乐录音流程一样但帧长和FFT点数要相应调整。整个模块的数据流是这样的输入从音频设备回调里拿到的PCM数据块一般是每10ms或20ms一块。16kHz采样率下20ms就是320个样本点。分帧为了做FFT我需要把样本点凑成2的幂次长度。我用的帧长是512点32ms每次处理时和上一帧重叠50%也就是256点重叠。这样做的目的是用重叠相加法抵消加窗造成的幅度调制。加窗对每一帧数据乘一个窗函数我用的是汉宁窗Hann Window目的是减少频谱泄漏。FFT把加窗后的时域帧变换到频域得到实部和虚部。噪声估计基于当前帧的功率谱更新噪声功率谱估计。增益计算根据噪声估计和当前帧的信噪比计算每个频点的增益值。谱处理把FFT结果乘以增益得到降噪后的频谱。IFFT逆变换回时域。重叠相加把当前帧处理结果和上一帧的结果重叠相加输出干净的PCM样本。代码结构上我把它封装成了一个类核心接口就两个process(buffer)和reset()。内部维护了上一帧的频谱缓冲区、噪声估计数组、窗函数表等状态。这样对调用方来说只需要在音频回调里调用process(buffer)就行完全不知道内部发生了什么。3.2 分帧、加窗与FFT先说数据结构。我用的帧长是FRAME_SIZE 512FFT大小也是512。对于16kHz采样率512点代表32ms这个长度对人声来说足够捕捉到一个音节的频谱特征同时延迟也不会太高。50%重叠意味着处理延迟大约相当于16ms加上系统音频缓冲的延迟总体在可接受范围内。分帧这一步其实没太多技巧关键是“保持状态”。因为音频回调传进来的数据块不一定正好是512点所以模块里必须有一个环形缓冲区或FIFO队列来缓存输入数据凑够512点才触发一次处理。我偷懒直接用了一个简单的vector做追加和弹出数据量不大性能没问题。接下来是加窗。我用的是汉宁窗公式是[ w(n) 0.5 \times (1 - \cos(2\pi n / (N - 1))) ]为什么用汉宁窗而不是矩形窗因为矩形窗在帧边缘会突然截断信号导致频域出现严重的频谱泄漏——就是本来集中在某个频点的能量会漏到旁边一大堆频点上去噪声估计和语音估计都会被污染。汉宁窗的旁瓣衰减比较快能有效减少泄漏代价是频率分辨率稍微降低但这个是划算的。这一步有个细节因为做了50%重叠而且用了汉宁窗窗函数的能量在重叠相加时不会完全重建原始信号。我在后面的重叠相加部分会专门处理这个问题。FFT我一开始是打算自己写的但后来项目里正好有现成的FFT实现——网上开源的pocketfft和KISS FFT都可以。如果你用的是CKISS FFT是不错的选择轻量、无依赖MIT协议。我这里为了演示方便就以伪代码形式展示逻辑实际代码里替换成你手头的FFT库就好。一个周期内对512点做实数FFT输出是257个复数bin包括直流和奈奎斯特频点。以下是分帧、加窗和FFT的核心代码片段这里用类C风格写方便理解// 假设 input 是累积满足 FRAME_SIZE 长度的待处理帧 std::vectorfloat frame(FRAME_SIZE); for (int i 0; i FRAME_SIZE; i) { // window[i] 提前算好是汉宁窗 frame[i] input[i] * window[i]; } // 执行实数FFT得到频域数据 fft_execute(frame.data(), fft_out); // fft_out[0..N] 复数 // 计算功率谱 for (int k 0; k FRAME_SIZE / 2; k) { float re fft_out[k * 2]; float im fft_out[k * 2 1]; power_spectrum[k] re * re im * im; }这里要注意功率谱实际上是复数模的平方也就是 ( |X(k)|^2 )。后面噪声估计和信噪比计算都用这个功率谱而不是直接用幅值因为功率谱把频率成分的能量表达得更直接。3.3 噪声估计与增益计算噪声估计是整个降噪模块的灵魂。这一步做不好后面一切都白搭。我在实际调的时候试过三种思路固定噪声底法。录音开始前先采一段纯噪声把它平均频谱当作固定的噪声模板之后每帧都用这个模板去减。这个方法在静音环境下有效但一旦环境噪声变了就失灵比如用户从安静的房间走到马路边固定模板就废了。不适合通用场景。最小值统计法Minimum Statistics。它维护一个比较长时间窗口比如1到2秒的功率谱最小值认为每个频点的噪声水平不会低于这个最小值。这个方法对噪声变化适应性更好但实现相对复杂需要维护历史功率谱缓存。时间递归平均法MCRA风格。用平滑因子对噪声功率谱做递归更新。关键是当前帧某个频点如果是语音主导就少更新甚至不更新如果是噪声主导就正常更新。这个方案实现简单跟踪速度快我最终选了这个作为主方案。MCRA的简化版实现逻辑如下对当前帧功率谱做时间平滑得到平滑后的功率谱 ( P(k, t) \alpha \cdot P(k, t-1) (1 - \alpha) \cdot |X(k, t)|^2 )平滑因子 ( \alpha ) 我取0.8大约对应20ms的时间常数。计算每个频点的后验信噪比 ( \gamma(k, t) P(k, t) / \hat{P}_d(k, t-1) )。然后计算语音活跃概率 ( p(k, t) )。如果 ( \gamma ) 大于某个阈值比如4约6dB就认为该频点大概率是语音语音活跃概率接近1。最终更新噪声估计[ \hat{P}_d(k, t) \alpha_d \cdot \hat{P}_d(k, t-1) (1 - \alpha_d) \cdot |X(k, t)|^2 ] 其中 ( \alpha_d ) 由 ( p(k, t) ) 动态调整语音活跃概率高时( \alpha_d ) 接近1极端情况下等于不更新语音活跃概率低时( \alpha_d ) 取0.9左右正常更新噪声。这里的关键参数是“语音活跃概率”的判定阈值。阈值设太高噪声估计更新太慢降噪跟不上环境变化阈值设太低人声的尾音会被当成噪声吃掉声音发闷。我实际调下来阈值在3到5之间比较合适具体需要配合前后帧的平滑来用。有了噪声估计 ( \hat{P}_d ) 和当前帧功率谱 ( |X|^2 )就可以计算先验信噪比 ( \xi(k, t) )。我用的是判决引导法Decision-Directed——先验信噪比不仅依赖当前帧还依赖前一帧的增益结果。公式是[ \xi(k, t) \beta \cdot \frac{|\hat{S}(k, t-1)|^2}{\hat{P}_d(k, t-1)} (1 - \beta) \cdot \max(\gamma(k, t) - 1, 0) ]这里 ( \beta ) 我取0.98它决定了先验信噪比的平滑程度。 ( |\hat{S}(k, t-1)|^2 ) 是上一帧降噪后的语音功率谱。这种做法的好处是能显著减少音乐噪声让增益随时间变化更平滑听感更自然。最后增益 ( G ) 用维纳滤波形式计算[ G(k, t) \frac{\xi(k, t)}{1 \xi(k, t)} ]可以再加一个增益下限 ( G_{min} )避免把某个频点完全消掉导致声音太干。我取 ( G_{min} 0.05 )也就是最多压制到原能量的0.05大约-26dB。这样既能有效降噪又保留了语音的自然感。3.4 逆变换与重叠相加频域处理完之后把降噪后的频谱带噪频谱的实部虚部乘以增益 ( G )做IFFT得到时域帧。这里有个必须要处理的问题因为前面加了汉宁窗直接重叠相加输出的话信号的幅度会被窗函数调制听上去会有一阵阵的起伏感。解决办法是重叠相加之后再除以窗函数的平方和。因为我对每一帧都乘了窗函数 ( w(n) )相邻两帧在同一位置相差256点都有值重叠相加后每个输出点的有效权重就是 ( w(n)^2 w(n FRAME_SIZE/2)^2 )。汉宁窗满足这个性质两个重叠窗的平方和在重叠区域内是常数从窗函数定义可以推导出来汉宁窗的平方和确实是常数1.5。所以重叠相加后直接除以1.5就能恢复原始信号幅度。简单来说流程是这样的// 每处理完一帧得到 ifft_outFRAME_SIZE点 for (int i 0; i FRAME_SIZE / 2; i) { int out_idx output_write_index i; // overlap_buffer 保存上一帧后半段的处理结果 float sample (overlap_buffer[i] ifft_out[i]) / 1.5f; output_queue.push(sample); overlap_buffer[i] ifft_out[i FRAME_SIZE / 2]; }这个“除1.5”是根据汉宁窗和50%重叠推导出来的具体验证方式可以在离线测试里跑一段正弦波看输出幅度是否和输入一致。我记得第一次做的时候没处理这个系数结果降噪后的声音整体小了一圈还以为是降噪把能量削掉了查了半天才发现是窗函数叠加的问题。要注意IFFT之后可能因为数值误差产生极小的虚部用实数FFT库时一般不会出现但如果你用的是通用复数FFT实现记得只取实部。4. 踩坑实录从频谱泄漏到音乐噪声4.1 降噪后人声发闷、发虚怎么办这是我最先遇到的问题。降噪之后底噪确实小了但人声也变闷了听起来隔着枕头说话。排查下来原因有三个第一个是高频增益压制过度。维纳滤波有个特性当信噪比不高时增益会显著低于1。齿音、气声这些高频成分能量本来就弱在底噪存在时信噪比很低结果被压在 ( G_{min} ) 附近整段高频全被削了人声当然闷。解决办法是给增益计算加一个频点权重对于4kHz以上的频段把 ( G_{min} ) 适当抬高比如抬到0.15让高频不要压得太狠。另外还可以对增益做一个频域平滑避免相邻频点增益差异过大。第二个是先验信噪比的 ( \beta ) 值太大。 ( \beta 0.98 ) 意味着先验信噪比高度依赖上一帧这会让增益变得很平滑降噪效果好但代价是语音的瞬态细节比如辅音起音被抹掉听起来反应迟钝。我试过把 ( \beta ) 降到0.92人声的清晰度立马上来了但音乐噪声也会稍微变多。这个值需要根据你能接受的音乐噪声程度来权衡。我最终用的是0.95算是一个中间值。第三个是噪声估计被语音污染。如果噪声估计更新得太快语音段的能量会被算进噪声里导致增益把所有频点都往下压。这个问题在语速快、停顿少的语音里特别明显。我之前用的 ( \alpha_d ) 是固定0.9后来改成根据语音活跃概率动态调整后情况改善了很多。核心还是那句语音活跃时千万别更新噪声不活跃时更新要快。4.2 音乐噪声音乐噪声怎么消降噪模块做好之后我的测试音频里出现了一种很典型的问题——原本平稳的底噪变成了一种“吱吱哇哇”的残余噪声像是音乐声里面夹杂的杂音业内人士管这个叫音乐噪声musical noise。产生的原因是谱减法对频点增益的随机波动太大有些频点这一帧被保留下一帧被压制产生了快速随机变化的残余频谱峰。处理音乐噪声有几个实用招数第一招加入增益平滑。对增益 ( G(k, t) ) 做时间方向的平滑。比如G_smooth(k, t) lambda * G_smooth(k, t - 1) (1 - lambda) * G(k, t);( \lambda ) 我取0.7到0.85之间。平滑后的增益变化更缓慢音乐噪声会大幅减轻。缺点是语音起音会变钝需要权衡。第二招用功率谱减法代替幅度谱减法。谱减法分两种一种减幅度谱一种减功率谱。幅度谱减法的音乐噪声更严重功率谱减法等价于维纳滤波的简化版相对温和因为它对频谱形状的改动更平滑。第三招先验信噪比下限约束。在计算 ( \xi ) 时设置一个下限比如 ( \xi_{min} -20dB )。这样即使某个频点的后验信噪比极低先验信噪比也不会掉到太低对应的增益不会波动太剧烈。我自己的经验是想完全没有音乐噪声是不可能的目标是把音乐噪声压制到“普通用户不刻意听就感觉不到”的程度。像上面这样组合起来处理实际听感就能接受。4.3 延迟、CPU和内存的平衡实时降噪模块的延迟由三部分构成音频设备缓冲一般10ms、算法自身的帧延迟我这里大约是16ms、以及编解码或者重采样的额外延迟如果链路里还有其他处理比如AEC回声消除还要再加。对于纯录音监听场景总和控制在40ms以内用户基本感觉不到。所以我的帧长设计是32ms、重叠50%算法延迟16ms这个数字是安全的。CPU占用方面512点实数FFT哪怕在低端ARM芯片上单次执行也只要几十微秒真正吃CPU的是噪声估计和增益计算里的log、exp操作。我在移植到嵌入式平台的时候把这些数学函数尽量换成了查表法或者用-ffast-math编译优化CPU占用能再降一截。内存方面模块内部主要开几个大小为FRAME_SIZE/2 1的float数组每个4KB左右完全不用担心。这里我有一个具体的调优建议测试时一定要用真实音频数据不要用纯净信号加白噪声来凑合。纯合成信号太“乖”噪声统计特性平稳降噪效果看着特别好但真实录音里的底噪往往是非平稳的有交流声的波动、有偶发的咔哒声这些才会真正考验你噪声估计的鲁棒性。我后来专门录了一堆不同环境的底噪做测试集效果才真正稳定下来。4.4 常见问题速查表为了方便你排查问题我把平时遇到的高频问题整理成了一张表问题现象可能原因解决思路底噪降了但人声发闷高频增益压制过度提高4kHz以上 ( G_{min} )或加入高频增益补偿有明显“水声”、音乐噪声增益波动剧烈增大 ( \beta )0.95以上、对增益做时间平滑环境噪声变化时降噪跟不上噪声估计更新太慢降低语音活跃概率阈值加快 ( \alpha_d ) 更新速度说话停顿时有抽吸感先验信噪比波动大增大 ( \beta )并设置 ( \xi_{min} )输出音量变小窗函数重叠相加未补偿重叠相加后除以窗平方和系数偶尔出现“啵啵”声帧边界不连续确保用50%重叠且窗函数正确高频齿音被误杀噪声估计混入了语音成分优化语音活跃概率检测动态调整噪声更新速度5. 进阶路线从这个模块还能再往哪走写到这里实时降噪模块的主干已经完整了。如果你做完这版之后还想继续深挖我建议从这几个方向延伸。5.1 加入风噪检测与抑制当前模块对平稳底噪效果好但对风噪这种非平稳噪声降噪效果一般。风噪的特点是低频能量特别大且频谱快速变化可以在频域加一个低频段的瞬时能量检测当低频能量突然飙升时临时压低低频增益同时不影响中频语音。这个功能对户外录音用户特别有用。5.2 引入轻量级神经网络后处理虽然我最终没有在主链路里用深度学习但如果你设备算力足够可以试试“传统频域预降噪 小模型后处理”的组合。先用维纳滤波把底噪压到一个较低水平再用一个小型DNN模型做语音增强效果比单用任一方案都更好。传统算法负责兜底DNN负责把残余的音乐噪声和语音畸变进一步清洗干净。5.3 多级级联降噪 压缩 限幅实际录音链路里降噪不是终点。降噪完成后通常会接一个压缩器让语音动态更稳定和限幅器防止削波。如果你的模块是给录音工具用的建议把这三者做成一条可配置的处理链用户可以在界面上依次调整。我个人在实际操作中的体会是降噪模块最难的不是算法本身而是参数调优和场景适配。你花在调 ( \beta )、 ( G_{min} )、语音活跃概率阈值上的时间可能比写代码的时间还多。但正是这些参数决定了一个降噪模块是“能用的玩具”还是“能拿出手的产品”。最后再分享一个小技巧做实时降噪模块一定要在真实设备上做听力测试用耳机听别用外放。很多处理痕迹在外放时糊成一片听不出来但一戴耳机就原形毕露。我在迭代过程中反复用几段标准语音素材加不同底噪来测试效果稳定之后才敢往项目里集成。

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

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

免费获取报价