4. 短时傅里叶变换(STFT):从原理到频谱图实战
聊到声学特征提取,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 的结果可视化出来。横轴是时间,纵轴是频率,颜色深浅代表能量强度。我经常跟团队说:频谱图就是声音的「指纹」。
生成步骤其实就三步:
- 分帧:把信号切成重叠的帧(典型帧长 25ms,帧移 10ms)
- 加窗:每帧乘以窗函数(减少频谱泄露)
- 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 的核心逻辑和关键参数:
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 频谱上做进一步处理。把这个基础打牢,后面的路会顺畅很多。