3. 数字信号处理基础
音频文件读写,是声学特征提取的第一步。说白了,你连数据都读不进来,后面再牛的算法也是白搭。我刚开始做音频处理时,也踩过不少坑——比如拿错采样率、声道搞反、MP3 文件死活读不出来。今天咱们就把这些基础操作捋清楚。
本章核心知识点:使用 soundfile 读取 WAV、使用 pydub 处理 MP3、numpy 数组表示音频、声道分离与合并、音频裁剪与拼接。
3.1 使用 soundfile 读取 WAV 文件
WAV 是音频处理中最常见的无损格式。我个人习惯用 soundfile 库,它比 scipy.io.wavfile 更稳定,而且直接返回 numpy 数组。
import soundfile as sf
# 读取 WAV 文件
data, samplerate = sf.read('example.wav')
print(f"采样率: {samplerate} Hz")
print(f"数据形状: {data.shape}")
print(f"数据类型: {data.dtype}")
这里要注意,sf.read() 默认返回的数值范围是 [-1.0, 1.0] 的浮点数。为什么这样设计?因为后续做特征提取时,浮点数运算更精确,不会出现整数溢出的问题。
小技巧:如果你需要原始整数数据,可以加上 dtype='int16' 参数。我在项目中经常用这个来对比不同编码格式的精度损失。
3.2 使用 pydub 处理 MP3 文件
MP3 是有损压缩格式,不能直接用 soundfile 读取。这时候 pydub 就派上用场了。它底层依赖 ffmpeg,但接口非常友好。
from pydub import AudioSegment
# 读取 MP3 文件
audio = AudioSegment.from_mp3('example.mp3')
# 转换为 numpy 数组
import numpy as np
samples = np.array(audio.get_array_of_samples())
samples = samples.reshape((-1, audio.channels))
print(f"声道数: {audio.channels}")
print(f"帧率: {audio.frame_rate}")
print(f"样本宽度: {audio.sample_width} 字节")
我曾经在一个语音识别项目中,客户给的数据全是 MP3 格式。一开始没注意采样率,结果特征提取出来的 MFCC 全是乱的。后来才发现 pydub 读取时默认不会重采样,需要手动处理。
避坑指南:pydub 读取 MP3 时,如果 ffmpeg 没装好,会直接报错。我曾经花了一下午排查这个问题——最后发现只是环境变量没配对。
3.3 音频数据的 numpy 数组表示
音频数据在 Python 里,本质上就是一个多维 numpy 数组。单声道是一维数组,双声道是二维数组(形状为 (样本数, 2))。
| 声道数 | 数组形状 | 示例 |
|---|---|---|
| 单声道 (Mono) | (N,) | (44100,) |
| 双声道 (Stereo) | (N, 2) | (44100, 2) |
| 5.1 声道 | (N, 6) | (44100, 6) |
你想想看,采样率 44100 Hz 意味着每秒有 44100 个样本点。一个 3 秒的立体声 WAV 文件,数组大小就是 3 × 44100 × 2 = 264600 个浮点数。这个量级对 numpy 来说小菜一碟。
# 查看音频数据的基本统计信息
print(f"最大值: {np.max(data):.4f}")
print(f"最小值: {np.min(data):.4f}")
print(f"均值: {np.mean(data):.4f}")
print(f"RMS: {np.sqrt(np.mean(data**2)):.4f}")
重要:音频数据的数值范围直接决定了后续处理的精度。浮点型 [-1.0, 1.0] 是最通用的表示方式,几乎所有特征提取算法都基于这个约定。
3.4 声道分离与合并
实际项目中,我们经常需要单独处理左声道或右声道。比如做声源定位时,左右声道的时延差就是关键信息。
# 声道分离
left_channel = data[:, 0] # 左声道
right_channel = data[:, 1] # 右声道
# 声道合并(重新组合为立体声)
stereo_data = np.column_stack((left_channel, right_channel))
# 单声道转立体声(复制)
mono_to_stereo = np.column_stack((left_channel, left_channel))
我记得有一次做语音增强,需要把立体声混合信号分离成独立声道。一开始直接用 data[:, 0] 和 data[:, 1] 切分,结果发现左右声道有相位差,导致后续处理效果很差。后来加了延迟补偿才搞定。
经验之谈:声道分离后,建议用 librosa.display.waveshow() 可视化一下波形,肉眼就能看出声道间有没有对齐问题。
3.5 音频裁剪与拼接
音频裁剪和拼接是数据增强的常用手段。比如从长录音中截取有效片段,或者把多个短音频拼接成训练样本。
# 裁剪:从第1秒到第3秒
start_sample = int(1.0 * samplerate)
end_sample = int(3.0 * samplerate)
cropped = data[start_sample:end_sample]
# 拼接:把两个音频片段连起来
concatenated = np.concatenate([data, cropped], axis=0)
# 保存裁剪后的音频
sf.write('cropped.wav', cropped, samplerate)
裁剪时最容易犯的错误是索引越界。你想想看,如果音频只有 2 秒,你却要裁剪第 3 秒到第 5 秒,numpy 不会报错,但返回的是空数组。我建议在裁剪前先检查总样本数。
def safe_crop(data, samplerate, start_sec, end_sec):
total_samples = len(data)
start_sample = int(start_sec * samplerate)
end_sample = int(end_sec * samplerate)
# 边界检查
start_sample = max(0, min(start_sample, total_samples))
end_sample = max(start_sample, min(end_sample, total_samples))
return data[start_sample:end_sample]
注意:拼接不同采样率的音频时,必须先重采样到统一采样率。否则播放时会出现音调突变,特征提取也会出错。
3.6 本章知识体系
下面这张图总结了音频文件读写与基础操作的核心流程。你可以把它当作一个快速参考。
嗯,到这里音频文件的基础操作就讲完了。这些技能看起来简单,但它们是整个声学特征提取系统的地基。我建议你动手跑一遍代码,把 WAV 和 MP3 都试一下,感受一下不同格式之间的差异。