2. 声学基础与信号预处理:声音的物理特性、采样定理、预加重、分帧加窗、端点检测(VAD)
各位同学,欢迎来到第二章。
这一章,我们聊聊声学基础。嗯,别急着划走。我知道有些同学觉得声学理论太枯燥,跟实际开发离得远。但我得说,这部分要是没吃透,后面做唤醒词识别、做降噪,你大概率会踩坑。我自己就吃过这个亏。
好,我们正式开始。
2.1 声音的物理特性:你得知道你的对手
声音是什么?说白了,就是空气的振动。麦克风把这种振动变成电信号,ADC再把电信号变成数字信号。就这么简单。
但有几个关键参数,你必须刻在脑子里:
- 频率(Frequency):单位Hz。人耳能听到的大概是20Hz到20kHz。语音信号的能量主要集中在300Hz到3.4kHz。我做座舱项目时,发现很多背景噪声(比如发动机轰鸣)都在低频段,这直接影响VAD的阈值设计。
- 幅度(Amplitude):代表声音的响度。单位是dB。座舱内,正常说话大概在60dB左右,但高速行驶时噪声可能到70-80dB。你想想看,信噪比这么低,唤醒算法得有多抗造。
- 相位(Phase):这个在单麦克风方案里用得少,但做麦克风阵列波束成形时,相位差就是你的命根子。
我的经验: 有一次,客户反馈说“唤醒率在怠速时还行,一上高速就崩了”。我排查了半天,发现是预处理阶段的带通滤波器截止频率设得太宽,把发动机的低频轰鸣也放过去了。从那以后,我习惯先拿一段实车噪声数据,看看频谱分布,再定滤波参数。
2.2 采样定理:别让你的信号“假摔”
采样定理,也叫奈奎斯特定理。公式很简单:fs ≥ 2 * fmax。采样频率必须大于等于信号最高频率的两倍。
为什么?因为采样频率不够,就会发生混叠(Aliasing)。高频信号会伪装成低频信号混进来。你以为是“你好”,结果算法听成了“你坏”。
座舱语音唤醒,我们一般用16kHz采样率。为什么?因为语音的有效信息在8kHz以内,16kHz刚好满足奈奎斯特定理,还能留点余量。有些同学问,用48kHz不是更好吗?数据量大了三倍,处理延迟也上去了,但唤醒率并不会因此提升。没必要。
避坑指南: 我曾经在一个项目里,ADC采样率设对了,但忘了在ADC前加抗混叠滤波器(低通滤波器)。结果高频噪声折叠到语音频带内,VAD死活检测不准。记住:采样率只是第一步,硬件上的抗混叠滤波同样重要。
2.3 预加重:给高频信号“撑腰”
语音信号有个特点:能量主要集中在低频,高频能量衰减很快。你看频谱,低频的幅度可能比高频大几十倍。这会导致后续的模型训练时,模型只关注低频特征,忽略了高频的辅音信息(比如“s”、“sh”、“f”)。
预加重就是为了解决这个问题。它本质上是一个一阶高通滤波器:
y[n] = x[n] - α * x[n-1]
其中α通常取0.95到0.97。α越接近1,高频提升越明显。
我个人习惯取0.97。为什么?试出来的。取0.95时,某些高频辅音还是不够突出;取0.98时,低频被压得太狠,听起来有点“尖”。0.97是个平衡点。
小技巧: 预加重不是必须的。如果你用的是深度学习模型,模型自己能从原始波形中学习到频率分布。但如果你用传统方法(比如MFCC),预加重几乎是标配。我建议新手先加上,等模型调优时再决定要不要去掉。
2.4 分帧加窗:把连续信号切成“小段”
语音信号是非平稳的。但如果我们取一个很短的时间段(比如20-30毫秒),在这个时间段内,信号可以近似认为是平稳的。这就是分帧的基本思想。
分帧有两个参数:
- 帧长(Frame Length):一般取20-30ms。16kHz采样率下,25ms对应400个采样点。
- 帧移(Frame Shift):相邻两帧之间的重叠部分。通常取帧长的一半或三分之一。比如帧长400点,帧移160点,这样相邻帧有240点的重叠。
为什么要重叠?因为直接切开会造成频谱泄漏。加窗就是为了缓解这个问题。
常用的窗函数有:
- 汉明窗(Hamming Window):最常用。旁瓣衰减大,能有效减少频谱泄漏。
- 汉宁窗(Hanning Window):和汉明窗类似,但两端更平滑。
- 矩形窗(Rectangular Window):不推荐。旁瓣太高,频谱泄漏严重。
代码实现很简单:
import numpy as np
def framing(signal, fs, frame_len_ms=25, frame_shift_ms=10):
frame_len = int(fs * frame_len_ms / 1000)
frame_shift = int(fs * frame_shift_ms / 1000)
num_frames = (len(signal) - frame_len) // frame_shift + 1
frames = []
for i in range(num_frames):
start = i * frame_shift
end = start + frame_len
frame = signal[start:end] * np.hamming(frame_len)
frames.append(frame)
return np.array(frames)
注意: 分帧加窗后,每一帧都是一个独立的“快照”。后续的VAD、特征提取,都是基于这些帧来做的。帧长太短,频率分辨率不够;帧长太长,平稳性假设不成立。25ms是个黄金值,我几乎所有的项目都用它。
2.5 端点检测(VAD):找到“人声”的起止点
VAD,全称Voice Activity Detection。说白了,就是判断当前这一段音频里,到底是人说话,还是背景噪声。
VAD做得好,唤醒率就高;做不好,要么漏唤醒(人说话没检测到),要么虚警(噪声被误判为人声)。
常用的VAD方法有几种:
- 基于能量阈值:最简单。计算每一帧的能量,超过阈值就判为语音。缺点是对噪声敏感。
- 基于过零率:语音的过零率通常比噪声高。但座舱内的敲击声、雨刮声,过零率也很高,容易误判。
- 基于谱熵:语音的频谱熵比噪声低。这个方法抗噪能力更强,但计算量稍大。
- 基于机器学习:用DNN或RNN做二分类。准确率高,但需要训练数据和模型部署。
在量产项目中,我一般用能量+过零率+谱熵的组合方案。为什么?纯能量阈值在安静环境下够用,但座舱里噪声复杂,必须多维度判断。
我的做法: 先计算每一帧的能量,设定一个自适应阈值(根据前几秒的噪声能量动态调整)。然后计算过零率,排除掉那些能量高但过零率异常的信号(比如空调风声)。最后用谱熵做二次确认。这样下来,虚警率能降低30%以上。
VAD还有一个关键参数:挂起时间(Hangover Time)。人说话时,字与字之间会有短暂的停顿。如果VAD一检测到停顿就立刻切掉,会导致语音被截断。挂起时间就是让VAD在检测到语音结束后,再保持“语音状态”一段时间(比如200-300ms)。
避坑指南: 我曾经在一个项目中,挂起时间设得太短(只有50ms)。结果用户说“你好,小X”,VAD只检测到了“你好”,后面的“小X”被当成噪声切掉了。唤醒率直接腰斩。后来我把挂起时间调到250ms,问题解决。
小结
这一章的内容,是后面所有章节的基石。采样定理决定了你的数据质量,预加重影响了特征提取的效果,分帧加窗是时频分析的起点,VAD则是唤醒系统的第一道关卡。
我建议你学完这一章后,找一段真实的座舱录音,自己动手写代码跑一遍预处理流程。看看不同参数下,VAD的检测结果有什么变化。纸上得来终觉浅,绝知此事要躬行。
下一章,我们聊聊特征提取——MFCC和Filter Bank。到时候你会发现,今天学的这些预处理,全都会用上。