4. 频域分析入门:傅里叶变换(FFT)基础、频谱与功率谱、频域特征提取、基于频域的门限检测
好,咱们进入第四章。说实话,很多做音频的工程师,一听到「傅里叶变换」就头大。我当年也一样,觉得这东西太数学了,跟我写代码有什么关系?
但后来我发现,不懂频域,你根本做不好噪声门限。时域里你看到的是波形乱跳,频域里你一眼就能看出「哦,这是低频轰鸣,那是高频嘶声」。说白了,频域就是给你一副透视眼镜。
4.1 从时域到频域:为什么要做傅里叶变换?
你想想看,时域信号长什么样?就是一条随时间变化的曲线。但这条曲线里,可能混着50Hz的工频干扰、200Hz的人声、3000Hz的齿音。你在时域里根本分不清谁是谁。
傅里叶变换干的事很简单:把信号拆成不同频率的正弦波。每个频率对应一个幅度和相位。这样你就能知道「哪个频率成分强,哪个弱」。
核心思想:任何连续信号,都可以分解为一系列正弦波的叠加。这就是傅里叶级数展开的直观理解。
我在项目中遇到过一件事:有个客户说他们的录音设备有「嗡嗡声」,时域波形看着挺干净,但就是听着难受。我一做FFT,好家伙,50Hz和它的谐波(100Hz、150Hz)全冒出来了。原来是电源滤波没做好。这就是频域分析的价值——让你看到时域里看不到的东西。
4.2 FFT 基础:离散傅里叶变换的快速实现
实际工程中,我们处理的是离散采样信号,所以要用离散傅里叶变换(DFT)。但DFT计算量太大,N点DFT需要N²次复数乘法。N=1024时,就是一百万次运算,实时系统根本扛不住。
FFT(快速傅里叶变换)就是DFT的快速算法。它利用旋转因子的对称性和周期性,把计算量降到N·log₂(N)。N=1024时,只需要约一万次运算,快了100倍。
我的习惯:做音频处理时,FFT点数一般选256、512、1024或2048。点数越大,频率分辨率越高,但时域分辨率越低。这是个权衡。我通常用1024点,兼顾精度和实时性。
下面是一个简单的FFT实现示例,用Python写的,方便你理解流程:
import numpy as np
def my_fft(x):
"""简易FFT实现,仅用于教学"""
N = len(x)
if N <= 1:
return x
# 分奇偶
even = my_fft(x[0::2])
odd = my_fft(x[1::2])
# 合并
T = [np.exp(-2j * np.pi * k / N) * odd[k] for k in range(N // 2)]
return [even[k] + T[k] for k in range(N // 2)] + \
[even[k] - T[k] for k in range(N // 2)]
# 生成测试信号:50Hz + 120Hz
fs = 1000 # 采样率1kHz
t = np.arange(0, 1, 1/fs)
x = np.sin(2*np.pi*50*t) + 0.5*np.sin(2*np.pi*120*t)
# 做FFT
X = my_fft(x)
freqs = np.fft.fftfreq(len(x), 1/fs)
# 只取正频率部分
idx = np.where(freqs >= 0)
print("频率峰值在:", freqs[idx][np.argmax(np.abs(X[idx]))], "Hz")
嗯,这里要注意:实际工程中别自己手写FFT,直接用numpy.fft或scipy.fftpack。它们经过高度优化,比手写快得多。我当年刚入行时傻乎乎自己写了一个,结果跑起来比库函数慢了20倍,还被同事笑话了。
4.3 频谱与功率谱:你到底在看什么?
做完FFT,你得到的是复数数组。每个复数代表一个频率分量的幅度和相位。但通常我们更关心幅度,也就是频谱。
频谱(Spectrum): 就是|X[k]|,表示每个频率分量的幅度。单位跟原始信号一样。
功率谱(Power Spectrum): 是|X[k]|²,表示每个频率分量的能量分布。单位是信号幅度的平方。
为什么要区分这两个?
- 频谱适合看「哪个频率成分强」,直观。
- 功率谱适合做能量分析,比如计算信噪比、门限检测。
我曾经踩过的坑:做功率谱时忘记除以N(FFT点数),结果算出来的能量值大得离谱。后来排查了半天才发现,功率谱的正确计算应该是 |X[k]|² / N。千万别忘了归一化!
另外,实际信号通常用功率谱密度(PSD)更准确。PSD考虑了频率分辨率的影响,单位是V²/Hz。计算时还要乘以2(因为只取了正频率部分)。公式如下:
PSD = 2 * |X[k]|² / (fs * N)
其中fs是采样率,N是FFT点数。这样算出来的PSD,积分后等于信号的总功率。
4.4 频域特征提取:从频谱中挖出有用信息
有了频谱和功率谱,我们能提取哪些特征?我常用的有这几个:
| 特征名称 | 计算公式 | 物理意义 | 应用场景 |
|---|---|---|---|
| 频谱质心 | Σ(f·P(f)) / ΣP(f) | 能量集中的平均频率 | 区分明亮/暗淡声音 |
| 频谱滚降点 | 能量累计到85%的频率 | 高频截止位置 | 判断带宽、噪声类型 |
| 子带能量 | 某频段内P(f)之和 | 特定频段的能量 | 噪声门限、语音检测 |
| 谱平坦度 | 几何均值/算术均值 | 接近1是噪声,接近0是纯音 | 区分噪声与信号 |
| 谱熵 | -Σ(P(f)·log(P(f))) | 频谱的混乱程度 | 语音/噪声分类 |
我个人最常用的是子带能量和谱平坦度。为什么?因为做噪声门限时,你不需要知道每个频率的精确值,只需要知道「这个频段有没有信号」。
举个例子:
def subband_energy(psd, freqs, low, high):
"""计算指定频段的能量"""
mask = (freqs >= low) & (freqs <= high)
return np.sum(psd[mask])
def spectral_flatness(psd):
"""计算谱平坦度"""
geometric = np.exp(np.mean(np.log(psd + 1e-12)))
arithmetic = np.mean(psd)
return geometric / arithmetic
我在项目中遇到过:用谱平坦度做噪声检测特别灵。当只有背景噪声时,谱平坦度接近1;当有人说话或音乐响起时,谱平坦度会骤降到0.3以下。这个特征比单纯看能量阈值稳定得多。
4.5 基于频域的门限检测:这才是实战
好了,前面铺垫了这么多,终于到核心了。基于频域的门限检测,说白了就是:
- 把信号分帧,每帧做FFT
- 计算功率谱或子带能量
- 跟预设的门限比较
- 超过门限判为「有信号」,否则判为「无信号」
但这里有个关键问题:门限怎么设?
方法一:固定门限
最简单,但最不实用。因为环境噪声会变,白天和晚上的背景噪声差好几个dB。固定门限要么漏检,要么误触发。
方法二:自适应门限
实时估计背景噪声,动态调整门限。我常用的方法是:
- 取前几帧作为噪声估计
- 每帧更新:如果当前帧能量低于门限,就把它纳入噪声统计
- 门限 = 噪声均值 + α × 噪声标准差
α一般取2~5,看你想要多灵敏。α=3时,误报率约0.3%(假设噪声服从高斯分布)。
实战代码片段:
class FreqDomainVAD:
def __init__(self, fs=16000, n_fft=512, alpha=3.0):
self.fs = fs
self.n_fft = n_fft
self.alpha = alpha
self.noise_mean = None
self.noise_std = None
self.learning = True
self.learn_frames = 0
def process(self, frame):
# 做FFT
spec = np.fft.rfft(frame * np.hanning(len(frame)))
power = np.abs(spec)**2 / self.n_fft
total_energy = np.sum(power)
if self.learning:
# 学习阶段:收集噪声统计
if self.noise_mean is None:
self.noise_mean = total_energy
self.noise_std = 0
else:
self.noise_mean = 0.95 * self.noise_mean + 0.05 * total_energy
self.noise_std = 0.95 * self.noise_std + 0.05 * abs(total_energy - self.noise_mean)
self.learn_frames += 1
if self.learn_frames > 50:
self.learning = False
return False
# 门限检测
threshold = self.noise_mean + self.alpha * self.noise_std
is_speech = total_energy > threshold
# 更新噪声估计(如果当前帧是噪声)
if not is_speech:
self.noise_mean = 0.95 * self.noise_mean + 0.05 * total_energy
self.noise_std = 0.95 * self.noise_std + 0.05 * abs(total_energy - self.noise_mean)
return is_speech
这段代码我用了很多年,效果稳定。注意几个细节:
- 加窗(汉宁窗)减少频谱泄漏
- 用指数平滑更新噪声统计,比滑动窗口更省内存
- 学习阶段至少50帧(约1.5秒),确保噪声估计准确
我曾经犯过的错:一开始没做加窗,结果频谱泄漏导致门限检测频繁误触发。后来加了汉宁窗,问题立刻解决。加窗不是可选项,是必选项。
4.6 本章知识体系总览
下面这张图,是我自己画的频域门限检测的完整流程。你照着这个思路走,基本不会出错。
这张图把整个流程串起来了。你从左上角开始,一步步往下走,最后到门限判决。每个环节都有对应的技术细节,我在前面都讲到了。
我的建议:刚开始做频域门限时,别追求花哨的算法。先把基础流程跑通,用固定门限试,再换成自适应门限。一步步来,你会发现频域分析其实没那么可怕。
好了,这一章的内容就到这里。频域分析是噪声门限设计的核心工具,掌握了它,你就能从「看波形猜问题」升级到「看频谱精准定位」。下一章我们会深入更高级的门限策略,但前提是——你得先把FFT和频谱玩熟。
公众号:蓝海资料掘金营,微信deep3321