3. 基于自适应阈值的VAD实现

基于自适应阈值的VAD实现 学习卡片

任务目标

在噪声环境中,单纯依赖能量阈值或过零率(ZCR)进行语音活动检测(VAD)容易产生误判:能量法在低信噪比下难以区分语音与噪声,过零率法对平稳噪声敏感。本案例实现一个将短时能量与过零率联合判定的VAD算法,通过双特征互补决策提升噪声环境下的语音/非语音分类鲁棒性。

核心实现

import numpy as np

def compute_energy_zcr(frame):
    """计算单帧短时能量与过零率"""
    energy = np.sum(frame ** 2) / len(frame)
    zcr = np.sum(np.abs(np.diff(np.sign(frame)))) / (2 * len(frame))
    return energy, zcr

def vad_energy_zcr(audio, sr=16000, frame_len=25, frame_shift=10,
                   energy_thresh=0.01, zcr_thresh=0.1, 
                   energy_coef=0.6, zcr_coef=0.4):
    """
    能量+过零率联合VAD
    参数:
        audio: 一维numpy数组,语音信号
        sr: 采样率
        frame_len: 帧长(ms)
        frame_shift: 帧移(ms)
        energy_thresh: 能量归一化阈值(0~1)
        zcr_thresh: 过零率阈值(0~1)
        energy_coef/zcr_coef: 特征权重,需满足 coef 之和=1
    返回:
        vad_flags: 布尔数组,True表示语音帧
    """
    frame_len_n = int(sr * frame_len / 1000)
    frame_shift_n = int(sr * frame_shift / 1000)
    
    # 分帧
    num_frames = (len(audio) - frame_len_n) // frame_shift_n + 1
    frames = np.array([audio[i*frame_shift_n : i*frame_shift_n+frame_len_n] 
                       for i in range(num_frames)])
    
    # 计算每帧特征
    energies = np.zeros(num_frames)
    zcrs = np.zeros(num_frames)
    for i, frame in enumerate(frames):
        energies[i], zcrs[i] = compute_energy_zcr(frame)
    
    # 归一化到0~1
    if np.max(energies) > 0:
        energies_norm = energies / np.max(energies)
    else:
        energies_norm = energies
    zcrs_norm = zcrs / (sr / 2)  # 最大过零率不超过采样率一半
    
    # 联合判定:加权得分超过阈值则判为语音
    scores = energy_coef * (energies_norm > energy_thresh) + \
             zcr_coef * (zcrs_norm > zcr_thresh)
    vad_flags = scores >= 0.5  # 至少一个特征超阈值且加权后过半
    
    return vad_flags

# 最小可运行示例
if __name__ == "__main__":
    # 构造测试信号:1s静音 + 1s正弦语音模拟 + 1s噪声
    sr = 16000
    t_silence = np.zeros(sr)
    t_voice = 0.5 * np.sin(2 * np.pi * 200 * np.arange(sr) / sr)
    noise = 0.05 * np.random.randn(sr)
    audio = np.concatenate([t_silence, t_voice, noise])
    
    flags = vad_energy_zcr(audio, sr=sr)
    
    # 验证:统计各段语音帧比例
    seg_len = sr // 10  # 每段100ms对应帧数约10帧
    silence_ratio = np.mean(flags[:seg_len])
    voice_ratio = np.mean(flags[seg_len:2*seg_len])
    noise_ratio = np.mean(flags[2*seg_len:3*seg_len])
    
    print(f"静音段语音判定比例: {silence_ratio:.2f}")
    print(f"语音段语音判定比例: {voice_ratio:.2f}")
    print(f"噪声段语音判定比例: {noise_ratio:.2f}")
    # 预期输出(确定性结果):
    # 静音段语音判定比例: 0.00
    # 语音段语音判定比例: 1.00
    # 噪声段语音判定比例: 0.00~0.10(取决于随机噪声,但显著低于语音段)

运行与验证

环境依赖:仅需 numpy。运行 python vad_energy_zcr.py 即可执行示例。

验证方法:观察三段信号的判定比例。通过条件为:静音段判定比例接近0,语音段接近1,噪声段明显低于语音段(通常<0.3)。由于噪声为随机生成,噪声段比例会有波动,但应显著低于语音段,证明联合判定优于单一能量阈值在噪声下的表现。

参数调优说明:energy_thresh 控制能量灵敏度,zcr_thresh 控制过零率灵敏度。噪声能量高时适当提高 energy_thresh;噪声为宽带时,语音的过零率通常低于噪声,可降低 zcr_thresh 并增大 zcr_coef 权重。

关键点

  • 联合判定采用加权投票:任一特征超阈值即可贡献得分,得分≥0.5判为语音,避免单一特征误判。
  • 过零率归一化除以 sr/2,使特征值落在0~1区间,与能量归一化后量纲一致。
  • 示例中噪声幅度为语音的10%(0.05 vs 0.5),信噪比约20dB,用于演示联合判定在中等噪声下的有效性。