4. 时域分析基础:短时能量、过零率与VAD

时域分析,说白了就是直接对着波形下手。不搞傅里叶变换,不看频谱,就看信号幅度随时间怎么变。我刚开始做语音处理时,觉得时域太简单了,后来才发现——简单的东西往往最实用。

这一章我们聊三个核心概念:短时能量、过零率(ZCR),以及它们怎么组合起来做静音检测和端点检测(VAD)。这些都是音频算法的基本功,你想想看,如果连语音从哪里开始、到哪里结束都搞不定,后面的识别、增强、压缩全白搭。

核心观点: 时域特征是音频信号最直接的“指纹”。短时能量告诉你声音有多响,过零率告诉你声音的“粗糙程度”,两者结合就能判断一段音频里到底有没有人说话。
时域分析基础 — 知识体系 时域分析 短时能量 过零率 (ZCR) 静音检测 / VAD 分帧 → 平方 → 求和 符号变化计数 / 帧长 双门限法 音量检测 / 端点粗定位 清浊音区分 / 噪声判断 语音活动检测 (VAD) 三者组合:能量 + ZCR + 门限 = 经典VAD方案

4.1 短时能量:声音有多“响”

短时能量的计算思路很简单:把音频切成一小段一小段(分帧),对每一帧里的采样点求平方和。为什么平方?因为幅度有正有负,直接求和会抵消,平方后能量就都是正的了。

公式长这样:

E(i) = Σ [x(n) * w(n)]²   (n 从 0 到 N-1)

其中 x(n) 是采样点,w(n) 是窗函数,N 是帧长。我习惯用汉明窗,旁瓣衰减好一些。

我的经验: 帧长一般取 20-30ms。太短了能量抖动大,太长了又看不清细节。采样率 16kHz 时,帧长 512 点(32ms)是个不错的起点。

短时能量能干什么?最直接的应用就是判断一段音频里有没有声音。安静环境下的背景噪声能量很低,人说话时能量会明显跳起来。但有个坑——能量对幅度敏感,同样的语音,离麦克风近和远,能量差好几倍。所以单纯用能量做门限,鲁棒性不够。

4.2 过零率(ZCR):声音有多“糙”

过零率统计一帧内信号穿过零轴的次数。说白了就是看波形上下抖得有多频繁。

计算公式:

ZCR(i) = (1/2N) * Σ |sign[x(n)] - sign[x(n-1)]|

sign 是取符号函数,正值返回 1,负值返回 -1。相邻两个采样点符号不同,说明过零了一次。

浊音(比如元音 a、o、e)的过零率低,因为声带振动产生的是低频信号。清音(比如 s、sh、f)的过零率高,因为摩擦噪声集中在高频。我做过一个项目,用 ZCR 区分语音里的清浊音,准确率还不错。

信号类型 典型 ZCR 范围 能量特征 典型场景
静音/背景噪声 0.1 - 0.3 低 安静房间
浊音(元音) 0.05 - 0.15 高 说话时的元音段
清音(摩擦音) 0.4 - 0.7 中低 s、sh、f 等音
音乐/复杂噪声 0.2 - 0.5 变化大 背景音乐、键盘声
避坑指南: 我曾经在 8kHz 采样率的电话语音上直接用 ZCR 做端点检测,结果发现静音段的 ZCR 比说话段还高——因为电话线路有 50Hz 工频干扰,波形一直在零轴附近抖动。后来加了高通滤波(截止频率 100Hz),问题才解决。

4.3 静音检测与端点检测(VAD)

VAD 的全称是 Voice Activity Detection,说白了就是判断“现在有没有人说话”。这是语音处理里最基础也最关键的模块之一。你想想看,语音识别系统如果不知道什么时候开始说话,那它就得一直听着,浪费算力不说,还容易把噪声当成语音。

经典的 VAD 方案就是能量 + ZCR 双门限法。我画个流程图你就明白了:

双门限 VAD 流程图 输入音频帧 计算短时能量 E 和 ZCR E > T1? 标记为语音段 ZCR > T2? 标记为静音/噪声

