4. 时域特征提取(一):短时能量、短时过零率、短时自相关函数、基频估计(F0)的数学原理与Python实现

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

很多人一上来就扎进频域,觉得FFT才是王道。我个人习惯是,先把手伸进时域摸一摸。为什么?因为时域特征计算量小、物理意义直观,而且很多场景下——比如端点检测、静音切除——它比频域方法更稳。

这一章我们啃四个基础特征:短时能量、短时过零率、短时自相关函数,以及基频估计(F0)。前两个是轻量级选手,后两个稍微重一点,但都是工程落地里的常客。

核心观点:时域特征不是“低级特征”,而是“高性价比特征”。在资源受限的嵌入式设备上,时域特征往往是第一道防线。

时域特征提取知识体系 时域特征提取 短时能量 短时过零率 短时自相关 基频估计(F0) 端点检测 / VAD 清/浊音判别 基频粗估计 音高检测 / 语音合成 工程落地要点:低延迟 + 低算力 + 高鲁棒 分帧 → 加窗 → 逐帧计算 → 后处理平滑

4.1 短时能量(Short-Time Energy)

短时能量,说白了就是衡量一帧语音信号有多“响”。数学上很简单:对一帧内的采样点求平方和。

公式长这样:

E = Σ_{n=0}^{N-1} x²(n)

其中 x(n) 是第 n 个采样点,N 是帧长。

我在项目中遇到过一个问题:直接用原始波形算能量,结果被直流偏置搞崩了。录音设备的ADC如果没做好隔直,会有一个固定的直流分量,平方之后能量值虚高。所以我的习惯是——先做去直流,再算能量。

工程小技巧:去直流可以用 x(n) - mean(x),或者直接加一个高通滤波器(比如截止频率 20Hz 的 IIR)。

Python实现很简单:

import numpy as np

def short_time_energy(frame):
    """
    计算一帧语音信号的短时能量
    :param frame: 一维numpy数组,一帧语音
    :return: 能量值(标量)
    """
    # 先去直流
    frame = frame - np.mean(frame)
    # 计算能量
    energy = np.sum(frame ** 2)
    return energy

嗯,这里要注意:如果你做的是实时流式处理,均值要用滑动窗口算,不能等整帧收齐再算——那会引入一个帧的延迟。

4.2 短时过零率(Zero-Crossing Rate, ZCR)

过零率,顾名思义,就是一帧信号穿过零轴的次数。对于离散信号,我们看相邻两个采样点符号是否相反。

公式:

ZCR = (1/2N) * Σ_{n=1}^{N-1} |sgn(x(n)) - sgn(x(n-1))|

其中 sgn 是符号函数,正数返回1,负数返回-1。

你想想看,这个特征有什么用?

清音(比如 /s/、/f/)的波形高频成分多,过零率很高;浊音(比如 /a/、/i/)波形比较平滑,过零率低。所以ZCR常用来做清/浊音判别。

我曾经在做一个噪声环境下的VAD系统时,发现单纯用能量阈值会漏掉弱清音。后来加了一个ZCR辅助判断,准确率直接提了8个点。

def zero_crossing_rate(frame):
    """
    计算一帧语音信号的短时过零率
    :param frame: 一维numpy数组
    :return: 过零率(标量,范围0~1)
    """
    # 符号函数
    sgn = np.sign(frame)
    # 计算相邻符号变化
    zcr = 0.5 * np.sum(np.abs(np.diff(sgn)))
    return zcr / len(frame)

避坑指南:如果信号中有小幅噪声抖动,过零率会虚高。我曾经吃过这个亏——安静环境下ZCR居然飙到0.3。解决办法:加一个死区阈值,比如只统计穿过 ±epsilon 区间的过零。

4.3 短时自相关函数(Short-Time Autocorrelation)

自相关函数,用来衡量信号与自身延迟版本之间的相似度。对于周期信号(比如浊音),自相关会在基频周期处出现峰值。

定义式:

R(k) = Σ_{n=0}^{N-1-k} x(n) * x(n+k)

其中 k 是延迟(lag),范围从 0 到 N-1。

我个人的经验是:自相关函数在基频估计中非常经典,但它有两个坑——

  • 计算量大:直接算 O(N²),帧长一长就扛不住。
  • 倍频干扰:基频的整数倍延迟处也会出现峰值,容易选错。

工程上常用归一化自相关函数(NCCF)来缓解幅度影响:

