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__)"
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')
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防止削波
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 本章知识体系
下面这张图把本章的核心知识点串起来了。你可以把它当作一个操作地图:拿到音频文件后,先走哪条路,再走哪条路。
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归一化的适用场景
- 切片时一定要用
时间 × 采样率计算样本索引