双门限法的核心逻辑:

  1. 能量门限 T1:能量高于 T1,大概率是语音(尤其是浊音)。
  2. ZCR 门限 T2:能量低但 ZCR 高,可能是清音(比如句子开头的 s 音)。
  3. 两者都低:静音或背景噪声。

实际实现时,我还会加一个“滞后”机制——检测到语音后,连续几帧都低于门限才判定结束。这样能避免说话中间换气时被误判为静音。

关键参数:
  • 能量门限 T1:取前 10 帧(静音段)平均能量的 2-3 倍
  • ZCR 门限 T2:取静音段平均 ZCR 的 1.5 倍
  • 最小语音长度:通常 50ms 以下认为是噪声毛刺
  • 挂起帧数:语音结束后等待 5-10 帧再切回静音

4.4 代码实现:一个简单的 VAD

下面是我写的一个轻量级 VAD 实现,用 Python 写的,核心逻辑不到 30 行。你可以在自己的数据上试试。

import numpy as np

def simple_vad(audio, sr=16000, frame_len=512, hop_len=256,
               energy_thresh=0.01, zcr_thresh=0.15, hangover=5):
    """
    简单的双门限 VAD
    audio: 一维 numpy 数组
    sr: 采样率
    frame_len: 帧长(采样点数)
    hop_len: 帧移
    energy_thresh: 能量门限(归一化后)
    zcr_thresh: 过零率门限
    hangover: 挂起帧数
    """
    # 分帧
    frames = []
    for start in range(0, len(audio) - frame_len, hop_len):
        frames.append(audio[start:start+frame_len])
    frames = np.array(frames)
    
    # 计算能量和 ZCR
    energy = np.sum(frames ** 2, axis=1)
    energy = energy / np.max(energy)  # 归一化
    
    zcr = np.sum(np.abs(np.diff(np.sign(frames), axis=1)), axis=1) / (2 * frame_len)
    
    # 双门限判决
    is_speech = np.zeros(len(frames), dtype=bool)
    for i in range(len(frames)):
        if energy[i] > energy_thresh or zcr[i] > zcr_thresh:
            is_speech[i] = True
    
    # 挂起处理:语音结束后保持 hangover 帧
    for i in range(1, len(is_speech)):
        if is_speech[i] == False and is_speech[i-1] == True:
            for j in range(i, min(i+hangover, len(is_speech))):
                is_speech[j] = True
    
    return is_speech
使用建议: 这段代码适合干净环境下的语音。如果背景有风扇声、空调声,建议先做高通滤波(截止频率 80-100Hz),把低频噪声去掉,ZCR 会更稳定。

4.5 实际项目中的坑与对策

做 VAD 这么多年,我踩过的坑不少。挑几个典型的说说:

  • 噪声突变:关门声、拍桌子声,能量和 ZCR 都会跳变。对策是加一个“最小语音长度”约束,短于 50ms 的突发信号直接忽略。
  • 弱语音:离麦克风远的人说话,能量可能和背景噪声差不多。这时候 ZCR 就派上用场了——清音的 ZCR 通常比噪声高。
  • 音乐背景:音乐的能量和 ZCR 变化都很大,容易误判。我做过一个方案,加了一个“频谱平坦度”特征,音乐和语音的平坦度差异明显。
  • 自适应门限:固定门限在安静环境好用,换到嘈杂环境就废了。我习惯用前几帧做背景估计,动态更新门限。
我曾经踩过的坑: 有一次做车载语音系统,车速一快,风噪把 ZCR 抬得老高,VAD 一直处于“语音”状态。后来发现风噪的 ZCR 虽然高,但能量很低,而且频谱集中在低频。我加了一个“能量-频率联合判决”,才把问题解决。所以你看,单靠能量和 ZCR 是不够的,实际项目里往往要加更多特征。

4.6 小结

时域分析是音频算法的基础,短时能量和过零率虽然简单,但组合起来能解决很多实际问题。VAD 更是语音处理的第一道关卡——做不好 VAD,后面的识别、增强、压缩全都会受影响。

我个人建议,刚开始做音频处理时,先把时域特征玩透。能量、ZCR、自相关这些基础特征,能帮你理解信号的本质。等这些基础打牢了,再去看频域、倒谱域的东西,会轻松很多。