4. 频域分析基础

聊到声学特征提取,STFT 绝对是个绕不开的核心工具。说白了,它就是给信号加了个「滑动窗口」,把一段长长的时域波形切成一小段一小段,然后对每一段做傅里叶变换。这样我们就能同时看到:什么时间点,有哪些频率成分。

我个人习惯把 STFT 比作「给声音拍连续快照」。你想想看,一段语音信号是非平稳的,直接对整个信号做 FFT,只能知道整体有哪些频率,却不知道这些频率在时间轴上是如何变化的。STFT 解决了这个问题。

4.1 STFT 的数学原理

STFT 的定义其实很直观:

X(m, ω) = Σ x[n] · w[n - m] · e^(-jωn)

其中:

  • x[n] — 原始时域信号
  • w[n] — 窗函数(汉明窗、汉宁窗等)
  • m — 帧索引(时间位置)
  • ω — 频率变量

嗯,这里要注意:窗函数的选择直接影响频谱质量。我在项目中遇到过用矩形窗做 STFT,结果频谱泄露严重,高频细节完全被旁瓣淹没了。后来换成汉明窗,效果立竿见影。

核心要点:STFT 的本质是「加窗 + FFT」的循环操作。每次移动一个步长(hop size),重复这个过程,最终得到一个二维的时频矩阵。

4.2 频谱图(Spectrogram)的生成

频谱图就是把 STFT 的结果可视化出来。横轴是时间,纵轴是频率,颜色深浅代表能量强度。我经常跟团队说:频谱图就是声音的「指纹」。

生成步骤其实就三步:

  1. 分帧:把信号切成重叠的帧(典型帧长 25ms,帧移 10ms)
  2. 加窗:每帧乘以窗函数(减少频谱泄露)
  3. FFT:对每帧做 N 点 FFT,取幅度平方或对数幅度

来看一段 Python 实现:

import numpy as np
import matplotlib.pyplot as plt

def stft(x, fs, frame_len=0.025, hop_len=0.010):
    """
    x: 输入信号
    fs: 采样率
    frame_len: 帧长(秒)
    hop_len: 帧移(秒)
    """
    frame_size = int(frame_len * fs)
    hop_size = int(hop_len * fs)
    
    # 汉明窗
    window = np.hamming(frame_size)
    
    # 计算帧数
    num_frames = 1 + (len(x) - frame_size) // hop_size
    
    # 初始化 STFT 矩阵
    stft_matrix = np.zeros((frame_size // 2 + 1, num_frames), dtype=np.complex64)
    
    for i in range(num_frames):
        start = i * hop_size
        end = start + frame_size
        frame = x[start:end] * window
        spectrum = np.fft.rfft(frame)
        stft_matrix[:, i] = spectrum
    
    return stft_matrix

# 使用示例
fs = 16000
t = np.linspace(0, 2, 2*fs)
x = np.sin(2*np.pi*440*t) + 0.5*np.sin(2*np.pi*880*t)

spec = stft(x, fs)
# 转成 dB 刻度
spec_db = 20 * np.log10(np.abs(spec) + 1e-6)

plt.figure(figsize=(10, 4))
plt.imshow(spec_db, aspect='auto', origin='lower',
           extent=[0, len(x)/fs, 0, fs/2])
plt.colorbar(label='Magnitude (dB)')
plt.xlabel('Time (s)')
plt.ylabel('Frequency (Hz)')
plt.title('Spectrogram')
plt.show()

避坑指南:我曾经在生成频谱图时忘记加 origin='lower',结果频率轴是倒过来的,低频在上高频在下,排查了半天才发现。另外,extent 参数一定要设置正确,否则坐标轴刻度对不上真实物理值。

4.3 时间分辨率与频率分辨率的权衡

这是 STFT 里最经典的一个矛盾。我直接说结论:你不可能同时获得高时间分辨率和高频率分辨率。

为什么会这样?因为:

  • 窗长越长 → 频率分辨率越高(能区分更近的频率)→ 但时间分辨率变差(不知道频率变化的具体时刻)
  • 窗长越短 → 时间分辨率越高(能捕捉瞬态变化)→ 但频率分辨率变差(频率模糊)

来看一个具体的对比表格:

窗长(采样点) 频率分辨率(Hz) 时间分辨率(ms) 适用场景
256(16ms @16kHz) 62.5 16 瞬态检测、打击乐器
512(32ms @16kHz) 31.25 32 语音识别(通用)
1024(64ms @16kHz) 15.625 64 音乐分析、谐波结构
2048(128ms @16kHz) 7.8125 128 慢变信号、基频提取

我在做语音端点检测(VAD)时踩过这个坑。一开始用了 1024 点窗长,频率分辨率是够了,但语音的起始点检测总是慢半拍。后来改成 512 点,时间分辨率提升了一倍,检测延迟明显降低。

重要提醒:不要盲目追求高频率分辨率。对于语音信号,25ms 帧长(400点 @16kHz) 是经过大量实验验证的黄金标准。它能在时间分辨率和频率分辨率之间取得较好的平衡。

4.4 核心知识体系

下面这张图总结了 STFT 的核心逻辑和关键参数:

STFT 核心知识体系 短时傅里叶变换 (STFT) 数学原理 工程实现 时频权衡 加窗操作 FFT 变换 帧移步长 频谱拼接 分帧加窗 复数频谱 幅度/相位 dB 刻度 窗长选择 帧移设置 应用场景 参数调优 核心权衡法则 窗长 ↑ → 频率分辨率 ↑ → 时间分辨率 ↓(反之亦然) 常用参数建议(语音信号 @16kHz) 帧长:400 点(25ms) | 帧移:160 点(10ms) | 窗函数:汉明窗 FFT 点数:512 或 1024(补零到 2 的幂次) 输出:257 个频率 bin(512 点 FFT)或 513 个(1024 点 FFT)

4.5 实战中的参数选择建议

根据我多年的工程经验,这里给出几条实用的参数选择建议:

  • 语音识别:帧长 25ms,帧移 10ms,汉明窗,512 点 FFT。这是最经典的配置,几乎适用于所有 ASR 系统。
  • 音乐分析:帧长 50-100ms,帧移 25-50ms,汉宁窗,1024-2048 点 FFT。音乐信号需要更高的频率分辨率来区分谐波。
  • 瞬态检测:帧长 5-10ms,帧移 2-5ms,矩形窗或凯泽窗,128-256 点 FFT。短窗长能捕捉到冲击信号的起始点。
  • 噪声估计:帧长 20-40ms,帧移 10-20ms,汉明窗,512 点 FFT。需要平衡时间稳定性和频率细节。

我的经验:如果你不确定选什么参数,先从 25ms 帧长 + 10ms 帧移 + 汉明窗开始。这个组合在 90% 的语音场景下都能工作得很好。等跑通流程后,再根据具体任务微调参数。

最后说一句:STFT 是声学特征提取的基石。你后面学到的 MFCC、滤波器组、甚至一些深度学习前端特征,本质上都是在 STFT 频谱上做进一步处理。把这个基础打牢,后面的路会顺畅很多。