3、频域特征提取:傅里叶变换、短时傅里叶变换、频谱图

说实话,刚入行那会儿,我总觉得时域信号看着挺直观的——波形上下跳动,振幅清清楚楚。但做音频处理久了你会发现,耳朵其实是个频域分析仪。我们听一首歌,分辨的是音高、音色、和弦,这些全是频率信息。时域波形?那只是空气振动的原始记录。

所以这一章,咱们就聊聊怎么把时域信号搬到频域去看。核心工具就是傅里叶变换,以及它的实战变体——短时傅里叶变换。

3.1 傅里叶变换:从时域到频域的桥梁

傅里叶变换的数学公式长这样:

X(f) = ∫ x(t) · e^(-j2πft) dt

别被积分吓到。说白了,它就是在干一件事:把一段信号,拆解成不同频率的正弦波之和。每个频率分量有多强,就是频谱上的幅度值。

我个人的理解方式更简单——想象你在听交响乐。傅里叶变换就是帮你从混合的乐器声中,单独拎出小提琴、大提琴、长笛各自在拉什么音。每个乐器的音量,就是频谱上的峰值。

核心要点:

  • 连续傅里叶变换(CFT):理论工具,实际用不了(信号是离散的)
  • 离散傅里叶变换(DFT):计算机能算的版本,复杂度 O(N²)
  • 快速傅里叶变换(FFT):DFT 的快速算法,复杂度 O(N log N),实际工程全用它

嗯,这里要注意:FFT 不是一种新的变换,只是 DFT 的加速实现。我在项目中见过有人把 FFT 和 DFT 当成两回事,结果代码里自己手写 DFT,处理一段 10 秒的音频等了半分钟……用 FFT 的话,毫秒级就出结果了。

3.2 短时傅里叶变换:给频谱加上时间轴

傅里叶变换有个硬伤——它假设信号是平稳的。但音频信号呢?说话、唱歌、音乐,全是随时间变化的。一段语音里,前半句是元音,后半句是辅音,频率成分完全不同。你用一整段的傅里叶变换,只能得到一个「平均频谱」,啥细节都丢了。

怎么办?加窗分段。这就是短时傅里叶变换(STFT)的思路:

  1. 把信号切成很多小段(帧),每帧 20-40 毫秒
  2. 对每一帧加窗(防止截断效应)
  3. 对每一帧做 FFT
  4. 把结果按时间排列起来

你想想看,这样我们就得到了一个时间-频率-幅度的三维表示。这就是频谱图(Spectrogram)的基础。

帧参数经验值:

参数 典型值 说明
帧长(window size) 1024 点(≈23ms @44.1kHz) 太短频率分辨率差,太长时间分辨率差
帧移(hop length) 512 点(50% 重叠) 常用 25%-75%,50% 是折中
FFT 点数(n_fft) 1024 或 2048 一般等于或大于帧长,不足补零
窗函数 汉宁窗(Hann) 减少频谱泄漏,比矩形窗好很多

我曾经踩过一个坑:做语音端点检测时,用了 512 点的帧长,结果辅音段(比如 /s/、/ʃ/ 这种摩擦音)的时间分辨率不够,短促的爆破音直接被抹平了。后来改成 256 点帧长 + 75% 重叠,才把那些瞬态特征抓出来。所以参数不是死的,得看你的任务需求。

3.3 频谱图:音频工程师的「X 光片」

频谱图就是把 STFT 的结果用图像展示出来:

  • 横轴:时间
  • 纵轴:频率
  • 颜色/亮度:幅度(能量)

我习惯把频谱图叫做音频的「X 光片」。你看一段语音的频谱图,能直接看出:

  • 基频在哪里(最低的那条亮线)
  • 谐波结构(基频的整数倍位置)
  • 共振峰(能量集中的频带)
  • 噪声段(均匀分布的模糊区域)

下面这张图展示了从原始音频到频谱图的完整流程:

STFT 频谱图生成流程 原始音频 x(t) 分帧 + 加窗 每帧 FFT 幅度谱 |X| 帧参数说明 帧长:20-40ms(如 1024 点 @44.1kHz) 帧移:帧长的 25%-75%(如 512 点) 窗函数:汉宁窗 / 汉明窗 频谱图生成 将所有帧的幅度谱按时间排列 横轴:时间(帧索引 × 帧移) 纵轴:频率(0 ~ 采样率/2) 频谱图 Spectrogram 实际工程中常用 librosa.stft() 或 torch.stft() 一步完成

3.4 实战:用 Python 提取频谱图

理论说完了,上代码。我个人最常用的是 librosa,接口设计得很顺手:

import librosa
import numpy as np
import matplotlib.pyplot as plt

# 加载音频
y, sr = librosa.load('speech.wav', sr=16000)

# STFT 计算
D = librosa.stft(y, n_fft=1024, hop_length=512, win_length=1024, window='hann')

# 幅度谱(dB 刻度)
S_db = librosa.amplitude_to_db(np.abs(D), ref=np.max)

# 绘制频谱图
plt.figure(figsize=(10, 4))
librosa.display.specshow(S_db, sr=sr, hop_length=512, x_axis='time', y_axis='hz')
plt.colorbar(format='%+2.0f dB')
plt.title('Spectrogram')
plt.tight_layout()
plt.show()

这段代码跑完,你就能看到一张漂亮的频谱图了。但我得提醒一句:别直接用默认参数就交差。我在做音乐音高估计时,默认的 2048 点 n_fft 频率分辨率够了,但时间分辨率太差,导致快速音阶的过渡段糊成一团。后来改成 512 点 + 75% 重叠,才把每个音符的起振点看清楚。

常见坑点:

  • 频谱泄漏:不加窗直接 FFT,频谱上会出现「拖尾」伪影。解决办法:加汉宁窗或汉明窗。
  • 直流分量:0 Hz 处的能量通常很大,会压扁其他频率的显示。用 librosa.stft 默认会去掉直流,或者自己用高通滤波器预处理。
  • 频率轴范围:FFT 的结果是对称的,实际只用 0 ~ sr/2 这一段。别把另一半也画出来,那是冗余信息。
  • 幅度刻度:线性刻度下,弱信号根本看不见。转成 dB 刻度(对数)才能看清全貌。

3.5 频谱图的变体与选择

实际项目中,我们很少直接用原始频谱图。根据任务不同,有几个常用变体:

类型 计算方式 适用场景
线性频谱图 |STFT| 谐波分析、基频估计
对数频谱图 20·log₁₀(|STFT|) 语音识别、通用可视化
Mel 频谱图 线性频谱 → Mel 滤波器组 语音识别、说话人识别
功率谱密度 |STFT|² / (n_fft · sr) 噪声分析、声学测量

我个人建议:做可视化分析时用对数频谱图,因为人耳对声音的感知本身就是对数的。做特征输入时,Mel 频谱图往往是更好的起点——它模拟了人耳的频率分辨率,后续接神经网络也更容易收敛。

有一次我做环境声音分类,直接用线性频谱图训练,模型在低频段(比如引擎轰鸣)表现很好,但高频段(比如鸟叫、玻璃破碎)几乎不响应。换成 Mel 频谱图后,高频细节被放大,分类准确率直接提升了 12%。这就是特征选择的力量。

3.6 小结

这一章我们聊了:

  • 傅里叶变换的本质——把时域信号拆成频率分量
  • 短时傅里叶变换——加窗分段,给频谱加上时间轴
  • 频谱图——时间-频率-幅度的三维可视化
  • 实战代码和参数调优经验