语音信号基础:声音的数字化表示
当你对智能音箱说“播放音乐”时,一个复杂而精妙的旅程便开始了:你喉部声带振动产生的声波,经过空气传播,被麦克风捕捉,最终转换成一系列数字——0和1的序列。机器正是通过这些数字,识别出你的意图。理解这个“从声音到数字”的转换过程,是进入语音人工智能世界的基石。本章将带你踏出这关键的第一步,我们将像拆解一台精密仪器一样,层层剖析语音信号的本质:从人类发声的物理原理,到声音如何被采样、量化、编码为离散序列;再到如何对原始数字信号进行预处理,使其成为适合后续特征提取的“干净原料”。我们将亲手用代码处理一段真实音频,在时域和频域中观察语音的形态,并首次接触短时傅里叶变换(STFT)这一核心工具。这不仅是技术细节的堆砌,更是为后续所有高级模型——从GMM-HMM到端到端深度学习——打下坚实、不可动摇的基础。准备好了吗?让我们从声音的源头开始。
---
从连续世界到离散数字:模拟信号与采样定理
人类发声与听觉的物理基础
语音的本质是机械波。当我们在说话时,肺部呼出的气流通过气管到达喉部,使声带振动,产生周期性的准周期脉冲(对于浊音,如元音/a/);对于清音(如/s/),气流通过狭窄通道产生湍流。这种原始的声源信号在咽腔、口腔、鼻腔等共鸣腔中被调制,通过舌头位置、嘴唇形状等构音器的运动,最终形成我们听到的千变万化的语音。从物理声学角度看,语音信号是一种非平稳的随机过程,其统计特性随时间快速变化,但其在极短时间(通常10-30毫秒)内可近似为平稳过程。这一“短时平稳”假设是整个现代语音信号处理大厦的基石。
声波在空气中传播,是一种模拟的、连续的压力波动。麦克风(传声器)的核心部件是振膜,声波压力使振膜产生相应的、连续的机械振动,进而通过电磁感应或电容变化,转换成连续变化的模拟电压信号。这个电压信号在时间轴上是连续的,其幅度可以取无限多个可能值,我们称之为模拟信号或连续信号。
采样定理:连接两个世界的桥梁
计算机无法处理连续信号,必须将其转换为离散的数字序列。这个转换过程称为模数转换,包含两个关键步骤:采样 和 量化。
采样 是在时间轴上对连续信号进行“离散化”。即以固定的时间间隔 $T_s$(采样周期)对信号幅度进行“测量”并记录。采样率 $f_s = 1/T_s$ 表示每秒采样的次数,单位是赫兹。例如,常见的电话语音采样率为8kHz,CD音质为44.1kHz。
这里必须引入信息论和信号处理的黄金法则——奈奎斯特-香农采样定理。该定理指出:对于一个带宽有限(最高频率为 $f_{max}$)的连续信号,要能够从采样后的离散信号中无失真地恢复原信号,采样率 $f_s$ 必须满足: $$ f_s \geq 2 f_{max} $$
为什么是2倍?直观理解是,为了描述一个最高频率为 $f_{max}$ 的正弦波,你至少需要在每个周期内采样2个点(峰值和谷值)。如果采样率过低($f_s < 2f_{max}$),就会发生混叠:高频信号被“伪装”成低频信号,造成不可逆的信息丢失。下图展示了混叠现象:一个高频正弦波(虚线)如果以低于奈奎斯特频率的速率采样,采样点(圆圈)看起来就像一个低频正弦波(实线)。
```mermaid graph TD A[连续模拟语音信号] --> B{采样率 fs?}; B -- fs ≥ 2*f_max --> C[无混叠<br/>可完美重建]; B -- fs < 2*f_max --> D[发生混叠<br/>高频变低频<br/>信息永久丢失]; C --> E[离散数字信号]; D --> E; ```
对于语音,其有效频率范围通常认为在300Hz到3400Hz之间(电话带宽),因此 $f_{max}=3400Hz$,$2f_{max}=6800Hz$,故采用8kHz的采样率已足够。对于高质量语音或音乐,需要更宽的带宽,因此采用44.1kHz或48kHz。采样定理是硬性约束,是数字信号处理的“宪法”,在后续任何处理中,都必须首先考虑混叠风险。
量化:从连续幅度到离散数值
采样解决了时间离散化,但每个采样点记录的电压幅度仍然是连续的。量化 是将每个采样点的幅度值映射到有限个离散电平上的过程。假设我们使用 $N$ 个比特位来表示一个采样值,则可以表示 $2^N$ 个不同的量化级。例如,CD音质使用16比特($N=16$),量化级数为65536。
量化过程必然引入误差,即量化噪声。量化信噪比大致与比特数 $N$ 成正比,每增加1比特,SNR约提升6dB。语音处理中,常用16比特(PCM编码)或8比特(如μ-law或A-law压缩编码,用于电话网络)的量化精度。
经过采样和量化,连续的模拟语音信号就转变为一个离散时间、离散幅度的序列 $x[n]$,其中 $n$ 是整数样本索引。这个序列就是计算机中存储的原始语音波形文件(如WAV格式)的本质。我们可以用一段简单的Python代码,加载一个WAV文件,查看其采样率、比特深度和波形。
```python import librosa import librosa.display import matplotlib.pyplot as plt import numpy as np
1. 加载音频文件 (假设当前目录下有一个example.wav)
y: 音频时间序列 (numpy数组)
sr: 采样率
y, sr = librosa.load('example.wav', sr=None) # sr=None 保留原始采样率
print(f"采样率 (Sampling Rate): {sr} Hz") print(f"音频时长 (Duration): {len(y)/sr:.2f} 秒") print(f"样本总数 (Total Samples): {len(y)}") print(f"数据类型 (Data Type): {y.dtype}") # 通常为float32,范围[-1.0, 1.0]
2. 绘制时域波形图
plt.figure(figsize=(12, 4)) librosa.display.waveshow(y, sr=sr, color='b') plt.title('原始语音波形 (时域)') plt.xlabel('时间 (秒)') plt.ylabel('幅度') plt.grid(True, alpha=0.3) plt.tight_layout() plt.show() ```
这段代码利用`librosa`库完成了从文件到数字序列的读取,并绘制了最直观的时域波形。横轴是时间,纵轴是声压幅度。这是我们分析语音的第一个视角。
---
语音信号预处理:为特征提取铺路
原始数字波形虽然包含了所有信息,但直接用于分析或识别效率低下,且包含大量冗余和不必要成分。预处理的目的就是对其进行“净化”和“整形”,使其更适合后续的数学处理。核心步骤有三:预加重、分帧、加窗。
预加重:提升高频能量
语音信号的功率谱密度通常随频率升高而下降,即高频部分能量较弱。从发音机理看,声带振动产生的基频谐波能量在低频区较强,而高频共振峰能量相对较弱。然而,语音的清晰度和可懂度很大程度上依赖于高频信息(如辅音)。为了在后续分析(如频谱计算)中平衡高低频分量,提升高频部分的相对能量,需要进行预加重。
预加重是一个简单的一阶高通滤波器,其差分方程为: $$ y[n] = x[n] - \alpha x[n-1] $$ 其中,$x[n]$是原始信号,$y[n]$是预加重后的信号,$\alpha$ 是预加重系数,通常取值范围为0.95到0.97。其频率响应为 $H(z) = 1 - \alpha z^{-1}$,在高频处($z=e^{j\omega}$,$\omega$接近$\pi$)增益接近 $1+\alpha$,低频处增益接近0,起到了提升高频的作用。
注意:预加重是可逆操作(通过逆滤波 $x[n] = y[n] + \alpha x[n-1]$ 可近似恢复),它不创造信息,只是调整频谱斜率,使后续的频谱包络估计(如MFCC)更准确。
```python def pre_emphasis(signal, alpha=0.97): """ 对信号进行预加重。 参数: signal: 一维numpy数组,输入信号。 alpha: 预加重系数,默认0.97。 返回: 预加重后的信号。 """ return np.append(signal[0], signal[1:] - alpha * signal[:-1])
应用预加重
y_preemph = pre_emphasis(y)
对比绘制原始波形与预加重波形(前1000个样本)
plt.figure(figsize=(12, 4)) plt.plot(y[:1000], label='原始信号', alpha=0.7) plt.plot(y_preemph[:1000], label='预加重后信号', alpha=0.7) plt.title('预加重效果对比 (前1000样本)') plt.xlabel('样本索引') plt.ylabel('幅度') plt.legend() plt.grid(True, alpha=0.3) plt.show() ```
从时域对比图可以看到,预加重后信号的整体幅度变化趋势更“陡峭”,高频细节(如快速过零点)相对更明显。
分帧:应对非平稳性
语音信号是非平稳的,但其“短时平稳”特性允许我们将长序列切割成一系列短时段(称为帧)来分别处理。每一帧内的信号可近似视为平稳的,从而可以使用基于平稳假设的数学工具(如傅里叶变换)。
分帧操作通过帧移 来实现。我们设定两个关键参数:
- **帧长**:每帧包含的样本数,通常对应20-40毫秒的时长。例如,在16kHz采样率下,256个样本约16ms,512个样本约32ms。
- **帧移**:相邻两帧起始点之间的样本数,通常为帧长的1/2到1/3(即10-15ms的重叠)。重叠是为了保证帧之间的平滑过渡,避免信息在边界处丢失。
分帧过程可以看作是用一个矩形窗在信号上滑动。设帧长为$L$,帧移为$S$,则第$m$帧的信号可以表示为: $$ x_m[n] = x[n + mS], \quad n = 0, 1, ..., L-1 $$ 其中$m$是帧索引。分帧后,我们得到一系列重叠的短信号段,为下一步的加窗和变换做准备。
加窗:减少频谱泄漏
直接使用矩形窗分帧,在帧的起始和结束处会产生 abrupt 的截断。这种不连续性在频域会引入大量高频成分,导致频谱泄漏:一个纯正弦信号的频谱能量会“泄漏”到相邻的频率 bin 中,使得频谱估计失真。为了减小这种截断效应,需要对每一帧乘以一个窗函数,使帧的两端平滑过渡到0。
最常用的窗函数是汉明窗,其公式为: $$ w[n] = 0.54 - 0.46 \cos\left(\frac{2\pi n}{L-1}\right), \quad n = 0, 1, ..., L-1 $$ 其他常用窗包括汉宁窗、布莱克曼窗等,它们的主瓣宽度和旁瓣衰减特性不同,需要根据需求权衡。
加窗操作 就是将分帧后的每一帧 $x_m[n]$ 与窗函数 $w[n]$ 进行点乘: $$ \tilde{x}_m[n] = x_m[n] \cdot w[n] $$ 经过预加重、分帧、加窗后,我们得到了一系列加权的、平稳的短时语音帧 $\tilde{x}_m[n]$,它们是后续时频分析(如STFT)的直接输入。
让我们用代码完成这一整套预处理流程,并观察一帧加窗前后的对比。
```python def framing(signal, frame_len, frame_shift): """ 对信号进行分帧。 参数: signal: 一维numpy数组,输入信号。 frame_len: 帧长(样本数)。 frame_shift: 帧移(样本数)。 返回: 二维numpy数组,形状为 (帧数, 帧长)。 """ signal_len = len(signal)
计算总帧数,确保最后一帧也能容纳(如果不够则补零,这里简单处理为丢弃尾部不足一帧的部分)
num_frames = 1 + int(np.ceil((signal_len - frame_len) / frame_shift))
初始化帧矩阵
frames = np.zeros((num_frames, frame_len)) for i in range(num_frames): start = i * frame_shift end = min(start + frame_len, signal_len) frames[i, :end-start] = signal[start:end]
如果最后一帧不足,已初始化为0,无需额外操作
return frames
def hamming_window(L): """ 生成汉明窗。 参数: L: 窗长。 返回: 一维numpy数组,汉明窗。 """ n = np.arange(L) return 0.54 - 0.46 * np.cos(2 * np.pi * n / (L - 1))
参数设置 (基于16kHz采样率,假设我们已重采样到16k)
target_sr = 16000 if sr != target_sr: y = librosa.resample(y, orig_sr=sr, target_sr=target_sr) sr = target_sr frame_len = int(0.025 * sr) # 25ms帧长 frame_shift = int(0.010 * sr) # 10ms帧移 print(f"帧长: {frame_len} 样本 ({frame_len/sr*1000:.1f}ms)") print(f"帧移: {frame_shift} 样本 ({frame_shift/sr*1000:.1f}ms)")
1. 预加重
y_preemph = pre_emphasis(y, alpha=0.97)
2. 分帧
frames = framing(y_preemph, frame_len, frame_shift) print(f"总帧数: {frames.shape[0]}")
3. 加窗
win = hamming_window(frame_len) frames_win = frames * win # 广播机制,每帧乘以窗
可视化:第0帧加窗前后对比
frame_idx = 0 plt.figure(figsize=(12, 4)) plt.plot(frames[frame_idx], label='分帧后(矩形窗)', alpha=0.7) plt.plot(frames_win[frame_idx], label='加汉明窗后', linewidth=2) plt.title(f'第 {frame_idx} 帧:分帧与加窗效果') plt.xlabel('样本索引(帧内)') plt.ylabel('幅度') plt.legend() plt.grid(True, alpha=0.3) plt.show()
可视化:所有帧的频谱泄漏对比(通过FFT观察)
def plot_spectrum(signal, sr, title): n_fft = len(signal) X = np.fft.rfft(signal * np.hamming(len(signal))) # 为了公平,都加个窗看主瓣 freq = np.fft.rfftfreq(n_fft, d=1./sr) magnitude = np.abs(X) plt.plot(freq, 20 * np.log10(mundance + 1e-10)) # 转换为dB plt.title(title) plt.xlabel('频率 (Hz)') plt.ylabel('幅度 (dB)') plt.grid(True, alpha=0.3) plt.xlim(0, 4000)
plt.figure(figsize=(12, 5)) plt.subplot(1, 2, 1)
取一帧纯正弦波模拟(理想情况)
t = np.arange(frame_len) / sr pure_tone = 0.5 * np.sin(2 * np.pi * 1000 * t) # 1kHz正弦波 plot_spectrum(pure_tone, sr, '理想1kHz正弦波频谱(无窗)') plt.subplot(1, 2, 2) plot_spectrum(frames[frame_idx], sr, '实际语音帧频谱(矩形窗)') plt.tight_layout() plt.show() ```
左图展示了理想1kHz正弦波(无窗)的频谱,理论上应是一条垂直线。右图是实际语音一帧(矩形窗)的频谱,可以看到主瓣很宽,两侧有很多旁瓣(“毛刺”),这就是频谱泄漏。加窗后(虽然上图未直接对比,但加窗会加宽主瓣但大幅降低旁瓣),主瓣变宽但旁瓣被抑制,使得频谱估计更“干净”,便于后续提取共振峰结构。
---
时域与频域:观察语音的两副眼镜
处理完预处理,我们拥有了加窗后的短时帧序列。现在,我们需要选择合适的“眼镜”来观察这些帧。最基础的两副眼镜是时域和频域。
时域分析:波形的直接观察
时域就是我们已经看到的波形图:横轴时间,纵轴幅度。时域分析直接、直观,可以观察:
- **振幅**:反映声音的响度。
- **过零点**:信号穿过零点(从正到负或负到正)的次数。清音(如摩擦音/s/、/f/)由于高频湍流,过零率很高;浊音(元音)由于准周期性,过零率相对稳定且较低。过零率是区分清浊音的一个简单有效特征。
- **短时能量**:帧内样本幅度的平方和,反映帧的能量大小,可用于区分静音、清音、浊音,以及进行端点检测(VAD)。
我们可以计算并绘制