4、音频文件读写与基础操作:WAV/MP3文件格式解析、使用soundfile和librosa读写音频、音频信息查看
好,咱们今天聊聊音频文件读写。这可以说是语音信号处理里最基础、也最绕不开的一步。你想想看,不管后面做多牛的算法,第一步总得把音频文件读进来吧?
我个人习惯,拿到一个音频文件,第一件事不是急着跑代码,而是先看看它到底是个什么来头。是WAV还是MP3?采样率多少?位深多少?声道数几个?这些信息搞不清楚,后面很容易踩坑。
4.1 WAV与MP3:两种最常见的音频格式
先说说WAV。WAV是微软和IBM联合开发的,说白了就是一段裸音频数据加上一个文件头。这个文件头里记录了采样率、位深、声道数这些关键信息。我刚开始做项目时,总觉得WAV太占空间,后来才发现,正是因为它不压缩,处理起来最省心,不会出现解码带来的各种奇怪问题。
MP3就不一样了。它是有损压缩格式,通过丢掉一些人耳不敏感的频率成分来减小体积。嗯,这里要注意:MP3的解码过程其实挺复杂的,涉及到心理声学模型、霍夫曼编码等等。不过好在,我们一般不用自己实现解码器,直接用现成的库就行。
核心区别一句话总结:WAV是原始数据,无损但体积大;MP3是压缩数据,体积小但有损。做算法开发时,我建议尽量用WAV,避免压缩带来的信息损失。
我遇到过最尴尬的一次,是客户给了一批MP3格式的录音,采样率标称是16kHz,但实际解码后发现是22.05kHz。你想想看,如果直接按16kHz去处理,后面的特征提取全都会偏掉。所以,读文件时一定要检查实际采样率,别信文件名。
4.2 使用soundfile读写音频
soundfile这个库,我个人非常喜欢。它底层用的是libsndfile,支持WAV、FLAC、OGG等格式,但不支持MP3。为什么?因为MP3的专利问题,libsndfile一直没集成MP3解码器。
来看看怎么用:
import soundfile as sf
# 读取音频文件
data, samplerate = sf.read('example.wav')
# data是numpy数组,shape为(样本数, 声道数)
# samplerate是采样率,整数
# 查看基本信息
print(f"采样率: {samplerate} Hz")
print(f"数据形状: {data.shape}")
print(f"数据类型: {data.dtype}")
print(f"时长: {data.shape[0] / samplerate:.2f} 秒")
# 写入音频文件
sf.write('output.wav', data, samplerate)
这里有个细节:sf.read()默认返回的数据类型是float64,范围在[-1.0, 1.0]之间。如果你想要整数类型,可以加参数dtype='int16'。我在项目中习惯用float,因为后续做特征提取时,浮点数运算更方便。
小技巧:如果你读到的音频是int16类型,范围在[-32768, 32767]之间,记得归一化到[-1.0, 1.0]再处理。我见过有人直接拿int16去做FFT,结果频谱图全是乱的。
4.3 使用librosa读写音频
librosa是音频分析领域的明星库。它不仅能读写音频,还集成了大量的特征提取函数。不过要注意,librosa底层默认使用soundfile来读WAV,用audioread来读MP3。
import librosa
# 读取音频,librosa会自动重采样到22050Hz
data, samplerate = librosa.load('example.mp3', sr=22050)
# sr=None表示保持原始采样率
# 查看音频信息
print(f"采样率: {samplerate} Hz")
print(f"数据形状: {data.shape}")
print(f"时长: {librosa.get_duration(y=data, sr=samplerate):.2f} 秒")
# 写入音频
librosa.output.write_wav('output.wav', data, samplerate)
# 注意:librosa的write_wav函数在新版本中已弃用,建议用soundfile
嗯,这里要提醒一下:librosa.load()默认会把音频重采样到22050Hz,而且会混音成单声道。如果你需要原始采样率和立体声,记得设置sr=None, mono=False。我曾经因为这个默认行为,浪费了半天排查特征提取结果不对的问题。
避坑指南:librosa的load()函数默认会做两件事:重采样到22050Hz、混音到单声道。如果你做的是多声道处理或者需要原始采样率,一定要显式设置参数。我曾经在做一个多声源定位项目时,没注意这个默认行为,结果所有声源方向信息全丢了。
4.4 音频信息查看:别只看文件名
我见过太多人,拿到音频文件后只看文件名就以为知道了采样率和格式。结果呢?跑出来的结果一塌糊涂。所以,我建议养成一个好习惯:每次读文件后,都打印一下关键信息。
这里给一个我自己常用的信息查看函数:
import soundfile as sf
import numpy as np
def inspect_audio(file_path):
"""查看音频文件的详细信息"""
# 用soundfile读取文件头信息
info = sf.info(file_path)
print(f"文件名: {file_path}")
print(f"采样率: {info.samplerate} Hz")
print(f"声道数: {info.channels}")
print(f"位深: {info.subtype}") # 比如'PCM_16', 'FLOAT'等
print(f"总样本数: {info.frames}")
print(f"时长: {info.duration:.2f} 秒")
print(f"文件格式: {info.format}") # 比如'WAV', 'FLAC'等
# 读取实际数据,验证
data, sr = sf.read(file_path)
print(f"实际数据形状: {data.shape}")
print(f"数据范围: [{data.min():.4f}, {data.max():.4f}]")
print(f"数据均值: {data.mean():.6f}")
return data, sr
这个函数会打印出文件头里的信息,还会读取实际数据做验证。我一般在项目开始时,先用这个函数扫一遍所有音频文件,看看有没有异常。比如采样率不一致、声道数不对、数据范围异常等等。
经验之谈:我遇到过最离谱的一次,是一个标注好的语音数据集,里面混进了一个采样率只有8kHz的文件。如果不检查,直接跟其他16kHz的文件一起处理,后面的模型训练肯定会出问题。所以,批量处理前一定要做数据质量检查。
4.5 常见问题与避坑指南
最后,总结几个我经常遇到的坑:
- 采样率不一致:不同来源的音频,采样率可能不同。处理前一定要统一,否则特征维度会乱。
- 数据类型问题:int16和float混用,会导致计算错误。建议统一转成float。
- 声道数混淆:立体声和单声道处理方式不同。做语音识别一般用单声道,做声源定位需要立体声。
- MP3解码问题:不同解码器可能得到不同的结果。建议统一用librosa或pydub解码。
- 文件损坏:有些音频文件头信息正确,但实际数据损坏。读取时最好加异常处理。
好了,关于音频文件读写和基础操作,今天就聊这么多。记住一句话:读文件时多看一眼,后面能省很多事。下一章我们会讲音频重采样和格式转换,到时候再细聊。