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做的事情很简单:拿每个频率的“模板”去和原始信号做匹配。匹配度越高,那个频率的幅度就越大。

我的经验: 刚开始学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倍。

避坑指南: 我曾经在项目里直接用DFT处理一段10秒的录音,程序跑了快两分钟才出结果。后来换成FFT,同样的数据不到0.1秒就搞定了。记住:实际工程中永远用FFT,不要自己写DFT。Python的numpy.fft、scipy.fftpack都是高度优化的FFT实现。

4.4 FFT的核心思想:分治与对称性

FFT为什么能这么快?说白了就两个技巧:

  1. 分治(Divide and Conquer):把一个大DFT拆成两个小DFT,再递归地拆下去,直到拆成2点的DFT。2点DFT的计算量几乎可以忽略。
  2. 利用对称性:旋转因子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)逐层向上合并,最终得到完整的频谱。

基2时间抽取FFT分治流程 输入 N 点序列 x[0]...x[N-1] N点DFT(原始问题) 偶数索引 N/2 点 奇数索引 N/2 点 N/4点DFT N/4点DFT N/4点DFT N/4点DFT ⋮ 递归分解直到2点DFT ⋮ 蝶形运算合并 → 逐层向上组合 输出完整频谱 X[0]...X[N-1] 核心思想:分治 + 蝶形运算,将O(N²)降为O(N log₂N)

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之后,你得到了一堆复数。怎么把它们变成能看的频谱?

两个关键步骤:

  1. 取幅度:np.abs(X),得到每个频率分量的强度
  2. 构建频率轴:告诉每个点对应多少Hz

频率轴的计算公式:

freqs = np.arange(N) * fs / N

其中:

  • fs:采样率(Hz)
  • N:FFT点数
  • 频率分辨率 = fs / N。比如fs=44100,N=1024,分辨率≈43Hz。这意味着你无法区分相差小于43Hz的两个频率。
我曾经踩过的坑: 做语音识别特征提取时,我用了N=256的FFT,采样率16kHz。频率分辨率是16000/256=62.5Hz。结果发现元音的高次谐波(比如3000Hz附近)完全糊在一起,根本分不清。后来把N提高到1024,分辨率降到15.6Hz,效果好了很多。记住:N越大,频率分辨率越高,但时间分辨率会降低。这是个trade-off。

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库,不要自己造轮子