FFT 实现快速卷积时,为什么会出现混叠?(一)如何用 Overlap-Add 解决?

很多工程师第一次使用 FFT 实现快速卷积时,都会默认认为“时域卷积 = FFT 乘法 + IFFT”,结果仿真一跑,分块边界附近却出现了明显失真。问题通常不在 FFT 本身,而在于:频域直接相乘再 IFFT,天然对应的是循环卷积,不是线性卷积。

如果没有处理好块边界和零填充,线性卷积尾部就会折返到块前部,形成时域混叠。对于长序列分块处理,这个问题会在每个块边界重复出现。

本文围绕一个可复现的 Python 仿真,把这个过程分成 4 个问题讲清楚:

  1. 为什么 FFT 实现快速卷积时会出现混叠现象,原理是什么?
  2. 结合仿真说明混叠现象是怎么混叠的?
  3. 结合仿真说明 overlap-add 方法是怎么解决这个问题的?
  4. 简要解释一下 OLA 和 WOLA 的区别。

下一期继续展开讲它的姊妹方法 overlap-save。本文完整的仿真代码放在个人网站VoxWorking上了,大家有兴趣免费自取即可。资源库 · VoxWorking

1. 为什么 FFT 实现快速卷积时会出现混叠现象,原理是什么?

我们熟悉的卷积定理是:

y[n] = x[n] * h[n]
<=>
Y[k] = X[k] H[k]

但这个公式在数值实现时有一个非常容易忽略的前提:

线性卷积和循环卷积什么时候才会一致?条件是 FFT 点数足够大,能够完整装下这一块卷积的全部结果。设:

那么至少要满足:

N >= L + M - 1

只有在这个条件成立时,频域乘法对应的 IFFT 结果才等价于这一块的线性卷积。否则就会发生下面这件事:

这就是本文所说的混叠。需要特别强调一下,这里讨论的不是采样率不足造成的频谱混叠,而是循环卷积导致的时域混叠(time aliasing)

下面这张图可以先建立整体直觉:

图中最关键的一句话就是:

当 N 不足以容纳 L + M - 1 个卷积结果时,末尾样本会折回到前部。

这也是为什么很多“按块 FFT 再 IFFT”的实现,看起来流程没问题,但边界总会出错。

2. 结合仿真说明混叠现象是怎么混叠的,末尾的数据是怎么折到前面污染数据的

为了让现象足够直观,本文配套脚本构造了一个测试信号:它同时包含低频分量、高频分量和两个瞬态脉冲,再通过一个短 FIR 滤波器。

仿真参数如下:

这里为什么 N = 64 会出问题?因为这一块线性卷积的真实长度应该是:

L + M - 1 = 64 + 13 - 1 = 76

但现在 IFFT 只能输出 64 点,所以线性卷积的最后 12 点必然会回卷到前面,形成污染。

单块卷积的对比如下:

这张图可以分成两层来理解。

第一层,上半图:

第二层,下半图:

如果用公式写,循环卷积和线性卷积的关系可以写成:

y_circ[n] = Σ y_lin[n + rN]

它的含义很直接:

从工程角度看,这也是为什么简单写成下面这种流程会错:

  1. 把长序列按 L 点分块
  2. 每块做 FFT
  3. 与滤波器频响相乘
  4. IFFT 回时域
  5. 把每块结果直接拼起来

问题在于,这里每块拿到的并不是已经处理好的线性卷积输出,而是一个带循环卷积边界效应的结果。如果你不额外处理重叠区,那么误差就会在每个块边界周期性重复。

全局结果对比如下:

从图中可以看到:

本文配套脚本输出的误差指标如下:

block_len=64
filter_len=13
nfft_bad=64
nfft_good=128
condition_min_nfft=76
naive_max_abs_error=1.3024473136
naive_rmse=0.2235986890
ola_max_abs_error=0.0000000000
ola_rmse=0.0000000000

对应误差分布图如下:

从结果上看,错误方法的最大绝对误差已经超过 1.3,说明块边界处的失真非常明显;而正确处理后的 OLA 结果则与时域直接卷积完全一致。

