4、Mel滤波器组与梅尔频谱:Mel刻度原理、三角滤波器组设计、梅尔频谱计算
好,咱们进入声学特征提取里非常核心的一环——梅尔频谱。
前面我们讲了预加重、分帧、加窗,最后做了FFT,拿到了每一帧的频谱。但说实话,这个频谱还不是人耳听起来的样子。人耳对频率的感知不是线性的,低频敏感,高频迟钝。梅尔刻度就是用来模拟这个特性的。
4.1 梅尔刻度原理
梅尔刻度,说白了就是一个非线性映射。它把物理频率(Hz)映射到人耳感知的“音高”上。
我记得刚入行时,导师让我去听不同频率的正弦波。1kHz和2kHz听起来差很多,但8kHz和9kHz听起来几乎一样。这就是梅尔刻度的直观感受。
常用的映射公式有两个:
// 标准公式(O'Shaughnessy)
mel = 2595 * log10(1 + f / 700)
// 另一种常见形式
mel = 1127 * ln(1 + f / 700)
这两个公式本质一样,只是底数不同。我个人习惯用第一个,因为log10在工程里更容易调试。
关键理解:梅尔刻度在低频段(<1000Hz)近似线性,高频段呈对数增长。这意味着在低频区域,滤波器组更密集;高频区域,滤波器组更稀疏。
为什么会这样?你想想看,人耳基底膜上的毛细胞分布就是这样的——低频区域细胞多,分辨率高;高频区域细胞少,分辨率低。梅尔滤波器组就是在模拟这个生物结构。
4.2 三角滤波器组设计
滤波器组的设计,是梅尔频谱计算中最容易出坑的地方。我踩过不少坑,今天把关键点都给你讲透。
4.2.1 设计步骤
- 确定频率范围:通常是 [0, fs/2],但实际应用中常去掉极低频(如 0-50Hz)和极高频(如接近奈奎斯特频率的区域)。
- 映射到梅尔刻度:将频率范围的起点和终点转换为梅尔值。
- 等间隔划分:在梅尔刻度上均匀取 M+2 个点(M 是滤波器个数,常用 26 或 40)。
- 映射回 Hz:将梅尔刻度上的点反变换回物理频率。
- 构造三角滤波器:每个滤波器在梅尔刻度上是等宽的三角形。
我的经验:滤波器个数 M 的选择很讲究。语音识别常用 26 个,音乐分析常用 40 个。我在做远场语音唤醒时试过 20 个,效果差不少——低频细节丢失了。后来改回 26 个,唤醒率直接提了 3 个点。
4.2.2 三角滤波器的数学表达
每个三角滤波器 H_m(k) 在频域上定义为:
H_m(k) = 0, k < f(m-1)
= (k - f(m-1)) / (f(m) - f(m-1)), f(m-1) ≤ k ≤ f(m)
= (f(m+1) - k) / (f(m+1) - f(m)), f(m) ≤ k ≤ f(m+1)
= 0, k > f(m+1)
其中 f(m) 是第 m 个滤波器的中心频率(Hz),k 是FFT的频点索引。
我曾经踩过的坑:滤波器之间的重叠区域处理不当。标准三角滤波器相邻之间在 50% 幅度处重叠。但有些实现用了不同的重叠比例,导致频谱能量泄漏或丢失。建议严格按 50% 重叠来设计,这是经过大量实验验证的最优值。
4.2.3 滤波器组的可视化
下面这张图展示了 26 个梅尔三角滤波器在频域上的分布。注意看低频区域滤波器密集,高频区域稀疏——这就是梅尔刻度的直观体现。
4.3 梅尔频谱计算
滤波器组设计好了,接下来就是计算梅尔频谱。这一步其实很简单——把每一帧的频谱能量和滤波器组做点乘。
4.3.1 计算步骤
- 计算功率谱:对每一帧FFT结果取模平方,得到 |X(k)|²。
- 滤波器组加权:每个滤波器的输出等于功率谱与该滤波器权重的加权和。
- 取对数:对每个滤波器的输出取10*log10(),得到对数梅尔频谱。
用公式表达就是:
// 第m个梅尔滤波器的输出
S(m) = Σ_{k=0}^{N-1} |X(k)|² * H_m(k)
// 对数梅尔频谱
LogMel(m) = 10 * log10(S(m))
其中 N 是FFT点数,H_m(k) 是第 m 个三角滤波器。
我的习惯:取对数之前,给 S(m) 加一个很小的常数(比如 1e-10),防止出现 log(0) 的情况。这个细节在嵌入式部署时尤其重要——定点数实现中很容易出现零值导致计算崩溃。
4.3.2 代码实现
下面是一个完整的梅尔频谱计算函数,我用 Python 写的,方便你理解整个流程:
import numpy as np
def compute_mel_spectrogram(signal, fs, n_fft=512, n_mels=26, fmin=0, fmax=None):
"""
计算梅尔频谱
参数:
signal: 输入音频信号 (1D array)
fs: 采样率 (Hz)
n_fft: FFT点数
n_mels: 梅尔滤波器个数
fmin: 最低频率 (Hz)
fmax: 最高频率 (Hz),默认 fs/2
返回:
mel_spec: 梅尔频谱 (n_mels x n_frames)
"""
if fmax is None:
fmax = fs / 2
# 1. 分帧 + 加窗 + FFT(简化处理,实际需分帧循环)
# 这里假设 signal 已经是单帧数据
window = np.hamming(n_fft)
framed = signal[:n_fft] * window
spectrum = np.fft.rfft(framed, n=n_fft)
power_spec = np.abs(spectrum) ** 2
# 2. 构建梅尔滤波器组
mel_filterbank = create_mel_filterbank(fs, n_fft, n_mels, fmin, fmax)
# 3. 滤波器组加权
mel_energy = np.dot(mel_filterbank, power_spec)
# 4. 取对数(加小常数防零)
mel_spec = 10 * np.log10(mel_energy + 1e-10)
return mel_spec
def create_mel_filterbank(fs, n_fft, n_mels, fmin, fmax):
"""构建梅尔三角滤波器组"""
# 频率轴
freqs = np.linspace(0, fs/2, n_fft//2 + 1)
# 梅尔刻度上的等间隔点
mel_min = 2595 * np.log10(1 + fmin / 700)
mel_max = 2595 * np.log10(1 + fmax / 700)
mel_points = np.linspace(mel_min, mel_max, n_mels + 2)
# 映射回Hz
hz_points = 700 * (10**(mel_points / 2595) - 1)
# 构建三角滤波器
filterbank = np.zeros((n_mels, n_fft//2 + 1))
for m in range(1, n_mels + 1):
left = hz_points[m-1]
center = hz_points[m]
right = hz_points[m+1]
for i, f in enumerate(freqs):
if left <= f <= center:
filterbank[m-1, i] = (f - left) / (center - left)
elif center <= f <= right:
filterbank[m-1, i] = (right - f) / (right - center)
return filterbank
我曾经踩过的坑:滤波器组的归一化问题。有些实现会对每个滤波器做面积归一化(让每个三角形的面积为1),有些不做。我建议做面积归一化,否则高频滤波器(带宽大)会天然比低频滤波器输出能量大,导致频谱倾斜。归一化后,每个滤波器的响应是等权的。
4.3.3 梅尔频谱的物理意义
梅尔频谱的每一帧是一个 M 维向量(M 是滤波器个数),代表该帧音频在不同梅尔频带上的能量分布。
举个例子:
| 帧索引 | 滤波器1 (0-100Hz) | 滤波器2 (100-200Hz) | 滤波器3 (200-300Hz) | ... | 滤波器26 (6000-8000Hz) |
|---|---|---|---|---|---|
| 第1帧 | 12.3 dB | 15.7 dB | 18.2 dB | ... | 3.1 dB |
| 第2帧 | 11.8 dB | 14.9 dB | 19.5 dB | ... | 2.8 dB |
| 第3帧 | 13.1 dB | 16.2 dB | 17.9 dB | ... | 3.5 dB |
你看,低频滤波器(1-3)的输出能量明显高于高频滤波器(26)。这是因为语音信号的能量主要集中在低频段,符合人耳的听觉特性。
4.4 梅尔频谱 vs 普通频谱
我整理了一个对比表,方便你理解两者的区别:
| 对比维度 | 普通频谱(FFT) | 梅尔频谱 |
|---|---|---|
| 频率刻度 | 线性 | 非线性(梅尔刻度) |
| 维度 | n_fft/2 + 1(通常256-1024) | M(通常26-40) |
| 信息冗余 | 高(相邻频点高度相关) | 低(去冗余,保留感知相关) |
| 人耳匹配 | 不匹配 | 匹配 |
| 后续处理 | 直接用于特征 | 通常继续做DCT得到MFCC |
核心结论:梅尔频谱是连接物理声学和感知声学的桥梁。它用更少的维度保留了人耳最敏感的信息,是语音和音频处理领域最成功的特征之一。