3. 时域与频域分析

好,咱们进入第三章。前两章我们把声音的物理本质和数字化的基本概念捋了一遍。从这章开始,要动真格的了——上手写代码。

你可能会问:「为什么跳过纯数学推导,直接上库?」我的回答是:音频算法工程师的核心能力,不是从零实现一个FFT,而是知道在什么场景下用什么工具,以及出了问题怎么排查。Librosa和SoundFile就是最趁手的两把刀。我个人习惯,拿到任何音频项目,第一件事就是拿这两个库把数据「摸」一遍。

3.1 Librosa库:音频分析的事实标准

Librosa,说白了就是Python生态里做音频分析最主流的库。它封装了从读取音频、提取特征到可视化的一整套流程。我刚开始做语音增强那会儿,还没这库,得自己写wav读取、自己算频谱,那叫一个痛苦。后来Librosa出来,效率直接翻倍。

3.4.1 安装与环境配置

安装很简单,但有个坑我得先告诉你:Librosa的依赖链比较长,尤其是llvmlite和numba这两个,在Windows上容易翻车。我的建议是直接用conda创建虚拟环境,省心。

# 推荐方式:用conda创建干净环境
conda create -n audio_learning python=3.9
conda activate audio_learning

# 安装Librosa(会自动安装numpy, scipy, soundfile等依赖)
pip install librosa

# 验证安装
python -c "import librosa; print(librosa.__version__)"
小提示:如果你在安装时遇到llvmlite报错,试试先装conda版本:conda install llvmlite,再装librosa。我曾经在这上面卡了半小时,后来发现是VS Build Tools没装全。

3.4.2 核心API速览

Librosa的API设计得很直觉。我把它最常用的几个函数整理成了表格,你写代码时可以直接翻回来查。

函数 功能 典型返回值
librosa.load() 读取音频文件,自动重采样 (numpy数组, 采样率)
librosa.get_duration() 获取音频时长(秒) float
librosa.stft() 短时傅里叶变换 复数矩阵 (频率×时间)
librosa.feature.mfcc() 提取MFCC特征 二维数组 (系数×帧数)
librosa.effects.split() 静音检测与分割 片段起止索引列表
librosa.output.write_wav() 写入wav文件(已弃用,建议用soundfile) 无
注意:librosa.output.write_wav()在0.9.0版本后已标记为弃用。官方推荐用soundfile.write()替代。我自己的项目里也早就全面切换到soundfile了,更稳定,支持的格式也更多。

3.2 SoundFile:读写音频的瑞士军刀

SoundFile这个库,说白了就是底层调用libsndfile的Python封装。它支持的格式比Librosa自带的读写器多得多——FLAC、OGG、WAV、AIFF全都能读写。而且它的接口非常干净,就两个核心函数。

3.2.1 读取音频文件

import soundfile as sf

# 读取音频,返回数据和采样率
data, sr = sf.read('example.wav')

# 读取时指定采样率(自动重采样)
data, sr = sf.read('example.wav', samplerate=16000)

# 只读取部分样本(适合大文件)
data, sr = sf.read('example.wav', start=0, stop=44100)  # 只读第一秒

这里有个细节:sf.read()默认返回的data是float64类型,范围在[-1.0, 1.0]之间。如果你需要整数编码(比如16-bit PCM),可以用dtype='int16'参数。嗯,这个转换我在做TTS数据预处理时经常用到。

3.2.2 写入音频文件

import numpy as np
import soundfile as sf

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

# 写入文件
sf.write('sine_440.wav', sine_wave, sr)

# 写入时指定子类型(比如16-bit PCM)
sf.write('sine_440_16bit.wav', sine_wave, sr, subtype='PCM_16')
避坑指南:我曾经在写入时忘记归一化,直接把[-1, 1]范围的float数组当成int16写进去,结果播放出来全是爆音。记住:sf.write()会根据dtype自动做缩放,但如果你手动转了int16,一定要确保数据范围在[-32768, 32767]内。

3.3 音频数组的基本操作

音频数据在Python里,本质上就是一个numpy数组。一维是单声道,二维是多声道(shape为[样本数, 声道数])。所有numpy的数组操作都能直接用。

3.3.1 切片:截取音频片段

import librosa

# 加载音频
y, sr = librosa.load('speech.wav', sr=16000)

# 截取第2秒到第4秒
start_sample = 2 * sr   # 第2秒对应的样本索引
end_sample = 4 * sr     # 第4秒对应的样本索引
segment = y[start_sample:end_sample]

# 截取前0.5秒
first_half_sec = y[:int(0.5 * sr)]

# 截取最后1秒
last_sec = y[-sr:]

切片操作看起来简单,但有个坑:采样率sr的单位是「样本/秒」,所以时间 × sr得到的是样本索引。我见过不少新手直接写y[2:4],以为截了2秒,结果只截了2个样本——那声音几乎听不见。

3.3.2 拼接:合并多个音频

import numpy as np

# 假设有两个音频片段
part1, sr = librosa.load('part1.wav', sr=16000)
part2, sr = librosa.load('part2.wav', sr=16000)

