4. 音频信号的读取与写入:soundfile 与 librosa 实战

音频文件的读写,是声学前端处理的第一步。说白了,你连数据都读不进来,后面那些降噪、回声消除、波束成形全是空谈。

我刚开始做音频算法时,觉得读个 wav 文件嘛,简单。结果第一次拿到一个 32 位浮点格式的测试文件,用默认参数一读,数据全乱了。嗯,从那以后我再也不敢小看这一步。

这一节,我们就来聊聊怎么用 Python 里最常用的两个库——soundfile 和 librosa——把不同格式的音频文件读进来,写出去。同时把采样率、位深、通道数这三个核心概念彻底搞清楚。

4.1 三个核心参数:采样率、位深、通道数

在动手写代码之前,我们先统一一下认知。这三个参数决定了音频文件的质量和大小,也决定了后续算法的处理方式。

参数 英文 含义 常见值
采样率 Sample Rate 每秒采集的样本点数,单位 Hz 8000, 16000, 44100, 48000
位深 Bit Depth 每个样本点用多少位二进制数表示 16-bit, 24-bit, 32-bit float
通道数 Channels 音频信号的独立声道数量 1 (Mono), 2 (Stereo)

核心关系:音频数据量 = 采样率 × 位深 × 通道数 × 时长。你想想看,同样的 10 秒音频,48kHz/32-bit/立体声 比 16kHz/16-bit/单声道 大了整整 12 倍。

我在项目中遇到过最坑的一次:客户给的音频文件标称是 16kHz,我直接用 librosa 加载,没检查采样率。结果算法跑出来的延迟全不对,排查了半天才发现文件实际是 48kHz 重采样下来的。所以,永远不要相信文件名和元数据,一定要在代码里显式检查。

4.2 使用 soundfile 读写音频

soundfile 是我个人最常用的底层读写库。它基于 libsndfile,支持绝大多数常见格式,而且读写速度非常快。

4.2.1 读取音频文件

import soundfile as sf

# 读取音频文件
data, samplerate = sf.read('example.wav')

print(f"采样率: {samplerate} Hz")
print(f"数据形状: {data.shape}")
print(f"数据类型: {data.dtype}")

这里要注意:sf.read() 返回的 data 默认是 numpy 数组。如果是单声道,形状是 (样本数,);如果是立体声,形状是 (样本数, 2)。

我的习惯:读取后立刻检查 data.dtype。如果是 float64 或 int16,后续处理可能需要统一转成 float32,因为大部分深度学习框架和 DSP 算法都默认用 float32。

4.2.2 写入音频文件

import numpy as np

# 生成一个 1 秒的 440Hz 正弦波
fs = 44100
t = np.linspace(0, 1, fs, endpoint=False)
sine_wave = 0.5 * np.sin(2 * np.pi * 440 * t)

# 写入文件
sf.write('sine_440.wav', sine_wave, fs, subtype='PCM_16')

# 也可以指定其他格式
sf.write('sine_440.flac', sine_wave, fs, subtype='PCM_24')
sf.write('sine_440.ogg', sine_wave, fs, format='OGG', subtype='VORBIS')

subtype 参数控制位深和编码格式。常用的有:

  • PCM_16:16 位整数,最通用
  • PCM_24:24 位整数,录音棚常用
  • PCM_32:32 位整数,少见
  • FLOAT:32 位浮点,内部处理常用

我曾经踩过的坑:写入 16-bit PCM 文件时,数据范围必须是 [-1.0, 1.0] 的浮点数,soundfile 会自动做量化。如果你传进去的是 int16 的 numpy 数组,它不会报错,但写出来的文件会完全失真。切记:写 PCM_16 时,输入数据归一化到 [-1, 1]。

4.3 使用 librosa 读取音频

librosa 是音频分析领域的瑞士军刀。它封装了 soundfile 和 audioread,提供了更高级的接口。我个人习惯在做特征提取时用 librosa,做底层 I/O 时用 soundfile。

4.3.1 基本读取

import librosa

# 读取音频,默认重采样到 22050 Hz
data, sr = librosa.load('example.wav')

