4. 信号预处理技术:预加重、分帧与加窗、端点检测(VAD)、降噪滤波

各位好,欢迎来到第四章。前面几章我们把麦克风阵列的物理基础、声学模型和信号模型都过了一遍。从这章开始,咱们要真正动手处理音频信号了。

预处理这一步,说白了就是给原始音频信号“洗个澡”。麦克风采集到的原始信号,直接拿去做波束成形或者声源定位,效果往往惨不忍睹。为什么?因为信号里有太多“杂质”——低频能量太强、噪声太大、静音段太长。我刚开始做阵列算法时,就吃过这个亏,以为算法牛逼就能搞定一切,结果被实测数据狠狠教育了一顿。

好,咱们一个一个来看。

信号预处理流程框架 原始音频信号 预加重 分帧 加窗(汉明窗) 端点检测(VAD) 降噪滤波 ⚠ 注意 预处理顺序不能乱 先预加重 → 分帧 → 加窗 VAD和降噪可并行处理

4.1 预加重:给高频信号“撑腰”

先问一个问题:为什么语音信号的高频部分总是“软绵绵”的?

原因在于声门脉冲的频谱特性——语音信号的能量主要集中在低频区域,高频部分衰减得厉害。你想想看,如果直接拿这样的信号去做后续处理,高频细节很容易被噪声淹没。

预加重的作用,就是用一个一阶高通滤波器,把高频部分抬起来。公式很简单:

y[n] = x[n] - α · x[n-1]

其中 α 通常取 0.95 到 0.97。我个人习惯用 0.97,这个值在大多数场景下表现都不错。

经验之谈: 预加重系数 α 越接近 1,高频提升越猛。但别超过 0.99,否则低频会被压得太狠,声音听起来会“发尖”。我在做车载语音系统时,试过 α=0.99,结果乘客的语音变得像机器人一样刺耳,后来调回 0.97 才正常。

Python 实现就几行:

def pre_emphasis(signal, alpha=0.97):
    """一阶高通预加重"""
    return np.append(signal[0], signal[1:] - alpha * signal[:-1])

嗯,这里要注意:第一个样本点要保持原值,否则会丢失起始信息。

4.2 分帧与加窗:把连续信号切成“小段”

语音信号是非平稳的,但在极短的时间尺度内(10-30ms),它可以近似看作平稳信号。这就是分帧的理论基础。

分帧有两个关键参数:

参数 典型值 说明
帧长 20-30 ms 每帧包含的样本点数
帧移 10-15 ms 相邻帧起始点之间的间隔

帧移通常取帧长的一半,这样相邻帧之间有 50% 的重叠。为什么要重叠?因为加窗后帧边缘的信号会被削弱,重叠可以补偿这部分信息损失。

说到加窗,最常用的是汉明窗(Hamming Window)。它的形状像一口钟,中间高两边低:

def hamming_window(N):
    """生成N点汉明窗"""
    n = np.arange(N)
    return 0.54 - 0.46 * np.cos(2 * np.pi * n / (N - 1))
避坑指南: 我曾经在项目里直接用矩形窗(不加窗),结果频谱泄露得一塌糊涂,相邻频点之间互相干扰,导致后续的波束成形算法完全失效。记住:分帧之后必须加窗,这是铁律。

完整的帧处理流程:

  1. 根据帧长和帧移计算总帧数
  2. 逐帧从信号中截取数据
  3. 每帧乘以窗函数
  4. 得到加窗后的帧序列,用于后续处理

4.3 端点检测(VAD):区分“说话”和“安静”

VAD 的全称是 Voice Activity Detection,说白了就是判断当前这一段信号里有没有人说话。

为什么需要 VAD?因为麦克风阵列采集到的信号,大部分时间可能是静音或者背景噪声。如果把这些无语音段也送去做处理,不仅浪费算力,还会引入噪声干扰。

经典的 VAD 方法基于能量和过零率:

  • 短时能量: 语音段的能量通常高于噪声段
  • 短时过零率: 清音(如 /s/、/f/)的过零率高,浊音(如 /a/、/i/)的过零率低

