4. 基于时域能量的语音活动检测(VAD)

任务目标

实现一个基于短时能量和过零率(ZCR)的时域VAD算法,用于区分语音段与非语音段(静音/噪声)。算法将逐帧处理PCM音频数据,输出每帧的语音/非语音判决结果。

核心原理

语音段通常具有较高的短时能量和适中的过零率,而静音段能量极低,纯噪声段过零率往往偏高或波动剧烈。本案例采用双门限判决:能量主判(能量高于阈值则判为语音),ZCR辅助(能量处于模糊区时,用ZCR是否落在合理语音范围内来修正判决)。

完整实现

// vad_time_domain.h
#ifndef VAD_TIME_DOMAIN_H
#define VAD_TIME_DOMAIN_H
#include <stdint.h>
#include <stdbool.h>

#define FRAME_SIZE   160    // 10ms @16kHz
#define ENERGY_TH_HI 1000.0f // 能量高门限(语音肯定)
#define ENERGY_TH_LO 100.0f  // 能量低门限(静音肯定)
#define ZCR_TH_HI    0.35f   // 过零率高门限(噪声/清音)
#define ZCR_TH_LO    0.05f   // 过零率低门限(浊音)

typedef struct {
    float energy;
    float zcr;
    bool is_speech;
} VADFrameResult;

// 计算单帧短时能量(归一化到每样本)
float compute_energy(const int16_t *frame, uint32_t len);

// 计算单帧过零率(每样本平均过零次数)
float compute_zcr(const int16_t *frame, uint32_t len);

// 时域VAD判决:返回语音/非语音
bool vad_decision(float energy, float zcr);

// 便捷接口:输入一帧PCM,输出判决结果
VADFrameResult vad_process_frame(const int16_t *frame, uint32_t len);

#endif // VAD_TIME_DOMAIN_H
// vad_time_domain.c
#include "vad_time_domain.h"
#include <math.h>

float compute_energy(const int16_t *frame, uint32_t len) {
    double sum = 0.0;
    for (uint32_t i = 0; i < len; i++) {
        double sample = (double)frame[i] / 32768.0; // 归一化到[-1,1]
        sum += sample * sample;
    }
    return (float)(sum / len);
}

float compute_zcr(const int16_t *frame, uint32_t len) {
    uint32_t crossings = 0;
    for (uint32_t i = 1; i < len; i++) {
        // 符号变化即为过零
        if ((frame[i-1] >= 0 && frame[i] < 0) ||
            (frame[i-1] < 0 && frame[i] >= 0)) {
            crossings++;
        }
    }
    return (float)crossings / (float)(len - 1);
}

bool vad_decision(float energy, float zcr) {
    if (energy > ENERGY_TH_HI) {
        return true;   // 高能量,肯定为语音
    }
    if (energy < ENERGY_TH_LO) {
        return false;  // 低能量,肯定为静音
    }
    // 能量处于中间模糊区,用ZCR辅助判断
    // 语音浊音段ZCR较低,清音/噪声ZCR较高
    return (zcr >= ZCR_TH_LO && zcr <= ZCR_TH_HI);
}

VADFrameResult vad_process_frame(const int16_t *frame, uint32_t len) {
    VADFrameResult result;
    result.energy = compute_energy(frame, len);
    result.zcr = compute_zcr(frame, len);
    result.is_speech = vad_decision(result.energy, result.zcr);
    return result;
}
// main.c 示例调用:读取16kHz/16bit单声道PCM文件,逐帧VAD
#include <stdio.h>
#include <stdlib.h>
#include "vad_time_domain.h"

int main(int argc, char *argv[]) {
    if (argc != 3) {
        printf("用法: %s input.pcm output.txt\n", argv[0]);
        return 1;
    }
    FILE *fin = fopen(argv[1], "rb");
    FILE *fout = fopen(argv[2], "w");
    if (!fin || !fout) { perror("文件打开失败"); return 1; }

    int16_t frame[FRAME_SIZE];
    size_t read_cnt;
    uint32_t frame_idx = 0;

    while ((read_cnt = fread(frame, sizeof(int16_t), FRAME_SIZE, fin)) == FRAME_SIZE) {
        VADFrameResult res = vad_process_frame(frame, FRAME_SIZE);
        fprintf(fout, "%u %.4f %.4f %d\n",
                frame_idx++, res.energy, res.zcr, res.is_speech);
    }
    printf("处理完成,共 %u 帧\n", frame_idx);
    fclose(fin);
    fclose(fout);
    return 0;
}

编译与运行

# 编译(Linux/macOS)
gcc -O2 -o vad_demo main.c vad_time_domain.c -lm

# 运行(需准备16kHz/16bit/单声道PCM文件)
./vad_demo speech.pcm vad_result.txt

验证方法

  1. 准备测试音频:录制一段约5秒的语音(如"你好,测试"),前后各留1秒静音,保存为16kHz/16bit/单声道PCM格式。
  2. 运行程序:执行上述编译后的程序,生成vad_result.txt。
  3. 判定标准:打开输出文件,每行格式为帧序号 能量 过零率 判决(1=语音,0=非语音)。预期结果:静音段(前1秒和后1秒)的帧判决为0,语音段判决为1。若语音段中个别帧因能量波动被判为0,可适当降低ENERGY_TH_LO(如调至50.0)后重新测试。

阈值调参建议

阈值与输入信号幅度强相关。若测试音频整体偏小或偏大,可先打印前几帧能量值,将ENERGY_TH_HI设为语音段典型能量的1/2,ENERGY_TH_LO设为静音段典型能量的2倍。ZCR阈值通常无需调整。