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 本章知识体系

下面这张图总结了音频文件读写与基础操作的核心流程。你可以把它当作一个快速参考。

音频文件读写与基础操作知识体系 音频文件输入 soundfile 读取 WAV pydub 处理 MP3 numpy 数组表示 声道分离与合并 音频裁剪 音频拼接 保存/输出 核心原则 先读后写 统一采样率 注意边界检查

嗯,到这里音频文件的基础操作就讲完了。这些技能看起来简单,但它们是整个声学特征提取系统的地基。我建议你动手跑一遍代码,把 WAV 和 MP3 都试一下,感受一下不同格式之间的差异。