3. 经典降噪算法(谱减法):原理推导、Python实现、参数调优与音乐噪声问题

谱减法,说白了就是语音降噪领域里最经典、最直观的一种方法。我刚开始做语音增强那会儿,第一个上手的算法就是它。为什么?因为它思路简单——既然噪声是加性的,那我直接从带噪信号里把噪声减掉不就行了?

但事情远没这么简单。你减多了,语音失真;减少了,噪声残留。更头疼的是,减完之后会冒出来一种奇怪的“音乐噪声”,听着像水里的气泡声,非常刺耳。这一章我们就把它彻底讲透。

3.1 谱减法的数学原理

先回顾一下基本模型。假设我们采集到的带噪信号是:

y(t) = x(t) + n(t)

其中 x(t) 是纯净语音,n(t) 是加性噪声。做短时傅里叶变换(STFT)后,频域表示为:

Y(ω) = X(ω) + N(ω)

谱减法的核心假设是:噪声是平稳的,也就是说,我们可以用无声段的噪声谱来估计当前帧的噪声。这个假设在实际场景中并不总是成立,但它是谱减法能工作的基石。

幅度谱减法公式如下:

|X̂(ω)| = |Y(ω)| - α · |N̂(ω)|

这里 α 是过减因子,一般取 1~3 之间。如果结果小于 0,就置为 0 或者一个很小的值(flooring)。

关键点:谱减法只修改幅度谱,相位保持不变。为什么?因为人耳对相位不敏感,直接用带噪信号的相位重建语音,听感上影响不大。我在项目里试过用纯净相位,效果提升微乎其微,但计算量翻了一倍,不划算。

完整的谱减法流程可以概括为:

  1. 对带噪信号分帧、加窗、做 STFT
  2. 用前几帧(无声段)估计噪声幅度谱 |N̂(ω)|
  3. 对每一帧计算增益函数 G(ω) = max(1 - α·|N̂(ω)|/|Y(ω)|, β)
  4. 应用增益:|X̂(ω)| = G(ω) · |Y(ω)|
  5. 结合原始相位,做 ISTFT 恢复时域信号

这里 β 是谱下限(spectral floor),通常取 0.01~0.1。它的作用是防止增益为 0 导致语音完全断裂。

3.2 Python 实现:从零手写谱减法

我个人习惯用 librosa 做 STFT/ISTFT,但为了让你看清每一步,这里用 numpy 和 scipy 手写一个简洁版本。

import numpy as np
from scipy.io import wavfile
from scipy.signal import stft, istft

def spectral_subtraction(y, sr, n_fft=512, hop_length=128, alpha=2.0, beta=0.01):
    # STFT
    f, t, Zxx = stft(y, fs=sr, nperseg=n_fft, noverlap=n_fft - hop_length)
    mag = np.abs(Zxx)
    phase = np.angle(Zxx)

    # 估计噪声:取前 5 帧的平均
    noise_frames = 5
    noise_mag = np.mean(mag[:, :noise_frames], axis=1, keepdims=True)

    # 计算增益
    gain = 1 - alpha * noise_mag / (mag + 1e-10)
    gain = np.maximum(gain, beta)

    # 应用增益
    mag_clean = gain * mag

    # ISTFT
    Zxx_clean = mag_clean * np.exp(1j * phase)
    _, y_clean = istft(Zxx_clean, fs=sr, nperseg=n_fft, noverlap=n_fft - hop_length)
    return y_clean

小技巧:代码里 noise_mag 用了 keepdims=True,这样广播的时候维度自动对齐。我早期写的时候没注意这个,结果矩阵形状不匹配,debug 了半天。

调用方式很简单:

sr, y = wavfile.read('noisy_speech.wav')
y_clean = spectral_subtraction(y, sr, alpha=2.5, beta=0.02)
wavfile.write('clean_speech.wav', sr, y_clean.astype(np.int16))

3.3 参数调优:过减因子与谱下限

谱减法就两个核心参数:α(过减因子)和 β(谱下限)。但调起来门道不少。

参数 作用 典型范围 调大后果 调小后果
α(过减因子) 控制噪声削减力度 1.0 ~ 3.0 语音失真,音乐噪声加重 噪声残留多
β(谱下限) 防止增益归零 0.01 ~ 0.1 噪声残留,但语音自然 语音断裂,有咔哒声

我一般这样调:先固定 β=0.02,从 α=1.5 开始试。如果噪声残留明显,逐步增加到 2.0~2.5。如果出现音乐噪声,就降低 α 或者提高 β。

