4. 基于能量和过零率的语音端点检测(VAD)

任务说明

本案例实现语音信号分帧后的短时能量计算。输入为一段含静音段与语音段的PCM原始音频数据,程序按固定帧长和帧移分帧,对每一帧计算短时能量(帧内所有采样点的平方和),输出每帧能量值序列,并生成能量曲线SVG图,为后续端点检测提供依据。

实现方案

采用C语言实现核心算法,通过标准输入读取16kHz、16bit单声道PCM数据。帧长取256点(16ms),帧移取128点(8ms,50%重叠)。短时能量公式:E(n)=Σ[x(m)·w(n-m)]²,其中w为矩形窗(即直接对帧内采样点求平方和)。程序输出帧索引与对应能量值,同时生成SVG格式能量曲线。

0 E_max 帧索引 短时能量 阈值 语音段(高能量) 静音段 静音段

核心代码


#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倍以上,且曲线形态与音频内容对应,即视为通过。