def autocorrelation(frame, max_lag=None):
    """
    计算一帧语音的短时自相关函数
    :param frame: 一维numpy数组
    :param max_lag: 最大延迟,默认帧长-1
    :return: 自相关序列
    """
    n = len(frame)
    if max_lag is None:
        max_lag = n - 1
    # 去直流
    frame = frame - np.mean(frame)
    # 计算自相关
    r = np.correlate(frame, frame, mode='full')
    # 只取正延迟部分(从 n-1 到 n-1+max_lag)
    r = r[n-1 : n-1+max_lag+1]
    return r

加速技巧:用 FFT 做自相关,复杂度降到 O(N log N)。原理是 R(k) = IFFT(|FFT(x)|²)。我在嵌入式设备上就是这么干的,帧长512时速度能快10倍。

4.4 基频估计(F0 Estimation)

基频,就是声带振动的频率。成年男性一般在 80-200Hz,女性在 150-350Hz。基频估计是语音分析、语音合成、歌唱评价的核心环节。

基于自相关的基频估计流程:

  1. 计算一帧语音的短时自相关函数 R(k)。
  2. 在基频对应的延迟范围内(比如 2ms~20ms,对应 50Hz~500Hz)寻找峰值。
  3. 峰值位置 k_max 对应的基频 F0 = fs / k_max。

但直接找最大值容易出问题——我遇到过好几次,峰值落在 k=1 处(即延迟1个采样点),那其实是高频噪声,不是基频。

所以工程上要加约束:

  • 限制搜索范围:只搜索 k_min 到 k_max 之间的峰值。
  • 峰值阈值:只接受超过最大值一定比例(比如 0.3)的峰值。
  • 后处理平滑:用中值滤波去掉跳变点。
def estimate_f0(frame, fs, f0_min=50, f0_max=500):
    """
    基于自相关的基频估计
    :param frame: 一帧语音信号
    :param fs: 采样率
    :param f0_min: 最小基频 (Hz)
    :param f0_max: 最大基频 (Hz)
    :return: 基频值 (Hz),无声时返回0
    """
    # 计算自相关
    r = autocorrelation(frame)
    n = len(frame)
    
    # 计算搜索范围(以采样点为单位)
    k_min = int(fs / f0_max)  # 最大频率对应最小延迟
    k_max = int(fs / f0_min)  # 最小频率对应最大延迟
    
    # 边界保护
    k_min = max(1, k_min)
    k_max = min(n - 1, k_max)
    
    # 在搜索范围内找峰值
    search_region = r[k_min:k_max+1]
    if len(search_region) == 0:
        return 0.0
    
    k_peak = np.argmax(search_region) + k_min
    
    # 峰值阈值检查(防止噪声误判)
    peak_val = r[k_peak]
    if peak_val < 0.3 * r[0]:  # r[0] 是自相关最大值
        return 0.0
    
    # 计算基频
    f0 = fs / k_peak
    return f0

我曾经踩过的坑:在低信噪比环境下,自相关峰值会被噪声淹没。后来我改用倒谱法做基频估计,抗噪性好了不少。倒谱法的思路是:对频谱取对数再做FFT,基频会在倒谱的基频周期处出现峰值。这个我们后面章节会细讲。

4.5 四个特征的对比与选型

我把这四个特征放在一起对比一下,方便你根据场景选型:

特征 计算复杂度 主要用途 工程落地难点
短时能量 O(N) 端点检测、静音切除 直流偏置、幅度归一化
短时过零率 O(N) 清/浊音判别、VAD辅助 噪声抖动、死区阈值设置
短时自相关 O(N²) / O(N log N) 用FFT 基频粗估计、周期检测 倍频干扰、计算量
基频估计(F0) 依赖自相关/倒谱 音高检测、语音合成、歌唱评价 低信噪比、倍频/半频错误、后处理平滑

我个人在项目中的选型原则是:

  • 如果只是做VAD,短时能量 + 过零率就够了,别上自相关。
  • 如果需要基频,优先用自相关法,简单可控。
  • 如果噪声大(比如车载环境),考虑倒谱法或YIN算法。

一句话总结:时域特征不是“低级”,而是“高效”。用对了地方,它们比频域特征更直接、更省资源。

好了,这一章的内容就到这里。四个特征——短时能量、短时过零率、短时自相关、基频估计——它们的数学原理和Python实现我都给你拆清楚了。代码可以直接拿去用,但记得根据你的实际场景调参。