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

下面这张图,帮你理清本章的核心逻辑:

音频文件读写与基础操作知识体系 音频文件读写与基础操作知识体系 音频文件操作 文件读取 元数据提取 裁剪与拼接 批量处理脚本 soundfile (WAV) pydub (MP3) sf.info() mutagen (ID3) audio[start:end] audio1 + audio2 格式转换 元数据报告 核心原则:WAV 用 soundfile,MP3 用 pydub,元数据用 mutagen 工程化落地:自动化批量处理

这张图把本章的知识点串起来了。从中心节点「音频文件操作」出发,四个分支分别对应读取、元数据、裁剪拼接和批量处理。每个分支下面又有具体的工具和方法。最下面的「工程化落地:自动化批量处理」是最终目标——把零散的操作整合成可复用的脚本。

好了,这一章的内容就到这里。音频文件读写看似基础,但如果你不注意细节,后面会踩很多坑。记住我提到的那些避坑点,尤其是 dtype、采样率统一、时长检查——这些都是在实际项目中真金白银换来的教训。