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。