3、音频文件的读写与基础操作
音频文件读写,是声学特征提取的第一步。说白了,你连文件都打不开,后面那些花哨的算法全是白搭。
我见过不少新手,一上来就调 librosa.load(),结果遇到 MP3 文件直接报错。为什么?因为 librosa 底层依赖 audioread,而 audioread 在某些系统上对 MP3 的支持并不稳定。所以,我个人的习惯是——针对不同格式,选不同的工具。
核心原则:WAV 用 soundfile,MP3 用 pydub,元数据用 mutagen。各司其职,别混用。
3.1 使用 soundfile 读取 WAV 文件
WAV 是音频处理中最常见的无损格式。soundfile 库基于 libsndfile,读写效率高,而且支持 24-bit、32-bit float 等深度格式。
基本用法很简单:
import soundfile as sf
# 读取 WAV 文件
data, samplerate = sf.read('speech.wav')
# data: numpy.ndarray, shape=(samples, channels)
# samplerate: int, 如 16000
# 写入 WAV 文件
sf.write('output.wav', data, samplerate)
这里有个坑——sf.read() 默认返回的 data 是 float64 类型,范围在 [-1.0, 1.0] 之间。如果你需要整数 PCM 数据,记得指定 dtype:
data_int, sr = sf.read('speech.wav', dtype='int16')
我曾经在做一个语音识别项目时,直接把 float 数据喂给特征提取模块,结果出来的 MFCC 全是 NaN。查了半天才发现,是数据范围不对。嗯,从那以后我每次读取都会确认 dtype。
| 参数 | 说明 | 常用值 |
|---|---|---|
| file | 文件路径或类文件对象 | 'audio.wav' |
| dtype | 返回的数据类型 | 'float64', 'int16', 'int32' |
| samplerate | 目标采样率(None 表示保持原样) | 16000, 44100 |
| channels | 读取的声道数(1 为单声道) | 1, 2 |
3.2 使用 pydub 处理 MP3
MP3 是有损压缩格式,libsndfile 不支持直接读取。这时候 pydub 就派上用场了。它底层依赖 ffmpeg,几乎能处理所有常见格式。
from pydub import AudioSegment
# 读取 MP3 文件
audio = AudioSegment.from_mp3('music.mp3')
# 转换为 WAV 并导出
audio.export('music.wav', format='wav')
# 获取基本信息
duration_ms = len(audio) # 毫秒
channels = audio.channels
frame_rate = audio.frame_rate
sample_width = audio.sample_width
你想想看,pydub 的 AudioSegment 对象其实就是一个内存中的音频片段。你可以对它做各种操作,比如调整音量、裁剪、拼接,甚至混音。
小技巧:pydub 读取 MP3 时依赖 ffmpeg。如果你在 Windows 上遇到 "ffmpeg not found" 的错误,记得把 ffmpeg 的 bin 目录加到系统 PATH 里,或者直接用 AudioSegment.ffmpeg = "ffmpeg.exe" 指定路径。
3.3 音频元数据提取
元数据是什么?就是音频文件里除了波形数据之外的那些信息——采样率、位深、声道数、时长、标签、专辑封面等等。
用 soundfile 可以获取基本元数据:
import soundfile as sf
info = sf.info('speech.wav')
print(f"采样率: {info.samplerate}")
print(f"声道数: {info.channels}")
print(f"时长: {info.duration} 秒")
print(f"格式: {info.format}")
print(f"子类型: {info.subtype}")
对于 MP3 或更复杂的元数据(如 ID3 标签),我推荐用 mutagen:
from mutagen.mp3 import MP3
audio = MP3('music.mp3')
print(f"时长: {audio.info.length} 秒")
print(f"比特率: {audio.info.bitrate} bps")
print(f"采样率: {audio.info.sample_rate}")
# 读取 ID3 标签
if audio.tags:
print(f"标题: {audio.tags.get('TIT2')}")
print(f"艺术家: {audio.tags.get('TPE1')}")
我在做音频数据集清洗时,经常需要批量提取元数据来检查文件是否损坏。比如采样率不一致、时长异常、声道数不对——这些都会影响后续的特征提取。
| 库 | 适用格式 | 元数据能力 |
|---|---|---|
| soundfile | WAV, FLAC, OGG | 基本格式信息 |
| mutagen | MP3, FLAC, M4A | ID3 标签、时长、比特率 |
| pydub | 几乎所有格式 | 时长、声道、采样率 |
3.4 音频裁剪与拼接
裁剪和拼接是音频预处理中最常见的操作。比如从一段长录音中截取有效语音段,或者把多个短音频拼接成一个训练样本。
裁剪:
from pydub import AudioSegment
audio = AudioSegment.from_wav('long_speech.wav')
# 裁剪前 10 秒(单位:毫秒)
segment = audio[:10000]
# 裁剪 5 秒到 15 秒
segment = audio[5000:15000]
# 导出
segment.export('segment.wav', format='wav')
拼接:
audio1 = AudioSegment.from_wav('part1.wav')
audio2 = AudioSegment.from_wav('part2.wav')
# 简单拼接
combined = audio1 + audio2
# 交叉淡入淡出(2 秒过渡)
combined = audio1.append(audio2, crossfade=2000)
combined.export('combined.wav', format='wav')
这里要注意——拼接时两个音频的采样率和声道数必须一致。如果不一致,pydub 会自动重采样,但重采样算法不一定是你想要的。我建议手动统一:
audio2 = audio2.set_frame_rate(audio1.frame_rate)
audio2 = audio2.set_channels(audio1.channels)
注意:裁剪时如果起始时间超出音频长度,pydub 不会报错,而是返回空片段。我曾经因为这个 bug 导致一批训练数据全是静音,模型训了三天才发现。所以,裁剪前一定要检查时长。
3.5 批量处理脚本编写
工程化落地的关键,就是批量处理。你不能每次都手动裁剪一个文件,对吧?
下面是一个典型的批量处理脚本框架:
import os
import soundfile as sf
from pathlib import Path
def batch_convert_to_wav(input_dir, output_dir, target_sr=16000):
"""
批量将 MP3 转换为 WAV,并统一采样率
"""
input_dir = Path(input_dir)
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
for mp3_path in input_dir.glob('*.mp3'):
# 读取 MP3
audio = AudioSegment.from_mp3(str(mp3_path))
# 统一采样率
audio = audio.set_frame_rate(target_sr)
# 转为单声道
audio = audio.set_channels(1)
# 导出 WAV
wav_path = output_dir / f"{mp3_path.stem}.wav"
audio.export(str(wav_path), format='wav')
print(f"转换完成: {mp3_path.name} -> {wav_path.name}")
def batch_extract_metadata(data_dir):
"""
批量提取音频元数据,生成 CSV 报告
"""
import csv
data_dir = Path(data_dir)
rows = []
for wav_path in data_dir.glob('*.wav'):
info = sf.info(str(wav_path))
rows.append({
'filename': wav_path.name,
'samplerate': info.samplerate,
'channels': info.channels,
'duration': info.duration,
'format': info.format,
})
# 写入 CSV
with open('metadata_report.csv', 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(rows)
print(f"元数据报告已生成: metadata_report.csv")
你想想看,有了这个脚本,你只需要指定输入输出目录,就能一键完成格式转换和元数据提取。这就是工程化的思维——把重复劳动自动化。
3.6 本章知识体系
下面这张图,帮你理清本章的核心逻辑:
这张图把本章的知识点串起来了。从中心节点「音频文件操作」出发,四个分支分别对应读取、元数据、裁剪拼接和批量处理。每个分支下面又有具体的工具和方法。最下面的「工程化落地:自动化批量处理」是最终目标——把零散的操作整合成可复用的脚本。
好了,这一章的内容就到这里。音频文件读写看似基础,但如果你不注意细节,后面会踩很多坑。记住我提到的那些避坑点,尤其是 dtype、采样率统一、时长检查——这些都是在实际项目中真金白银换来的教训。