4. 音频特征提取
时域分析,说白了就是直接对着波形下手。不搞傅里叶变换,不看频谱,就看信号幅度随时间怎么变。我刚开始做语音处理时,觉得时域太简单了,后来才发现——简单的东西往往最实用。
这一章我们聊三个核心概念:短时能量、过零率(ZCR),以及它们怎么组合起来做静音检测和端点检测(VAD)。这些都是音频算法的基本功,你想想看,如果连语音从哪里开始、到哪里结束都搞不定,后面的识别、增强、压缩全白搭。
4.1 短时能量:声音有多“响”
短时能量的计算思路很简单:把音频切成一小段一小段(分帧),对每一帧里的采样点求平方和。为什么平方?因为幅度有正有负,直接求和会抵消,平方后能量就都是正的了。
公式长这样:
E(i) = Σ [x(n) * w(n)]² (n 从 0 到 N-1)
其中 x(n) 是采样点,w(n) 是窗函数,N 是帧长。我习惯用汉明窗,旁瓣衰减好一些。
短时能量能干什么?最直接的应用就是判断一段音频里有没有声音。安静环境下的背景噪声能量很低,人说话时能量会明显跳起来。但有个坑——能量对幅度敏感,同样的语音,离麦克风近和远,能量差好几倍。所以单纯用能量做门限,鲁棒性不够。
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 | 变化大 | 背景音乐、键盘声 |
4.3 静音检测与端点检测(VAD)
VAD 的全称是 Voice Activity Detection,说白了就是判断“现在有没有人说话”。这是语音处理里最基础也最关键的模块之一。你想想看,语音识别系统如果不知道什么时候开始说话,那它就得一直听着,浪费算力不说,还容易把噪声当成语音。
经典的 VAD 方案就是能量 + ZCR 双门限法。我画个流程图你就明白了:
双门限法的核心逻辑:
- 能量门限 T1:能量高于 T1,大概率是语音(尤其是浊音)。
- ZCR 门限 T2:能量低但 ZCR 高,可能是清音(比如句子开头的 s 音)。
- 两者都低:静音或背景噪声。
实际实现时,我还会加一个“滞后”机制——检测到语音后,连续几帧都低于门限才判定结束。这样能避免说话中间换气时被误判为静音。
- 能量门限 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
4.5 实际项目中的坑与对策
做 VAD 这么多年,我踩过的坑不少。挑几个典型的说说:
- 噪声突变:关门声、拍桌子声,能量和 ZCR 都会跳变。对策是加一个“最小语音长度”约束,短于 50ms 的突发信号直接忽略。
- 弱语音:离麦克风远的人说话,能量可能和背景噪声差不多。这时候 ZCR 就派上用场了——清音的 ZCR 通常比噪声高。
- 音乐背景:音乐的能量和 ZCR 变化都很大,容易误判。我做过一个方案,加了一个“频谱平坦度”特征,音乐和语音的平坦度差异明显。
- 自适应门限:固定门限在安静环境好用,换到嘈杂环境就废了。我习惯用前几帧做背景估计,动态更新门限。
4.6 小结
时域分析是音频算法的基础,短时能量和过零率虽然简单,但组合起来能解决很多实际问题。VAD 更是语音处理的第一道关卡——做不好 VAD,后面的识别、增强、压缩全都会受影响。
我个人建议,刚开始做音频处理时,先把时域特征玩透。能量、ZCR、自相关这些基础特征,能帮你理解信号的本质。等这些基础打牢了,再去看频域、倒谱域的东西,会轻松很多。