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
验证方法
- 准备测试音频:录制一段约5秒的语音(如"你好,测试"),前后各留1秒静音,保存为16kHz/16bit/单声道PCM格式。
- 运行程序:执行上述编译后的程序,生成
vad_result.txt。 - 判定标准:打开输出文件,每行格式为
帧序号 能量 过零率 判决(1=语音,0=非语音)。预期结果:静音段(前1秒和后1秒)的帧判决为0,语音段判决为1。若语音段中个别帧因能量波动被判为0,可适当降低ENERGY_TH_LO(如调至50.0)后重新测试。
阈值调参建议
阈值与输入信号幅度强相关。若测试音频整体偏小或偏大,可先打印前几帧能量值,将ENERGY_TH_HI设为语音段典型能量的1/2,ENERGY_TH_LO设为静音段典型能量的2倍。ZCR阈值通常无需调整。