3. 时域特征
音频文件读写,是声学特征提取的第一步。说白了,你连数据都读不进来,后面那些花里胡哨的特征计算全是空谈。我见过不少同学,模型搭得挺漂亮,结果卡在文件读取上——采样率对不上、通道数搞错、时长算不准,折腾半天。
这一章,我们就来搞定这些基本功。我会用 librosa 和 soundfile 这两个库,带你走一遍音频读写的完整流程。
3.1 为什么选这两个库?
Python 里能读音频的库不少,比如 scipy.io.wavfile、pydub、audioread。但我个人习惯,主力用 librosa 和 soundfile。原因很简单:
- librosa:生态好,读进来直接就是 numpy 数组,后面做特征提取无缝衔接。它底层其实也调了
soundfile或audioread。 - soundfile:轻量、稳定,对 WAV/FLAC/OGG 支持极好。我在项目里遇到过大文件读取崩溃的问题,换成
soundfile就稳了。
3.2 读取音频文件:获取采样率、时长、通道数
先看最基础的操作——读文件。我们用一个实际例子来说明。
import librosa
import soundfile as sf
import numpy as np
# 用 librosa 读取
file_path = "speech_sample.wav"
y, sr = librosa.load(file_path, sr=None) # sr=None 保持原始采样率
# 用 soundfile 读取
y_sf, sr_sf = sf.read(file_path)
print(f"librosa: 采样率={sr}, 样本点数={len(y)}, 时长={len(y)/sr:.2f}s")
print(f"soundfile: 采样率={sr_sf}, 样本点数={len(y_sf)}, 时长={len(y_sf)/sr_sf:.2f}s")
这里有个细节:librosa.load 默认会把音频重采样到 22050 Hz,并且转成单声道。如果你不想它乱改,记得传 sr=None 和 mono=False。嗯,这个坑我踩过不止一次。
获取通道数,不同库方式不一样:
# librosa 默认返回单声道,shape 是 (n_samples,)
# 如果想保留多通道,需要指定 mono=False
y_stereo, sr = librosa.load(file_path, sr=None, mono=False)
if y_stereo.ndim == 1:
channels = 1
else:
channels = y_stereo.shape[0]
# soundfile 更直接
info = sf.info(file_path)
print(f"通道数: {info.channels}")
print(f"采样率: {info.samplerate}")
print(f"时长: {info.duration:.2f}s")
3.3 音频裁剪:精确到样本点
音频裁剪,说白了就是从时间轴上切一段。我做过一个语音唤醒项目,需要从长录音里精确切出“你好小X”这个片段。那时候用的就是下面这套逻辑。
def crop_audio(y, sr, start_time, end_time):
"""
裁剪音频片段
:param y: 音频数组
:param sr: 采样率
:param start_time: 起始时间(秒)
:param end_time: 结束时间(秒)
:return: 裁剪后的音频数组
"""
start_sample = int(start_time * sr)
end_sample = int(end_time * sr)
# 边界检查,防止越界
start_sample = max(0, start_sample)
end_sample = min(len(y), end_sample)
return y[start_sample:end_sample]
# 使用示例
y, sr = librosa.load("long_recording.wav", sr=None)
cropped = crop_audio(y, sr, 5.0, 10.5) # 切出 5~10.5 秒
print(f"原始长度: {len(y)/sr:.2f}s, 裁剪后: {len(cropped)/sr:.2f}s")
为什么会强调边界检查?我曾经在处理一批数据时,有个标注文件的时间戳超出了音频总长,程序直接崩了。从那以后,我所有裁剪函数都会加 max/min 保护。
3.4 音频拼接:无缝连接
拼接比裁剪稍微复杂一点。最简单的做法就是 np.concatenate,但要注意两个问题:采样率必须一致,而且直接拼接会在连接点产生“咔哒”声。
def concatenate_audio(y_list, sr_list):
"""
拼接多个音频片段
:param y_list: 音频数组列表
:param sr_list: 对应采样率列表
:return: 拼接后的音频数组
"""
# 检查采样率是否一致
if len(set(sr_list)) != 1:
raise ValueError("所有片段的采样率必须一致")
return np.concatenate(y_list)
# 示例:拼接两个片段
y1, sr = librosa.load("part1.wav", sr=None)
y2, _ = librosa.load("part2.wav", sr=None)
y_combined = concatenate_audio([y1, y2], [sr, sr])
sf.write("combined.wav", y_combined, sr)
3.5 音频文件的写入
写完数据要存盘,soundfile.write 是我的首选。它支持 WAV、FLAC、OGG 等格式,而且写入质量可控。
# 写入 WAV 文件
sf.write("output.wav", y_combined, sr)
# 写入 FLAC(无损压缩,文件小一半)
sf.write("output.flac", y_combined, sr)
# 指定子类型(比如 24-bit PCM)
sf.write("output_24bit.wav", y_combined, sr, subtype='PCM_24')
librosa 也提供了 librosa.output.write_wav,但新版已经废弃了,官方推荐直接用 soundfile。我个人也一直这么用。
3.6 本章知识体系
下面这张图,帮你把本章的核心逻辑串起来:
3.7 常见问题与避坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 读取后音频变短 | librosa 默认重采样到 22050 Hz | 传参 sr=None |
| 多通道变单通道 | librosa 默认 mono=True | 传参 mono=False |
| 拼接处有爆音 | 波形不连续 | 做交叉淡入淡出 |
| 写入后文件损坏 | 数组类型不对(如 float64) | 转成 float32 或 int16 |
np.max(np.abs(y)),如果接近 0 就标记异常。
好了,这一章的内容就到这里。音频读写看似简单,但细节不少。把基础打牢,后面做特征提取才能顺手。记住我上面提到的几个关键点:采样率一致性、数组形状差异、边界检查——这些在工程里都是血泪教训换来的。