第三章 语音信号处理基础:时域与频域、采样率与量化、短时傅里叶变换(STFT)、梅尔频率倒谱系数(MFCC)
好,咱们进入第三章。这一章是语音唤醒算法的“地基”。
说实话,很多做语音唤醒的工程师,算法调来调去效果上不去,回头一看,往往是信号处理的基本功没打牢。我自己早年也踩过这个坑——花了两周调一个神经网络,结果发现是采样率设错了,白白浪费了时间。
所以这一章,咱们把时域、频域、采样、量化、STFT、MFCC这几个核心概念彻底讲透。你想想看,这些东西搞明白了,后面做特征提取、模型训练,心里就有底了。
3.1 时域与频域:看波形 vs 看频谱
语音信号本质上是个随时间变化的压力波。我们拿麦克风录下来,得到的就是一个“时间-幅度”的曲线,这叫时域表示。
时域的好处是直观。你一眼就能看出这段语音是安静还是有人在说话,音量大小如何。但时域有个致命缺点——它看不出来声音的“成分”。
举个例子,一个200Hz的纯音和一个2000Hz的纯音,在时域上都是正弦波,长得差不多。但人耳一听就知道,一个低沉,一个尖锐。这个“低沉”和“尖锐”的区别,就是频率的区别。
所以我们需要频域。频域告诉我们:这段声音里,各个频率的能量分布是怎样的。
核心思想:时域看“什么时候响”,频域看“什么频率响”。语音唤醒里,我们更关心后者。
从时域变到频域,靠的是傅里叶变换。不过这里我要提醒一句:傅里叶变换假设信号是平稳的、无限长的。但语音信号是非平稳的——你想想看,一句话里,元音、辅音、静音段,特性完全不一样。所以直接用傅里叶变换处理整段语音,结果会一团糟。
怎么办?我们后面会讲STFT,它就是来解决这个问题的。
3.2 采样率与量化:把模拟信号变成数字信号
麦克风出来的信号是模拟的,连续变化的。但我们的芯片只能处理离散的数字。所以需要采样和量化。
3.2.1 采样率
采样率就是每秒钟采集多少个样本点。单位是Hz(赫兹)。
这里有个黄金法则叫奈奎斯特采样定理:采样率必须大于信号最高频率的两倍,才能无失真地恢复原始信号。
避坑指南:我曾经在一个项目里,为了省功耗,把采样率从16kHz降到了8kHz。结果发现唤醒率暴跌。后来一查,语音唤醒常用的特征MFCC需要覆盖到8kHz的频率范围,8kHz采样率只能覆盖到4kHz,高频信息全丢了。所以,语音唤醒一般用16kHz采样率,这是行业惯例。
常见的采样率选择:
| 应用场景 | 采样率 | 说明 |
|---|---|---|
| 电话语音 | 8 kHz | 带宽有限,只覆盖300Hz-3.4kHz |
| 语音唤醒/语音识别 | 16 kHz | 覆盖0-8kHz,人声主要能量都在这里 |
| 高保真音频 | 44.1 kHz / 48 kHz | 音乐、广播等场景 |
3.2.2 量化
采样是把时间离散化,量化是把幅度离散化。
每个样本点用多少位来表示?常见的是16位(bit)。16位量化,意味着幅度被分成了2^16 = 65536个等级。
量化位数越高,动态范围越大,声音细节越丰富。但代价是数据量变大,存储和传输压力增加。
我的经验:在低功耗嵌入式设备上,16位量化是主流。8位量化虽然省存储,但信噪比会下降,唤醒率可能掉1-2个百分点。除非你芯片资源极度紧张,否则别省这个。
3.3 短时傅里叶变换(STFT):给语音信号“加窗”
前面说了,语音是非平稳信号。那怎么分析?
思路很简单:把一段长语音切成很多个短段,每个短段里,我们近似认为信号是平稳的。然后对每个短段做傅里叶变换。这就是短时傅里叶变换(STFT)。
STFT的步骤:
- 分帧:把语音信号分成一帧一帧的,每帧通常20-40毫秒。比如16kHz采样率,一帧25毫秒就是400个样本点。
- 加窗:每一帧乘以一个窗函数(比如汉明窗),减少频谱泄露。
- FFT:对加窗后的每一帧做快速傅里叶变换,得到频谱。
- 帧移:相邻帧之间会有重叠,通常重叠50%-75%。这样不会丢失信息。
STFT的输出是一个二维矩阵:横轴是时间帧,纵轴是频率,颜色深浅代表能量大小。这就是我们常说的语谱图。
关键参数:
- 帧长:决定了频率分辨率。帧越长,频率分辨率越高,但时间分辨率越低。
- 帧移:决定了时间分辨率。帧移越小,时间分辨率越高,但计算量越大。
- FFT点数:通常取2的整数次幂,比如512、1024。点数越多,频率分辨率越细。
举个代码示例,用Python做STFT:
import numpy as np
import librosa
# 加载语音,16kHz采样率
y, sr = librosa.load('wake_word.wav', sr=16000)
# 计算STFT
# n_fft=512, hop_length=160 (10ms帧移), win_length=400 (25ms帧长)
D = librosa.stft(y, n_fft=512, hop_length=160, win_length=400)
# 取幅度谱
magnitude = np.abs(D)
print(f"STFT输出形状: {magnitude.shape}")
# 输出: (257, T) 其中257 = 512/2 + 1,T是帧数
避坑指南:我曾经在移植STFT到Cortex-M4芯片时,发现FFT计算太慢,一帧要算好几毫秒,实时性根本达不到。后来改用定点FFT,并且把FFT点数从1024降到512,才勉强跑起来。所以,在嵌入式上,FFT点数不是越大越好,要平衡精度和速度。
3.4 梅尔频率倒谱系数(MFCC):模拟人耳的听觉特性
STFT得到的频谱,是线性频率的。但人耳对频率的感知不是线性的——我们对低频更敏感,对高频不那么敏感。
具体来说,人耳在1000Hz以下近似线性,在1000Hz以上是对数关系。这就是梅尔尺度。
MFCC的计算流程:
- 预加重:提升高频分量,补偿语音信号中高频能量的衰减。公式很简单:y[n] = x[n] - 0.97 * x[n-1]。
- 分帧加窗:和STFT一样。
- FFT:得到频谱。
- 梅尔滤波器组:把线性频率映射到梅尔尺度。通常用20-40个三角滤波器。
- 取对数:模拟人耳对音量的对数感知。
- DCT(离散余弦变换):去相关,得到倒谱系数。通常取前12-13个系数。
为什么取前12-13个?因为后面的系数代表的是频谱的精细结构,对语音唤醒来说,这些细节反而容易引入噪声。
我的习惯:在语音唤醒任务中,我一般用13维MFCC(包括第0个系数),再加上一阶差分和二阶差分,总共39维。这个配置在大多数场景下效果都不错。如果你芯片资源紧张,可以只取13维,去掉差分,唤醒率大概会掉1-2个点。
代码示例:
import librosa
y, sr = librosa.load('wake_word.wav', sr=16000)
# 提取13维MFCC
mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13,
n_fft=512, hop_length=160, win_length=400)
# 计算一阶差分
delta = librosa.feature.delta(mfcc)
# 计算二阶差分
delta2 = librosa.feature.delta(mfcc, order=2)
# 拼接成39维特征
features = np.vstack([mfcc, delta, delta2])
print(f"MFCC特征形状: {features.shape}")
# 输出: (39, T)
3.5 低功耗设计中的信号处理考量
好了,理论讲完了。咱们聊聊实际落地时,低功耗设计要注意什么。
第一,采样率的选择直接影响功耗。采样率越高,ADC(模数转换器)工作频率越高,功耗越大。16kHz是语音唤醒的黄金点,再高就浪费了。
第二,FFT点数要精打细算。FFT的计算量是O(N log N)。512点FFT和1024点FFT,计算量差了一倍多。在低功耗芯片上,我建议用512点,频率分辨率已经够用了。
第三,MFCC的计算可以简化。比如,梅尔滤波器组可以用定点数实现,避免浮点运算。DCT也可以用查表法加速。
总结一下:
- 时域看波形,频域看成分。语音唤醒主要用频域特征。
- 采样率16kHz,量化16位,这是行业标配。
- STFT把非平稳信号切成短段,每段做FFT。
- MFCC模拟人耳,13维系数+差分,效果最好。
- 低功耗设计要抠细节:采样率、FFT点数、定点化,一个都不能放过。
下一章,咱们会讲怎么把这些特征喂给神经网络,实现真正的语音唤醒。到时候你会发现,前面这些信号处理的知识,全都能用上。