3. 音频数据预处理

任务目标

在ESP32上进行麦克风音频采集后,原始信号常包含静音段且音量波动大,直接送入语音识别会降低准确率。本案例实现一个轻量级预处理模块,完成两件事:静音检测(VAD)——判断当前音频帧是否包含有效语音;自动增益控制(AGC)——将有效语音帧的幅度归一化到目标电平范围。处理后的数据可直接送入后续KWS或ASR引擎。

核心实现

以下代码基于ESP-IDF的I2S接口读取麦克风数据,实现逐帧处理。VAD采用能量阈值与过零率双重判断,AGC使用峰值归一化并限制最大增益倍数,避免噪声被过度放大。

// audio_preprocess.h
#ifndef AUDIO_PREPROCESS_H
#define AUDIO_PREPROCESS_H

#include <stdint.h>
#include <stdbool.h>

#define FRAME_SIZE 320          // 每帧采样点数 (20ms @16kHz)
#define SAMPLE_RATE 16000
#define TARGET_PEAK 8000        // AGC目标峰值 (16bit范围0-32767)
#define MAX_GAIN 20.0f          // 最大增益倍数
#define ENERGY_THRESHOLD 500.0f // 静音能量阈值
#define ZCR_THRESHOLD 0.15f     // 过零率阈值 (归一化)

typedef struct {
    float energy;
    float zcr;
    bool is_speech;
    float gain;
} audio_frame_info_t;

// 处理一帧PCM数据,返回帧信息和AGC后的数据
audio_frame_info_t audio_preprocess_frame(int16_t *pcm_data);

#endif
// audio_preprocess.c
#include "audio_preprocess.h"
#include <math.h>
#include <string.h>

static float calculate_energy(const int16_t *data) {
    float sum = 0.0f;
    for (int i = 0; i < FRAME_SIZE; i++) {
        sum += (float)data[i] * data[i];
    }
    return sum / FRAME_SIZE;
}

static float calculate_zcr(const int16_t *data) {
    int crossings = 0;
    for (int i = 1; i < FRAME_SIZE; i++) {
        if ((data[i-1] >= 0 && data[i] < 0) || (data[i-1] < 0 && data[i] >= 0)) {
            crossings++;
        }
    }
    return (float)crossings / FRAME_SIZE;
}

audio_frame_info_t audio_preprocess_frame(int16_t *pcm_data) {
    audio_frame_info_t info;
    
    // 1. 计算特征
    info.energy = calculate_energy(pcm_data);
    info.zcr = calculate_zcr(pcm_data);
    
    // 2. 静音检测:能量高于阈值且过零率适中(排除纯噪声)
    info.is_speech = (info.energy > ENERGY_THRESHOLD) && 
                     (info.zcr > 0.05f && info.zcr < 0.5f);
    
    // 3. 自动增益控制(仅对语音帧)
    if (info.is_speech) {
        // 找当前帧峰值
        int16_t peak = 0;
        for (int i = 0; i < FRAME_SIZE; i++) {
            int16_t abs_val = abs(pcm_data[i]);
            if (abs_val > peak) peak = abs_val;
        }
        
        // 计算增益并限制范围
        float gain = (peak > 0) ? (float)TARGET_PEAK / peak : 1.0f;
        info.gain = (gain > MAX_GAIN) ? MAX_GAIN : gain;
        
        // 应用增益
        for (int i = 0; i < FRAME_SIZE; i++) {
            float amplified = pcm_data[i] * info.gain;
            // 防止溢出
            if (amplified > 32767.0f) pcm_data[i] = 32767;
            else if (amplified < -32768.0f) pcm_data[i] = -32768;
            else pcm_data[i] = (int16_t)amplified;
        }
    } else {
        info.gain = 0.0f;
        // 静音帧直接静音输出,避免背景噪声进入识别器
        memset(pcm_data, 0, FRAME_SIZE * sizeof(int16_t));
    }
    
    return info;
}

集成与调用

在ESP32的I2S麦克风采集任务中,每读取一帧数据后调用一次预处理函数:

// main.c 片段 - I2S读取循环
#include "driver/i2s.h"
#include "audio_preprocess.h"

void audio_capture_task(void *arg) {
    int16_t raw_buffer[FRAME_SIZE];
    size_t bytes_read;
    
    while (1) {
        // 从I2S读取原始PCM数据 (假设已配置为16kHz, 16bit, 单声道)
        i2s_read(I2S_NUM_0, raw_buffer, FRAME_SIZE * sizeof(int16_t), &bytes_read, portMAX_DELAY);
        
        // 预处理:静音检测 + AGC
        audio_frame_info_t info = audio_preprocess_frame(raw_buffer);
        
        // 若为语音帧,将raw_buffer送入KWS/ASR引擎
        if (info.is_speech) {
            // 此处调用你的语音识别处理函数
            // process_speech_frame(raw_buffer, FRAME_SIZE);
        }
    }
}

验证方法

通过条件:

  1. 将麦克风对准环境噪声(无语音),观察info.is_speech应为false,且输出数据全为0。
  2. 距离麦克风10cm说“你好”,info.is_speech应为true,且输出数据峰值接近8000(原始峰值较低时会被放大)。
  3. 用串口打印每帧的energy、zcr和gain值,确认语音帧增益在1.0~20.0之间,静音帧增益为0。

调试建议:若语音帧被误判为静音,适当降低ENERGY_THRESHOLD;若环境噪声触发语音检测,提高阈值或检查ZCR范围。AGC增益上限MAX_GAIN防止弱信号被过度放大导致噪声同步增强。

预处理流程与判定逻辑 PCM帧输入 能量计算 过零率计算 能量>阈值 且ZCR适中? 是(语音) AGC增益归一化 输出至识别器 否(静音) 输出全零(静音帧)

环境依赖:ESP-IDF v4.4及以上,I2S驱动已正确初始化(采样率16kHz、16bit、单声道)。若使用其他采样率,需同步调整FRAME_SIZE(保持20ms帧长)。