3. 数字音频处理入门:时域与频域、傅里叶变换(FFT)概念、频谱图怎么看

好,咱们进入正题。前面两章我们把麦克风采集到的原始声音数据拿到了,就是那一串随时间变化的电压值。这一章,我们要学会怎么“看懂”这串数据。

你可能会问:“不就是一堆数字吗?有什么好看的?” 嗯,这里有个关键问题——我们人耳听到的是“声音”,而计算机看到的只是“振幅”。这两者之间,差了一个维度。

3.1 时域:我们最熟悉的陌生人

时域,说白了就是“时间-振幅”坐标系。横轴是时间,纵轴是声音的响度(振幅)。你打开 Audacity 或者任何录音软件,看到的那条波浪线,就是时域波形。

我个人习惯把时域波形叫做“原始心电图”。为什么?因为它只告诉你“什么时候声音大,什么时候声音小”,但完全不说“这是什么声音”。

时域波形的特点:

  • 直观:一眼看出声音的起止、强弱
  • 局限:无法区分不同音色(比如钢琴和吉他弹同一个音,波形看起来差不多)
  • 致命:噪声和语音混在一起时,你根本分不清谁是谁

我在项目中遇到过这样一个坑:用麦克风采集“你好”的唤醒词,时域波形看起来挺干净。结果一跑识别,准确率惨不忍睹。后来才发现,波形里混着空调的低频嗡嗡声,人眼根本看不出来。

3.2 频域:声音的“指纹”

频域就不一样了。它告诉你“这个声音里,各个频率的成分有多少”。横轴是频率(Hz),纵轴是能量(dB 或幅度)。

你想想看,为什么我们能听出是妈妈在说话,而不是爸爸?因为每个人的声带振动频率(基频)不同,再加上口腔、鼻腔的共鸣,形成了独特的“频率分布”。这个分布,就是声音的指纹。

维度 时域 频域
横轴 时间 (秒) 频率 (Hz)
纵轴 振幅 能量/幅度
告诉你什么 什么时候响 什么频率在响
适合做什么 检测声音有无、音量大小 识别音色、区分噪声

3.3 傅里叶变换:从时域到频域的“翻译官”

怎么把时域波形变成频域图?靠的就是傅里叶变换。数学公式我就不写了,你只需要记住一句话:任何复杂的波形,都可以拆成若干个不同频率、不同振幅的正弦波的叠加。

傅里叶变换就是干这个拆解活的。它把一段时域信号,变成一堆频率成分。实际工程中我们用的是 FFT(快速傅里叶变换),它比原始傅里叶变换快得多,能在嵌入式芯片上跑。

我的经验: 在 STM32 上做 FFT,采样点一般取 256、512 或 1024。为什么是 2 的幂?因为 FFT 算法就是基于 2 的幂优化的。取 1024 点,采样率 16kHz,那频率分辨率就是 16000/1024 ≈ 15.6 Hz。够用。

3.4 频谱图:唤醒词模型的“眼睛”

频谱图(Spectrogram)是时域和频域的结合体。它有三个维度:

  • 横轴:时间
  • 纵轴:频率
  • 颜色:能量大小(越亮/越红,能量越大)

唤醒词模型不看原始波形,就看频谱图。为什么?因为频谱图把“声音”变成了“图像”,而卷积神经网络(CNN)最擅长处理图像。

我曾经踩过的坑: 刚开始做唤醒词时,我直接把原始音频丢给模型训练,结果模型死活不收敛。后来才发现,时域波形对相位信息太敏感,稍微挪动几个采样点,波形就变了。而频谱图只关心频率能量分布,对微小的时移不敏感——这才是模型要的特征。

3.5 怎么看频谱图?三步法

  1. 看整体亮度:亮的地方能量大,通常是语音的元音部分(a、o、e)。暗的地方可能是辅音或静音。
  2. 看横条纹:水平亮线代表某个固定频率持续存在——这通常是噪声(比如 50Hz 工频干扰)。
  3. 看竖条纹:垂直方向的亮条代表某个瞬间的冲击声——比如拍手、关门、爆破音(p、t、k)。

唤醒词“你好”的频谱图,通常会在 200-500Hz 有持续能量(声带振动),同时在 2000-4000Hz 有较亮的区域(齿音和共鸣)。这就是模型要学习的模式。

3.6 本章知识体系

下面这张图,把时域、频域、FFT 和频谱图的关系串起来了。建议你多看几遍,理解每个模块的输入输出。

数字音频处理核心流程 时域信号 (时间-振幅) FFT (快速傅里叶变换) 频域 (频率-能量) 分帧 (20-40ms/帧) 加窗 (汉明窗/海宁窗) 频谱图 (时间-频率-能量) 关键说明 1. 原始音频先分帧(每帧20-40ms),再对每帧做FFT 2. 加窗是为了减少频谱泄露,汉明窗最常用 3. 频谱图 = 多帧FFT结果堆叠,横轴时间、纵轴频率、颜色能量 4. 唤醒词模型直接拿频谱图作为输入特征

3.7 动手试试:用 Python 看频谱图

光说不练假把式。下面这段代码,读取一个音频文件,画出它的时域波形和频谱图。你可以在自己的电脑上跑一下,看看“你好”和“噪声”的频谱图有什么区别。

import numpy as np
import matplotlib.pyplot as plt
import scipy.io.wavfile as wav

# 读取音频文件
sample_rate, data = wav.read('your_audio.wav')

# 如果立体声,取左声道
if len(data.shape) > 1:
    data = data[:, 0]

# 时域波形
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(data[:4000])
plt.title('时域波形')
plt.xlabel('采样点')
plt.ylabel('振幅')

# 频谱图
plt.subplot(1, 2, 2)
plt.specgram(data, NFFT=256, Fs=sample_rate, cmap='viridis')
plt.title('频谱图')
plt.xlabel('时间 (s)')
plt.ylabel('频率 (Hz)')
plt.colorbar(label='能量 (dB)')

plt.tight_layout()
plt.show()

小提示: 如果你手边没有音频文件,可以用 np.sin() 生成一个 440Hz 的正弦波试试。它的频谱图就是一条水平亮线——纯音嘛,只有一个频率成分。