4. 基于能量和过零率的语音端点检测(VAD)
任务说明
本案例实现语音信号分帧后的短时能量计算。输入为一段含静音段与语音段的PCM原始音频数据,程序按固定帧长和帧移分帧,对每一帧计算短时能量(帧内所有采样点的平方和),输出每帧能量值序列,并生成能量曲线SVG图,为后续端点检测提供依据。
实现方案
采用C语言实现核心算法,通过标准输入读取16kHz、16bit单声道PCM数据。帧长取256点(16ms),帧移取128点(8ms,50%重叠)。短时能量公式:E(n)=Σ[x(m)·w(n-m)]²,其中w为矩形窗(即直接对帧内采样点求平方和)。程序输出帧索引与对应能量值,同时生成SVG格式能量曲线。
核心代码
#include <stdio.h>
#include <stdint.h>
#include <math.h>
#define FRAME_LEN 256 // 帧长:256点 @16kHz = 16ms
#define FRAME_SHIFT 128 // 帧移:128点 = 8ms(50%重叠)
#define SAMPLE_RATE 16000
// 计算单帧短时能量(矩形窗,直接平方和)
double frame_energy(const int16_t *frame, int len) {
double sum = 0.0;
for (int i = 0; i < len; i++) {
sum += (double)frame[i] * (double)frame[i];
}
return sum;
}
int main(void) {
int16_t pcm[FRAME_LEN];
int frame_idx = 0;
size_t read_cnt;
double max_energy = 0.0;
double energy_buf[4096]; // 最多存储4096帧能量
int total_frames = 0;
// 读取第一帧
read_cnt = fread(pcm, sizeof(int16_t), FRAME_LEN, stdin);
if (read_cnt < FRAME_LEN) {
fprintf(stderr, "数据不足一帧\n");
return 1;
}
// 循环分帧处理(重叠帧通过保留尾部数据实现)
while (read_cnt == FRAME_LEN) {
double e = frame_energy(pcm, FRAME_LEN);
energy_buf[total_frames] = e;
if (e > max_energy) max_energy = e;
total_frames++;
// 帧移:将尾部FRAME_SHIFT点移到头部,再读入新数据
for (int i = 0; i < FRAME_SHIFT; i++) {
pcm[i] = pcm[i + FRAME_SHIFT];
}
read_cnt = fread(pcm + FRAME_SHIFT, sizeof(int16_t),
FRAME_SHIFT, stdin);
if (read_cnt < FRAME_SHIFT) break; // 不足一帧移则结束
frame_idx++;
}
// 输出能量序列(文本格式:帧号 能量值)
printf("# 帧号\t短时能量\n");
for (int i = 0; i < total_frames; i++) {
printf("%d\t%.2f\n", i, energy_buf[i]);
}
// 生成SVG能量曲线(归一化到0-200高度)
if (total_frames > 0) {
FILE *fp = fopen("energy_curve.svg", "w");
if (!fp) { perror("fopen"); return 1; }
int width = 800, height = 300;
int margin_l = 80, margin_r = 40, margin_t = 30, margin_b = 50;
int plot_w = width - margin_l - margin_r;
int plot_h = height - margin_t - margin_b;
fprintf(fp, "<svg xmlns='http://www.w3.org/2000/svg' width='%d' height='%d'>\n",
width, height);
fprintf(fp, "<rect width='100%%' height='100%%' fill='#f8f9fa'/>\n");
// 坐标轴
fprintf(fp, "<line x1='%d' y1='%d' x2='%d' y2='%d' stroke='#333' stroke-width='2'/>\n",
margin_l, height-margin_b, width-margin_r, height-margin_b);
fprintf(fp, "<line x1='%d' y1='%d' x2='%d' y2='%d' stroke='#333' stroke-width='2'/>\n",
margin_l, margin_t, margin_l, height-margin_b);
// 阈值线(示例:最大能量的10%)
double thresh = max_energy * 0.1;
int thresh_y = margin_t + (int)(plot_h * (1.0 - thresh/max_energy));
fprintf(fp, "<line x1='%d' y1='%d' x2='%d' y2='%d' stroke='#1976d2' stroke-width='1.5' stroke-dasharray='6,3'/>\n",
margin_l, thresh_y, width-margin_r, thresh_y);
// 绘制能量曲线
fprintf(fp, "<polyline points='");
for (int i = 0; i < total_frames; i++) {
double norm = energy_buf[i] / max_energy;
int x = margin_l + (int)((double)i / (total_frames-1) * plot_w);
int y = margin_t + (int)(plot_h * (1.0 - norm));
fprintf(fp, "%d,%d ", x, y);
}
fprintf(fp, "' fill='none' stroke='#d32f2f' stroke-width='2.5' stroke-linejoin='round'/>\n");
fprintf(fp, "</svg>\n");
fclose(fp);
fprintf(stderr, "已生成 energy_curve.svg(共%d帧)\n", total_frames);
}
return 0;
}
编译与运行
# 编译
gcc -o energy_calc energy_calc.c -lm
# 准备测试音频(生成1秒含语音段的PCM:0~0.3s静音,0.3~0.7s正弦波,0.7~1s静音)
python3 -c "
import math, struct
fs = 16000
with open('test.pcm', 'wb') as f:
for n in range(fs):
if 0.3*fs <= n < 0.7*fs:
val = int(8000 * math.sin(2*math.pi*440*n/fs))
else:
val = 0
f.write(struct.pack('<h', val))
"
# 运行
./energy_calc < test.pcm
验证方法
运行程序后,观察终端输出的帧号与能量值:前约24帧(0~0.3s静音段)能量应接近0,中间约32帧(0.3~0.7s语音段)能量显著增大,后24帧能量回落。同时打开生成的energy_curve.svg,应看到曲线呈现“低—高—低”三段形态,与输入音频的静音/语音分布一致。判定标准:语音段帧能量至少为静音段能量的10倍以上,且曲线形态与音频内容对应,即视为通过。