3. 结合仿真说明 overlap-add 方法是怎么解决这个问题的?

Overlap-Add 的核心思想并不复杂,关键是它把“错误折返”改成了“正确重叠相加”。

具体流程如下:

  1. 把长输入序列分成若干块,每块长度为 L
  2. 对每一块做零填充,使 FFT 点数满足 N >= L + M - 1
  3. 在频域乘法后 IFFT,得到这一块完整的线性卷积结果
  4. 由于每块卷积长度变成 L + M - 1,相邻块输出天然会有重叠区
  5. 对这些重叠区做加法,恢复整体输出

这个方法为什么有效?因为它同时解决了两个问题:

换句话说:

这两者最大的区别不在于“有没有用 FFT”,而在于有没有把循环卷积重新组织成线性卷积

从仿真图上看,第三行的 overlap-add 结果已经和时域直接卷积重合,这说明:

本文配套代码的核心函数如下:

fft_overlap_add_demo.py

错误示范是“每块做循环卷积后直接拼接”:

def naive_block_fft_filter(x, h, block_len, nfft):
    outputs = []
    h_fft = np.fft.fft(h, nfft)
    for _, block in block_view(x, block_len):
        padded = np.zeros(nfft)
        padded[: len(block)] = block
        y_block = np.fft.ifft(np.fft.fft(padded) * h_fft).real
        outputs.append(y_block[: len(block)])
    return np.concatenate(outputs)

正确做法是:每块零填充后,保留完整有效卷积长度,再做 overlap-add:

def overlap_add_filter(x, h, block_len, nfft):
    h_fft = np.fft.fft(h, nfft)
    out = np.zeros(len(x) + len(h) - 1)
    for start, block in block_view(x, block_len):
        padded = np.zeros(nfft)
        padded[: len(block)] = block
        y_block = np.fft.ifft(np.fft.fft(padded) * h_fft).real
        valid = len(block) + len(h) - 1
        out[start : start + valid] += y_block[:valid]
    return out

对于工程实现,可以记住一个最重要的经验:

做块 FFT 卷积时,别只关心频域乘法,更要关心边界怎么还原成线性卷积。

FFT 提供的是计算效率,OLA 提供的是边界正确性。两者缺一不可。

4. 简要解释一下 OLA 和 WOLA 的区别

虽然名字很像,但 OLA 和 WOLA 关注的问题并不完全一样。

4.1 OLA 是什么

OLA(Overlap-Add)主要用于快速卷积 / 分块 FIR 实现。它的出发点是:先把长信号分块,每块在频域里完成卷积,再把块输出的重叠部分相加,它主要解决的问题是:如何把每块的循环卷积结果重构成正确的线性卷积,如何在保证计算效率的同时,正确处理块边界,所以 OLA 更偏向“卷积实现方法”。

4.2 WOLA 是什么

WOLA(Weighted Overlap-Add)可以理解为“带窗函数的 Overlap-Add”。它更常见于:STFT/ISTFT、子带处理、语音增强、频谱修改、音频编解码。它的典型流程是:对每帧先乘分析窗,做频域处理,IFFT 后再乘合成窗,最后通过带权重的 overlap-add 重构时域信号。

WOLA 主要关注的是:分帧分析与重构时的平滑拼接,降低频谱泄漏和块边界不连续,在时频域处理之后实现低失真重建,所以 WOLA 更偏向“时频分析重构框架”。

4.3 二者的核心区别

它们都包含“overlap-add”这个动作,但应用场景并不一样,针对的问题也不一样。

结论

本文总结:

  1. FFT 频域乘法再 IFFT,天然得到的是循环卷积,而不是线性卷积。
  2. N < L + M - 1 时,线性卷积尾部会按模 N 折返到前面,形成时域混叠。
  3. Overlap-Add 通过“零填充 + 正确叠加重叠区”,把频域块处理重新组织成正确的线性卷积。
  4. OLA 更偏快速卷积实现,WOLA 更偏时频分析重构,它们名字相近,但关注点不同。

附:运行方式

在当前目录执行:

python fft_overlap_add_demo.py

脚本会生成: