MFCC音频特征提取流程demo

5.8k 词

MFCC的核心思想是模仿人耳对声音的非线性感知特性(人耳对低频声音比高频声音更敏感),从而更好地表示语音特征。其流程可以概括为以下步骤

分帧

  • 目的:语音信号是非平稳信号,其特性是随时间变化的。但在一个非常短的时间段内,可以近似认为是平稳的。分帧就是将长信号切分成许多短片段来分析。
  • 操作:使用一个固定长度的窗口以一定的帧移沿着信号滑动并截取数据。

加窗

  • 目的:减少每一帧信号在其两端处的频谱泄漏,使帧两端平滑地衰减到零,从而降低后续傅里叶变换后旁瓣的强度。
  • 操作:将每一帧信号乘上一个窗函数。

傅里叶变换和功率谱计算

  • 目的:将信号从时域转换到频域。
  • 操作
    • 对每一帧加窗后的信号进行FFT,得到复数频谱。
    • 然后计算其功率谱(取模的平方)。P = |FFT(frame)|² / N (N是FFT点数)

梅尔滤波器组滤波

  • 目的:模拟人耳的听觉特性。人耳对于不同频率的感知能力是不同的,在低频区域区分度高,在高频区域区分度低。梅尔刻度是一种将实际频率转换为更符合人耳感知的频率刻度。
  • 操作
    • 定义一组三角带通滤波器(梅尔滤波器组),这些滤波器在梅尔刻度上是等宽的,但在线性频率刻度上是不等宽的(低频区域滤波器窄且密集,高频区域宽且稀疏)。
    • 将上一步得到的功率谱与每一个梅尔滤波器进行积分(实际上是相乘并求和),得到每个滤波器输出的能量。这样就将频谱压缩为n个更能反映人耳听觉特性的滤波器组能量。

离散余弦变换

  • 目的:对梅尔滤波器组能量取对数后,进行离散余弦变换 (DCT)。这一步也称为倒谱分析。

    • 取对数:因为人耳对声音强度的感知也是对数的。同时,对数操作可以将卷积信号(声源激励和声道滤波)转化为可加的。

    • DCT:对取对数后的滤波器组能量进行DCT,相当于对频谱进行“压缩”。DCT的结果中,低维(前几个)系数代表了包络信息(反映了声道形状,是语音内容的关键),而高维系数代表了细节信息(如激励源)。

最终的输出通常只保留DCT后的前12-13个系数,再加上一个能量项(第0项或直接从时域帧中计算),共同构成最终的MFCC特征向量。

这个流程为音频信号的每一帧都计算出了一个固定维度的MFCC特征向量,这些向量序列就可以用于后续的语音识别、说话人识别等机器学习任务。

 

下面是在一个项目中使用MFCC方法进行特征提取的预处理部分,这里用的不是标准MFCC,省去了最后的DCT部分,更直接简单。这个处理流程是在python仿真后迁移的,python中运用了librosa库,不过该库里面有一些隐式处理,因此在转换成C版本代码时遇到了较多不匹配的情况,不过调试中也是逐步改进了。

分帧函数如下,实际使用时调整传参使其能够保持60%的重合率,让每一帧平滑过渡,不过这个函数只计算帧数,真正的分帧操作在下面的流式处理里面循环执行。

// 分帧函数,使用局部缓存处理每一帧数据
int frame_signal(const int16_t* y, int y_len, int frame_length, int hop_length, int* n_frames) {
    *n_frames = 1 + (y_len - frame_length) / hop_length;
    return 0; // 假设不会出错
}
//n_frames 经过处理后为实际帧数
//实际运行时,hoplength和framlength定义如下
// #define WIN_LENGTH 400 窗长400
// #define HOP_LENGTH 160 帧移160

 

手动FFT实现,经典的Cooley-Tukey迭代FFT算法,采用了位反转置换和蝶形运算:

// Complex结构体定义
typedef struct {
    float real;
    float imag;
} Complex;


void fft(Complex* x) {
    // 预先计算旋转因子
    static Complex W[N_FFT/2];
    static int initialized = 0;
    if (!initialized) {
        for (int k = 0; k < N_FFT/2; k++) {
            float angle = -2 * PI * k / N_FFT;
            W[k].real = cosf(angle);
            W[k].imag = sinf(angle);
        }
        initialized = 1;
    }

    // 位反转置换
    int j = 0;
    for (int i = 0; i < N_FFT - 1; i++) {
        if (i < j) {
            Complex temp = x[i];
            x[i] = x[j];
            x[j] = temp;
        }
        int k = N_FFT >> 1;
        while (k <= j) {
            j -= k;
            k >>= 1;
        }
        j += k;
    }

    // 迭代FFT
    for (int stride = 2; stride <= N_FFT; stride <<= 1) {
        int half_stride = stride >> 1;
        int W_step = N_FFT / stride;
        
        for (int k = 0; k < N_FFT; k += stride) {
            for (int m = 0; m < half_stride; m++) {
                int idx_e = k + m;
                int idx_o = idx_e + half_stride;
                int W_idx = m * W_step;
                
                Complex t = {
                    W[W_idx].real * x[idx_o].real - W[W_idx].imag * x[idx_o].imag,
                    W[W_idx].real * x[idx_o].imag + W[W_idx].imag * x[idx_o].real
                };
                
                x[idx_o].real = x[idx_e].real - t.real;
                x[idx_o].imag = x[idx_e].imag - t.imag;
                x[idx_e].real += t.real;
                x[idx_e].imag += t.imag;
            }
        }
    }
}

 

Mel功率谱计算,函数中的汉明窗与Mel滤波器组都是事先生成的。

// 计算Mel功率谱
void manual_melspectrogram(const int16_t* y, int y_len, int sr,
                           float* mel_spec, int* n_mels_out,
                           int* n_frames_out) {
    int n_frames;

    // 计算最大绝对值(用于归一化)
    float max_val = 0.0f;
    for (int i = 0; i < y_len; i++) {
        float abs_val = fabsf((float)y[i]);
        if (abs_val > max_val) {
            max_val = abs_val;
        }
    }

    // 分配并归一化为 float 缓冲区
    static float y_norm[8000];  // 假设最大长度8000
    if (y_len > 8000) {
        *n_frames_out = 0;
        return;  // 防止越界
    }
    if (max_val > 0.0f) {
        for (int i = 0; i < y_len; i++) {
            y_norm[i] = (float)y[i] / max_val;
        }
    } else {
        for (int i = 0; i < y_len; i++) {
            y_norm[i] = 0.0f;
        }
    }

    // 计算帧数
    frame_signal(y, y_len, WIN_LENGTH, HOP_LENGTH, &n_frames);  
    // 注意这里使用原 y,因为只用于 n_frames 计算
    *n_mels_out = N_MELS;
    *n_frames_out = n_frames;

    // 建立FFT缓冲区
    static Complex fft_input[N_FFT];
    static float power_spectrum[MEL_BINS];

    for (int t = 0; t < n_frames; t++) {
        // 加窗处理(改为使用 y_norm)
        for (int n = 0; n < WIN_LENGTH; n++) {
            fft_input[n].real = y_norm[t * HOP_LENGTH + n] * HAMMING_WINDOW[n];
            fft_input[n].imag = 0.0f;
        }
        // 清零剩余部分
        for (int n = WIN_LENGTH; n < N_FFT; n++) {
            fft_input[n].real = 0.0f;
            fft_input[n].imag = 0.0f;
        }

        // FFT计算
        fft(fft_input);

        // 计算功率谱 (直接存储到power_spectrum)
        for (int k = 0; k < MEL_BINS; k++) {
            float re = fft_input[k].real / N_FFT;
            float im = fft_input[k].imag / N_FFT;
            power_spectrum[k] = re * re + im * im;
        }

        // 梅尔滤波 (直接计算到输出mel_spec)
        for (int m = 0; m < N_MELS; m++) {
            float mel_energy = 0.0f;
            for (int k = 0; k < MEL_BINS; k++) {
                mel_energy += power_spectrum[k] * mel_filter[m][k];
            }
            mel_spec[m * n_frames + t] = mel_energy;
        }
    }
}

 

最终处理函数,这里是将上面流程输出的值每帧取均值后放入后续神经网络的input。

#include "algo.h"
#include "MFCC.h"
#include <stdint.h>
#define FRAME_LENGTH 8000
#define N_MELS 40

// 静态缓冲区,用于存储帧、梅尔频谱和对数梅尔特征
//static float frame_global[FRAME_LENGTH];
float mel_spec_global[N_MELS * MAX_FRAMES];
//static float mean_log_mel_global[N_MELS];
int speaker_recognition_run(int16_t* pcm_data, int len) {
    // 确保只有一帧数据
    if (len != FRAME_LENGTH) {
        printf("lengtherror: need %d ,current %d\n", FRAME_LENGTH, len);
        return -1;  // 如果数据长度不匹配,返回错误
    }
    // 计算梅尔频谱
    int sr = 8000;
    int n_mels, n_frames;
    // 直接在全局缓冲区上操作,避免分配新内存
    manual_melspectrogram(pcm_data, len, sr, mel_spec_global, &n_mels, &n_frames);
    // 计算对数梅尔特征
    float* log_mel_features = compute_log_mel_mean(mel_spec_global, n_mels, n_frames);
    if (!log_mel_features) {
        // 处理错误
        printf("log error\n");
        return 1;
    }

    // Prepare input (1 sample, 40 features)
    float input[BATCH_SIZE * SEQ_LENGTH * INPUT_DIM];
    for (int i = 0; i < INPUT_DIM; i++) {
        input[i] = log_mel_features[i];
    }

    free(log_mel_features);

    // Output array for the classification results
    float output[BATCH_SIZE * NUM_CLASSES];

    // Forward pass through the model
    forward(input, output, BATCH_SIZE, SEQ_LENGTH);

    // FindMax
    float max = output[0];
    int out = 0;
    for (int j = 1; j < NUM_CLASSES; j++) {
        if (max < output[j]) {
            max = output[j];  // 更新最大值
            out = j;           // 更新输出的索引
        }
    }

    return out;
}

其中compute_log_mel_mean函数如下定义:

// 计算对数梅尔特征并取均值
float* compute_log_mel_mean(float* mel_spec, int n_mels, int n_frames) {
    if (n_mels < 40) {
        fprintf(stderr, "The audio is too short to extract Mel features.\n");
        return NULL;
    }

    float* mean_log_mel = (float*)malloc(40 * sizeof(float));  // 保留40个梅尔频率带

    // 计算每个梅尔频率带的对数均值
    for (int i = 0; i < n_mels; ++i) {
        float sum = 0.0f;
        for (int j = 0; j < n_frames; ++j) {
            float value = mel_spec[i * n_frames + j]; // 按列优先排列
            value = log10f(value + 1e-6);  // 加上一个小常数以避免对数的零值
            sum += value;
        }
        mean_log_mel[i] = sum / n_frames * SCALE_FACTOR;  // 按帧数取均值并进行缩放
    }

    return mean_log_mel;
}

其中SCALE_FACTOR 10.0 是比例因子,用于补偿与 Python 输出的差异。

当时仿真效果还可以,就没有再考虑添加DCT转换为标准MFCC,主打能跑就行。