我常用的一个简单 VAD 实现:

def simple_vad(frame, energy_thresh, zcr_thresh):
    energy = np.sum(frame ** 2)
    zcr = np.sum(np.abs(np.diff(np.sign(frame)))) / 2
    return (energy > energy_thresh) or (zcr > zcr_thresh)

当然,实际项目中很少用这么简单的阈值法。噪声环境变化时,阈值需要自适应调整。我记得有一次在工厂车间做测试,背景噪声突然增大,固定阈值的 VAD 直接“失聪”了——把所有信号都判成了噪声。后来改用基于统计模型的 VAD(比如 G.729B 标准里的方法),才解决了这个问题。

核心要点: VAD 的准确率直接影响后续处理的质量。漏检(把语音判成噪声)会导致语音丢失,误检(把噪声判成语音)会导致噪声被放大。在阵列系统中,VAD 的输出通常用来控制波束成形和降噪模块的开关。

4.4 降噪滤波:维纳滤波与谱减法

降噪是预处理的重头戏。这里介绍两种经典方法:维纳滤波和谱减法。

4.4.1 维纳滤波

维纳滤波的核心思想是:在最小均方误差准则下,估计出纯净语音信号。它假设噪声是平稳的,并且与语音不相关。

频域维纳滤波器的表达式:

H(ω) = P_s(ω) / [P_s(ω) + P_n(ω)]

其中 P_s(ω) 是语音功率谱,P_n(ω) 是噪声功率谱。你看,当信噪比高时(P_s >> P_n),H(ω) ≈ 1,滤波器几乎不衰减;当信噪比低时(P_s << P_n),H(ω) ≈ 0,滤波器把这段频率切掉。

实际实现时,噪声功率谱需要在无语音段估计——这就用到了前面说的 VAD。

def wiener_filter(noisy_spectrum, noise_psd):
    """维纳滤波频域实现"""
    speech_psd = np.maximum(np.abs(noisy_spectrum)**2 - noise_psd, 0)
    H = speech_psd / (speech_psd + noise_psd + 1e-10)
    return noisy_spectrum * H
个人经验: 维纳滤波对平稳噪声效果很好,但对非平稳噪声(比如突然的关门声、键盘敲击声)就力不从心了。我在做智能音箱项目时,维纳滤波能很好地抑制空调噪声,但对厨房里的碗碟碰撞声基本无效。这时候需要结合更高级的方法,比如基于深度学习的降噪。

4.4.2 谱减法

谱减法更直接:从带噪信号的幅度谱中减去噪声的幅度谱。公式:

|Ŝ(ω)| = max(|Y(ω)| - |N(ω)|, 0)

谱减法实现简单,计算量小,但有个臭名昭著的副作用——音乐噪声(Musical Noise)。就是降噪后的信号里会出现一些“叮叮当当”的残留噪声,听起来像背景里有人在弹钢琴。

为什么会有音乐噪声?因为谱减法是逐帧独立处理的,帧与帧之间的噪声估计有随机波动,这些波动在重建信号时就变成了离散的频点噪声。

缓解方法:

  • 使用过减因子(over-subtraction factor),多减一点噪声
  • 加一个频谱平滑操作
  • 引入噪声估计的更新机制

维纳滤波和谱减法对比:

方法 优点 缺点 适用场景
维纳滤波 理论最优(MMSE准则),噪声残留少 需要准确估计噪声功率谱 平稳噪声环境
谱减法 实现简单,计算量小 容易产生音乐噪声 实时性要求高的场景
曾经踩过的坑: 我在一个嵌入式项目里用了谱减法,结果音乐噪声导致语音识别率从 95% 掉到了 70%。后来改用维纳滤波 + 噪声跟踪(noise tracking)的方案,才把识别率拉回来。所以,如果你的系统对音质要求高,优先考虑维纳滤波。

好了,这一章的内容就到这里。预加重、分帧加窗、VAD、降噪滤波,这四个步骤构成了麦克风阵列信号处理的第一道防线。把这些基础打牢,后面做波束成形和声源定位时,你会发现数据干净了很多,算法的效果也会上一个台阶。