4、频域分析入门:离散傅里叶变换(DFT)原理、快速傅里叶变换(FFT)实现、频谱与功率谱密度、语谱图绘制

做语音降噪,你天天跟时域波形打交道。但说实话,光看波形很难看出门道。噪声和语音在时域里缠在一起,你根本分不清谁是谁。

这时候就得换一个视角——频域。

频域分析,说白了就是把声音拆成不同频率的成分。低频是啥样,高频是啥样,一目了然。我刚开始做语音算法时,总觉得时域才是根本,频域只是花架子。直到有一次,一个简单的低通滤波在时域折腾了两天没搞定,换到频域十分钟就解决了。嗯,从那以后我再也不敢小看傅里叶变换了。

4.1 离散傅里叶变换(DFT)原理

先说说 DFT 是干什么的。你有一段离散的音频信号,比如 16kHz 采样率下的一帧 512 个点。DFT 能把这 512 个时域点,变成 512 个频域点。每个频域点代表一个特定频率的幅度和相位。

公式长这样:

X[k] = Σ_{n=0}^{N-1} x[n] · e^{-j·2π·k·n/N}

别被公式吓到。我来拆解一下:

  • x[n]:输入的时域信号,n 是时间索引
  • X[k]:输出的频域信号,k 是频率索引
  • N:一帧的点数,比如 512 或 1024
  • e^{-j·2π·k·n/N}:这就是个旋转因子,本质上是不同频率的正弦波和余弦波

你想想看,DFT 其实就是把信号跟一堆不同频率的“模板”做内积。匹配上了,那个频率的系数就大;匹配不上,系数就小。我在项目中遇到过一个问题:采样率 8kHz,帧长 256 点,结果低频部分总是分析不准。后来发现是频率分辨率不够——频率分辨率 = 采样率 / 帧长,8kHz/256 = 31.25Hz。也就是说,两个频率相差不到 31.25Hz 就分不开了。这个坑,我踩过。

频率分辨率公式:

Δf = Fs / N

其中 Fs 是采样率,N 是 DFT 点数。帧长越长,频率分辨率越高,但时间分辨率会下降。这是个 trade-off。

4.2 快速傅里叶变换(FFT)实现

DFT 原理简单,但计算量太大。直接算 DFT,复杂度是 O(N²)。N=1024 时,要算一百多万次乘法。这在嵌入式设备上根本跑不动。

FFT 就是 DFT 的快速版本。它利用旋转因子的对称性和周期性,把复杂度降到 O(N·log₂N)。N=1024 时,只需要约一万次乘法。快了上百倍。

最常用的是基-2 时间抽取 FFT。它的核心思想是“分而治之”:

  1. 把 N 点序列按奇偶分成两个 N/2 点子序列
  2. 分别对两个子序列做 DFT
  3. 用蝶形运算合并结果

蝶形运算长这样:

// 基-2 蝶形运算核心
// A 和 B 是两个输入,W 是旋转因子
// 输出:
//   A' = A + W * B
//   B' = A - W * B

我个人习惯用 C 语言手写 FFT,而不是直接调库。为什么?因为部署到嵌入式平台时,库函数不一定有,或者有但性能不行。手写 FFT 虽然麻烦,但你能完全控制内存和计算流程。

我曾经在一个 ARM Cortex-M4 芯片上部署降噪算法,芯片没有 FPU,只能用定点数。库里的 FFT 全是浮点,根本没法用。最后我手写了一个 16 位定点 FFT,才把问题解决。

我的 FFT 实现建议:

  • 先确认平台是否支持浮点运算。不支持就用 Q15 或 Q31 定点格式
  • 旋转因子提前算好存表,不要每次实时计算
  • 注意位反转排序——这是 FFT 的固有步骤,容易出错
  • 帧长选 2 的幂次,比如 256、512、1024。非 2 的幂次会大幅增加复杂度

4.3 频谱与功率谱密度

做完 FFT,你得到的是复数 X[k]。这个复数包含幅度和相位信息。但在语音降噪里,我们更关心幅度,相位通常不太敏感。

幅度谱:|X[k]| = sqrt(Re² + Im²)

功率谱:P[k] = |X[k]|² / N

功率谱密度(PSD):PSD[k] = |X[k]|² / (N · Fs)

三者的区别我列个表:

名称 公式 物理意义 常用场景
幅度谱 |X[k]| 各频率分量的幅度 可视化、特征提取
功率谱 |X[k]|² / N 各频率分量的能量 噪声估计、信噪比计算
功率谱密度 |X[k]|² / (N·Fs) 单位频率上的功率 不同采样率系统对比

实际部署时,我几乎只用功率谱。因为降噪算法里,噪声估计和增益计算都基于能量,功率谱直接反映能量分布。幅度谱反而用得少。

嗯,这里要注意:功率谱是实数,不是复数。所以 FFT 之后,你只需要取模平方,不需要保留相位。这能省一半内存。

我曾经踩过的坑:

做噪声估计时,直接用单帧的功率谱做平滑,结果噪声估计总是滞后。后来发现,应该用一阶递归平滑:P_est = α · P_est + (1-α) · P_current。α 取 0.9~0.99,既能跟踪变化,又能抑制抖动。

4.4 语谱图绘制

语谱图,就是把多帧的频谱堆在一起,用颜色表示能量大小。横轴是时间,纵轴是频率,颜色深浅代表能量高低。

绘制语谱图的步骤:

  1. 把音频分帧,每帧加窗(汉宁窗或汉明窗)
  2. 对每帧做 FFT,得到频谱
  3. 取功率谱或幅度谱,转成 dB 单位:20·log10(|X[k]|)
  4. 把多帧结果按时间排列,形成二维矩阵
  5. 用颜色映射显示

为什么加窗?因为分帧相当于在时域上截断,会产生频谱泄漏。加窗能抑制泄漏。我习惯用汉宁窗,旁瓣衰减够快,主瓣宽度也能接受。

语谱图在语音降噪里有什么用?我举两个例子:

  • 噪声类型识别:平稳噪声在语谱图上是均匀的横条纹,非平稳噪声是杂乱的斑点。看一眼语谱图,就知道该用哪种降噪策略。
  • 算法效果验证:降噪前后的语谱图一对比,语音部分是否保留完整,噪声是否被抑制,一目了然。比听音更直观。

我曾经调试一个降噪算法,听音觉得还行,但语谱图一看,高频语音被削掉了一大块。这就是所谓的“音乐噪声”问题。没有语谱图,我可能一直发现不了。

语谱图参数建议:

  • 帧长:20~30ms(16kHz 采样率对应 320~480 点)
  • 帧移:帧长的一半,保证重叠
  • 窗函数:汉宁窗或汉明窗
  • 颜色映射:灰度或热力图,dB 范围 -80~0
频域分析知识体系 频域分析 DFT 原理 FFT 实现 频谱与 PSD 语谱图绘制 时域→频域 频率分辨率 蝶形运算 位反转 幅度/功率 dB 转换 分帧加窗 颜色映射 核心逻辑:时域信号 → 分帧 → 加窗 → FFT → 频谱/PSD → 语谱图 部署时重点关注:帧长选择、FFT 定点化、内存管理

这张图把本章的知识体系串起来了。从时域信号出发,经过分帧加窗,做 FFT 得到频谱,再计算功率谱或 PSD,最后绘制语谱图。每一步都有坑,每一步都有技巧。

我个人觉得,频域分析是语音降噪的基石。你把这个基础打牢了,后面的维纳滤波、谱减法、甚至深度学习降噪,理解起来都会轻松很多。