3. 前端处理流程概览
各位好,欢迎来到第三章。前两章我们聊了语音信号的基础和采样量化,今天要进入真正动手调优的环节了。
预加重和端点检测,这两个东西听起来有点学术,但说白了——预加重是给高频信号“加油”,端点检测是告诉程序“人开始说话了”。没有它们,你的语音识别系统就像戴着耳塞听人讲话,还分不清哪句是废话。
我个人习惯把这两个模块放在一起讲,因为它们在流水线上是紧挨着的:先做预加重,再做端点检测,最后切出干净的语音段。好,我们一个一个来。
3.1 预加重滤波器设计
先问个问题:为什么需要预加重?
你想想看,人在说话时,声带发出的声音能量主要集中在低频(几百赫兹),而高频部分(比如齿音、摩擦音)能量天然就弱。但高频恰恰承载了很多语音辨识度信息——比如“s”和“sh”的区别,全靠高频。如果直接拿原始信号去做特征提取,高频分量会被低频淹没,识别率自然上不去。
预加重就是一个简单的高通滤波器,它的作用就是提升高频,压低低频,让整个频谱更平坦。公式很简单:
y[n] = x[n] - α * x[n-1]
其中 α 是预加重系数,通常取 0.95 ~ 0.97。α 越接近 1,高频提升越猛。
核心参数:α = 0.97 是语音识别领域的“万金油”值。我做过对比实验,α 从 0.95 调到 0.97,在安静环境下识别率提升约 0.5%,但在噪声环境下提升更明显,能达到 1-2%。
代码实现也很直接:
def pre_emphasis(signal, alpha=0.97):
"""
预加重滤波器
signal: 输入语音信号 (numpy array)
alpha: 预加重系数,默认0.97
返回: 预加重后的信号
"""
emphasized = np.append(signal[0], signal[1:] - alpha * signal[:-1])
return emphasized
我在项目中遇到过一个问题:有些麦克风本身就有高频预加重硬件,这时候软件再做一次预加重,高频会过冲,反而引入噪声。所以拿到数据后,我建议先看一眼频谱,确认原始信号的高频衰减情况,再决定 α 值。
小技巧:如果你用的是手机麦克风录制的语音,α 可以设小一点(0.95);如果是专业麦克风(比如会议麦克风阵列),α 可以设大一点(0.97-0.98)。因为手机麦克风的高频响应本来就差,再加重反而放大底噪。
3.2 VAD 算法原理
预加重做完,接下来就是端点检测(Voice Activity Detection, VAD)。VAD 的任务很简单:判断当前这段音频里,哪些是语音,哪些是静音或噪声。
为什么重要?因为一段录音里,可能 60% 都是沉默或背景噪声。如果不切除,这些无效片段会浪费计算资源,还会干扰声学模型的训练。我见过一个项目,没做 VAD 直接训练,模型学了一堆“呼吸声”和“椅子摩擦声”,识别率惨不忍睹。
VAD 算法大致分三代,我按自己的经验给你梳理一下:
| 方法 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 能量法 | 计算短时能量,超过阈值判为语音 | 简单、速度快 | 噪声下容易误判 | 安静环境、高信噪比 |
| 过零率法 | 计算信号穿过零轴的次数 | 对清音(摩擦音)敏感 | 对浊音不敏感 | 与能量法配合使用 |
| 深度学习 VAD | 用 DNN/RNN 分类语音/非语音 | 鲁棒性强、噪声适应好 | 计算量大、需要训练数据 | 复杂噪声环境、实时性要求不高 |
3.2.1 能量法
能量法是最基础的 VAD。把信号分成短帧(通常 20-30ms),每帧计算能量:
E = Σ (x[n]²) (n 从 0 到 N-1)
然后设定一个阈值,能量高于阈值判为语音,低于阈值判为静音。
阈值怎么定?我常用的方法是:取前 200ms 作为噪声估计,计算平均能量 E_noise,然后阈值设为 E_noise * 2 ~ 3。但要注意,如果噪声突然变大(比如有人关门),阈值需要动态更新。
避坑指南:我曾经在一个工厂环境做 VAD,能量法死活调不好——机器轰鸣声的能量比人说话还大。后来我改用“能量比”而不是“绝对能量”:计算当前帧能量与噪声能量的比值,比值超过 5 倍才判为语音。效果好了很多。
3.2.2 过零率法
过零率(Zero Crossing Rate, ZCR)计算的是信号穿过零轴的次数。清音(如 /s/、/f/)的过零率很高,浊音(如 /a/、/i/)的过零率较低。所以过零率法特别适合检测清音开头的词,比如“四”、“书”。
实际应用中,我通常把能量法和过零率法结合起来:
- 能量法检测浊音段(能量高)
- 过零率法检测清音段(过零率高但能量低)
- 两者取并集,得到完整的语音段
代码实现:
def vad_energy_zcr(frame, energy_thresh, zcr_thresh):
"""
能量+过零率联合VAD
frame: 单帧信号
energy_thresh: 能量阈值
zcr_thresh: 过零率阈值
返回: True=语音, False=静音
"""
energy = np.sum(frame ** 2)
zcr = np.sum(np.abs(np.diff(np.sign(frame)))) / (2 * len(frame))
# 能量高 或 过零率高 都判为语音
return (energy > energy_thresh) or (zcr > zcr_thresh)
3.2.3 深度学习 VAD
如果环境噪声复杂(比如咖啡馆、马路旁),传统方法基本歇菜。这时候就得请出深度学习 VAD。
深度学习 VAD 的核心思路是:把音频帧的特征(MFCC、Fbank 等)输入一个分类网络,输出“语音概率”。常用的网络结构有:
- DNN:3-4 层全连接,输入 13 维 MFCC,输出 0/1
- LSTM:利用时序信息,效果更好,但延迟高
- CNN:把频谱图当图像处理,适合噪声分类
我推荐一个轻量方案:用 3 层 DNN + 上下文窗口(前后各 5 帧),在 WebRTC VAD 基础上微调。这样既保留了实时性,又比传统方法鲁棒。
经验之谈:深度学习 VAD 的训练数据很关键。我建议至少包含 10 种噪声类型(白噪声、风扇、键盘、街道、餐厅等),每种噪声混入不同信噪比(0dB、5dB、10dB、15dB)。否则模型在真实场景下会“翻车”。
3.3 静音切除策略
VAD 判断出哪些帧是语音后,下一步就是静音切除。这一步看似简单,但处理不好会切掉词头词尾,或者留下大段空白。
我常用的策略是“三段式”:
- 前端静音切除:从音频开头开始,连续 N 帧非语音,则标记为起始点
- 后端静音切除:从音频末尾倒推,连续 M 帧非语音,则标记为结束点
- 中间静音保留:语音段内部的短静音(比如词语间的停顿)保留,不切除
参数设置:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 前端连续非语音帧数 N | 10-15 帧(约 200-300ms) | 防止把呼吸声误判为语音起始 |
| 后端连续非语音帧数 M | 20-30 帧(约 400-600ms) | 防止说话中途停顿被切断 |
| 中间静音最大保留长度 | 500ms | 超过此长度则切分为两个语音段 |
注意:静音切除的“回退”机制很重要。我曾经遇到一个案例:说话人语速很慢,每个词之间停顿 300ms,结果后端静音切除把词尾切掉了,导致“今天”变成了“今”。后来我加了“回退保护”——如果切除点附近 100ms 内有语音能量回升,就延长保留时间。
3.4 本章知识体系总览
下面这张图总结了预加重和端点检测的完整流程,以及各模块之间的关系:
从图中可以看到,预加重是第一步,然后分帧,接着用 VAD 判断每帧是否语音,最后静音切除输出干净段。实际项目中,我通常把能量法和过零率法作为“快速通道”,深度学习 VAD 作为“备用方案”——只有在噪声复杂时才启用。