4. 时域特征提取

各位同学,欢迎来到时域特征提取的第一讲。

说实话,很多做语音的同行一上来就扎进频谱、MFCC里,觉得时域特征太简单、不够"高级"。但我个人经验告诉我——时域特征是声学分析的基石。你想想看,一个端点检测做不好,后面提取再花哨的特征也是白搭。我在做语音唤醒项目时,就吃过这个亏:模型在安静环境下跑得挺好,一到嘈杂的办公室就疯狂误触发。后来排查发现,问题就出在VAD(语音活动检测)的时域阈值设得太粗糙。

所以,咱们今天就把这几个最基础的时域特征彻底吃透:短时能量(STE)、短时平均幅度、过零率(ZCR),以及它们如何组合起来做静音检测和端点检测。

4.1 为什么需要"短时"分析?

语音信号是非平稳的。但有一个关键假设:在10~30毫秒这个尺度上,信号是平稳的。这就是"短时"处理的理论基础。

我习惯把语音信号想象成一条蜿蜒的河流。整条河的水流变化无常,但如果你只看一小段河面,它的流速、波纹几乎是稳定的。我们用一个固定长度的窗口(比如25毫秒)去截取信号,这个窗口就叫帧。然后对每一帧单独计算能量、幅度、过零率等特征。

核心概念:短时分析 = 加窗 + 分帧 + 逐帧计算特征。

帧长通常取20~30ms,帧移取帧长的一半(10~15ms),保证帧与帧之间有重叠,避免信息丢失。

4.2 短时能量(Short-Time Energy, STE)

短时能量,说白了就是衡量一帧信号有多"响"。

4.2.1 数学原理

对于第 \( n \) 帧信号 \( x_n(m) \),其短时能量定义为:

E_n = Σ_{m=0}^{N-1} [x_n(m)]²

其中 \( N \) 是帧长。注意,这里是对信号值平方再求和。为什么要平方?两个原因:

  • 保证能量值为正(信号有正有负,直接求和会正负抵消)
  • 放大高幅值样本的贡献(响的声音在能量上会显著高于轻的声音)

4.2.2 Python实现

实现起来其实很简单。我一般用NumPy的向量化操作,比for循环快一个数量级。

import numpy as np

def short_time_energy(signal, frame_size, hop_size):
    """
    计算短时能量
    :param signal: 输入信号 (1D array)
    :param frame_size: 帧长(样本点数)
    :param hop_size: 帧移(样本点数)
    :return: 能量序列
    """
    num_frames = (len(signal) - frame_size) // hop_size + 1
    energy = np.zeros(num_frames)
    
    for i in range(num_frames):
        start = i * hop_size
        end = start + frame_size
        frame = signal[start:end]
        energy[i] = np.sum(frame ** 2)
    
    return energy

避坑指南:我曾经在计算能量时忘记检查帧尾是否越界。如果信号长度不是帧移的整数倍,最后一帧可能会访问到不存在的样本。建议在循环前用 min(end, len(signal)) 做截断,或者直接丢弃不完整的最后一帧。

4.3 短时平均幅度

短时能量有个小缺点:它对信号值做了平方,导致动态范围太大。安静环境和说话声的能量可能差好几个数量级,画图时很难看清细节。

短时平均幅度就是来解决这个问题的。它用绝对值代替平方:

M_n = (1/N) * Σ_{m=0}^{N-1} |x_n(m)|

说白了,就是取一帧内所有样本的绝对值,然后求平均。这样既保留了能量信息,又压缩了动态范围。

4.3.1 代码实现

def short_time_magnitude(signal, frame_size, hop_size):
    num_frames = (len(signal) - frame_size) // hop_size + 1
    magnitude = np.zeros(num_frames)
    
    for i in range(num_frames):
        start = i * hop_size
        end = start + frame_size
        frame = signal[start:end]
        magnitude[i] = np.mean(np.abs(frame))
    
    return magnitude

嗯,这里要注意:平均幅度对噪声更敏感。如果背景有持续的底噪,幅度值会整体抬升,不像能量那样可以通过平方操作把弱噪声"压下去"。所以做静音检测时,我通常优先用能量,而不是幅度。

4.4 过零率(Zero-Crossing Rate, ZCR)

