4. 自适应维纳滤波

任务说明

本案例实现基于语音存在概率(SPP)的噪声估计,并据此动态更新维纳滤波器,对带噪语音进行降噪。核心目标是:在非平稳噪声环境下,噪声估计能随语音活动变化而更新,维纳滤波增益随之调整,最终输出降噪后的语音。

实现方案

采用判决引导(Decision-Directed)方法估计先验信噪比,结合语音存在概率递归平滑噪声功率谱。维纳滤波增益由先验SNR计算得到。整体处理按帧进行,每帧包含:加窗、FFT、噪声估计更新、增益计算、频谱加权、IFFT与重叠相加。

关键完整代码

import numpy as np

class AdaptiveWienerFilter:
    def __init__(self, n_fft=512, hop=128, alpha_s=0.98, alpha_n=0.9,
                 alpha_p=0.9, floor_db=-25, q=0.5):
        self.n_fft = n_fft
        self.hop = hop
        self.window = np.hanning(n_fft)
        self.alpha_s = alpha_s      # 先验SNR平滑系数
        self.alpha_n = alpha_n      # 噪声更新平滑系数
        self.alpha_p = alpha_p      # SPP平滑系数
        self.floor = 10 ** (floor_db / 10)
        self.q = q                  # 语音先验概率
        self.n_freq = n_fft // 2 + 1
        self.noise_psd = None       # 噪声功率谱
        self.spp_smooth = 0.5       # 平滑后的语音存在概率
        self.snr_prior = 1.0        # 先验SNR(线性)

    def _spp(self, snr_post):
        """语音存在概率估计(基于似然比)"""
        # 简化似然比:使用后验SNR的单调函数
        xi = max(self.snr_prior, 1e-4)
        gamma = max(snr_post, 1e-4)
        # 似然比近似
        lr = np.exp(gamma * xi / (1 + xi)) / (1 + xi)
        spp = lr * self.q / (lr * self.q + (1 - self.q))
        return min(max(spp, 0.01), 0.99)

    def process_frame(self, x_frame):
        """处理单帧,返回降噪后时域信号"""
        n = len(x_frame)
        X = np.fft.rfft(x_frame * self.window, self.n_fft)
        mag2 = np.abs(X) ** 2

        # 初始化噪声PSD(第一帧)
        if self.noise_psd is None:
            self.noise_psd = mag2.copy()
            self.spp_smooth = 0.1
            return np.zeros(n)

        # 后验SNR
        snr_post = mag2 / (self.noise_psd + 1e-10)

        # 语音存在概率
        spp = np.array([self._spp(s) for s in snr_post])
        self.spp_smooth = self.alpha_p * self.spp_smooth + (1 - self.alpha_p) * spp

        # 噪声PSD更新(仅在语音概率低时更新)
        update_mask = (1 - self.spp_smooth) ** 2
        self.noise_psd = (self.alpha_n * self.noise_psd +
                          (1 - self.alpha_n) * mag2 * update_mask)

        # 先验SNR(判决引导)
        snr_prior_prev = self.snr_prior
        snr_prior = (self.alpha_s * snr_prior_prev +
                     (1 - self.alpha_s) * np.maximum(snr_post - 1, 0))
        self.snr_prior = np.maximum(snr_prior, self.floor)

        # 维纳增益
        G = self.snr_prior / (1 + self.snr_prior)
        G = np.maximum(G, self.floor)

        # 频谱加权
        Y = G * X
        y = np.fft.irfft(Y, self.n_fft)[:n]
        return y * self.window

    def denoise(self, x):
        """整段语音降噪(重叠相加)"""
        n_samples = len(x)
        n_frames = (n_samples - self.n_fft) // self.hop + 1
        y_out = np.zeros(n_samples)
        window_sum = np.zeros(n_samples)

        for i in range(n_frames):
            start = i * self.hop
            x_frame = x[start:start + self.n_fft]
            y_frame = self.process_frame(x_frame)
            y_out[start:start + self.n_fft] += y_frame
            window_sum[start:start + self.n_fft] += self.window ** 2

        # 归一化(避免除以零)
        window_sum = np.maximum(window_sum, 1e-10)
        return y_out / window_sum

运行与验证

import soundfile as sf

# 读取带噪语音(16kHz单声道)
x, sr = sf.read('noisy_speech.wav')
if sr != 16000:
    raise ValueError('采样率需为16kHz')

# 创建滤波器并处理
wf = AdaptiveWienerFilter(n_fft=512, hop=128)
y = wf.denoise(x)

# 保存结果
sf.write('enhanced.wav', y, sr)

# 客观验证:计算分段SNR提升(需干净语音参考)
from scipy.io import wavfile
_, clean = wavfile.read('clean_speech.wav')
seg_snr_in = 10 * np.log10(np.mean(clean**2) / np.mean((clean - x)**2 + 1e-12))
seg_snr_out = 10 * np.log10(np.mean(clean**2) / np.mean((clean - y)**2 + 1e-12))
print(f'输入SNR: {seg_snr_in:.2f} dB, 输出SNR: {seg_snr_out:.2f} dB')

验证方法与通过条件

  • 主观听感:播放enhanced.wav,背景噪声应明显降低,语音清晰度保持,无音乐噪声。
  • 客观指标:若提供干净参考,输出分段SNR应高于输入SNR至少3dB。
  • 噪声跟踪验证:在纯噪声段(无语音),噪声PSD应快速收敛;在语音段,噪声PSD不应大幅上升。可通过打印wf.noise_psd的均值变化观察。
  • 收敛性检查:处理前2秒纯噪声后,噪声PSD估计值应接近真实噪声功率(可用np.mean(wf.noise_psd)与噪声段实际功率对比)。

常见问题

  • 语音段被过度衰减:调低alpha_n(如0.85)或提高q(如0.7),使噪声更新更保守。
  • 残留音乐噪声:增大floor_db(如-20dB)作为增益下限,或提高alpha_s至0.99。
  • 首帧噪声估计不准:建议输入信号前200ms为纯噪声段,或预先用前10帧初始化noise_psd。