You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Python基于独立噪声WAV文件去除音频噪声?

音频去噪:基于FFT/STFT的实现问题解答

一、直接FFT相减去噪的问题

你提到直接对FFT结果相减后频谱图看起来不错,但逆FFT后的音频干扰更严重,核心原因有两个:

  1. 丢弃了相位信息:你的代码里用了abs(scipy.fft.fft(signal)),只保留了频谱的振幅,丢失了关键的相位数据。逆FFT需要完整的复数频谱(振幅+相位)才能重构出正常的音频,仅用振幅重构会导致严重失真。
  2. 全局频谱的局限性:普通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),原因是:

  1. nperseg设置过小:nperseg=10远小于合理值(一般用2的幂,比如256、512),导致STFT输出的维度异常
  2. pcolormesh的维度不匹配:sig.stft返回的Zxx形状是(频率点数, 时间帧数),而pcolormesh要求C的维度与X(时间)、Y(频率)对应,需要确保输入的维度正确
  3. 未处理立体声:如果音频是立体声,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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 00:00:33