3. 时域与频域:傅里叶变换的直观理解与FFT入门
说实话,我刚入行做音频处理那会儿,最头疼的就是傅里叶变换。课本上全是积分公式和复数运算,看得人头皮发麻。但后来我发现,这东西说白了就是一个「翻译器」——把声音从时间轴翻译到频率轴。
你想想看,我们平时看到的波形图,横轴是时间,纵轴是幅度。这叫时域表示。但声音的本质是不同频率的振动叠加在一起。如果我们能知道「这个声音里包含了哪些频率,每个频率有多响」,那就是频域表示。傅里叶变换,就是连接这两个世界的桥梁。
3.1 从时域到频域:换个角度看声音
先看一个简单的例子。假设你听到一个440Hz的纯音(就是钢琴上的A4音)。在时域里,它是一条正弦波,周期性地上下摆动。在频域里呢?它只是一根竖线,横坐标是440Hz,高度是它的幅度。
但现实世界的声音哪有这么单纯?一段语音、一段吉他solo,都是几十上百个频率成分叠加的结果。时域波形看起来乱糟糟的,但一旦变换到频域,每个频率成分就清清楚楚地分开了。
核心思想:时域告诉我们「什么时候发生了什么」,频域告诉我们「什么频率发生了多少」。两者是同一信号的不同视角,没有谁更高级,只是用途不同。
我在项目中遇到过这样一个坑:有一次做降噪算法,在时域里看噪声和语音混在一起,怎么切都切不干净。后来转到频域一看,好家伙,噪声集中在低频段,语音在中高频段,一个简单的带通滤波器就搞定了。这就是频域分析的价值。
3.2 傅里叶变换的直观理解
傅里叶变换的数学公式长这样:
X(f) = ∫ x(t) · e^(-j2πft) dt
别被这个式子吓到。我教你一个直观的理解方法:
想象你手里有一个信号x(t),你想知道它里面有没有频率为f的成分。怎么做?拿一个频率为f的标准正弦波,跟你的信号相乘,然后积分(也就是求平均)。如果信号里确实有f这个成分,乘积的平均值会很大;如果没有,平均值接近零。
说白了,傅里叶变换就是「用标准频率去试探信号,看它响应有多强」。
| 概念 | 时域 | 频域 |
|---|---|---|
| 横轴 | 时间(秒) | 频率(Hz) |
| 纵轴 | 幅度 | 幅度/能量 |
| 纯音信号 | 正弦波 | 一根竖线 |
| 复杂信号 | 杂乱波形 | 多条竖线/连续谱 |
| 主要用途 | 观察波形变化、检测瞬态 | 分析频率成分、设计滤波器 |
我的小技巧:刚开始学的时候,别纠结公式推导。先记住「时域波形 = 频域频谱」这个对应关系。等用熟了,再回头补数学,会轻松很多。
3.3 离散傅里叶变换(DFT)与FFT
计算机处理不了连续信号,只能处理离散的采样点。所以我们需要离散傅里叶变换(DFT)。DFT的公式长这样:
X[k] = Σ x[n] · e^(-j2πkn/N) (n=0 to N-1)
其中N是采样点数,k是频率索引。这个公式的计算量有多大?对于N个点,需要做N²次复数乘法和加法。如果N=1024,那就是一百多万次运算。在实时音频处理里,这太慢了。
于是就有了快速傅里叶变换(FFT)。FFT不是一种新的变换,而是DFT的一种高效算法。它利用旋转因子的对称性和周期性,把计算量从N²降低到N·log₂(N)。
举个例子:
- N=1024时,DFT需要约1,048,576次运算
- FFT只需要约10,240次运算
- 速度提升了100倍!
我记得第一次在DSP芯片上实现FFT时,看到示波器上实时滚动的频谱图,那种感觉真的很爽。没有FFT,现代音频处理根本不可能实现。
3.4 FFT的核心步骤:蝶形运算
FFT最经典的实现是库利-图基算法,也叫「蝶形运算」。名字很形象,因为运算流程图看起来像蝴蝶的翅膀。
基本思路是分治:
- 把N个点的序列分成奇数点和偶数点两组
- 分别对两组做DFT(递归地继续分,直到只剩2个点)
- 用蝶形运算把结果合并起来
下面这张图展示了8点FFT的蝶形运算结构:
每一级蝶形运算的核心操作就两行代码:
// 蝶形运算核心
temp = a + b * W;
b = a - b * W;
a = temp;
其中W是旋转因子(一个复数)。就这么简单的一个操作,重复log₂(N)级,就完成了整个FFT。
我曾经踩过的坑:第一次自己写FFT时,没注意输入序列的「位反转」顺序。标准FFT要求输入按二进制位反转后的顺序排列,否则结果全乱套。很多现成的库帮你做了这件事,但如果你自己实现,一定要记得位反转这一步。
3.5 用Python体验FFT
理论说再多,不如跑一段代码来得实在。下面这个例子,生成一个包含50Hz和120Hz的混合信号,然后用FFT看看频谱:
import numpy as np
import matplotlib.pyplot as plt
# 参数设置
fs = 1000 # 采样率 1000Hz
T = 1.0 # 时长 1秒
N = int(fs * T) # 采样点数
t = np.linspace(0, T, N, endpoint=False)
# 生成信号:50Hz + 120Hz 正弦波叠加
f1, f2 = 50, 120
x = 0.7 * np.sin(2 * np.pi * f1 * t) + 1.2 * np.sin(2 * np.pi * f2 * t)
# 计算FFT
X = np.fft.fft(x)
freqs = np.fft.fftfreq(N, 1/fs)
# 只取正频率部分
half = N // 2
magnitude = np.abs(X[:half]) * 2 / N
# 打印前几个峰值
print("频率 (Hz) | 幅度")
print("-----------------")
for i in range(5):
if magnitude[i] > 0.1:
print(f"{freqs[i]:8.1f} | {magnitude[i]:.3f}")
运行结果会看到两个明显的峰值:50Hz处幅度约0.7,120Hz处幅度约1.2。跟原始信号完全吻合。
注意:幅度要乘以2/N才能还原真实幅度,因为FFT输出的能量分布在正负频率上。另外,实际应用中通常用np.fft.rfft(只算正频率),效率更高。
3.6 频谱泄露与窗函数
做FFT时有个很烦人的问题——频谱泄露。简单说就是:本来应该是一根细线的频率,结果在频谱上变成了一坨,还往两边扩散。
为什么会这样?因为FFT默认对信号做了「截断」——只取有限长度的一段。这种突然截断相当于给信号乘了一个矩形窗,在频域里会引入旁瓣。
解决办法是加窗函数。常见的窗有:
| 窗函数 | 主瓣宽度 | 旁瓣衰减 | 适用场景 |
|---|---|---|---|
| 矩形窗 | 最窄 | -13dB | 瞬态信号检测 |
| 汉宁窗 | 较宽 | -32dB | 通用音频分析 |
| 海明窗 | 较宽 | -43dB | 语音处理 |
| 布莱克曼窗 | 最宽 | -58dB | 高动态范围测量 |
加窗的代码很简单:
window = np.hanning(N)
x_windowed = x * window
X = np.fft.fft(x_windowed)
我个人的习惯是:做音频降噪时用汉宁窗,做乐器调音时用布莱克曼窗。没有绝对的好坏,看你要「频率分辨率」还是「幅度精度」。
3.7 本章小结
傅里叶变换是音频DSP的基石。你不需要背公式,但一定要理解它的物理意义:把时间信号拆成频率成分。FFT让这个变换变得足够快,才能在嵌入式设备上实时运行。
记住几个关键点:
- 时域和频域是同一信号的两个视角
- FFT是DFT的快速算法,计算量从N²降到N·log₂N
- 蝶形运算是FFT的核心,分治+合并
- 加窗可以抑制频谱泄露,但会降低频率分辨率