4. 短时傅里叶变换(STFT):为什么需要STFT?窗函数的选择,时频分辨率的权衡
好,咱们进入第四章。说实话,傅里叶变换(FFT)虽然强大,但它有个“死穴”——它假设信号是平稳的。你想想看,一段语音里,有清音、浊音、爆破音,频谱特征随时间剧烈变化。FFT 一出手,把整个时间段的频率成分全混在一起,你根本不知道哪个频率在什么时候出现。
这就好比你把一整部电影的所有帧叠加成一张照片——你还能看出剧情吗?显然不能。所以,我们需要一种能同时看到“时间”和“频率”的工具。这就是短时傅里叶变换(STFT)登场的理由。
4.1 为什么需要STFT?——从“全局”到“局部”
STFT 的思路其实很朴素:既然信号是非平稳的,那我就假设它在很短的一段时间内是平稳的。然后,我在这段“短时”信号上做 FFT,再把窗口滑过去,重复这个过程。
用公式表达就是:
X(m, ω) = Σ x[n] · w[n - m] · e^(-jωn)
这里,w[n] 就是窗函数,m 是窗口的位移。说白了,STFT 就是“加窗后的 FFT”。
核心思想: 用时间换频率,用局部换全局。STFT 输出的不是一条频谱线,而是一个二维的“时频谱图”(Spectrogram)。横轴是时间,纵轴是频率,颜色深浅代表能量大小。
我在做语音端点检测(VAD)项目时,就吃过 FFT 的亏。直接用整段音频的 FFT 做特征,结果静音段和语音段的频谱混在一起,阈值根本没法设。换成 STFT 后,每一帧的频谱清晰可辨,VAD 准确率直接提升了 15%。
4.2 窗函数的选择——汉明窗 vs 汉宁窗
窗函数是 STFT 的灵魂。你可能会问:“为什么非要加窗?直接截一段不行吗?”
嗯,直接截断相当于加了一个矩形窗。矩形窗在频域有严重的“频谱泄漏”——主瓣窄但旁瓣高,能量会泄漏到不该有的频率上。这在实际应用中非常致命。
我常用的窗函数有两个:汉明窗(Hamming)和汉宁窗(Hanning)。它们长得像,但性格不同。
| 特性 | 汉明窗(Hamming) | 汉宁窗(Hanning) |
|---|---|---|
| 时域表达式 | 0.54 - 0.46·cos(2πn/N) | 0.5 - 0.5·cos(2πn/N) |
| 主瓣宽度 | 较窄(约 8π/N) | 稍宽(约 8π/N) |
| 旁瓣峰值 | -43 dB | -32 dB |
| 旁瓣衰减速率 | 较慢(6 dB/oct) | 较快(18 dB/oct) |
| 适用场景 | 语音分析、窄带信号 | 噪声估计、宽带信号 |
简单说:
- 汉明窗:旁瓣更低,适合提取语音的共振峰(Formant)。我个人的习惯是,做语音识别前端特征时,首选汉明窗。
- 汉宁窗:旁瓣衰减快,适合做噪声估计或宽带信号分析。如果你在做谱减法降噪,汉宁窗往往效果更好。
我的经验: 别纠结“哪个更好”。先试汉明窗,如果频谱泄漏严重,再换汉宁窗。我曾经在一个音乐节拍检测项目里,试了 5 种窗函数,最后发现凯泽窗(Kaiser)效果最好——但那是另一个故事了。
4.3 时频分辨率的权衡——海森堡测不准原理的“工程版”
这是 STFT 最让人头疼的地方。你没法同时获得高时间分辨率和高频率分辨率。这就像拍照——你不可能既看清远处的山,又看清近处的花。
具体来说:
- 窗长越长:频率分辨率越高(能区分两个相近的频率),但时间分辨率越低(你不知道这个频率具体在哪个时刻变化)。
- 窗长越短:时间分辨率越高(能捕捉到瞬态变化),但频率分辨率越低(频谱变得模糊)。
用数学语言说:
Δf · Δt ≥ 1/(4π)
Δf 是频率分辨率,Δt 是时间分辨率。它们的乘积有下限。
避坑指南: 我曾经在一个语音合成项目里,为了追求“清晰的频谱”,把窗长设到了 1024 点(采样率 16kHz)。结果频谱是漂亮了,但辅音(如 /t/、/k/)的爆破瞬间完全被抹平了,合成出来的声音像“含着一口水在说话”。后来我把窗长降到 512 点,辅音清晰度才恢复正常。
所以,窗长怎么选?我一般遵循这个原则:
- 先看应用场景:语音识别通常用 20-30ms 的窗长(对应 320-480 点 @16kHz)。音乐分析可能需要 40-60ms。
- 再考虑帧移:帧移通常是窗长的 1/4 到 1/2。帧移越小,时频谱越平滑,但计算量越大。
- 最后做实验:没有万能参数。我建议你画几个不同窗长的语谱图,肉眼对比一下。
4.4 知识体系图:STFT 的核心逻辑
下面这张图,帮你把 STFT 的整个流程串起来:
4.5 实践建议:如何快速上手 STFT
如果你用的是 Python,我建议直接用 librosa.stft 或 scipy.signal.stft。下面是一个最小示例:
import numpy as np
import librosa
# 加载音频
y, sr = librosa.load('speech.wav', sr=16000)
# STFT 参数
n_fft = 512 # 窗长(点数)
hop_length = 128 # 帧移(点数)
window = 'hamming'
# 计算 STFT
D = librosa.stft(y, n_fft=n_fft, hop_length=hop_length, window=window)
# 转换为 dB 刻度
S_db = librosa.amplitude_to_db(np.abs(D), ref=np.max)
print(f"时频谱形状: {S_db.shape}") # (频率点数, 时间帧数)
小技巧: 如果你发现语谱图“糊成一片”,试试减小 n_fft。如果你发现语谱图“锯齿感”太强,试试减小 hop_length。这两个参数是你调优 STFT 的“旋钮”。
好了,关于 STFT 的核心原理、窗函数选择、时频分辨率权衡,就讲到这里。记住一句话:没有完美的参数,只有适合你任务的参数。多动手试,多对比语谱图,你很快就能找到感觉。