过零率,顾名思义,就是一帧内信号穿过零轴的次数。

为什么这个特征有用?因为不同声音的过零率差异很大:

  • 清音(如/s/、/f/):频率高、波形振荡快,过零率很高
  • 浊音(如/a/、/i/):频率低、波形平滑,过零率较低
  • 静音/噪声:过零率通常介于两者之间,但噪声的过零率往往更随机

4.4.1 数学定义

Z_n = (1/2) * Σ_{m=1}^{N-1} |sgn[x_n(m)] - sgn[x_n(m-1)]|

其中 \( sgn() \) 是符号函数:正值取1,负值取-1,零值取0。相邻两个样本符号不同,就说明穿过了一次零轴。

4.4.2 Python实现

def zero_crossing_rate(signal, frame_size, hop_size):
    num_frames = (len(signal) - frame_size) // hop_size + 1
    zcr = np.zeros(num_frames)
    
    for i in range(num_frames):
        start = i * hop_size
        end = start + frame_size
        frame = signal[start:end]
        # 计算符号变化
        sign_diff = np.diff(np.sign(frame))
        # 非零位置即为过零点
        zcr[i] = np.sum(np.abs(sign_diff) > 0) / 2
    
    return zcr

重要提醒:过零率对直流偏移非常敏感。如果信号有直流分量(均值不为0),过零率会严重偏低甚至为0。我建议在计算ZCR之前,先对每一帧做去直流处理:frame = frame - np.mean(frame)。

4.5 静音检测与端点检测(VAD)基础

有了能量和过零率,我们就可以做最基础的VAD了。

VAD的目标很简单:判断当前帧是语音还是非语音(静音/噪声)。

4.5.1 双门限法

这是我个人最常用的方法,也是很多工业级VAD的雏形。它同时使用能量和过零率两个特征:

  1. 能量门限:语音帧的能量通常高于静音帧。设定一个高门限 \( T_H \) 和一个低门限 \( T_L \)。
  2. 过零率门限:清音段过零率高,浊音段过零率低。设定一个过零率范围 \( [Z_{min}, Z_{max}] \)。
  3. 判决逻辑:
    • 能量 > \( T_H \) → 判定为语音
    • 能量 < \( T_L \) → 判定为静音
    • 能量介于两者之间 → 结合过零率做二次判决

4.5.2 门限如何确定?

这个问题我当年也纠结过。后来在实践中总结了一个经验:

  • 取前10帧(约200ms)作为背景噪声估计,计算这10帧的平均能量 \( E_{noise} \)
  • 高门限 \( T_H = E_{noise} \times 10 \)(经验值,可根据实际环境调整)
  • 低门限 \( T_L = E_{noise} \times 3 \)
  • 过零率门限:取前10帧的过零率均值 ± 标准差

实战技巧:我曾经在工厂车间做语音采集,背景噪声很大,固定门限完全失效。后来改用自适应门限——每过1秒重新估计一次背景噪声,效果好了很多。记住:门限不是死的,要跟着环境走。

4.6 本章知识体系

下面这张图总结了本章的核心逻辑,从信号输入到特征提取再到VAD判决的完整流程:

时域特征提取与VAD基础——知识体系图 输入语音信号 分帧 + 加窗 短时能量 (STE) 短时平均幅度 过零率 (ZCR) 特征融合:能量 + 过零率 VAD判决:语音 / 非语音 双门限法:能量高门限 T_H 能量低门限 T_L + ZCR 范围

4.7 本章小结

今天我们完成了三件事:

  • 理解了短时分析为什么是语音处理的起点
  • 掌握了短时能量、短时平均幅度、过零率的计算原理和Python实现
  • 学会了用双门限法做基础的静音检测和端点检测

这些特征虽然简单,但组合起来威力不小。我记得有一次做会议录音的自动切分,就是用能量+过零率做的VAD,准确率达到了92%以上。当然,如果遇到更复杂的噪声环境,我们后面会引入谱减法、深度学习等方法。但基础打牢了,后面学起来才不慌。

课后建议:找一段自己的录音,分别计算STE和ZCR,观察说话段和静音段的特征差异。然后试着调一下双门限的阈值,看看VAD效果怎么变化。动手做一遍,比看十遍代码都管用。