2. 语音信号基础
说实话,做语音算法这几年,我最大的感触就是——数字信号处理是语音算法的地基。你想想看,我们每天处理的语音,本质上就是空气振动产生的模拟信号。但计算机只认0和1,怎么把声波变成它能理解的东西?这就是本章要聊的核心。
我个人习惯把数字信号处理比作「翻译官」:它把连续的声音翻译成离散的数字,再把这些数字变成我们能分析的频谱。嗯,这个过程里坑不少,我踩过,今天一并告诉你。
本章核心脉络:模拟信号 → 采样 → 量化 → 数字信号 → 时域分析 → 频域分析(FFT) → 时频分析(STFT) → 梅尔尺度映射 → 梅尔频谱
2.1 采样与量化:连续世界的「离散化」
先聊采样。说白了,采样就是每隔一小段时间,记录一次信号的幅度值。这个「一小段时间」的倒数,就是采样率。
奈奎斯特采样定理:采样率必须 ≥ 信号最高频率的 2 倍。否则会发生混叠,信号就失真了。
经验之谈:语音信号通常用 16kHz 采样率,因为人声能量集中在 8kHz 以下。电话语音用 8kHz,高清语音用 16kHz,音乐用 44.1kHz。我在做语音识别项目时,一开始用了 8kHz,结果识别率死活上不去——后来发现高频信息丢了,换成 16kHz 后直接提升了 5 个点。
量化呢?就是把采样得到的连续幅度值,映射到有限个离散值上。比如 16 位量化,就是把幅度分成 65536 个等级。
| 参数 | 典型值 | 说明 |
|---|---|---|
| 采样率 | 16 kHz | 每秒采集 16000 个样本点 |
| 量化位数 | 16 bit | 每个样本用 16 位二进制表示 |
| 比特率 | 256 kbps | 16k × 16 = 256 kbps |
| 频率分辨率 | 取决于帧长 | 帧长 25ms 时,分辨率约 40 Hz |
避坑指南:我曾经在嵌入式设备上用了 8 位量化,结果语音质量差到无法识别。量化位数太低,量化噪声会淹没弱信号。语音至少用 16 位,如果做音乐或高保真,用 24 位。
2.2 时域与频域:一枚硬币的两面
时域信号,就是横轴为时间、纵轴为幅度的波形图。你看一眼波形,能看出声音大小、有没有静音段,但看不出频率成分。
频域信号,横轴是频率,纵轴是幅度(或能量)。你看频谱,能知道声音里有哪些频率成分,但看不出这些频率在什么时间出现。
为什么会这样?因为时域和频域是同一个信号的两种表示方式,通过傅里叶变换连接。你想想看,一个钢琴弹奏的 C 调,时域上是一串正弦波,频域上就是一根谱线。
核心理解:时域告诉你「什么时候响」,频域告诉你「什么频率响」。语音分析需要两者结合,所以有了 STFT。
2.3 傅里叶变换(FFT):从时域到频域的「魔法」
傅里叶变换的数学公式看起来挺吓人,但核心思想很简单:任何信号都可以分解成不同频率的正弦波之和。
实际工程中,我们用的是快速傅里叶变换(FFT),它是 DFT 的高效实现。FFT 把计算复杂度从 O(N²) 降到了 O(N log N),这才让实时语音处理成为可能。
import numpy as np
# 生成一个 1kHz 的正弦波
fs = 16000 # 采样率 16kHz
t = np.arange(0, 1.0, 1.0/fs)
f = 1000 # 频率 1kHz
x = np.sin(2 * np.pi * f * t)
# 做 FFT
X = np.fft.fft(x)
freqs = np.fft.fftfreq(len(x), 1/fs)
# 只取正频率部分
X_mag = np.abs(X[:len(X)//2])
freqs = freqs[:len(freqs)//2]
# 找到峰值频率
peak_idx = np.argmax(X_mag)
print(f"峰值频率: {freqs[peak_idx]:.2f} Hz")
# 输出: 峰值频率: 1000.00 Hz
这段代码很简单,但我在实际项目中踩过一个坑:FFT 的结果是对称的,正频率和负频率各一半。如果你忘了只取前半部分,画出来的频谱图会多出一半的镜像,看着很诡异。
2.4 短时傅里叶变换(STFT):给频谱加上时间轴
语音信号是非平稳的——它的频率成分随时间变化。你想想看,一个人说话,每个音节的频率都不一样。如果用整段语音做 FFT,得到的是所有时间的平均频谱,完全看不出「哪个时间说了哪个音」。
STFT 的思路很简单:把信号切成一小段一小段(加窗),对每一段做 FFT。这样你就得到了一个「时间-频率」二维矩阵,横轴是时间帧,纵轴是频率,颜色深浅代表能量大小。
我的习惯:帧长一般取 25ms,帧移 10ms。为什么?25ms 能保证频率分辨率够用,10ms 的帧移让帧与帧之间有重叠,避免信息丢失。窗函数我用汉明窗(Hamming window),它能减少频谱泄漏。
import numpy as np
import librosa
# 加载语音(假设已有音频文件)
y, sr = librosa.load('speech.wav', sr=16000)
# 计算 STFT
D = librosa.stft(y, n_fft=512, hop_length=160, win_length=400)
# n_fft=512 → 频率点数为 257(512/2+1)
# hop_length=160 → 帧移 10ms(160/16000)
# win_length=400 → 帧长 25ms(400/16000)
# 转换为幅度谱
magnitude = np.abs(D)
print(f"STFT 形状: {magnitude.shape}")
# 输出: (257, 时间帧数)
避坑指南:我曾经在 STFT 的帧移上吃过亏。帧移设得太大(比如 30ms),结果相邻帧之间信息不连续,语音听起来有「卡顿感」。帧移一般不超过帧长的一半,10ms 是安全值。
2.5 梅尔滤波器组与梅尔频谱:让机器听懂人话
人耳对频率的感知不是线性的。低频区域,人耳分辨能力强;高频区域,分辨能力弱。梅尔尺度(Mel scale)就是模拟人耳的这种特性。
梅尔频率与线性频率的转换公式:
Mel(f) = 2595 × log₁₀(1 + f / 700)
梅尔滤波器组,就是一组在梅尔尺度上等间距分布的三角滤波器。每个滤波器对应一个梅尔频带,把 STFT 的幅度谱映射到梅尔尺度上。
| 参数 | 典型值 | 作用 |
|---|---|---|
| 滤波器数量 | 40 或 80 | 40 用于传统语音识别,80 用于深度学习 |
| 频率范围 | 0 ~ 采样率/2 | 语音一般用 0~8kHz(16kHz 采样率) |
| 滤波器形状 | 三角滤波器 | 中心频率处增益为 1,两侧线性衰减到 0 |
import librosa
import numpy as np
# 加载语音
y, sr = librosa.load('speech.wav', sr=16000)
# 计算 STFT 幅度谱
D = librosa.stft(y, n_fft=512, hop_length=160, win_length=400)
magnitude = np.abs(D)
# 应用梅尔滤波器组
n_mels = 40
mel_spec = librosa.feature.melspectrogram(
S=magnitude,
sr=sr,
n_fft=512,
hop_length=160,
win_length=400,
n_mels=n_mels,
fmin=0,
fmax=8000
)
# 转换为对数尺度(dB)
log_mel_spec = librosa.power_to_db(mel_spec, ref=np.max)
print(f"梅尔频谱形状: {log_mel_spec.shape}")
# 输出: (40, 时间帧数)
为什么用梅尔频谱? 因为人耳对低频更敏感。梅尔尺度把低频区域「放大」了,高频区域「压缩」了。这样提取的特征更符合人类听觉感知,语音识别和说话人识别任务中效果更好。
我个人习惯用 40 维梅尔滤波器组做传统语音识别,用 80 维做深度学习模型。为什么?40 维够用,计算量小;80 维信息更丰富,模型能学到更多细节。但要注意,维数太高会引入噪声,反而降低性能。
经验之谈:梅尔频谱的「对数」操作很重要。语音信号的能量动态范围很大(从静音到响亮可能差 100 倍),取对数后压缩到合理范围,模型训练更稳定。我见过有人忘了取对数,结果模型 loss 一直降不下去——其实就是数值范围没处理好。
好了,数字信号处理的基础就聊到这儿。采样量化让你拿到数字信号,FFT 让你看到频率,STFT 让你看到频率随时间的变化,梅尔滤波器组让你模拟人耳感知。这一套流程,就是语音算法的「预处理流水线」,几乎所有语音任务都绕不开它。