4. 傅里叶变换入门:从时域到频域、DFT与FFT原理
说实话,我刚入行做音频处理那会儿,最头疼的就是傅里叶变换。
课本上全是积分符号和复数,看得人头皮发麻。但后来我发现,这东西说白了就一句话:把声音拆成不同频率的纯音。就像你吃一碗面,能尝出里面有盐、有酱油、有醋——傅里叶变换就是帮我们把声音里的“调料”一样样挑出来。
4.1 为什么非要从时域跑到频域?
你想想看,我们录到的音频信号长什么样?
横轴是时间,纵轴是幅度。这就是时域。时域能告诉你“这个声音有多响”,但它说不清楚“这个声音是什么调”。
举个例子。我录了一段钢琴的中央C(约261Hz)和一段小提琴的中央C。在时域里看,两个波形长得完全不一样——钢琴的波形有尖锐的起振,小提琴的更平滑。但它们的音高是一样的。
时域看不出来音高,但频域可以。
频域的横轴是频率,纵轴是幅度(或能量)。在频域里,钢琴和小提琴的基频都在261Hz那个位置出现一个峰。区别在于,钢琴的高次谐波更丰富,小提琴的谐波分布更集中。这些细节,时域根本看不出来。
4.2 从连续到离散:DFT登场
傅里叶变换的原始版本是连续的——输入一个连续函数,输出一个连续频谱。但我们的计算机只能处理离散的采样点。所以就有了离散傅里叶变换(DFT)。
DFT的公式长这样:
X[k] = Σ_{n=0}^{N-1} x[n] · e^{-j·2π·k·n/N}
别被符号吓到。我来拆开说:
- x[n]:你采集到的音频采样点,n从0到N-1
- X[k]:第k个频率分量的复数结果(包含幅度和相位)
- N:总共用了多少个采样点来做变换
- e^{-j·2π·k·n/N}:这就是个旋转因子,说白了就是不同频率的“模板”
DFT做的事情很简单:拿每个频率的“模板”去和原始信号做匹配。匹配度越高,那个频率的幅度就越大。
4.3 DFT的计算量问题——为什么需要FFT?
DFT的原理很直观,但它的计算量是个大问题。
你算一下:对于N个采样点,每个频率k都要做N次乘法和加法。如果有N个频率分量(k从0到N-1),总计算量就是N²次操作。
N=1024时,N²≈100万次。N=44100(1秒的CD音质),N²≈19亿次。这还只是1秒的数据。处理一首3分钟的歌?算到天荒地老。
所以就有了快速傅里叶变换(FFT)。
| 算法 | 计算量 | N=1024 | N=44100 |
|---|---|---|---|
| DFT(直接计算) | O(N²) | ~1,048,576次 | ~1.94×10⁹次 |
| FFT(基2算法) | O(N log₂N) | ~10,240次 | ~676,000次 |
看到了吗?FFT把计算量从N²降到了N log₂N。N越大,优势越明显。N=44100时,FFT比DFT快了将近3000倍。
4.4 FFT的核心思想:分治与对称性
FFT为什么能这么快?说白了就两个技巧:
- 分治(Divide and Conquer):把一个大DFT拆成两个小DFT,再递归地拆下去,直到拆成2点的DFT。2点DFT的计算量几乎可以忽略。
- 利用对称性:旋转因子e^{-j·2π·k·n/N}有很多重复和对称的值。算一次可以复用多次,避免重复计算。
最常见的FFT是基2时间抽取FFT,要求N是2的整数次幂(如256、512、1024、2048等)。
它的流程大致是这样:
输入: x[0], x[1], ..., x[N-1] (N=2^m)
步骤1: 把x[n]按奇偶分成两组
偶数索引: x[0], x[2], x[4], ...
奇数索引: x[1], x[3], x[5], ...
步骤2: 分别对两组做N/2点的DFT(递归调用FFT)
步骤3: 用“蝶形运算”合并两组结果
每个蝶形: 一次复数乘法 + 两次复数加法
步骤4: 重复步骤1-3,直到所有子问题变成2点DFT
整个过程就像一棵二叉树,从叶子节点(2点DFT)逐层向上合并,最终得到完整的频谱。
4.5 用Python实现一个简单的FFT
理论说完了,咱们动手写代码。下面是一个最简的基2时间抽取FFT实现:
import numpy as np
def fft_radix2(x):
"""
基2时间抽取FFT
要求 len(x) 是2的整数次幂
"""
N = len(x)
if N <= 1:
return x
# 分治:奇偶拆分
even = fft_radix2(x[0::2])
odd = fft_radix2(x[1::2])
# 蝶形运算合并
result = np.zeros(N, dtype=complex)
for k in range(N // 2):
# 旋转因子
W = np.exp(-2j * np.pi * k / N)
result[k] = even[k] + W * odd[k]
result[k + N // 2] = even[k] - W * odd[k]
return result
# 测试:生成一个100Hz + 200Hz的混合信号
fs = 1000 # 采样率 1000Hz
t = np.arange(0, 1, 1/fs) # 1秒
x = np.sin(2 * np.pi * 100 * t) + 0.5 * np.sin(2 * np.pi * 200 * t)
# 调用自定义FFT
X_custom = fft_radix2(x)
# 对比numpy的FFT
X_numpy = np.fft.fft(x)
# 检查误差
print("最大误差:", np.max(np.abs(X_custom - X_numpy)))
# 输出: 最大误差: 1.4210854715202004e-14 (浮点误差,基本一致)
np.fft.fft() 或 scipy.fft.fft()。它们不仅支持任意长度(不限于2的幂),而且底层用C/Fortran实现,比纯Python快几百倍。
4.6 频谱的物理意义:频率分辨率与频率轴
做完FFT之后,你得到了一堆复数。怎么把它们变成能看的频谱?
两个关键步骤:
- 取幅度:
np.abs(X),得到每个频率分量的强度 - 构建频率轴:告诉每个点对应多少Hz
频率轴的计算公式:
freqs = np.arange(N) * fs / N
其中:
- fs:采样率(Hz)
- N:FFT点数
- 频率分辨率 = fs / N。比如fs=44100,N=1024,分辨率≈43Hz。这意味着你无法区分相差小于43Hz的两个频率。
4.7 频谱的对称性:只取一半
对于实信号(我们录到的音频都是实信号),FFT的结果是共轭对称的:
X[N-k] = conj(X[k])
也就是说,频谱的后一半是前一半的镜像。所以我们通常只取前一半(0到N/2-1),对应的频率范围是0到fs/2(奈奎斯特频率)。
完整的频谱可视化代码:
import numpy as np
import matplotlib.pyplot as plt
fs = 1000
N = 1024
t = np.arange(N) / fs
x = np.sin(2 * np.pi * 100 * t) + 0.5 * np.sin(2 * np.pi * 200 * t)
X = np.fft.fft(x)
# 只取前一半
X_half = X[:N//2]
freqs = np.arange(N//2) * fs / N
plt.figure(figsize=(10, 4))
plt.plot(freqs, np.abs(X_half))
plt.xlabel('频率 (Hz)')
plt.ylabel('幅度')
plt.title('幅度谱(单边)')
plt.grid(True)
plt.show()
你会看到在100Hz和200Hz处有两个清晰的峰,幅度比例大约是2:1(因为第二个正弦波的幅度是0.5)。
4.8 本章小结
来,咱们捋一下这章的核心:
- DFT 把离散时域信号变换到频域,原理直观但计算量大
- FFT 利用分治和对称性,把计算量从O(N²)降到O(N log₂N)
- 频率分辨率 = fs / N,N越大分辨率越高,但时间分辨率会降低
- 实信号的频谱是对称的,通常只取前一半(0 ~ fs/2)
- 实际工程中永远用现成的FFT库,不要自己造轮子