3. 时域分析入门:短时能量计算、过零率、时域特征提取、基于时域的简单门限检测
各位同学,欢迎来到第三讲。
上一章我们聊了噪声门限的基本概念,说白了就是给信号画一条线,超过就放行,低于就关掉。但问题来了——这条线怎么画?凭什么画在这里?
答案就在时域分析里。
我个人习惯把时域分析比作「听诊器」。你不用管信号是什么频率、什么相位,直接看它的幅度变化就够了。今天我们就从最基础的三个工具入手:短时能量、过零率,以及基于它们的门限检测。
3.1 短时能量:信号强弱的「体温计」
先问一个问题:你怎么判断一段音频里有没有人说话?
最简单的办法就是看能量。安静的时候能量低,说话的时候能量高。但音频信号是随时间变化的,你不能拿一整段去算平均能量——那样会把语音和静音混在一起。
所以我们要做「短时」处理。
3.1.1 什么是短时能量?
把信号切成一小段一小段,每段叫一帧。对每一帧计算能量,公式很简单:
E(n) = Σ [x(m) * w(n - m)]²
其中 x(m) 是原始信号,w(n-m) 是窗函数。说白了就是:取一帧信号,每个样点平方,然后求和。
为什么平方?因为平方能放大差异。安静时样点值接近0,平方后更接近0;说话时样点值大,平方后更大。这个对比一下子就拉开了。
关键参数:帧长与帧移
- 帧长:一般取 20~40ms。太短了能量波动剧烈,太长了反应迟钝。
- 帧移:通常取帧长的一半或1/3。保证帧与帧之间有重叠,避免信息丢失。
我做过一个项目,采样率16kHz,帧长取512点(32ms),帧移256点。这个配置在大多数场景下都够用。
3.1.2 代码实现
下面是一个简单的短时能量计算函数。我习惯用Python写原型,方便调试:
import numpy as np
def short_time_energy(signal, frame_size, hop_size):
"""
计算短时能量
:param signal: 输入信号 (1D array)
:param frame_size: 帧长(样点数)
:param hop_size: 帧移(样点数)
:return: 能量序列
"""
energy = []
for start in range(0, len(signal) - frame_size, hop_size):
frame = signal[start:start + frame_size]
e = np.sum(frame ** 2)
energy.append(e)
return np.array(energy)
嗯,这里要注意:如果信号里有直流分量,记得先做去直流处理。不然能量计算会偏大,门限也跟着偏。
避坑指南
我曾经在一个语音唤醒项目里,直接用原始信号算能量,结果发现静音段的能量比预期高很多。排查了半天,原来是麦克风偏置电压带来的直流分量。加一个高通滤波器(比如截止频率80Hz)就解决了。
3.2 过零率:信号「抖动」的度量
能量能区分有声和无声,但区分不了语音和噪声。这时候就需要过零率了。
过零率,顾名思义,就是信号在一帧内穿过零轴的次数。语音信号有过零率高的特点吗?其实不一定。清音(比如「s」、「f」)过零率高,浊音(比如「a」、「o」)过零率低。但噪声呢?白噪声的过零率通常比语音高,而且更稳定。
3.2.1 计算公式
ZCR(n) = (1/2) * Σ |sign(x(m)) - sign(x(m-1))|
简单理解:相邻两个样点符号不同,就算一次过零。除以2是因为一次过零会被两个样点各计一次。
你想想看,如果信号全是正数或全是负数,过零率就是0。如果信号在零轴附近来回震荡,过零率就高。
3.2.2 代码实现
def zero_crossing_rate(signal, frame_size, hop_size):
zcr = []
for start in range(0, len(signal) - frame_size, hop_size):
frame = signal[start:start + frame_size]
# 符号变化次数
sign_changes = np.sum(np.abs(np.diff(np.sign(frame)))) // 2
zcr.append(sign_changes / frame_size) # 归一化
return np.array(zcr)
注意:过零率对噪声非常敏感。如果信号里有高频噪声,过零率会异常偏高。我建议在计算之前先做一次低通滤波,把8kHz以上的成分滤掉。这样过零率才能真实反映语音的特性。
3.3 时域特征提取:把信号变成「特征向量」
有了短时能量和过零率,我们就可以提取时域特征了。说白了,就是把每一帧信号压缩成几个数字,方便后续处理。
常用的时域特征包括:
| 特征名称 | 含义 | 典型用途 |
|---|---|---|
| 短时能量 | 帧内样点平方和 | 区分有声/无声 |
| 过零率 | 帧内穿过零轴的次数 | 区分清音/浊音/噪声 |
| 短时幅度 | 帧内样点绝对值之和 | 能量的一种替代,计算量更小 |
| 短时自相关 | 帧内信号与自身延迟的相似度 | 基音周期检测 |
我个人习惯把短时能量和过零率组合成一个二维特征向量。这样既能反映信号强弱,又能反映信号结构。很多简单的VAD(语音活动检测)算法就是基于这个组合。
3.4 基于时域的简单门限检测
好了,特征有了,接下来就是画线了。
门限检测的核心思想:设定一个阈值,超过阈值认为是「活动段」,低于阈值认为是「静音段」。
3.4.1 单门限检测
最简单的做法:只用一个能量门限。
if energy > threshold:
label = "active"
else:
label = "silence"
但这样做有个问题:如果噪声能量和语音能量接近,门限很难选。选高了漏检,选低了误检。
实际经验:单门限只适用于信噪比大于15dB的场景。低于这个值,我建议用双门限。
3.4.2 双门限检测
双门限的思路是:设一个高门限和一个低门限。
- 能量超过高门限 → 立即判定为活动段
- 能量低于低门限 → 立即判定为静音段
- 能量在两者之间 → 看前后帧的状态,用「滞后」逻辑
这样做的好处是:避免信号在门限附近来回跳变。我做过一个测试,双门限比单门限的误判率降低了约40%。
3.4.3 结合过零率的门限
有时候能量门限不够用。比如一段只有噪声的音频,突然来一个脉冲噪声,能量很高,但过零率也很高。而语音的能量高,过零率相对低。
所以我们可以把两个特征结合起来:
if energy > energy_threshold and zcr < zcr_threshold:
label = "speech"
else:
label = "noise"
这个组合在大多数安静环境下效果不错。但要注意,不同人的语音过零率差异很大。女性声音偏高频,过零率会高一些;男性声音偏低频,过零率低一些。所以门限最好能自适应调整。
自适应门限的小技巧:取前N帧(比如前100ms)作为噪声估计段,计算这段时间的平均能量和平均过零率。然后以这个均值为基准,加上一个偏移量作为门限。这样即使环境变化,门限也能跟着变。
3.5 本章知识体系
下面这张图总结了时域分析的核心流程:
从图中可以看到,整个流程是线性的:原始信号 → 分帧加窗 → 提取能量和过零率 → 融合判决 → 输出标签。每一步都很简单,但组合起来就能解决实际问题。
3.6 小结
今天的内容就到这里。总结一下:
- 短时能量:反映信号强弱,是门限检测的基础。
- 过零率:反映信号结构,辅助区分语音和噪声。
- 双门限:比单门限更稳定,适合实际场景。
- 自适应门限:用前几帧估计噪声,动态调整阈值。
这些方法虽然简单,但非常实用。我至今还记得第一次用双门限做VAD时,看到波形图上干净利落的起止点标记,那种成就感——嗯,这就是工程的美妙之处。
下一章我们会进入频域,看看傅里叶变换能给我们带来什么新视角。不过在那之前,建议你把今天的代码跑一遍,亲手调一调门限参数。纸上得来终觉浅,绝知此事要躬行。