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 点,辅音清晰度才恢复正常。

所以,窗长怎么选?我一般遵循这个原则:

  1. 先看应用场景:语音识别通常用 20-30ms 的窗长(对应 320-480 点 @16kHz)。音乐分析可能需要 40-60ms。
  2. 再考虑帧移:帧移通常是窗长的 1/4 到 1/2。帧移越小,时频谱越平滑,但计算量越大。
  3. 最后做实验:没有万能参数。我建议你画几个不同窗长的语谱图,肉眼对比一下。

4.4 知识体系图:STFT 的核心逻辑

下面这张图,帮你把 STFT 的整个流程串起来:

STFT 核心逻辑与参数权衡 输入信号 x[n] 分帧(帧长 + 帧移) 加窗(窗函数选择) FFT 变换 时频谱图(Spectrogram) 窗长 ↑ → 频率分辨率 ↑,时间分辨率 ↓ 窗长 ↓ → 时间分辨率 ↑,频率分辨率 ↓ 帧移 ↓ → 时频谱更平滑,计算量 ↑ 帧移 ↑ → 时频谱更粗糙,计算量 ↓

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 的核心原理、窗函数选择、时频分辨率权衡,就讲到这里。记住一句话:没有完美的参数,只有适合你任务的参数。多动手试,多对比语谱图,你很快就能找到感觉。