4. 短时傅里叶变换(STFT):原理与实现、窗函数选择、时频分辨率权衡

聊到语音杂音处理,咱们绕不开一个核心工具——短时傅里叶变换,也就是 STFT。说实话,我见过不少新手工程师,一上来就调模型、上深度学习,结果连 STFT 的频谱图都看不懂。嗯,这其实是个基本功。你想想看,语音信号是非平稳的,它的频率成分随时间变化。直接对整个信号做 FFT,你只能知道“有哪些频率”,却不知道“这些频率什么时候出现”。这就像看一部电影,你只记住了总时长,却不知道剧情在哪个时间点发生了转折。

STFT 的思路很简单:把信号切成一小段一小段,假设每一段是平稳的,然后对每一段做 FFT。这样,你就能看到频率随时间的变化了。我个人习惯把 STFT 看作是“给信号拍快照”——每隔一小段时间拍一张频率照片,然后把这些照片拼起来,就是一张时频谱图。

4.1 STFT 的数学原理与实现

STFT 的数学表达式其实不复杂:

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

这里,x[n] 是原始信号,w[n] 是窗函数,m 是时间索引,ω 是频率。说白了,就是先把窗函数滑动到位置 m,截取一段信号,再做 FFT。

我在项目中遇到过一个问题:直接用 Python 的 librosa.stft 虽然方便,但有时候你需要自己实现,才能理解参数的含义。比如帧长、帧移、FFT 点数,这三个参数决定了你的时频谱长什么样。

核心参数速查表

参数 含义 典型值 影响
帧长(win_length) 每次 FFT 的信号长度 512 / 1024 / 2048 越长,频率分辨率越高;越短,时间分辨率越高
帧移(hop_length) 相邻两帧的起始点间隔 帧长的 1/4 或 1/2 越小,时频谱越平滑;越大,计算量越小
FFT 点数(n_fft) FFT 计算的频率点数 ≥ 帧长,通常取 2 的幂 越大,频率轴越精细,但计算量增加

下面是一个简单的 STFT 实现示例,我习惯用 numpy 手写,方便调试:

import numpy as np

def stft(x, win_length=1024, hop_length=256, n_fft=1024):
    # 生成汉宁窗
    window = np.hanning(win_length)
    # 计算帧数
    num_frames = 1 + (len(x) - win_length) // hop_length
    # 初始化频谱矩阵
    stft_matrix = np.zeros((n_fft // 2 + 1, num_frames), dtype=complex)
    
    for i in range(num_frames):
        start = i * hop_length
        end = start + win_length
        # 加窗
        frame = x[start:end] * window
        # FFT
        spectrum = np.fft.rfft(frame, n=n_fft)
        stft_matrix[:, i] = spectrum
    
    return stft_matrix

我的小技巧: 实际项目中,我通常用 librosa.stft 做快速原型,但一旦要部署到嵌入式设备,我会用 C 语言重写这个逻辑。记得有一次,我在一个低功耗芯片上跑语音增强,发现 librosa 的默认参数导致内存溢出——帧长设成了 2048,但芯片的 RAM 只有 64KB。后来我把帧长降到 512,问题就解决了。

4.2 窗函数的选择:汉宁窗 vs 海明窗

窗函数是 STFT 里容易被忽略的细节。很多人觉得随便选一个就行,其实不然。窗函数的作用是减少频谱泄漏——也就是 FFT 时,能量从一个频率“漏”到另一个频率的现象。

我刚开始做语音处理时,用过矩形窗,结果频谱图上一片模糊,根本看不清谐波结构。后来才明白,矩形窗的旁瓣太高,泄漏太严重。常用的窗函数有汉宁窗(Hanning)和海明窗(Hamming),它们长得像,但性格不同。

窗函数 公式 主瓣宽度 旁瓣衰减 适用场景
汉宁窗 0.5 - 0.5·cos(2πn/N) 较宽 -31 dB 语音分析、噪声估计
海明窗 0.54 - 0.46·cos(2πn/N) 稍窄 -43 dB 语音编码、窄带信号
矩形窗 1 最窄 -13 dB 瞬态检测(不推荐用于语音)

汉宁窗和海明窗的区别在哪?说白了,海明窗在两端没有完全归零,所以它的旁瓣衰减更大,但主瓣稍微窄一点。我个人习惯在语音增强任务中用汉宁窗,因为它能更好地抑制频谱泄漏,尤其是在做噪声估计时。而海明窗,我一般在语音编码或特征提取时用,比如 MFCC 的前端处理。

避坑指南: 我曾经在一个项目里,用海明窗做语音分离,结果发现分离出来的语音有“音乐噪声”——就是那种吱吱呀呀的伪影。后来排查了半天,发现是窗函数导致的频谱不连续。换回汉宁窗后,问题就消失了。所以,如果你在做语音增强或分离,优先考虑汉宁窗。

4.3 时频分辨率的权衡

这是 STFT 里最核心的 trade-off。你想想看,帧长越长,频率分辨率越高——你能区分出两个相近的频率。但代价是时间分辨率变差——你不知道某个频率是在帧的哪个位置出现的。反过来,帧长越短,时间分辨率越好,但频率分辨率就差了。

这就像拍照:长曝光能拍出清晰的夜景,但运动物体会模糊;短曝光能捕捉瞬间动作,但画面可能太暗。STFT 也是一样,你没法同时拥有高时间分辨率和高频率分辨率。这就是海森堡测不准原理在信号处理中的体现。

我画了一张图,帮你理解这个权衡:

STFT 时频分辨率权衡示意图 长帧长(如 2048) 频率分辨率高 时间分辨率低 适合:稳态噪声分析 谐波结构提取 短帧长(如 256) 时间分辨率高 频率分辨率低 适合:瞬态噪声检测 语音端点检测 权衡法则:帧长翻倍,频率分辨率翻倍,时间分辨率减半 实际项目中,常用帧长 512~1024,帧移 128~256 语音增强 帧长 1024,帧移 256 语音识别 帧长 512,帧移 160 噪声检测 帧长 256,帧移 64

在实际项目中,我一般这样选参数:

  • 语音增强/降噪: 帧长 1024,帧移 256。这个配置在频率分辨率和时间分辨率之间比较平衡,能较好地保留语音的谐波结构,同时抑制噪声。
  • 语音识别前端: 帧长 512,帧移 160。因为识别模型更关注时间上的变化,短帧能捕捉到发音的快速过渡。
  • 瞬态噪声检测(比如敲击声、门声): 帧长 256,帧移 64。这时候时间分辨率最重要,你需要知道噪声发生的精确时刻。

我的经验: 如果你不确定选什么参数,先试试帧长 1024、帧移 256、汉宁窗。这个组合在 80% 的语音处理任务里都表现不错。然后根据你的具体场景微调——比如发现频谱图太模糊,就增加帧长;发现时间上的细节丢失了,就减小帧移。

嗯,STFT 的内容就这些。记住,窗函数和帧长帧移的选择,直接决定了你后续算法的上限。别小看这些“基础参数”,它们往往是项目成败的关键。