第4章:MFCC特征提取——梅尔滤波器组、计算流程与MCU实现考量

好,咱们进入语音识别里最核心的一环——MFCC特征提取。

说实话,很多做嵌入式语音识别的朋友,一上来就急着跑模型,结果发现效果差得离谱。为什么?因为前端特征没做好。你想想看,模型吃进去的是垃圾,吐出来的能是黄金吗?

MFCC,全称是梅尔频率倒谱系数。它模拟了人耳的听觉特性——人耳对低频更敏感,对高频没那么敏感。说白了,就是让机器像人一样去“听”声音。

4.1 梅尔滤波器组

先聊聊梅尔滤波器组。这是MFCC里最关键的模块之一。

梅尔频率和普通频率的转换关系是这样的:

Mel(f) = 2595 * log10(1 + f/700)

这个公式什么意思?就是把线性频率映射到梅尔刻度上。低频区域,滤波器排得密;高频区域,滤波器排得疏。我刚开始做这个的时候,总觉得这玩意儿有点玄学,直到有一次在项目中调试噪声环境下的识别效果——嗯,换了梅尔滤波器之后,识别率直接提升了15%。

滤波器组的设计有几个关键参数:

  • 滤波器个数:通常取20-40个。MCU上我建议用24个,够用且不浪费算力。
  • 频率范围:一般取0-8kHz(采样率16kHz时)。
  • 滤波器形状:三角滤波器,中心频率处增益为1,两端衰减到0。

每个滤波器本质上是一个加权窗口。输入是频谱能量,输出是每个频带的能量总和。

核心要点:梅尔滤波器组的作用,就是把线性频谱压缩成更符合人耳感知的频谱。这一步做不好,后面的模型再强也白搭。

4.2 MFCC计算流程

完整的MFCC提取流程,我习惯把它拆成6步。每一步都有坑,咱们一个一个说。

  1. 预加重:提升高频分量。公式是 y[n] = x[n] - 0.97 * x[n-1]。这个系数0.97是我常用的,别问为什么,试出来的。
  2. 分帧:把音频切成短帧,每帧20-40ms。MCU上我推荐30ms帧长,10ms帧移。
  3. 加窗:汉明窗。减少频谱泄露。代码里就一行:window[i] = 0.54 - 0.46 * cos(2*PI*i/(N-1))
  4. FFT:把时域信号转到频域。MCU上一般用256点或512点FFT。
  5. 梅尔滤波:把FFT能量通过梅尔滤波器组,得到梅尔频谱。
  6. DCT:离散余弦变换。把梅尔频谱转成倒谱系数。通常取前12-13个系数。

这里有个细节——DCT这一步,很多人会忽略。为什么要做DCT?因为梅尔频谱各维度之间有相关性,DCT可以解相关,让特征更紧凑。我在一个项目里试过跳过DCT,结果模型训练时间长了3倍,效果还差了一截。

我的经验:MFCC的前12个系数包含了大部分语音信息。加上帧能量(第0个系数),一共13维,足够大多数MCU上的语音识别任务使用。

4.3 在MCU上的实现考量

好了,理论说完了。咱们聊聊怎么在MCU上落地。这才是真正的硬骨头。

MCU的资源有多紧张?我举个例子:一个常见的Cortex-M4芯片,RAM可能只有64KB,Flash 256KB,主频80MHz。你要在这种环境下跑MFCC,必须精打细算。

我个人总结了几个关键点:

4.3.1 定点数运算

MCU没有FPU(浮点运算单元)的话,浮点运算慢得吓人。我建议全部转成定点数。

比如,预加重系数0.97,可以表示为Q15格式的0x7C28。乘法之后右移15位就行。

// Q15定点数示例
int16_t pre_emphasis(int16_t x, int16_t prev_x) {
    // 0.97 的 Q15 表示为 0x7C28
    int32_t temp = (int32_t)x - ((int32_t)0x7C28 * prev_x >> 15);
    // 饱和处理
    if (temp > 32767) temp = 32767;
    if (temp < -32768) temp = -32768;
    return (int16_t)temp;
}

我曾经在一个项目里,因为偷懒用了浮点,结果MCU跑MFCC花了200ms一帧。改成定点后,直接降到15ms。差距就是这么大。

4.3.2 查表法加速

FFT的旋转因子、汉明窗系数、梅尔滤波器的权重——这些都可以提前算好,存成表格。

比如汉明窗,256点的窗口系数,提前算好存在Flash里。运行时直接查表,省掉大量计算。

注意:查表法虽然快,但会占用Flash空间。256点的汉明窗表,每个系数2字节,就是512字节。梅尔滤波器组更占空间——24个滤波器,每个滤波器覆盖几十个频点,算下来可能几KB。你要在速度和空间之间做权衡。

4.3.3 内存复用

MCU的RAM很金贵。我习惯的做法是:

  • 用一个全局数组存放原始音频数据
  • FFT计算时,原地修改这个数组(in-place FFT)
  • 梅尔滤波结果覆盖FFT结果
  • DCT结果覆盖梅尔滤波结果

这样从头到尾,只需要一个缓冲区。我见过有人每个步骤都开一个新数组,256点的FFT,硬生生用了4个256点的数组,RAM直接爆了。

4.3.4 帧移与缓存

分帧时,帧移通常小于帧长。这意味着相邻帧有重叠部分。怎么处理?

我的做法是:维护一个环形缓冲区。每次新来10ms数据,就把旧数据往前推,然后取30ms的数据做处理。这样既不用每次都重新采集,也避免了数据拷贝的开销。

// 环形缓冲区示例
#define FRAME_LEN 480   // 30ms @ 16kHz
#define FRAME_SHIFT 160 // 10ms @ 16kHz
int16_t ring_buf[FRAME_LEN];
int16_t buf_idx = 0;

void process_frame(int16_t *new_data) {
    // 把新数据写入缓冲区
    for (int i = 0; i < FRAME_SHIFT; i++) {
        ring_buf[buf_idx] = new_data[i];
        buf_idx = (buf_idx + 1) % FRAME_LEN;
    }
    // 此时 ring_buf 里就是最新的30ms数据
    // 但注意,数据不是连续的,需要做一次重排
    // 或者用两个缓冲区交替使用
}

嗯,这里要注意——环形缓冲区虽然省内存,但数据不连续,做FFT之前需要整理一下。我后来改用双缓冲区方案,一个存当前帧,一个存下一帧,交替使用。代码更清晰,调试也方便。

4.4 性能评估

最后,给个参考数据。我在STM32F407(Cortex-M4,168MHz)上实测过:

步骤 浮点版本耗时 定点版本耗时
预加重 0.5ms 0.05ms
加窗+FFT(256点) 8ms 1.2ms
梅尔滤波(24通道) 3ms 0.4ms
DCT(13系数) 1ms 0.15ms
总计 12.5ms 1.8ms

你看,定点化之后,一帧处理时间不到2ms。而帧移是10ms,所以完全来得及。我曾经在一个项目里,因为没做定点化,导致处理速度跟不上采集速度,音频数据越积越多,最后系统直接崩了。从那以后,我再也不敢轻视定点化这一步。

总结一下:MFCC在MCU上落地的核心就是三点——定点化、查表法、内存复用。把这三点做好,你的语音识别前端就稳了。

下一章,咱们聊聊怎么把MFCC特征喂给神经网络模型。那又是另一番天地了。