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最经典的实现是库利-图基算法,也叫「蝶形运算」。名字很形象,因为运算流程图看起来像蝴蝶的翅膀。

基本思路是分治:

  1. 把N个点的序列分成奇数点和偶数点两组
  2. 分别对两组做DFT(递归地继续分,直到只剩2个点)
  3. 用蝶形运算把结果合并起来

下面这张图展示了8点FFT的蝶形运算结构:

8点FFT蝶形运算结构图 x[0] x[4] x[2] x[6] x[1] x[5] x[3] x[7] X[0] X[1] X[2] X[3] X[4] X[5] X[6] X[7] 第1级 第2级 第3级 实线:直接连接 虚线:乘以旋转因子

每一级蝶形运算的核心操作就两行代码:

// 蝶形运算核心
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的核心,分治+合并
  • 加窗可以抑制频谱泄露,但会降低频率分辨率