2. 时域特征提取:短时能量、短时过零率、短时自相关函数、端点检测(VAD)
时域特征,说白了就是直接对着音频信号的波形下手。不做傅里叶变换,不关心频率成分,就看信号幅度随时间怎么变。我刚开始做语音处理那会儿,觉得时域特征太“原始”了,总想上频域。后来踩过几次坑才明白——很多场景下,时域特征又快又稳,尤其是做端点检测和静音切除,频域反而容易过拟合。
这一章我们聊四个经典时域特征:短时能量、短时过零率、短时自相关函数,以及它们组合起来做端点检测(VAD)。每个我都会给出数学定义、代码实现,以及我在项目中踩过的坑。
2.1 短时能量(Short-Time Energy)
短时能量衡量的是某一帧信号的“响度”。语音段能量高,静音段能量低——这是最直观的区分依据。
定义很简单:对第 n 帧,计算所有采样点的平方和。
E(n) = Σ_{m=0}^{N-1} [x_n(m)]²
其中 N 是帧长,x_n(m) 是第 n 帧的第 m 个采样点。
实际部署时,我一般用这个实现:
import numpy as np
def short_time_energy(frame):
"""
计算单帧的短时能量
frame: 1D numpy array, 帧数据
返回: 标量能量值
"""
return np.sum(frame ** 2) / len(frame) # 归一化,避免帧长影响
短时能量的用途很直接:
- 静音检测:能量低于阈值就判为静音
- 语音活动检测:能量突变往往意味着说话开始或结束
- 端点检测辅助:配合过零率一起用,准确率更高
2.2 短时过零率(Short-Time Zero Crossing Rate)
过零率统计一帧内信号穿过零轴的次数。说白了就是看波形“抖”得有多快。
清音(如 /s/、/f/)频率高,过零率大;浊音(如 /a/、/i/)频率低,过零率小。这个特性在区分清浊音时特别好用。
定义:
Z(n) = (1/2) * Σ_{m=1}^{N-1} |sgn[x_n(m)] - sgn[x_n(m-1)]|
其中 sgn 是符号函数,正值返回 1,负值返回 -1,零返回 0。
代码实现:
def zero_crossing_rate(frame):
"""
计算单帧的短时过零率
frame: 1D numpy array
返回: 标量 ZCR 值
"""
# 符号变化次数
signs = np.sign(frame)
zcr = np.sum(np.abs(np.diff(signs))) / (2 * len(frame))
return zcr
过零率的典型应用场景:
- 清浊音判别:浊音 ZCR 通常 < 0.2,清音 > 0.3
- 端点检测:静音段 ZCR 低且稳定,语音段 ZCR 波动大
- 打击乐检测:打击乐信号过零率突变明显
2.3 短时自相关函数(Short-Time Auto-Correlation Function)
自相关函数衡量信号与自身延迟版本之间的相似度。对于周期信号(比如浊音),自相关会在基音周期处出现峰值。
定义:
R_n(k) = Σ_{m=0}^{N-1-k} x_n(m) * x_n(m + k)
其中 k 是延迟量,取值范围通常为 0 到 N-1。
实现时要注意计算效率——直接按定义算复杂度是 O(N²),部署时扛不住。我一般用两种优化方式:
- FFT加速:利用 Wiener–Khinchin 定理,自相关 = IFFT(|FFT(x)|²)
- 截断计算:只算前几百个延迟点(基音周期一般不超过 20ms)
def autocorrelation_fft(frame):
"""
使用 FFT 加速计算自相关函数
frame: 1D numpy array
返回: 自相关序列(前 N 点)
"""
N = len(frame)
# 补零到 2N 避免循环卷积
fft_len = 2 ** int(np.ceil(np.log2(2 * N)))
spec = np.fft.rfft(frame, n=fft_len)
power = np.abs(spec) ** 2
acf = np.fft.irfft(power)[:N]
return acf / acf[0] # 归一化到 [0, 1]
2.4 端点检测(VAD)
端点检测的目标是:从一段音频里,精确找到语音开始和结束的位置。说白了就是“切掉静音,留下人声”。
我见过很多团队在 VAD 上翻车——要么切多了把词头切掉,要么切少了留一大段静音。其实核心就两点:特征选对,阈值调好。
最经典的双门限法,结合了短时能量和过零率:
- 高能量门限:确定语音的“肯定存在”区域
- 低能量门限:扩展语音边界,避免切掉弱音
- 过零率门限:在低能量段区分清音和静音
流程图如下:
代码实现:
def vad_double_threshold(frames, energy_high, energy_low, zcr_thresh, min_speech_len=5):
"""
双门限 VAD
frames: list of 1D numpy arrays
energy_high, energy_low: 能量门限(归一化后)
zcr_thresh: 过零率门限
min_speech_len: 最小语音段长度(帧数),用于去毛刺
返回: (start_frame, end_frame) 语音段的起止帧索引
"""
labels = []
for frame in frames:
energy = short_time_energy(frame)
zcr = zero_crossing_rate(frame)
if energy > energy_high:
labels.append(1)
elif energy > energy_low and zcr > zcr_thresh:
labels.append(1)
else:
labels.append(0)
# 后处理:去除短时噪声
# 将长度小于 min_speech_len 的语音段标记为静音
count = 0
for i in range(len(labels)):
if labels[i] == 1:
count += 1
else:
if 0 < count < min_speech_len:
for j in range(i - count, i):
labels[j] = 0
count = 0
# 找到第一个和最后一个 1
speech_indices = np.where(np.array(labels) == 1)[0]
if len(speech_indices) == 0:
return None, None
return speech_indices[0], speech_indices[-1]
2.5 四种特征的对比与选型
部署时选哪个特征,取决于你的硬件和场景。我整理了一张表,方便你快速决策:
| 特征 | 计算量 | 主要用途 | 部署注意事项 |
|---|---|---|---|
| 短时能量 | 极低(O(N)) | 静音检测、VAD 主特征 | 需归一化,避免帧长影响 |
| 短时过零率 | 极低(O(N)) | 清浊音判别、VAD 辅助 | 先去除直流分量 |
| 短时自相关 | 中(O(N log N) FFT) | 基音检测、周期估计 | FFT 实现,注意补零 |
| 双门限 VAD | 低(组合特征) | 语音端点检测 | 门限需根据信噪比自适应 |