如何用Python基于独立噪声WAV文件去除音频噪声?
音频去噪:基于FFT/STFT的实现问题解答
一、直接FFT相减去噪的问题
你提到直接对FFT结果相减后频谱图看起来不错,但逆FFT后的音频干扰更严重,核心原因有两个:
- 丢弃了相位信息:你的代码里用了
abs(scipy.fft.fft(signal)),只保留了频谱的振幅,丢失了关键的相位数据。逆FFT需要完整的复数频谱(振幅+相位)才能重构出正常的音频,仅用振幅重构会导致严重失真。 - 全局频谱的局限性:普通FFT是对整个音频的全局频谱分析,假设噪声是平稳且在整个时段内一致,但实际音频和噪声的频率成分会随时间变化,全局相减会误减掉信号的频率成分,同时残留未匹配的噪声。
修正后的FFT去噪实现思路
如果要尝试FFT去噪,需要:
- 保留复数形式的FFT结果,不取绝对值
- 确保噪声文件和带噪音频的长度一致
- 用噪声的平均频谱(而非单次FFT)来做减法,减少随机误差
- 对相减后的频谱做阈值处理(比如只减去噪声能量高于信号的频段)
示例代码:
import scipy.io.wavfile as wavfile import numpy as np from scipy.fft import fft, ifft, fftfreq # 读取带噪音频和噪声文件 fs, noisy_signal = wavfile.read("sound.wav") fs_noise, noise = wavfile.read("noise.wav") # 转单声道 if len(noisy_signal.shape) == 2: noisy_signal = noisy_signal.sum(axis=1) / 2 if len(noise.shape) == 2: noise = noise.sum(axis=1) / 2 # 对齐长度 min_len = min(len(noisy_signal), len(noise)) noisy_signal = noisy_signal[:min_len] noise = noise[:min_len] # 计算复数FFT noisy_fft = fft(noisy_signal) noise_fft = fft(noise) # 计算噪声的平均能量(用绝对值平方表示能量) noise_power = np.abs(noise_fft) ** 2 # 设定阈值:只减去噪声能量高于信号能量的部分 denoised_fft = noisy_fft.copy() mask = np.abs(noisy_fft) ** 2 < noise_power * 1.2 # 1.2是余量系数 denoised_fft[mask] = noisy_fft[mask] - noise_fft[mask] * (np.abs(noisy_fft[mask]) / np.abs(noise_fft[mask])) # 逆FFT重构音频 denoised_signal = np.real(ifft(denoised_fft)) # 转换为整数格式(WAV文件要求) denoised_signal = np.int16(denoised_signal / np.max(np.abs(denoised_signal)) * 32767) # 保存结果 wavfile.write("denoised_fft.wav", fs, denoised_signal)
二、STFT去噪的报错修复与正确实现
你的STFT代码出现ValueError: too many values to unpack (expected 2),原因是:
- nperseg设置过小:
nperseg=10远小于合理值(一般用2的幂,比如256、512),导致STFT输出的维度异常 - pcolormesh的维度不匹配:
sig.stft返回的Zxx形状是(频率点数, 时间帧数),而pcolormesh要求C的维度与X(时间)、Y(频率)对应,需要确保输入的维度正确 - 未处理立体声:如果音频是立体声,STFT会返回额外的通道维度,导致后续计算出错
修正后的STFT去噪完整代码
import numpy as np import matplotlib.pyplot as plt import scipy.io.wavfile as wavfile from scipy import signal as sig # 读取带噪音频和噪声文件 fs, noisy_signal = wavfile.read("sound.wav") fs_noise, noise = wavfile.read("noise.wav") # 转单声道处理 if len(noisy_signal.shape) == 2: noisy_signal = noisy_signal.sum(axis=1) / 2 if len(noise.shape) == 2: noise = noise.sum(axis=1) / 2 # 对齐两个音频的长度 min_len = min(len(noisy_signal), len(noise)) noisy_signal = noisy_signal[:min_len] noise = noise[:min_len] # 进行STFT分析,使用合理的帧长(256) nperseg = 256 f, t, Zxx_noisy = sig.stft(noisy_signal, fs, nperseg=nperseg) _, _, Zxx_noise = sig.stft(noise, fs, nperseg=nperseg) # 计算噪声的平均能量谱,避免单次STFT的随机误差 noise_power = np.mean(np.abs(Zxx_noise) ** 2, axis=1, keepdims=True) noisy_power = np.abs(Zxx_noisy) ** 2 # 构建掩码:只去除噪声能量显著高于信号的频段 mask = noisy_power < noise_power * 1.3 # 1.3为容错系数 Zxx_denoised = Zxx_noisy.copy() Zxx_denoised[mask] = Zxx_noisy[mask] - Zxx_noise[mask] * (np.abs(Zxx_noisy[mask]) / np.abs(Zxx_noise[mask])) # 逆STFT重构音频 _, denoised_signal = sig.istft(Zxx_denoised, fs) # 转换为WAV兼容的整数格式 denoised_signal = np.int16(denoised_signal / np.max(np.abs(denoised_signal)) * 32767) # 绘制频谱图(修复维度问题) fig, ax = plt.subplots(ncols=2, nrows=1, figsize=(12, 4)) # pcolormesh需要X(t)、Y(f)、C(Zxx的形状为(频率,时间),与f、t维度匹配) ax[0].pcolormesh(t, f, np.abs(Zxx_noisy), vmin=0, vmax=np.percentile(np.abs(Zxx_noisy), 95), shading='gouraud') ax[0].set_title('带噪信号频谱') ax[0].set_ylabel('频率 [Hz]') ax[0].set_xlabel('时间 [秒]') ax[1].pcolormesh(t, f, np.abs(Zxx_denoised), vmin=0, vmax=np.percentile(np.abs(Zxx_denoised), 95), shading='gouraud') ax[1].set_title('去噪后信号频谱') ax[1].set_ylabel('频率 [Hz]') ax[1].set_xlabel('时间 [秒]') plt.tight_layout() plt.show() # 保存去噪后的音频 wavfile.write("denoised_stft.wav", fs, denoised_signal)
关键修复点说明
- 将
nperseg改为256(或512),保证STFT输出的维度正常 - 处理立体声转单声道,避免多通道导致的维度错误
- 绘制频谱图时,直接使用
np.abs(Zxx),其形状(频率数, 时间帧数)与f(频率数组)、t(时间数组)的维度匹配 - 引入噪声能量的平均计算,用掩码方式选择性去除噪声,减少对信号的损伤
内容的提问来源于stack exchange,提问作者kush agrawal
相关产品推荐
相关产品推荐

