2. FFT在信号处理中的应用:频谱分析基础、滤波器设计中的FFT、卷积与FFT的关系、重叠相加法与重叠保留法

好,咱们进入第二章。这一章我打算聊聊FFT在信号处理里的几个经典应用场景。说实话,这些内容是我当年刚入行时觉得最“绕”的部分,但后来在实际项目中才发现——嗯,这些才是真正能帮你省时间的硬功夫。

2.1 频谱分析基础

先说说频谱分析。你想想看,一个时域信号摆在你面前,你能看出啥?无非是波形长什么样、幅度有多大。但如果你把它变换到频域,就能看到这个信号是由哪些频率成分组成的。这就是FFT最直接的应用。

我个人习惯把频谱分析拆成三步:

  1. 采样:把连续信号变成离散点。这里要注意采样率必须满足奈奎斯特定理,否则会出现混叠。我在项目中遇到过有人用44.1kHz采样一个20kHz的信号,结果频谱里出现了假频率,排查了半天才发现是采样率不够。
  2. 加窗:直接对截断的信号做FFT,频谱会泄漏。所以一般要加窗函数,比如汉宁窗、海明窗。我建议你根据信号特性选窗:如果是分析稳态信号,用汉宁窗;如果是瞬态信号,用矩形窗就行。
  3. 计算功率谱:对FFT结果取模平方,得到功率谱密度。这个能直观看出信号的能量分布。

重要提示:频谱分析的分辨率取决于采样时长。分辨率 = 采样率 / FFT点数。想提高分辨率,要么增加采样时长,要么增加FFT点数(补零只能让频谱更平滑,不能提高真实分辨率)。

2.2 滤波器设计中的FFT

滤波器设计这块,很多人第一反应是时域的卷积操作。但你知道吗?在频域设计滤波器其实更直观。

举个例子。你想设计一个低通滤波器,截止频率是1kHz。在频域里,你只需要把1kHz以上的频率分量全部置零,然后做IFFT变回时域,就得到了滤波器的冲激响应。这就是所谓的“频域采样法”。

不过这里有个坑——直接截断会导致吉布斯现象,也就是滤波器通带边缘会出现振荡。我曾经在做一个音频降噪项目时,直接用理想低通滤波器的频域响应做IFFT,结果出来的滤波器冲激响应有严重的振铃,导致输出信号有“回声”感。后来加了窗函数才解决。

我的经验:用FFT设计滤波器时,建议在频域加一个过渡带(比如用升余弦窗),这样能有效抑制吉布斯现象。过渡带越宽,滤波器越平滑,但选择性会下降。这是个trade-off。

2.3 卷积与FFT的关系

卷积和FFT的关系,说白了就是一句话:时域卷积等于频域相乘。这个性质是FFT加速卷积计算的理论基础。

你想想看,直接做时域卷积,复杂度是O(N²)。但如果用FFT把信号和卷积核都变换到频域,做点乘,再IFFT回来,复杂度就降到了O(N log N)。对于长序列,这个加速比非常可观。

我在做语音信号处理时,经常需要做长序列的卷积。比如一个10秒的语音信号,采样率16kHz,那就是160000个点。直接卷积?算到天荒地老。用FFT做,几毫秒就搞定了。

核心公式
x[n] * h[n] = IFFT( FFT(x[n]) · FFT(h[n]) )
其中 · 表示逐元素相乘。

2.4 重叠相加法与重叠保留法

好,接下来是重点。当你的信号很长,而卷积核比较短时,直接对整个信号做FFT会面临两个问题:一是内存不够,二是实时性要求。这时候就需要分块处理。

重叠相加法和重叠保留法就是两种经典的分块卷积方法。我当年在做一个实时音频效果器时,就是靠这两种方法解决了延迟问题。

重叠相加法

思路很简单:把长信号分成若干块,每块分别和卷积核做FFT卷积,然后把结果重叠的部分相加。

具体步骤:

  1. 把输入信号x[n]分成不重叠的块,每块长度L。
  2. 对每块做FFT卷积(补零到N = L + M - 1,M是卷积核长度)。
  3. 每块卷积结果的前M-1个点会和下一块的结果重叠,把这些重叠部分相加。

我的建议:块长度L一般取卷积核长度M的2-4倍。太短了FFT开销大,太长了延迟高。我在项目中通常取L = 4M,这样FFT效率高,延迟也能接受。

重叠保留法

这个方法稍微绕一点,但效率更高。它不补零,而是保留重叠部分。

步骤:

  1. 把输入信号分成重叠的块,每块长度N,相邻块重叠M-1个点。
  2. 对每块做N点FFT,和卷积核的N点FFT相乘,再IFFT。
  3. 只保留每块结果的后L个点(L = N - M + 1),丢弃前M-1个点。

注意:重叠保留法里,丢弃的前M-1个点是“坏数据”,因为循环卷积的混叠效应。我第一次用这个方法时没注意,直接把所有点都保留了,结果输出信号全是噪声。排查了半天才发现是这里出了问题。

2.5 两种方法的对比

对比项 重叠相加法 重叠保留法
分块方式 不重叠 重叠
补零 需要补零到N = L+M-1 不需要补零
结果处理 重叠部分相加 丢弃前M-1个点
计算效率 略低(需要额外加法) 略高(无加法,但FFT点数固定)
实现难度 简单直观 稍微复杂

我个人更倾向于用重叠保留法,因为它的FFT点数固定,方便做内存管理。但如果你刚接触分块卷积,建议先从重叠相加法入手,逻辑更清晰。

2.6 代码示例:重叠保留法实现

最后,给一个简单的Python实现,帮你理解重叠保留法的流程。

import numpy as np

def overlap_save_conv(x, h, N):
    """
    重叠保留法实现长序列卷积
    x: 输入信号
    h: 卷积核
    N: FFT点数
    """
    M = len(h)
    L = N - M + 1  # 每块有效输出长度
    
    # 补零到N点
    H = np.fft.fft(h, N)
    
    # 分块处理
    y = []
    idx = 0
    while idx < len(x):
        # 取一块数据,长度N,重叠M-1个点
        block = x[idx:idx+N]
        if len(block) < N:
            block = np.pad(block, (0, N-len(block)), 'constant')
        
        # FFT卷积
        Y_block = np.fft.ifft(np.fft.fft(block) * H)
        
        # 只保留后L个点
        y.extend(Y_block[M-1:].real)
        
        idx += L
    
    return np.array(y)

# 示例
x = np.random.randn(1000)
h = np.array([1, -0.5, 0.25])
N = 64
y = overlap_save_conv(x, h, N)

小技巧:实际项目中,N一般取2的幂次,这样FFT效率最高。比如N=256、512、1024。我习惯用N=1024,因为大多数场景下这个点数能兼顾效率和延迟。

好了,这一章的内容就到这里。FFT在信号处理中的应用远不止这些,但频谱分析、滤波器设计、卷积加速和分块卷积,是四个最核心的切入点。下一章我会聊聊FFT在CNN推理优化中的具体应用,到时候你会发现——嗯,这些信号处理的知识,其实和深度学习是相通的。