3. 能量门限与过零率联合静音检测

任务目标

实现一个前端语音活动检测(VAD)函数,利用短时能量和过零率(ZCR)的双门限联合判断,从连续音频流中区分静音帧与语音帧。该检测器可作为关键词唤醒系统的前置模块,用于降低后端计算功耗。

核心原理

静音帧通常具有低短时能量和相对稳定的过零率;语音帧(尤其是清音/摩擦音)能量较高或过零率波动明显。本案例采用双门限策略:

  • 帧能量低于能量低门限 E_low 且过零率低于ZCR门限 Z_th → 判为静音
  • 帧能量高于能量高门限 E_high → 判为语音(浊音段)
  • 介于两者之间时,利用过零率辅助判断(清音段ZCR较高)

完整实现

import numpy as np

def compute_energy(frame):
    """计算帧短时能量(归一化)"""
    return np.mean(frame ** 2)

def compute_zcr(frame):
    """计算帧过零率(单位:次/样本)"""
    if len(frame) < 2:
        return 0.0
    signs = np.signbit(frame)
    return np.mean(signs[1:] != signs[:-1])

def vad_double_threshold(audio, sr=16000, frame_len_ms=25, frame_shift_ms=10,
                         E_low=0.001, E_high=0.01, Z_th=0.3):
    """
    基于短时能量和过零率的双门限VAD
    参数:
        audio: 一维float数组,范围[-1, 1]
        sr: 采样率
        frame_len_ms / frame_shift_ms: 帧长/帧移
        E_low / E_high: 能量低/高门限
        Z_th: 过零率门限
    返回:
        vad_flags: bool数组,True表示语音帧
    """
    frame_len = int(sr * frame_len_ms / 1000)
    frame_shift = int(sr * frame_shift_ms / 1000)
    n_frames = (len(audio) - frame_len) // frame_shift + 1
    if n_frames <= 0:
        return np.array([], dtype=bool)

    vad_flags = np.zeros(n_frames, dtype=bool)
    for i in range(n_frames):
        start = i * frame_shift
        frame = audio[start:start + frame_len]
        energy = compute_energy(frame)
        zcr = compute_zcr(frame)

        if energy > E_high:
            vad_flags[i] = True          # 浊音/高能量段
        elif energy > E_low and zcr > Z_th:
            vad_flags[i] = True          # 清音/摩擦音段(低能量但高ZCR)
        # 其余情况保持False(静音)

    return vad_flags

# ---- 最小验证示例 ----
if __name__ == "__main__":
    sr = 16000
    t = np.arange(sr * 1.0) / sr  # 1秒音频

    # 构造测试信号:0.2s静音 + 0.4s语音(带清音成分) + 0.4s静音
    silence = np.zeros(int(0.2 * sr))
    voiced = 0.5 * np.sin(2 * np.pi * 200 * t[:int(0.4 * sr)])
    # 叠加高频噪声模拟清音(高ZCR)
    unvoiced = 0.05 * np.sin(2 * np.pi * 3000 * t[:int(0.4 * sr)])
    speech = voiced + unvoiced
    tail_silence = np.zeros(int(0.4 * sr))

    test_audio = np.concatenate([silence, speech, tail_silence])
    flags = vad_double_threshold(test_audio)

    # 统计各段语音帧比例(预期:中间段比例高,两端接近0)
    mid_start = int(0.2 * sr / 160)  # 帧移10ms=160样本
    mid_end = int(0.6 * sr / 160)
    voiced_ratio = np.mean(flags[mid_start:mid_end])
    silence_ratio = np.mean(flags[:mid_start])
    print(f"语音段激活帧比例: {voiced_ratio:.2f}")
    print(f"静音段激活帧比例: {silence_ratio:.2f}")
    print(f"总帧数: {len(flags)}")

运行与验证

  1. 依赖:仅需 numpy(pip install numpy)
  2. 运行:直接执行脚本 python vad_demo.py
  3. 通过条件:观察输出——语音段激活帧比例应明显高于静音段(例如语音段>0.8,静音段<0.1)。若比例接近,可调整 E_low/E_high/Z_th 门限适配实际音频范围

关键参数调优建议

  • 能量门限:先计算纯静音段平均能量作为 E_low 基准,E_high 设为该值的5~10倍
  • ZCR门限:清音/摩擦音ZCR通常>0.3,浊音段通常<0.1,静音接近0
  • 帧长/帧移:25ms/10ms为语音处理常用配置,唤醒场景可适当增大帧移至20ms降低计算量

常见问题

  • 环境噪声导致误检:建议先做高通滤波(截止频率80~100Hz)去除直流和低频哼声
  • 门限不通用:不同麦克风增益下能量范围差异大,可在运行时用前200ms估计噪声底,动态更新门限
  • 端点毛刺:可在帧级判决后增加中值滤波(窗口3~5帧)平滑VAD输出