第2章:声学基础与数字信号处理回顾
各位同学,欢迎来到第二章。
做语音前端处理,说白了就是跟声音打交道。你连声音的脾气都没摸透,后面那些降噪、回声消除的算法,学起来肯定吃力。这一章,我带大家快速过一遍声学和数字信号处理的基础。别嫌啰嗦,这些是地基。
2.1 声音的物理特性
声音是什么?就是振动。空气被扰动,形成疏密波,传到我们耳朵里。
描述一个声音,主要看三个物理量:
- 频率(Frequency):单位是赫兹(Hz)。人耳能听到的大概是20Hz到20kHz。语音信号的能量主要集中在300Hz到3.4kHz,也就是电话语音的带宽。我当年做第一版降噪算法时,没注意滤掉了低频的鼻音,结果测试时声音听起来像感冒了,后来才意识到低频保护的重要性。
- 幅度(Amplitude):代表声音的强弱。我们通常用分贝(dB)来表示。0dB是人耳能听到的最小声压。你想想看,在算法里,我们经常要处理动态范围,比如从-30dB的轻声细语到-6dB的大声喊叫,这中间差了16倍的能量。
- 相位(Phase):描述振动的起始位置。这个在单麦克风算法里容易被忽略,但在波束成形(Beamforming)里,相位差就是灵魂。我曾经因为一个ADC的时钟抖动,导致两个麦克风的相位不一致,结果波束成形出来的方向图完全歪了,排查了整整两天。
核心要点:频率决定音调,幅度决定响度,相位决定空间位置。做算法时,这三者要时刻记在心里。
2.2 时域与频域
我们采集到的原始音频信号,是随时间变化的波形。这叫时域表示。横轴是时间,纵轴是幅度。
但时域信号有个问题——你看不出它包含哪些频率成分。比如一段音乐,你盯着波形看,根本分不清是钢琴还是小提琴在响。
这时候就需要频域了。频域告诉我们,信号里有哪些频率,每个频率的能量有多大。
时域和频域,就像一枚硬币的两面。它们通过傅里叶变换联系起来。我个人习惯,在分析噪声特性时,一定先看频谱图。比如风扇噪声,时域波形乱糟糟的,但频域里能看到明显的窄带尖峰,这就是我们做陷波滤波器的依据。
我的经验:在调试算法时,建议同时打开时域波形和频谱图。时域看有没有削波、有没有突发噪声;频域看噪声分布、看共振峰。两个视角互补,能帮你快速定位问题。
2.3 采样定理
模拟信号要变成数字信号,第一步就是采样。这里有个铁律——奈奎斯特采样定理:采样频率必须大于信号最高频率的两倍。
为什么?因为采样频率不够,就会发生混叠(Aliasing)。高频信号会伪装成低频信号,混进你的数据里。你想想看,本来想采集8kHz的语音,结果因为采样率只有16kHz,一个9kHz的噪声被采样后,变成了7kHz的信号,混在语音里,你怎么滤都滤不掉。
实际工程中,我们通常留有余量。比如语音信号最高频率是8kHz,我们一般用16kHz或48kHz采样。48kHz是很多音频设备的标配,因为它能覆盖人耳听觉范围,而且做后续处理时,过采样能降低量化噪声。
避坑指南:我曾经在一个项目中,ADC前端的抗混叠滤波器设计得不够陡峭,结果高频噪声混叠到了通带内。后来我养成了一个习惯:在ADC之前,一定要加一个模拟低通滤波器,截止频率设为采样率的一半以下。这个滤波器,省不得。
2.4 傅里叶变换基础
傅里叶变换,是数字信号处理里最核心的工具。它能把时域信号变到频域。
公式我就不写了,网上一搜一大把。我想说的是它的物理意义:任何一个周期信号,都可以分解成一系列不同频率、不同幅度、不同相位的正弦波之和。
在语音处理中,我们用的是短时傅里叶变换(STFT)。因为语音是非平稳信号,它的频率特性随时间变化。STFT的做法是:把信号切成一小段一小段(通常20-40ms),对每一段做傅里叶变换,得到频谱。这样我们就能看到频率随时间的变化了。
代码实现上,我习惯用Python的scipy.signal.stft,或者C语言里用FFTW库。下面是一个简单的Python示例:
import numpy as np
from scipy.signal import stft
import matplotlib.pyplot as plt
# 生成一个示例信号:1kHz正弦波 + 3kHz正弦波
fs = 16000 # 采样率
t = np.linspace(0, 1, fs, endpoint=False)
x = np.sin(2 * np.pi * 1000 * t) + 0.5 * np.sin(2 * np.pi * 3000 * t)
# 做STFT
f, t, Zxx = stft(x, fs, nperseg=512, noverlap=256)
# 绘制频谱图
plt.pcolormesh(t, f, np.abs(Zxx), shading='gouraud')
plt.title('STFT Magnitude')
plt.ylabel('Frequency [Hz]')
plt.xlabel('Time [sec]')
plt.colorbar()
plt.show()
这段代码里,nperseg=512是帧长,对应32ms(512/16000=0.032s)。noverlap=256是帧移,也就是50%重叠。重叠的目的是为了减少帧与帧之间的不连续性,这在做语音增强时特别重要。
关键参数:帧长决定了频率分辨率,帧移决定了时间分辨率。帧长越长,频率分辨率越高,但时间分辨率越低。这是一个权衡。我一般做语音降噪时,帧长取20-30ms,帧移取10-15ms。这个经验值,你可以直接拿去用。
2.5 窗函数
做STFT时,直接截取一段信号,相当于加了一个矩形窗。矩形窗的旁瓣很高,会导致频谱泄漏。所以我们需要用其他窗函数来抑制旁瓣。
常用的窗函数有:
| 窗函数 | 主瓣宽度 | 旁瓣衰减 | 适用场景 |
|---|---|---|---|
| 矩形窗 | 窄 | -13dB | 瞬态信号检测 |
| 汉宁窗 | 中等 | -31dB | 语音处理(最常用) |
| 海明窗 | 中等 | -41dB | 窄带信号分析 |
| 布莱克曼窗 | 宽 | -57dB | 高动态范围分析 |
我个人习惯,做语音增强时首选汉宁窗。它旁瓣衰减适中,主瓣宽度也能接受。如果你需要检测两个频率很近的信号,可以考虑海明窗。但要注意,窗函数的选择会影响后续的合成(ISTFT),需要做重叠相加(Overlap-Add)来保证信号重建的完整性。
小技巧:在C语言实现中,窗函数可以预先计算好,存成查找表。这样在实时处理时,省去了每次计算的开销。我当年在DSP上做优化时,把窗函数表放在L1缓存里,速度提升了30%。
2.6 本章小结
这一章,我们回顾了声音的物理特性、时域与频域的概念、采样定理以及傅里叶变换的基础。这些内容,是后面所有语音前端算法的基石。
嗯,这里要注意:采样定理和窗函数的选择,是实际工程中最容易出问题的地方。我建议你动手写几行代码,生成一个带噪声的信号,试试不同的窗函数和帧长,看看频谱图的变化。只有亲手做过,才能真正理解。
下一章,我们开始讲实际的语音增强算法。到时候,这些基础都会用上。