1. 为什么谱减法值得单独拿出来讲语音增强这个方向做的人多但真正能把一个经典算法吃透的人少。谱减法算是语音增强里最老、最朴素、也最容易被低估的一类方法。很多人第一次接触语音增强就是从谱减法开始的但往往止步于“跑通一个demo”没有深挖它为什么这么设计、参数怎么调、实际场景下会踩哪些坑。我这次把谱减法单独拎出来做一次完整的梳理原因很简单它是理解整个语音增强技术栈的入口。你搞懂了谱减法的底层逻辑后面去看维纳滤波、MMSE、深度学习降噪很多概念都是相通的。而且谱减法本身在嵌入式设备、实时通话、助听器这些算力受限的场景里至今仍然有它的位置——计算量小、原理直观、容易落地这些优势不是随便一个模型能替代的。这篇文章适合谁看如果你正在做语音降噪相关的项目或者准备入门语音信号处理又或者你已经在用谱减法但效果总是不理想那这篇内容应该能帮你把思路理顺。我会从算法原理、参数选择、实操步骤、常见问题几个维度展开尽量把每个环节的“为什么”讲清楚而不是只丢一堆公式。先给一个整体判断谱减法不是万能的它的核心假设是“噪声是平稳的”这个假设在真实环境里经常不成立。但正因为它的局限性很明确你反而能清楚地知道什么时候该用它、什么时候该换方案。这种边界感比盲目追新算法更有价值。2. 谱减法的核心思路拆解2.1 从“减法”这个直觉说起谱减法的名字已经把核心思想说完了在频域上把噪声的频谱从带噪语音的频谱里减掉。你可以把它想象成一张被水渍弄脏的照片如果你知道水渍的纹理大概长什么样就可以尝试从照片里把水渍的图案减掉还原出原本的画面。语音增强里的“水渍”就是噪声“照片”就是带噪语音。具体到数学上假设带噪语音信号是 y(t)它由干净语音 x(t) 和噪声 n(t) 叠加而成y(t) x(t) n(t)做短时傅里叶变换之后在频域上近似有Y(k, m) ≈ X(k, m) N(k, m)其中 k 是频率索引m 是帧索引。谱减法的目标就是估计出 |X(k, m)|也就是干净语音的幅度谱。相位部分通常直接沿用带噪语音的相位因为人耳对相位不敏感而且相位估计本身很困难。那怎么估计噪声呢最常用的做法是假设前几帧只有噪声没有语音用这几帧的幅度谱平均值作为噪声估计。然后在每一帧里用带噪幅度谱减去噪声幅度谱得到增强后的幅度谱。最后结合原始相位做逆傅里叶变换还原成时域信号。这个流程听起来很简单但里面有几个关键决策点噪声估计怎么更新、减法怎么减、减完之后负值怎么处理。这几个点直接决定了最终效果的好坏。2.2 功率谱减 vs 幅度谱减谱减法有两个常见变体幅度谱减和功率谱减。幅度谱减就是直接对幅度做减法功率谱减则是先平方再减减完再开方。两者在公式上差一个平方但实际效果有差异。功率谱减的公式是|X̂(k, m)|² |Y(k, m)|² - α · |N̂(k, m)|²其中 α 是过减因子通常大于1。幅度谱减的公式是|X̂(k, m)| |Y(k, m)| - α · |N̂(k, m)|我实测下来功率谱减在抑制噪声方面更激进但容易产生音乐噪声幅度谱减相对温和但残留噪声会多一些。选择哪个取决于你的应用场景。如果是通话降噪我一般倾向功率谱减因为对噪声的压制更彻底如果是助听器或者音乐场景幅度谱减可能更合适因为音质更自然。2.3 过减因子和谱底参数的作用谱减法里有两个非常重要的参数过减因子 α 和谱底参数 β。α 控制减多少β 控制减完之后保留多少底噪。α 大于1的时候相当于多减一点噪声目的是把噪声压得更干净。但 α 太大会导致语音失真因为语音的弱音部分可能被一起减掉。β 的作用是给减完之后的频谱设一个下限避免出现负值或者过小的值从而减少音乐噪声。这两个参数的典型取值范围α 在 1.0 到 5.0 之间β 在 0.001 到 0.1 之间。具体怎么选后面我会给一个实操中的调参方法。3. 完整实操流程与关键环节3.1 音频预处理与分帧加窗拿到一段带噪语音第一步不是直接上谱减法而是做预处理。预处理的目的是让信号更适合做短时傅里叶变换。采样率方面语音增强通常用 16kHz 就够了电话场景 8kHz 也够用。如果你用的是 44.1kHz 的音频建议先降采样到 16kHz这样计算量小而且语音的主要能量集中在 4kHz 以下高频部分对可懂度贡献有限。分帧的时候帧长一般取 20ms 到 32ms。16kHz 采样率下20ms 就是 320 个采样点32ms 是 512 个采样点。我一般用 512 点因为正好是 2 的幂次做 FFT 方便。帧移通常取帧长的一半也就是 256 点这样有 50% 的重叠可以减少帧边界的不连续性。加窗是必须的不加窗会导致频谱泄漏。汉明窗是最常用的选择它的主瓣宽度和旁瓣衰减比较平衡。加窗的公式很简单import numpy as np def frame_signal(signal, frame_length, hop_length): num_frames 1 (len(signal) - frame_length) // hop_length frames np.zeros((num_frames, frame_length)) for i in range(num_frames): start i * hop_length frames[i] signal[start:start frame_length] return frames def apply_window(frames, window_typehamming): if window_type hamming: window np.hamming(frames.shape[1]) elif window_type hanning: window np.hanning(frames.shape[1]) else: window np.ones(frames.shape[1]) return frames * window注意分帧的时候如果信号长度不是帧移的整数倍最后一帧可能不够长需要补零。补零之后记得在还原时把对应的部分去掉。3.2 噪声估计的几种策略噪声估计是谱减法里最关键的环节没有之一。噪声估得准谱减法效果就好噪声估不准后面怎么调参数都白搭。最简单的策略是“前几帧平均法”假设前 5 到 10 帧是纯噪声取这些帧的功率谱平均值作为噪声估计。这个方法在实验室环境下能用但在真实场景里你很难保证前几帧一定是纯噪声。稍微好一点的是“最小值跟踪法”在每一帧里跟踪每个频点的最小值然后用这个最小值作为噪声估计。这个方法的假设是在一段时间内每个频点的最小值一定对应噪声因为语音不会一直占据所有频点。具体实现的时候可以用一个滑动窗口窗口长度一般取 1 到 2 秒。还有一种“递归平滑法”用一阶递归滤波器更新噪声估计公式是N̂(k, m) λ · N̂(k, m-1) (1-λ) · |Y(k, m)|²其中 λ 是平滑因子通常取 0.9 到 0.99。这个方法的优点是计算量小适合实时处理缺点是如果 λ 太大噪声估计跟不上噪声的变化如果 λ 太小噪声估计会被语音污染。我一般会结合使用先用前几帧初始化然后用最小值跟踪做粗估计再用递归平滑做细调。这样在大多数场景下都能得到一个比较稳的噪声估计。3.3 谱减操作与负值处理有了噪声估计之后就可以做谱减了。功率谱减的代码大概长这样def spectral_subtraction(noisy_spectrum, noise_spectrum, alpha2.0, beta0.01): noisy_power np.abs(noisy_spectrum) ** 2 noise_power np.abs(noise_spectrum) ** 2 enhanced_power noisy_power - alpha * noise_power enhanced_power np.maximum(enhanced_power, beta * noise_power) enhanced_spectrum np.sqrt(enhanced_power) * np.exp(1j * np.angle(noisy_spectrum)) return enhanced_spectrum这里有几个细节值得说。第一np.maximum那一步就是谱底参数 β 的作用它保证增强后的功率谱不会小于噪声功率的 β 倍。第二相位直接用了带噪语音的相位这是谱减法的标准做法。第三α 和 β 的选择需要根据实际信号调整后面会讲怎么调。减完之后做逆傅里叶变换回到时域然后做重叠相加得到最终的增强信号。重叠相加的时候要注意加窗之后的帧在重叠区域需要做归一化否则会出现幅度波动。3.4 参数调优的实操方法α 和 β 这两个参数理论上的最优值取决于信噪比和噪声类型。但在实操中我一般用网格搜索的方式来找。具体做法是准备一段带噪语音和对应的干净语音然后在一个合理的范围内遍历 α 和 β计算增强后的语音和干净语音之间的分段信噪比或者 PESQ 分数选分数最高的那一组。α 的搜索范围可以设 1.0 到 5.0步长 0.5β 的搜索范围可以设 0.001 到 0.1步长按对数分布取。这样大概有几十组组合跑一遍用不了多少时间。如果你没有干净语音做参考那就只能靠耳朵听了。我的经验是先固定 β0.01调 α 到噪声压制和语音失真之间找到一个平衡点然后固定 α微调 β 到音乐噪声不明显为止。提示音乐噪声是谱减法最典型的副作用听起来像水滴声或者鸟叫声。它的来源是谱减之后某些频点被减到接近零而相邻频点还有残留导致频谱出现随机起伏。增大 β 可以缓解音乐噪声但会牺牲降噪量。4. 常见问题与排查技巧实录4.1 增强后语音发闷、不清晰这是最常见的问题原因通常是过减因子 α 太大把语音的弱音部分也减掉了。语音里的清音和弱辅音能量本来就低如果 α 太大这些部分会被噪声估计“淹没”。解决办法降低 α或者对不同的频点采用不同的 α。比如低频段用大一点的 α因为噪声主要集中在低频高频段用小一点的 α保护语音的清晰度。这种频率相关的过减因子在实际项目里很常用。4.2 残留噪声忽大忽小这个问题的根源是噪声估计不准确。如果噪声估计偏低残留噪声就会多如果噪声估计偏高语音失真就会严重。在非平稳噪声环境下这个问题尤其明显。解决办法改用最小值跟踪或者递归平滑做噪声估计不要用简单的前几帧平均。另外可以引入语音存在概率的概念在语音存在的帧里减慢噪声估计的更新速度在语音不存在的帧里加快更新。4.3 音乐噪声太明显音乐噪声是谱减法的“胎里带”只能缓解不能根除。除了增大 β 之外还可以用以下几种方法对增强后的频谱做平滑减少相邻频点之间的随机起伏。用多帧平均代替单帧谱减降低噪声估计的方差。在谱减之后加一个后置滤波器进一步抑制残留噪声。我试过在谱减之后接一个简单的维纳滤波器音乐噪声能明显改善但计算量会增加一些。如果算力允许这个组合值得一试。4.4 实时处理时的延迟问题谱减法本身的计算量不大但分帧加窗会引入至少一帧的延迟。如果你做的是实时通话帧长 32ms 就意味着 32ms 的算法延迟加上前后处理总延迟可能到 50ms 以上。解决办法减小帧长比如用 16ms 甚至 8ms。但帧长太小会导致频率分辨率下降谱减法的效果会变差。这是一个权衡需要根据你的延迟预算来定。我一般用 16ms 帧长、8ms 帧移延迟和效果比较平衡。4.5 常见问题速查表问题现象可能原因排查方向解决建议语音发闷α 过大检查 α 取值降低 α 或分频段设置残留噪声大噪声估计偏低检查噪声估计方法改用最小值跟踪音乐噪声明显β 过小检查 β 取值增大 β 或加后置滤波延迟过高帧长过大检查帧长和帧移减小帧长至 16ms音量忽大忽小重叠相加归一化错误检查窗函数和归一化修正重叠相加逻辑5. 谱减法的适用边界与替代思路谱减法最大的假设是噪声平稳这个假设在风扇声、空调声、白噪声这些场景下基本成立但在人声嘈杂、交通噪声、音乐噪声这些场景下就不成立了。如果你发现谱减法怎么调都达不到要求那大概率不是参数问题而是方法本身不适合。这时候可以考虑几个方向。一是改用维纳滤波它在非平稳噪声下表现更好但需要更准确的噪声估计。二是用基于统计模型的方法比如 MMSE它对音乐噪声的抑制更好。三是上深度学习用一个小型的降噪网络在算力允许的情况下效果提升明显。但我想说的是谱减法作为一个基线它的价值不在于效果最好而在于它足够简单、足够快、足够容易理解。在很多嵌入式场景里你根本没有算力跑深度学习模型这时候谱减法就是最务实的选择。而且理解了谱减法的局限性你才能更好地理解那些更复杂的方法到底在解决什么问题。我个人在实际操作中的体会是谱减法调参的过程其实是在“降噪量”和“语音失真”之间找平衡。这个平衡点没有标准答案取决于你的应用场景和用户容忍度。通话场景可以接受多一点失真换更干净的背景助听器场景则要优先保证语音自然度宁可留一点噪声。把这个平衡点想清楚比盲目调参重要得多。最后再分享一个小技巧如果你手头的带噪语音信噪比很低可以先做一次谱减法把结果作为第二次谱减法的输入做两级级联。第二级的噪声估计用第一级的残留噪声这样能进一步压低噪声。但级联次数不要超过两次否则语音失真会累积得很严重。