print(f"采样率: {sr} Hz")
print(f"数据形状: {data.shape}")
print(f"时长: {len(data)/sr:.2f} 秒")

注意:librosa.load() 默认会做两件事:

  1. 重采样到 22050 Hz(除非你指定 sr=None)
  2. 混音到单声道(立体声会取平均)

如果你需要保持原始采样率和通道数,一定要显式设置:

# 保持原始采样率,保持立体声
data, sr = librosa.load('example.wav', sr=None, mono=False)

print(f"通道数: {data.shape[0]}")  # 注意:librosa 返回的是 (通道, 样本) 格式

重要区别:soundfile 返回的数组形状是 (样本, 通道),而 librosa 返回的是 (通道, 样本)。这个差异在写代码时很容易搞混,我至少因为这个 bug 排查过三次。

4.3.2 指定读取片段

处理长音频时,我们经常只需要读取某一段。librosa 提供了很方便的参数:

# 只读取第 10 秒到第 15 秒的片段
data, sr = librosa.load('long_audio.wav', offset=10.0, duration=5.0)

# 从第 30 秒开始,读取 10 秒
data, sr = librosa.load('long_audio.wav', offset=30.0, duration=10.0)

这个功能在做大数据集训练时特别有用。你想想看,一个 2 小时的会议录音,你不需要全部加载到内存里,按需读取片段就行了。

4.4 格式支持对比

不同库支持的格式不一样。我整理了一个表格,方便你快速查阅:

格式 soundfile librosa 备注
WAV (PCM) ✅ 完整支持 ✅ 完整支持 最通用的无损格式
FLAC ✅ 支持 ✅ 支持 无损压缩,文件小
OGG/Vorbis ✅ 支持 ✅ 支持 有损压缩,适合流媒体
MP3 ❌ 不支持 ✅ 支持(需 audioread) 有损压缩,最普及
M4A/AAC ❌ 不支持 ✅ 支持(需 audioread) 苹果生态常用

我的建议:在工程落地时,尽量统一用 WAV 或 FLAC 格式。MP3 和 M4A 的解码依赖外部库,不同环境下的解码结果可能有微小差异,这在做算法对比时是个大坑。

4.5 知识体系总览

下面这张图总结了本章的核心逻辑:从文件到内存数组,再回到文件,中间涉及的所有关键参数和注意事项。

音频读写知识体系图 音频文件读写核心流程 音频文件 WAV / FLAC / MP3 读取方式 soundfile / librosa 内存数组 numpy.ndarray 采样率 (Sample Rate) 位深 (Bit Depth) 通道数 (Channels) ⚠️ 常见注意事项 1. 检查 dtype:float64 → float32 转换 2. 形状差异:soundfile (样本, 通道) vs librosa (通道, 样本) 写入文件 指定 subtype PCM_16 / FLOAT 输出文件

4.6 实战建议与避坑指南

最后,分享几个我在实际项目中积累的经验:

  • 统一采样率:在训练数据准备阶段,先把所有音频重采样到同一个采样率(比如 16kHz 或 48kHz),避免后续处理时反复重采样引入失真。
  • 检查文件完整性:我曾经遇到过一批 WAV 文件,头部信息正常,但实际数据是截断的。建议读取后检查 len(data) / sr 是否接近预期时长。
  • 批量处理时用 soundfile:librosa 的 load() 每次都会创建新的音频解码器,批量处理上千个文件时,soundfile 的速度优势非常明显。
  • 写入时注意数据范围:再次强调,写 16-bit PCM 时数据范围 [-1, 1],写 32-bit float 时范围 [-1, 1] 或更大都可以,但写 24-bit 时最好也归一化到 [-1, 1]。

我曾经犯过的错:有一次做数据增强,把音频乘以 2 后直接写回文件,没做 clipping 处理。结果 16-bit 文件里全是削波失真,听起来像破锣。从那以后,我每次写文件前都会加一句 np.clip(data, -1.0, 1.0)。

好了,这一节的内容就到这里。音频读写看似简单,但细节决定成败。你把这些基础打牢了,后面做滤波、降噪、特征提取时才能得心应手。