第二章 时域与频域:傅里叶变换原理、DFT与FFT、频谱泄露与窗函数、时频分析基础

好,我们进入第二章。说实话,这一章是整个DSP算法的基石。你想想看,我们做音响DSP,天天跟声音打交道。声音是什么?是空气的振动,是随时间变化的压力波。这就是时域。但光看时域波形,你能看出这是小提琴还是钢琴吗?很难。所以我们需要频域。

我个人习惯把傅里叶变换比作「翻译官」。它把时间这个语言,翻译成频率这个语言。翻译完了,你就能看清声音的「成分」了。低频多少、中频多少、高频多少,一目了然。

2.1 从傅里叶变换到DFT

傅里叶变换的数学公式,大家可能都见过:

X(f) = ∫ x(t) · e^(-j2πft) dt

但这是连续信号的公式。我们DSP处理的是离散信号,是采样后的数据点。所以得用离散傅里叶变换,也就是DFT:

X[k] = Σ x[n] · e^(-j2πkn/N)   (n=0 to N-1)

这里N是采样点数,k是频率索引。说白了,就是把N个时域点,变成N个频域点。

核心理解:DFT的本质是「相关性计算」。每个频点k,就是在问:原始信号里,有多少成分跟这个频率的正弦波长得像?

我在项目中遇到过一个问题:采样率48kHz,做1024点DFT。那频率分辨率是多少?48k / 1024 = 46.875 Hz。这意味着你只能分辨出间隔大于46.875Hz的两个频率。如果两个频率只差20Hz,在频谱上就糊在一起了。

2.2 FFT:工程界的救星

DFT的计算量有多大?N点DFT需要N²次复数乘加。N=1024时,就是100多万次。这在实时系统里根本跑不动。

FFT(快速傅里叶变换)的出现,把计算量降到了N·log₂(N)。同样是1024点,只需要约1万次。差了100倍!

为什么能这么快?核心思想是「分治」。把一个大DFT拆成两个小DFT,再拆,再拆...直到拆成2点DFT。这就是著名的基2-FFT算法。

// 8点FFT的蝶形运算示意(伪代码)
for stage = 1 to log2(N)
    for butterfly in each group
        // 蝶形运算:两个复数加减乘旋转因子
        temp = x[i] + W * x[i+half]
        x[i+half] = x[i] - W * x[i+half]
        x[i] = temp
    end
end

嗯,这里要注意:FFT要求N是2的整数次幂。如果采样点数不是2的幂,就得补零或者截断。我个人习惯是补零,因为补零还能提高频谱的「视觉分辨率」。

工程小技巧:很多DSP芯片都有硬件FFT加速器。比如ADAU系列,直接调用硬件模块就行,比自己写软件快10倍以上。但理解原理仍然重要——出了问题你才知道怎么排查。

2.3 频谱泄露与窗函数

好,现在问题来了。你拿一段有限长度的信号做FFT,相当于把无限长的信号「咔嚓」截断了。这个截断操作,在频域上会引入什么?

频谱泄露。

为什么会这样?因为截断相当于在时域乘了一个矩形窗。矩形窗的频谱是sinc函数,有很多旁瓣。这些旁瓣的能量会「泄露」到相邻的频率点上。

我曾经调试一个EQ算法,明明只加了1kHz的增益,结果800Hz和1.2kHz也跟着变了。查了半天,就是频谱泄露搞的鬼。

解决办法?加窗函数。用窗函数平滑地截断信号,而不是生硬地一刀切。

窗函数 主瓣宽度 旁瓣衰减 适用场景
矩形窗 最窄 -13dB 频率分辨率要求高,幅值精度要求低
汉宁窗 较宽 -32dB 通用场景,兼顾分辨率和泄露抑制
海明窗 较宽 -43dB 旁瓣衰减更好,适合语音处理
布莱克曼窗 最宽 -58dB 对泄露极度敏感的场景

我建议你记住一个原则:主瓣越宽,频率分辨率越差;旁瓣越低,泄露抑制越好。这是鱼和熊掌的关系,没有完美的窗函数。

// 汉宁窗的C语言实现
void hanning_window(float *data, int N) {
    for (int i = 0; i < N; i++) {
        float w = 0.5 * (1.0 - cos(2.0 * M_PI * i / (N - 1)));
        data[i] *= w;
    }
}

避坑指南:我曾经在做一个啸叫检测算法时,忘了加窗。结果FFT出来的频谱里,啸叫频率旁边总有几个假峰。加上海明窗后,假峰消失了,检测准确率从70%提升到了95%。记住:做FFT之前,先加窗!

2.4 时频分析基础

FFT有个局限:它只能告诉你信号里有哪些频率,但不知道这些频率在什么时间出现。对于平稳信号(比如稳定的正弦波),这没问题。但对于音乐、语音这种非平稳信号,就不够了。

比如一段钢琴曲,前半段是低音,后半段是高音。FFT只能告诉你「整段音乐里有低音也有高音」,但分不清先后顺序。

怎么办?短时傅里叶变换(STFT)。

思路很简单:把信号切成一小段一小段,每段分别做FFT。这样你就得到了「时间-频率」的二维信息。

// STFT的伪代码
frame_size = 1024
hop_size = 512  // 帧移,通常为帧长的一半

for start = 0 to len(signal) - frame_size step hop_size
    frame = signal[start : start + frame_size]
    windowed_frame = frame * hanning_window(frame_size)
    spectrum = FFT(windowed_frame)
    // 保存 spectrum 到 spectrogram 矩阵的第 start/hop_size 行
end

这里有两个关键参数:帧长和帧移。帧长决定了频率分辨率,帧移决定了时间分辨率。帧长越长,频率越精细,但时间越模糊。反过来也一样。

经验之谈:做语音处理时,我一般用20-30ms的帧长。采样率16kHz的话,就是320-480点。帧移取帧长的一半,保证50%重叠。这样既能看清语音的共振峰,又能跟踪到快速的音节变化。

STFT的结果叫语谱图(Spectrogram)。横轴是时间,纵轴是频率,颜色深浅代表能量大小。你想想看,一张语谱图就能把声音的「前世今生」都讲清楚。哪个时间点有什么频率成分,一目了然。

嗯,这一章的内容就到这里。傅里叶变换是DSP的「内功心法」,不练好它,后面的滤波器设计、均衡器、压缩器都无从谈起。下一章我们讲数字滤波器,到时候你会频繁用到今天学的频域知识。