4. 基于子空间的语音增强
任务目标
实现基于信号子空间的语音增强算法,对叠加有色噪声(如低通滤波后的白噪声)的带噪语音进行降噪处理。算法核心思想:将带噪信号的协方差矩阵进行特征分解,将特征空间划分为信号子空间与噪声子空间,仅保留信号子空间分量并做谱修正,从而抑制噪声。
环境依赖
Python 3.8+,仅需 numpy 与 scipy(用于读取/生成测试音频)。若需听感验证,可选用 soundfile 读写 wav 文件。
核心实现
以下代码实现完整的子空间增强流程:分帧加窗 → 协方差估计 → 特征分解 → 信号子空间维数估计 → 子空间加权 → 重叠相加重建。噪声方差通过前几帧(纯噪声段)估计。
import numpy as np
from scipy.linalg import eigh
def subspace_enhance(x, fs, noise_frames=10, win_len=512, hop=256, alpha=0.98):
"""
信号子空间语音增强(适用于有色噪声)
x: 带噪语音信号 (一维数组)
fs: 采样率
noise_frames: 用于估计噪声方差的前若干帧数
win_len: 帧长(样本数)
hop: 帧移
alpha: 谱修正系数 (0~1)
返回: 增强后信号 (与x等长)
"""
# 分帧加汉宁窗
n_frames = 1 + (len(x) - win_len) // hop
frames = np.zeros((n_frames, win_len))
win = np.hanning(win_len)
for i in range(n_frames):
start = i * hop
frames[i] = x[start:start+win_len] * win
# 用前几帧估计噪声方差(假设起始段为纯噪声)
noise_var = np.mean(frames[:noise_frames] ** 2)
# 逐帧处理
enh_frames = np.zeros_like(frames)
for i in range(n_frames):
# 帧内去均值(避免直流偏置影响协方差估计)
frame = frames[i] - np.mean(frames[i])
# 协方差矩阵(使用空间相关,此处用帧内延迟嵌入构造托普利兹矩阵)
# 简化:直接使用帧的自相关构造协方差(频域子空间等价做法)
# 此处采用时域延迟嵌入:构造 m 维子空间
m = 64 # 子空间维度(嵌入维数)
if win_len < m:
m = win_len // 2
# 构造 Hankel 矩阵(延迟嵌入)
H = np.zeros((win_len - m + 1, m))
for j in range(m):
H[:, j] = frame[j : j + win_len - m + 1]
# 协方差矩阵 R = H^T H / (行数)
R = (H.T @ H) / (win_len - m + 1)
# 特征分解(升序)
eigvals, eigvecs = eigh(R)
# 信号子空间维数估计:特征值 > 噪声方差 即视为信号
noise_floor = noise_var * 1.5 # 有色噪声放宽阈值
sig_dim = np.sum(eigvals > noise_floor)
sig_dim = max(1, min(sig_dim, m - 1)) # 至少1维,至多m-1
# 构造加权矩阵:信号子空间保留,噪声子空间置零,并做谱修正
G = np.zeros((m, m))
for j in range(sig_dim):
# 维纳型修正:lambda_j / (lambda_j + alpha * noise_var)
gain = eigvals[j] / (eigvals[j] + alpha * noise_var)
G[j, j] = gain
# 重建增强帧(在嵌入空间)
enh_embedded = eigvecs @ G @ eigvecs.T @ H.T
# 平均重叠部分还原为帧长
enh_frame = np.zeros(win_len)
count = np.zeros(win_len)
for j in range(win_len - m + 1):
enh_frame[j : j+m] += enh_embedded[:, j]
count[j : j+m] += 1
enh_frame = enh_frame / np.maximum(count, 1)
enh_frames[i] = enh_frame * win # 重新加窗
# 重叠相加重建
out = np.zeros(len(x))
overlap_sum = np.zeros(len(x))
for i in range(n_frames):
start = i * hop
out[start:start+win_len] += enh_frames[i]
overlap_sum[start:start+win_len] += win ** 2 # 加窗平方归一化
# 归一化避免重叠区能量不均
out = out / np.maximum(overlap_sum, 1e-8)
return out
# 使用示例(生成测试信号并调用)
if __name__ == "__main__":
fs = 16000
t = np.arange(fs * 2) / fs
# 干净语音:1kHz 正弦 + 3kHz 正弦(模拟语音共振峰)
clean = 0.5 * np.sin(2*np.pi*1000*t) + 0.3 * np.sin(2*np.pi*3000*t)
# 有色噪声:白噪声经低通滤波(模拟车内/室内噪声)
rng = np.random.default_rng(0)
white = rng.standard_normal(len(t))
# 简单低通:移动平均
b = np.ones(50) / 50
colored_noise = np.convolve(white, b, mode='same')
colored_noise = colored_noise / np.max(np.abs(colored_noise)) * 0.3
noisy = clean + colored_noise
# 前0.5秒为纯噪声段(用于噪声估计)
noisy[:int(0.5*fs)] = colored_noise[:int(0.5*fs)]
enhanced = subspace_enhance(noisy, fs)
# 输出信噪比对比(仅作数值验证)
def snr(clean, sig):
noise = sig - clean
return 10*np.log10(np.sum(clean**2) / np.sum(noise**2))
snr_in = snr(clean, noisy)
snr_out = snr(clean, enhanced)
print(f"输入SNR: {snr_in:.2f} dB")
print(f"输出SNR: {snr_out:.2f} dB")
print(f"提升: {snr_out - snr_in:.2f} dB")
运行与验证
- 直接运行脚本:
python subspace_enhance.py - 观察终端输出的 SNR 数值。若输出 SNR 高于输入 SNR(通常提升 3~10 dB),且无报错,则算法流程正确。
- 如需听感验证,可将
noisy与enhanced用soundfile.write()导出为 wav 文件,主观对比背景噪声是否明显降低、语音是否保留清晰。
通过条件:程序正常结束,输出 SNR 提升为正数,且增强后信号无断裂或爆音(可观察波形连续性)。
关键说明
- 噪声方差估计依赖前
noise_frames帧为纯噪声段,实际应用中需通过语音活动检测(VAD)获取噪声段。 - 嵌入维数
m影响子空间分辨率:m越大频率分辨率越高,但计算量增大且需要更多帧样本。本实现取 64,对 16kHz 信号在 32ms 帧长下效果良好。 - 有色噪声的协方差矩阵特征值分布较平坦,阈值
noise_floor = 1.5 * noise_var为经验值,可根据噪声颜色适当调整。