4. 基于谱熵的语音活动检测优化

任务目标

在噪声环境下,能量VAD(语音活动检测)容易因背景噪声能量过高而失效。本案例实现基于谱熵的VAD算法,利用语音信号在频域上谱熵较低(频谱集中)而噪声谱熵较高(频谱平坦)的特性进行判别,并与能量VAD在含噪语音上做对比,同时演示平滑参数与熵阈值的调整方法。

环境依赖

需要 numpy 与 scipy。若需绘制对比图,额外安装 matplotlib(本案例不强制绘图,以输出数值指标验证)。

核心实现

import numpy as np
from scipy.io import wavfile

def frame_signal(x, fs, frame_len=0.025, frame_shift=0.010):
    """分帧加汉明窗,返回形状 (num_frames, frame_size)"""
    n = int(fs * frame_len)
    shift = int(fs * frame_shift)
    if len(x) < n:
        raise ValueError("信号长度小于单帧长度")
    num_frames = (len(x) - n) // shift + 1
    idx = np.arange(n)[None, :] + np.arange(num_frames)[:, None] * shift
    frames = x[idx]
    return frames * np.hamming(n)

def spectral_entropy_vad(frames, alpha=0.9, thr=0.85, eps=1e-8):
    """
    谱熵VAD:对每帧计算归一化谱熵,用一阶平滑后与阈值比较。
    返回 (vad_flags, entropy_smoothed)
    """
    n_frames = frames.shape[0]
    N = frames.shape[1]
    ent = np.zeros(n_frames)
    for i in range(n_frames):
        mag = np.abs(np.fft.rfft(frames[i])) + eps
        prob = mag / mag.sum()
        ent[i] = -np.sum(prob * np.log(prob)) / np.log(N // 2 + 1)  # 归一化到[0,1]
    smoothed = np.zeros_like(ent)
    smoothed[0] = ent[0]
    for i in range(1, n_frames):
        smoothed[i] = alpha * smoothed[i-1] + (1 - alpha) * ent[i]
    vad = (smoothed < thr).astype(int)
    return vad, smoothed

def energy_vad(frames, energy_thr_ratio=1.5):
    """能量VAD:帧能量 > 全局平均能量 * ratio 判为语音"""
    energy = np.sum(frames ** 2, axis=1)
    thr = energy.mean() * energy_thr_ratio
    return (energy > thr).astype(int), energy

def compute_metrics(vad, ground_truth):
    """返回 (准确率, 语音帧召回率, 非语音帧精确率)"""
    correct = np.mean(vad == ground_truth)
    speech_idx = ground_truth == 1
    recall = np.sum(vad[speech_idx] == 1) / max(1, np.sum(speech_idx))
    noise_idx = ground_truth == 0
    noise_prec = np.sum(vad[noise_idx] == 0) / max(1, np.sum(noise_idx))
    return correct, recall, noise_prec

if __name__ == "__main__":
    # 生成测试信号:1s干净语音(8kHz正弦串)+ 白噪声,SNR=5dB
    fs = 8000
    t = np.arange(fs) / fs
    clean = np.sin(2 * np.pi * 300 * t) * (t < 0.5) + 0.5 * np.sin(2 * np.pi * 600 * t) * (t >= 0.5)
    noise = np.random.default_rng(0).normal(0, 1, fs)
    noise_pwr = np.mean(noise ** 2)
    clean_pwr = np.mean(clean ** 2)
    noise_scaled = noise * np.sqrt(clean_pwr / (10 ** (5 / 10) * noise_pwr))
    noisy = clean + noise_scaled

    # 真实标签(每帧)
    frames = frame_signal(noisy, fs)
    gt_frames = frame_signal(clean, fs)
    ground_truth = (np.mean(gt_frames ** 2, axis=1) > 1e-4).astype(int)

    # 谱熵VAD
    vad_spec, ent_s = spectral_entropy_vad(frames, alpha=0.9, thr=0.85)
    # 能量VAD
    vad_eng, eng = energy_vad(frames, energy_thr_ratio=1.5)

    # 对比指标
    m_spec = compute_metrics(vad_spec, ground_truth)
    m_eng = compute_metrics(vad_eng, ground_truth)
    print(f"谱熵VAD  准确率={m_spec[0]:.3f} 语音召回={m_spec[1]:.3f} 噪声精确={m_spec[2]:.3f}")
    print(f"能量VAD  准确率={m_eng[0]:.3f} 语音召回={m_eng[1]:.3f} 噪声精确={m_eng[2]:.3f}")
    print(f"谱熵平滑后范围: [{ent_s.min():.3f}, {ent_s.max():.3f}]")

运行与验证

  1. 将代码保存为 spectral_entropy_vad.py,在命令行执行 python spectral_entropy_vad.py。
  2. 程序自动生成5dB信噪比的含噪语音(前0.5s为300Hz,后0.5s为600Hz),并构造真实帧标签。
  3. 观察输出:谱熵VAD的准确率与噪声精确率应明显高于能量VAD(能量VAD在噪声段易误判为语音)。
  4. 判定通过条件:谱熵VAD的准确率 > 0.85 且噪声精确率 > 0.8,同时能量VAD的噪声精确率低于谱熵VAD至少0.15。

参数调整说明

  • 平滑系数 alpha:增大(如0.95)使谱熵轨迹更平滑,减少短促噪声误触发,但会略微延迟语音起止点;减小(如0.7)响应更快但易产生毛刺。
  • 熵阈值 thr:语音帧谱熵通常低于0.7,纯噪声接近0.9~1.0。若噪声强可适当降低阈值(如0.8),若语音本身频谱平坦(如摩擦音)则需提高阈值(如0.9)。
  • 调整后重新运行,观察准确率与召回率的变化趋势是否符合上述规律。
0 时间 (帧序号) 谱熵值 阈值 thr 噪声段(谱熵高) 语音段(谱熵低) 平滑后谱熵曲线 能量VAD在此处易误判

上图示意谱熵VAD的核心判别逻辑:噪声段谱熵值高且平坦,语音段因频谱集中而出现明显下凹,通过阈值即可区分。能量VAD在噪声能量较高时无法形成类似区分。