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时,看到波形图上干净利落的起止点标记,那种成就感——嗯,这就是工程的美妙之处。

下一章我们会进入频域,看看傅里叶变换能给我们带来什么新视角。不过在那之前,建议你把今天的代码跑一遍,亲手调一调门限参数。纸上得来终觉浅,绝知此事要躬行。


专注资料整理