3. 数字信号基础

聊完了声学基础,咱们正式进入特征提取环节。时域特征,说白了就是直接在波形上算出来的统计量。你想想看,一段语音信号,本质上就是一串随时间变化的幅度值。我们能不能直接从这些数值里挖出有用的信息?当然能。

我个人习惯把时域特征比作「信号的体检报告」。短时能量告诉你声音有多响,短时过零率告诉你声音有多「碎」,短时自相关函数则帮你找到信号里的周期性规律。这三个特征组合起来,已经能解决不少实际问题了。

核心思路:语音信号是非平稳的,但在10~30ms的短时窗内可以视为平稳。所以我们加窗、分帧,然后在每一帧上计算特征。这就是「短时」二字的由来。

时域特征提取流程 原始语音信号 预加重 分帧 + 加窗 短时能量 短时过零率 短时自相关 每一帧独立计算三个特征,形成特征序列,供后续分析或识别使用

3.1 短时能量

短时能量是最直观的特征。它衡量一帧信号的总能量大小。公式很简单:对一帧内所有采样点的平方求和(或者取绝对值求和)。

def short_time_energy(frame):
    """
    计算一帧信号的短时能量
    frame: 一维numpy数组,表示一帧语音信号
    返回: 标量能量值
    """
    energy = np.sum(frame ** 2)
    return energy

我在项目中遇到过一个问题:直接用平方和算能量,数值范围波动很大。安静环境和嘈杂环境可能差好几个数量级。后来我习惯取对数或者做归一化处理,这样特征更稳定。

实用技巧:实际工程中常用 10 * log10(energy + eps) 转换成分贝值。加一个小常数 eps 是为了防止 log(0)。

短时能量能干什么?最经典的应用就是端点检测(VAD)。说话的时候能量高,静音段能量低。设个阈值就能把语音段切出来。但要注意——背景噪声大的时候,这个阈值不好选。我曾经在工厂环境里做语音唤醒,背景机器轰鸣声让能量阈值完全失效,后来不得不结合过零率一起判断。

3.2 短时过零率

短时过零率(ZCR)统计一帧内信号穿过零轴的次数。说白了就是看波形正负交替有多频繁。

def zero_crossing_rate(frame):
    """
    计算一帧信号的短时过零率
    frame: 一维numpy数组
    返回: 过零次数(或归一化后的比率)
    """
    # 相邻采样点符号不同则计数+1
    zcr = np.sum(np.abs(np.diff(np.sign(frame)))) / 2
    return zcr

你想想看,清音(比如 /s/、/f/)的波形高频成分多,过零率就高;浊音(比如 /a/、/i/)的波形低频为主,过零率就低。这个差异非常明显。

音素类型 典型过零率范围 波形特点
浊音(元音) 10~40 次/帧 周期性明显,幅度大
清音(摩擦音) 40~80 次/帧 类噪声,幅度小
静音/背景噪声 不稳定,通常较低 随机波动

注意:过零率对直流偏移非常敏感。如果信号有直流分量,过零率会严重失真。预处理时一定要做去直流(减去均值)操作。我曾经因为ADC采集卡有直流偏置没处理,算出来的过零率全是错的,排查了半天才发现。

短时能量 + 短时过零率联合使用,是经典的语音端点检测方案。能量判断有声/无声,过零率判断是清音还是浊音。两者互补,效果比单用能量好得多。

3.3 短时自相关函数

短时自相关函数(ACF)用来检测信号的周期性。它的思路很简单:把信号和它自己延迟 τ 个点后的版本做内积。如果 τ 正好等于基音周期,两个波形对齐得最好,相关值最大。

def short_time_autocorr(frame, max_lag=None):
    """
    计算一帧信号的短时自相关函数
    frame: 一维numpy数组
    max_lag: 最大延迟点数,默认等于帧长
    返回: 自相关序列
    """
    n = len(frame)
    if max_lag is None:
        max_lag = n
    # 零均值化
    frame = frame - np.mean(frame)
    # 计算自相关
    acf = np.correlate(frame, frame, mode='full')
    # 只取正延迟部分 [0, max_lag]
    acf = acf[n-1 : n-1 + max_lag]
    # 归一化(可选)
    acf = acf / acf[0]  # 除以零延迟值,使范围在[-1, 1]
    return acf

自相关函数的第一个峰值(除了 τ=0 处)对应的延迟就是基音周期。基音频率 = 采样率 / 延迟点数。嗯,这里要注意:实际语音的基频范围大概在 80~500 Hz,所以搜索峰值时只在这个范围内找就行,别去搜那些没意义的延迟。

基音检测的坑:自相关函数在倍频处也会出现峰值。比如实际基频是 100 Hz,在 200 Hz、300 Hz 的位置也可能出现次高峰。我早期做基音检测时经常被倍频干扰,后来加了动态规划和平滑约束才稳定下来。

3.4 三个特征的工程应用

这三个特征单独用都有局限,但组合起来威力不小。我列几个实际场景:

  • 语音端点检测:能量 + 过零率双阈值法。能量判断有声段,过零率区分清音和噪声。
  • 基音检测:自相关函数找峰值,配合中值滤波去野点。
  • 语音/音乐分类:语音的过零率变化大,音乐的过零率相对稳定。能量包络也能提供区分信息。
  • 异常声音检测:短时能量的突变往往对应撞击声、爆炸声等异常事件。

我个人习惯在项目初期先用这三个时域特征快速验证想法。它们计算量小,实时性高,跑在树莓派上都没问题。等验证通过后,再上更复杂的频域特征。

工程建议:帧长一般取 20~30ms,帧移取 10ms。这样相邻帧有重叠,特征序列更平滑。采样率 16kHz 时,帧长 512 点(32ms),帧移 160 点(10ms)是个不错的起点。

好了,时域特征就聊到这儿。这三个特征虽然简单,但它们是后续所有声学特征的基石。你动手跑一遍代码,用自己录的语音试试,感受一下不同音素在能量、过零率和自相关上的差异——比光看公式直观得多。