避坑指南:我曾经在一个车载语音项目里把 α 调到 3.5,结果降噪效果确实好,但语音听起来像机器人,而且音乐噪声大到无法忍受。后来发现,对于非平稳噪声(比如风噪、路噪),过减因子不能太大,否则得不偿失。

3.4 音乐噪声问题:成因与解决方案

音乐噪声(musical noise)是谱减法最臭名昭著的副作用。它听起来像是一串随机出现的短促纯音,频率忽高忽低,非常不自然。

成因分析:

  • 噪声估计不准确:实际噪声和估计噪声有偏差,导致某些频点被过度削减或削减不足
  • 帧间不连续:相邻帧的增益函数变化剧烈,在频域产生随机尖峰
  • 相位随机性:噪声的相位是随机的,幅度谱减法无法处理相位信息

解决方案:

  1. 平滑增益函数:对增益做帧间平滑,减少突变
  2. 使用谱下限:β 不要太小,保留一点背景噪声可以掩蔽音乐噪声
  3. 噪声估计更新:用更鲁棒的方法(如最小值跟踪)估计噪声,而不是只取前几帧
  4. 过减因子自适应:根据信噪比动态调整 α,高 SNR 时用小 α,低 SNR 时用大 α

下面是一个带平滑的改进版本:

def spectral_subtraction_smooth(y, sr, n_fft=512, hop_length=128, alpha=2.0, beta=0.02, smooth_factor=0.8):
    f, t, Zxx = stft(y, fs=sr, nperseg=n_fft, noverlap=n_fft - hop_length)
    mag = np.abs(Zxx)
    phase = np.angle(Zxx)

    noise_frames = 5
    noise_mag = np.mean(mag[:, :noise_frames], axis=1, keepdims=True)

    gain = 1 - alpha * noise_mag / (mag + 1e-10)
    gain = np.maximum(gain, beta)

    # 帧间平滑
    gain_smooth = np.zeros_like(gain)
    gain_smooth[:, 0] = gain[:, 0]
    for i in range(1, gain.shape[1]):
        gain_smooth[:, i] = smooth_factor * gain_smooth[:, i-1] + (1 - smooth_factor) * gain[:, i]

    mag_clean = gain_smooth * mag
    Zxx_clean = mag_clean * np.exp(1j * phase)
    _, y_clean = istft(Zxx_clean, fs=sr, nperseg=n_fft, noverlap=n_fft - hop_length)
    return y_clean

经验之谈:平滑因子取 0.7~0.9 效果比较好。太小了平滑不够,音乐噪声还在;太大了语音会变得模糊,像蒙了一层纱。我一般从 0.8 开始试,听感满意就停。

3.5 谱减法知识体系总览

下面这张图把谱减法的核心流程、参数影响和音乐噪声的解决路径串起来了,方便你对照理解。

谱减法知识体系总览 带噪语音 y(t) STFT → 幅度谱 + 相位 噪声估计 |N̂(ω)| 增益 G(ω) = max(1 - α·|N̂|/|Y|, β) 参数 α, β |X̂(ω)| = G(ω) · |Y(ω)| ISTFT → 增强语音 x̂(t) 音乐噪声问题 解决方案:增益平滑 / 自适应 α / 噪声更新 α↑ → 降噪强但音乐噪声重

从图上可以清楚看到,谱减法是一个开环系统——噪声估计的准确性直接决定了最终效果。而音乐噪声问题,本质上就是噪声估计误差和增益不连续共同作用的结果。

3.6 小结与避坑清单

最后,我把谱减法实践中的常见坑和应对方法列出来,你照着做基本不会翻车:

  • 噪声估计不要只取前几帧:如果噪声是非平稳的,用 VAD(语音活动检测)动态更新噪声谱效果更好
  • α 和 β 要联动调整:α 大了,β 也要适当提高,否则音乐噪声会爆炸
  • 不要忽略相位:虽然谱减法不修改相位,但 STFT 的窗函数选择和帧移会影响相位连续性,进而影响听感
  • 先听再调参:客观指标(如 PESQ、STOI)只能做参考,最终要以人耳听感为准。我见过 PESQ 提升 0.3 但听感更差的案例

我曾经在一个智能音箱项目里,用谱减法做前端降噪,结果音乐噪声被误识别为唤醒词,导致设备频繁误唤醒。后来加了增益平滑和噪声门限,才把误唤醒率降下来。所以,谱减法虽然简单,但部署到产品里一定要做充分的边界测试。