# 时间维度的拼接(前后接起来)
combined = np.concatenate([part1, part2])

# 声道维度的拼接(左右声道合并)
# 要求两个数组长度相同
stereo = np.column_stack([part1, part2])

# 混音(叠加)
# 要求两个数组长度相同
mixed = (part1 + part2) / 2  # 除以2防止削波
小技巧:做混音时,直接相加很容易导致数值溢出(超过[-1, 1]范围)。我一般先分别乘以一个系数(比如0.7和0.3),再相加,这样能保留主次关系,又不会爆音。

3.3.3 归一化:统一音量尺度

归一化是音频预处理里最基础也最重要的操作。不同录音设备的音量差异可能很大,归一化能保证后续特征提取的一致性。

import numpy as np

def normalize_peak(y):
    """峰值归一化:将最大振幅映射到±1"""
    max_val = np.max(np.abs(y))
    if max_val == 0:
        return y
    return y / max_val

def normalize_rms(y, target_dB=-20):
    """RMS归一化:将均方根值调整到目标分贝"""
    target_rms = 10 ** (target_dB / 20)
    current_rms = np.sqrt(np.mean(y ** 2))
    if current_rms == 0:
        return y
    return y * (target_rms / current_rms)

# 使用示例
y, sr = librosa.load('loud_speech.wav', sr=16000)
y_peak_norm = normalize_peak(y)
y_rms_norm = normalize_rms(y, target_dB=-23)  # 广播标准通常用-23dB

峰值归一化和RMS归一化有什么区别?我简单解释一下:峰值归一化只看最大绝对值,适合防止削波;RMS归一化看整体能量,适合让不同音频听起来音量一致。在语音合成数据预处理中,我几乎只用RMS归一化,因为峰值归一化对背景噪声太敏感了——一段安静但有突发噪音的音频,峰值归一化会把整体音量压得很低。

3.4 本章知识体系

下面这张图把本章的核心知识点串起来了。你可以把它当作一个操作地图:拿到音频文件后,先走哪条路,再走哪条路。

第三章知识体系:Python音频处理入门 第三章:Python音频处理入门 — 知识体系 音频文件 Librosa 分析库 安装与依赖管理 核心API:load / stft / mfcc 特征提取与可视化 SoundFile 读写库 sf.read() 读取参数详解 sf.write() 格式与子类型 dtype与数值范围控制 NumPy 数组操作 切片:时间轴截取 拼接:concatenate / 混音 归一化:峰值 / RMS 输出:标准化后的NumPy数组 → 后续特征提取 💡 三条主线并行不悖:Librosa负责分析,SoundFile负责IO,NumPy负责底层操作 实际项目中通常三者配合使用:SoundFile读入 → NumPy预处理 → Librosa提取特征

3.5 实战:完整的音频预处理流水线

最后,我把本章所有知识点串成一个完整的脚本。你可以直接拿它处理任意音频文件。

import soundfile as sf
import numpy as np
import librosa

def preprocess_audio(input_path, output_path, target_sr=16000, target_dB=-23):
    """
    完整的音频预处理流水线
    步骤:读取 → 重采样 → 归一化 → 静音切除 → 保存
    """
    # 1. 读取音频(自动重采样到target_sr)
    y, sr = sf.read(input_path, samplerate=target_sr)
    print(f"原始采样率:{sr},时长:{len(y)/sr:.2f}秒")

    # 2. 如果是多声道,转为单声道(取平均)
    if y.ndim > 1:
        y = np.mean(y, axis=1)
        print("已转为单声道")

    # 3. RMS归一化到目标分贝
    target_rms = 10 ** (target_dB / 20)
    current_rms = np.sqrt(np.mean(y ** 2))
    if current_rms > 0:
        y = y * (target_rms / current_rms)
    print(f"归一化完成,目标RMS:{target_dB}dB")

    # 4. 静音切除(Librosa的split方法)
    # 返回的是非静音片段的起止索引
    non_silent_intervals = librosa.effects.split(y, top_db=30)
    if len(non_silent_intervals) > 0:
        # 把所有非静音片段拼接起来
        y_trimmed = np.concatenate([y[start:end] for start, end in non_silent_intervals])
        print(f"静音切除后时长:{len(y_trimmed)/sr:.2f}秒")
    else:
        y_trimmed = y
        print("未检测到静音,保留原音频")

    # 5. 保存处理后的音频
    sf.write(output_path, y_trimmed, sr, subtype='PCM_16')
    print(f"处理完成,保存至:{output_path}")

    return y_trimmed, sr

# 使用示例
if __name__ == "__main__":
    processed_audio, sr = preprocess_audio(
        input_path='raw_recording.wav',
        output_path='processed_speech.wav',
        target_sr=16000,
        target_dB=-23
    )
核心要点回顾:
  • Librosa是分析工具,SoundFile是IO工具,各司其职
  • 音频数组就是NumPy数组,所有numpy操作都适用
  • 归一化时注意区分峰值归一化和RMS归一化的适用场景
  • 切片时一定要用时间 × 采样率计算样本索引