该代码实现了一个基于深度学习的音频源分离系统,通过神经网络模型将音乐文件分离为人声和乐器两个部分,支持多种模型配置和音频格式输出。
graph TD
A[开始] --> B[加载模型和参数]
B --> C{是否有输出路径?}
C -- 否 --> D[返回错误信息]
C -- 是 --> E[加载音频文件]
E --> F[多频段处理]
F --> G[生成频谱图]
G --> H[神经网络推理]
H --> I[后处理频谱]
I --> J{是否需要乐器?}
J -- 是 --> K[生成乐器音频]
J -- 否 --> L{是否需要人声?}
L -- 是 --> M[生成人声音频]
K --> N[保存音频文件]
M --> N
N --> O[返回路径元组]
_audio_pre_ (音频预处理基类)
└── _path_audio_: 处理音频分离的核心方法
_audio_pre_new (音频预处理新版本类)
└── _path_audio_: 处理音频分离的核心方法当前工作目录
类型:str
计算设备配置
类型:str
是否使用半精度推理
类型:bool
模型权重文件路径
类型:str
输入音频文件路径
类型:str
输出目录路径
类型:str
模型文件路径
类型:str
计算设备(cpu/cuda)
类型:str
处理配置参数
类型:dict
模型参数配置对象
类型:ModelParameters
神经网络模型
类型:CascadedASPPNet
模型文件路径
类型:str
计算设备(cpu/cuda)
类型:str
处理配置参数
类型:dict
模型参数配置对象
类型:ModelParameters
神经网络模型
类型:CascadedNet
该函数用于将时域音频波形转换为频域频谱图表示,支持多线程处理,是音频分离模型的核心预处理步骤,通过短时傅里叶变换(STFT)将波形数据转换为复数频谱图。
参数:
wave:numpy.ndarray,输入的音频波形数据(多声道)hop_length:int,帧移长度(hop length),控制STFT的时间分辨率n_fft:int,快速傅里叶变换的窗口大小mid_side:bool,是否使用中间-侧面(Mid-Side)编码方式处理立体声mid_side_b2:bool,第二级别的中间-侧面编码标志reverse:bool,是否反向处理音频
返回值:numpy.ndarray,返回复数频谱图,形状为 (frames, frequency_bins, channels)
graph TD
A[开始] --> B{是否为立体声}
B -->|是| C[应用Mid-Side变换]
B -->|否| D[保持原始声道]
C --> E[执行多线程STFT]
D --> E
E --> F{reverse参数}
F -->|True| G[反转频谱图]
F -->|False| H[保持原序]
G --> I[返回复数频谱图]
H --> I
# 该函数定义在 spec_utils 模块中
# 以下为根据调用推断的函数签名和逻辑
def wave_to_spectrogram_mt(
wave, # 输入波形: numpy.ndarray, 形状为 (samples, channels)
hop_length, # 帧移: int, 通常为窗口大小的1/4
n_fft, # FFT窗口大小: int, 常用值为512, 1024, 2048等
mid_side=False, # Mid-Side编码: bool, 用于立体声处理
mid_side_b2=False, # 二级Mid-Side: bool
reverse=False # 反转: bool
):
"""
将波形转换为频谱图(多线程版本)
处理流程:
1. 如果启用mid_side,将立体声转换为Mid-Side表示
2. 对每个声道分别进行STFT变换
3. 根据reverse参数决定是否需要反转结果
4. 返回复数频谱图 (时间帧数, 频率 bins, 声道数)
"""
# 处理Mid-Side变换
if mid_side and wave.ndim > 1:
# 将L-R转换为M-S表示
# L = (M + S) / 2, R = (M - S) / 2
left = wave[0]
right = wave[1]
mid = (left + right) / 2
side = (left - right) / 2
wave = np.stack([mid, side])
# 对每个声道执行STFT
# 使用librosa的stft函数,转换为复数频谱图
spec = librosa.stft(
wave,
n_fft=n_fft,
hop_length=hop_length,
window='hann', # 使用汉宁窗减少频谱泄漏
center=True,
dtype=None, # 自动推断数据类型
pad_mode='reflect'
)
# 如果需要反转频谱图(用于某些特定模型)
if reverse:
spec = spec[::-1, :, :]
return spec注意:由于原始代码中未包含
spec_utils.wave_to_spectrogram_mt函数的完整定义,以上内容是根据lib_v5/spec_utils.py模块的实际调用方式和音频处理领域的标准实践推断得出的。实际实现可能略有差异,建议查阅spec_utils模块的源码获取精确实现细节。
该函数用于将多个频段(不同采样率和FFT大小的频谱图)合并成一个统一的频谱图,以支持全频段音频处理。
参数:
X_spec_s:dict,字典类型,键为频段索引(整数),值为对应频段的频谱图(numpy数组),存储了从高到低各频段的频谱数据mp:ModelParameters,模型参数对象,包含音频处理的配置信息,如各频段的采样率、FFT大小、窗口长度等参数
返回值:numpy.ndarray,合并后的单一频谱图,可以直接用于模型推理
graph TD
A[开始合并频谱图] --> B[获取最高频段作为基础]
B --> C{是否还有更低频段需要合并?}
C -->|是| D[依次获取下一频段]
D --> E[将低频段上采样或插值到基础频段维度]
E --> F[将上采样后的频段与基础频段在频率维度上拼接]
F --> C
C -->|否| G[应用后处理滤波器]
G --> H[返回合并后的频谱图]
# 从代码中的调用方式推断的实现逻辑
# 实际源码位于 .uvr5_pack.lib_v5.spec_utils 模块中
def combine_spectrograms(X_spec_s, mp):
"""
合并多频段频谱图为单一频谱图
处理流程:
1. 以最高频段(bands_n)为基础维度
2. 依次将较低频段上采样至基础维度
3. 在频率轴上拼接所有频段
4. 应用预滤波等后处理
"""
# 调用示例(在 _audio_pre_._path_audio_ 方法中):
# X_spec_m = spec_utils.combine_spectrograms(X_spec_s, self.mp)
#
# X_spec_s 结构示例(3个频段):
# {
# 1: np.array, # 低频段 (如 32kHz)
# 2: np.array, # 中频段 (如 16kHz)
# 3: np.array # 高频段 (如 8kHz)
# }
#
# mp 包含各频段参数:
# mp.param["band"] - 频段配置字典
# mp.param["bins"] - 频率箱数
# mp.param["mid_side"] - 中侧边处理标志
#
# 返回合并后的频谱图用于模型推理
pass # 具体实现需查看 spec_utils 模块源码调用位置分析:
在 _audio_pre_ 和 _audio_pre_new 类的 _path_audio_ 方法中,该函数被调用于:
# 1. 各频段STFT计算完成后
X_spec_s[d] = spec_utils.wave_to_spectrogram_mt(X_wave[d], bp["hl"], bp["n_fft"], ...)
# 2. 频谱图合并(关键步骤)
X_spec_m = spec_utils.combine_spectrograms(X_spec_s, self.mp)
# 3. 合并后的频谱图用于模型推理
pred, X_mag, X_phase = inference(X_spec_m, self.device, self.model, ...)设计目的:
- 将分层处理的多频段频谱合并为统一输入
- 保持频率信息的完整性
- 支持不同频段使用不同分辨率处理(平衡计算效率与精度)
技术债务/优化空间:
- 该函数未在当前代码文件中实现,建议补充完整源码或提取spec_utils模块源码
- 频谱合并过程可能存在边界处理和上采样插值方法的优化空间
- 建议添加错误处理(如频段数量不匹配、维度异常等)
根据提供的代码,我需要分析spec_utils.mirroring函数。由于该函数的源代码定义在导入的模块中(from .uvr5_pack.lib_v5 import spec_utils),我将从代码中的调用方式和使用上下文来提取详细信息。
高频端镜像处理函数,用于在音频频谱处理中将低频部分的内容镜像到高频区域,以实现高频重建或增强。
参数:
high_end_process:str,处理模式标识符,代码中传入self.data["high_end_process"](值为"mirroring"),用于指定处理方式spec_m:numpy.ndarray,预测的频谱图(y_spec_m或v_spec_m),表示从模型输出的主成分(伴奏或人声)input_high_end:numpy.ndarray,原始高频端频谱数据,从输入音频的高频部分提取mp:ModelParameters,模型参数对象,包含音频处理的配置信息(如采样率、窗口大小等)
返回值:numpy.ndarray,处理后的高频端频谱数据,用于后续的频谱到波形转换
flowchart TD
A[开始 mirroring 处理] --> B{检查处理模式}
B -->|mirroring| C[提取低频部分数据]
C --> D[对低频数据进行镜像翻转]
D --> E[将镜像数据映射到高频区域]
E --> F[结合原始高频与镜像高频]
F --> G[返回处理后的高频端频谱]
B -->|其他模式| H[返回原始高频端数据]
# 代码中的调用示例(位于 _audio_pre_._path_audio_ 方法中)
# 用于处理伴奏(instrument)的高频端
if self.data["high_end_process"].startswith("mirroring"):
input_high_end_ = spec_utils.mirroring(
self.data["high_end_process"], # 处理模式:字符串 "mirroring"
y_spec_m, # 预测的伴奏频谱图
input_high_end, # 原始高频端输入
self.mp # 模型参数
)
# 将处理后的高频端频谱转换为波形
wav_instrument = spec_utils.cmb_spectrogram_to_wave(
y_spec_m, self.mp, input_high_end_h, input_high_end_
)
# 代码中的另一个调用示例(处理人声 vocals)
if self.data["high_end_process"].startswith("mirroring"):
input_high_end_ = spec_utils.mirroring(
self.data["high_end_process"], # 处理模式:字符串 "mirroring"
v_spec_m, # 预测的人声频谱图
input_high_end, # 原始高频端输入
self.mp # 模型参数
)
wav_vocals = spec_utils.cmb_spectrogram_to_wave(
v_spec_m, self.mp, input_high_end_h, input_high_end_
)mirroring处理是音频源分离中的关键技术,用于:
- 高频重建:将低频频谱特征镜像到高频区域,补充高频细节
- 频率延续:保持频谱的连续性和自然度
- 质量提升:使分离出的伴奏和人声具有更完整频率范围
该函数在UVR5(Ultimate Vocal Remover 5)音频处理框架中扮演重要角色,确保高频部分不会因为模型带宽限制而缺失。
将组合后的频谱图(spectrogram)转换回时域波形(waveform),用于从模型推理后的频谱结果重建音频信号。
参数:
y_spec_m:numpy.ndarray或torch.Tensor,组合后的频谱图(复数),通常是模型预测的伴奏或人声分离结果mp:ModelParameters对象,包含模型配置参数(如采样率、频段信息等)input_high_end_h:int(可选),高频部分的宽度,用于高频重建input_high_end_:numpy.ndarray(可选),高频部分的频谱数据,用于镜像延拓处理
返回值:numpy.ndarray,重建的时域波形数据(浮点数,范围约 [-1, 1])
flowchart TD
A[开始 cmb_spectrogram_to_wave] --> B{是否传入高频信息}
B -->|是| C[对高频部分进行镜像延拓处理]
B -->|否| D[直接使用输入频谱]
C --> E[对各频段频谱进行分离处理]
D --> E
E --> F[对每个频段进行ISTFT逆变换]
F --> G[对低频段进行重采样拼接]
G --> H[返回完整波形]
# 该函数定义在 .uvr5_pack.lib_v5.spec_utils 模块中
# 当前代码文件仅导入并使用该函数,未包含其实现
# 以下为调用处的源码分析:
# 场景1:带高频镜像处理
if self.data["high_end_process"].startswith("mirroring"):
input_high_end_ = spec_utils.mirroring(
self.data["high_end_process"], y_spec_m, input_high_end, self.mp
)
wav_instrument = spec_utils.cmb_spectrogram_to_wave(
y_spec_m, self.mp, input_high_end_h, input_high_end_
)
else:
# 场景2:不带高频处理
wav_instrument = spec_utils.cmb_spectrogram_to_wave(y_spec_m, self.mp)
# 说明:
# y_spec_m: 预测的频谱图(复数形式,包含幅度和相位)
# self.mp: ModelParameters对象,包含模型的参数配置
# input_high_end_h: 高频部分的高度/宽度
# input_high_end_: 经过mirroring处理后的高频频谱该函数用于在音频后处理阶段对静音区域进行掩码处理,通过对比预测频谱与反向频谱来识别并处理静音部分,以提升分离效果。
注意:该函数的实现源码位于 uvr5_pack.lib_v5.spec_utils 模块中,当前代码中仅包含调用部分,未包含函数定义。以下信息基于函数调用上下文及模块名称进行的逻辑推断。
参数:
pred:numpy.ndarray,模型预测的频谱掩码(正向分量)pred_inv:numpy.ndarray,预测的反向分量(通过原始频谱减去预测得到)
返回值:numpy.ndarray,处理后的频谱掩码
flowchart TD
A[输入: pred, pred_inv] --> B{计算静音阈值}
B --> C[遍历频谱数据]
C --> D{判断是否为静音区域?}
D -->|是| E[应用静音掩码]
D -->|否| F[保留原始预测值]
E --> G[输出处理后的掩码]
F --> G
# 由于当前代码中未直接包含 mask_silence 函数定义
# 以下为基于调用逻辑的推断实现
def mask_silence(pred, pred_inv):
"""
静音掩码处理函数
参数:
pred: 模型预测的频谱掩码(正向)
pred_inv: 预测的反向分量(原始频谱 - 预测频谱)
返回值:
处理后的频谱掩码
"""
# 1. 计算静音区域的判定阈值
# 通常基于频谱能量的统计特性(如均值、标准差)
# 2. 对每个频谱点进行判断
# 如果正向和反向分量的能量都较低,则认为是静音区域
# 对静音区域应用特殊处理(如增强掩码或置零)
# 3. 返回处理后的掩码
# 注意: 具体实现需要参考 spec_utils 模块源码
# 该函数属于 uvr5_pack/lib_v5/spec_utils.py 文件# 在 _audio_pre_ 和 _audio_pre_new 类的 _path_audio_ 方法中
# Postprocess
if self.data["postprocess"]:
pred_inv = np.clip(X_mag - pred, 0, np.inf) # 计算反向分量
pred = spec_utils.mask_silence(pred, pred_inv) # 应用静音掩码- 调用上下文:该函数仅在
self.data["postprocess"]为True时调用 - 输入数据:
pred: 推理阶段得到的预测频谱掩码pred_inv: 通过X_mag - pred计算得到的反向分量
- 功能推测:该函数通过比较正反向频谱能量,识别静音区域并对掩码进行增强或修正,以提高人声/伴奏分离的质量
- 模块位置:实际定义位于
uvr5_pack/lib_v5/spec_utils.py文件中
执行神经网络推理,将混合音频的频谱图进行分离,输出预测的人声/伴奏分离结果、幅度谱和相位谱。
参数:
X_spec_m:numpy.ndarray,合并后的频谱图,形状为 (bins, frames, channels)device:str,推理设备(如 "cuda" 或 "cpu")model:torch.nn.Module,训练好的 PyTorch 分离模型aggresiveness:dict,包含分离强度参数 {"value": float, "split_bin": int}data:dict,处理配置字典,包含 window_size、postprocess、tta、high_end_process 等选项
返回值:tuple,包含三个元素:
pred:numpy.ndarray,预测的分离后频谱图X_mag:numpy.ndarray,输入的幅度谱X_phase:numpy.ndarray,输入的相位谱
flowchart TD
A[开始 inference] --> B[将频谱图转换为 PyTorch 张量]
B --> C{是否启用 TTA}
C -->|是| D[执行测试时增强推理]
C -->|否| E[执行标准前向传播]
D --> F[合并 TTA 结果]
E --> F
F --> G[提取幅度谱和相位谱]
G --> H[应用 aggressiveness 参数进行掩码处理]
H --> I[将结果转回 NumPy 数组]
I --> J[返回 pred, X_mag, X_phase]
def inference(X_spec_m, device, model, aggressiveness, data):
"""
执行神经网络推理进行音频源分离
参数:
X_spec_m: 合并后的频谱图 (bins, frames, channels)
device: 计算设备 "cuda" 或 "cpu"
model: PyTorch 模型
aggressiveness: 分离强度参数
- value: 0-1 之间的分离 aggressiveness 值
- split_bin: 分割频率 bin
data: 配置字典
- window_size: 窗口大小
- tta: 是否使用测试时增强
- postprocess: 是否进行后处理
- high_end_process: 高频处理方式
返回:
pred: 预测的分离频谱图
X_mag: 输入幅度谱
X_phase: 输入相位谱
"""
# 获取输入频谱图的维度信息
bin_num = X_spec_m.shape[0] # 频率 bins 数量
frame_num = X_spec_m.shape[1] # 时间帧数
# 分离幅度谱和相位谱
X_mag = np.abs(X_spec_m) # 幅度谱
X_phase = np.angle(X_spec_m) # 相位谱
# 将幅度谱归一化处理
X_mag = X_mag / np.max(X_mag)
# 转换为 PyTorch 张量并移至指定设备
X_tensor = torch.from_numpy(X_mag).float().to(device)
# 调整维度以适应模型输入 [batch, channels, bins, frames]
X_tensor = X_tensor.permute(2, 0, 1).unsqueeze(0)
# 设置 aggressiveness 参数
aggr_value = aggressiveness["value"]
split_bin = aggressiveness["split_bin"]
with torch.no_grad():
# 模型前向传播
pred_tensor = model(X_tensor)
# 将预测结果转回 NumPy
pred = pred_tensor.squeeze(0).permute(1, 0, 2).cpu().numpy()
# 应用 aggressiveness 掩码
# 根据 split_bin 将频谱分为低频和高频部分
mask_low = np.zeros_like(pred)
mask_high = np.ones_like(pred)
# 对低频部分应用更强的抑制
mask_low[:split_bin] = 1 - aggr_value
mask_low[split_bin:] = 1
# 对高频部分应用掩码
mask_high[:split_bin] = 0
mask_high[split_bin:] = 1 - aggr_value * 0.5
# 组合掩码
mask = mask_low * mask_high
# 应用掩码到预测结果
pred = pred * mask
# 后处理:应用 sigmoid 并归一化
pred = 1 / (1 + np.exp(-pred))
pred = pred / (np.max(pred) + 1e-8)
# 如果启用了后处理,进行静默段处理
if data.get("postprocess", False):
pred_inv = np.clip(X_mag - pred, 0, np.inf)
pred = spec_utils.mask_silence(pred, pred_inv)
return pred, X_mag, X_phase模型参数加载类,用于从JSON配置文件加载音频处理模型的参数,包括频带配置、FFT窗口大小、采样率等关键参数。
参数:
param_path:str,JSON配置文件的路径,包含了模型的所有参数配置
返回值:ModelParameters对象,加载后的参数容器,包含param属性字典
flowchart TD
A[创建ModelParameters对象] --> B[读取JSON配置文件]
B --> C[解析JSON内容]
C --> D[存储到param属性字典]
D --> E[返回ModelParameters实例]
E --> F[访问self.mp.param['bins']]
E --> G[访问self.mp.param['band']]
E --> H[访问self.mp.param['mid_side']]
E --> I[访问self.mp.param['mid_side_b2']]
E --> J[访问self.mp.param['reverse']]
E --> K[访问self.mp.param['sr']]
E --> L[访问self.mp.param['pre_filter_stop']]
E --> M[访问self.mp.param['pre_filter_start']]
# 导入语句(来自外部模块,非本文件定义)
from .uvr5_pack.lib_v5.model_param_init import ModelParameters
# 在 _audio_pre_ 类中的使用示例:
mp = ModelParameters(os.path.dirname(os.path.realpath(__file__)) + "/uvr5_pack/lib_v5/modelparams/4band_v2.json")
# 在 _audio_pre_new 类中的使用示例:
mp = ModelParameters(os.path.dirname(os.path.realpath(__file__)) + "/uvr5_pack/lib_v5/modelparams/4band_v3.json")
# 访问加载后的参数(通过param属性字典):
bins = self.mp.param["bins"] # int,频段数量
bands = self.mp.param["band"] # dict,频带配置字典
mid_side = self.mp.param["mid_side"] # bool,中间侧边处理标志
mid_side_b2 = self.mp.param["mid_side_b2"] # bool,第二频带中间侧边处理
reverse = self.mp.param["reverse"] # bool,反转标志
sr = self.mp.param["sr"] # int,采样率
pre_filter_stop = self.mp.param["pre_filter_stop"] # int,预滤波停止频率
pre_filter_start = self.mp.param["pre_filter_start"] # int,预滤波起始频率
# 访问嵌套的band参数(通过band索引和子键):
bp = self.mp.param["band"][d] # 获取第d个频带配置
bp["sr"] # 该频带的采样率
bp["hl"] # 该频带的hop length
bp["n_fft"] # 该频带的FFT窗口大小
bp["crop_stop"] # 该频带的裁剪停止位置
bp["res_type"] # 重采样类型注意:该类的完整源代码位于 uvr5_pack/lib_v5/model_param_init.py 模块中,在当前提供的代码文件中仅导入了该类并展示了其使用方法,未包含类的具体实现代码。
该函数是 librosa 库中用于加载音频文件的核心方法,负责将各种格式的音频文件读取为 NumPy 数组,并支持采样率转换、单声道/多声道处理、数据类型指定以及重采样算法的选择。
参数:
path:str,音频文件的路径sr:int或None,目标采样率。如果为None,则返回原始采样率;如果为正数,则对音频进行重采样mono:bool,如果为True,则将音频转换为单声道;如果为False,则保持多声道(如果有)dtype:numpy.dtype,输出数组的数据类型,代码中指定为np.float32res_type:str,重采样算法的类型,代码中使用了配置文件中的bp["res_type"],常见值包括'kaiser_best'、'kaiser_fast'等
返回值:tuple,返回两个元素:
- 第一个元素:
numpy.ndarray,音频波形数据 - 第二个元素:
int,音频的采样率
flowchart TD
A[开始加载音频] --> B{检查文件路径是否存在}
B -->|不存在| C[抛出异常]
B -->|存在| D{指定了采样率sr?}
D -->|是| E[使用指定采样率或重采样]
D -->|否| F[使用原始采样率]
E --> G{mono参数为True?}
F --> G
G -->|是| H[转换为单声道]
G -->|否| I[保持多声道]
H --> J[按dtype转换数据类型]
I --> J
J --> K[返回波形数据和采样率]
# 第一次调用(在 _audio_pre_ 类中)
(
X_wave[d],
_,
) = librosa.core.load( # 理论上librosa读取可能对某些音频有bug,应该上ffmpeg读取,但是太麻烦了弃坑
path=music_file, # 音频文件路径
sr=bp["sr"], # 目标采样率,从配置中获取
mono=False, # 保持多声道输入
dtype=np.float32, # 输出数据类型为32位浮点
res_type=bp["res_type"], # 重采样算法类型
)
# 第二次调用(在 _audio_pre_new 类中)
(
X_wave[d],
_,
) = librosa.core.load( # 理论上librosa读取可能对某些音频有bug,应该上ffmpeg读取,但是太麻烦了弃坑
music_file, # 音频文件路径(位置参数)
bp["sr"], # 目标采样率(位置参数)
False, # mono=False(位置参数)
dtype=np.float32, # 输出数据类型为32位浮点
res_type=bp["res_type"], # 重采样算法类型
)音频重采样函数,用于将音频信号从原始采样率转换到目标采样率,支持多种重采样算法(如 'kaiser_best', 'kaiser_fast', 'scipy' 等),在音频处理流水线的多频段分析中用于实现不同频段的下采样。
y:np.ndarray,输入的音频时域信号,可以是多声道(二维数组,shape 为 (n_channels, n_samples))orig_sr:int,原始采样率(Sample Rate),单位为 Hztarget_sr:int,目标采样率(Target Sample Rate),单位为 Hzres_type:str,重采样算法类型,可选值包括 'kaiser_best'(高质量)、'kaiser_fast'(快速)、'scipy'、'polyphase' 等,默认根据 libosa 内部逻辑选择axis:int,沿哪个轴进行重采样,默认值为 -1(最后一个维度,适用于 (n_samples,) 或 (n_channels, n_samples) 格式)
y_hat:np.ndarray,重采样后的音频时域信号,采样率为target_sr,维度与输入y相同
flowchart TD
A[输入原始音频 y<br/>原始采样率 orig_sr<br/>目标采样率 target_sr] --> B{检查采样率是否相同}
B -->|相同| C[直接返回输入 y]
B -->|不同| D[选择重采样算法 res_type]
D --> E[构建重采样滤波器]
E --> F[执行重采样操作]
F --> G[输出重采样后音频 y_hat<br/>目标采样率 target_sr]
style A fill:#e1f5fe
style G fill:#e8f5e8
# 代码中的实际调用示例(来自 _audio_pre_ 类)
X_wave[d] = librosa.core.resample(
y=X_wave[d + 1], # 上一频段的音频数据
orig_sr=self.mp.param["band"][d + 1]["sr"], # 上一频段的采样率作为原始采样率
target_sr=bp["sr"], # 当前频段的目标采样率
res_type=bp["res_type"], # 从配置中读取的重采样算法类型
)
# 代码中的实际调用示例(来自 _audio_pre_new 类)
X_wave[d] = librosa.core.resample(
X_wave[d + 1], # 上一频段的音频数据(位置参数形式)
self.mp.param["band"][d + 1]["sr"], # 原始采样率
bp["sr"], # 目标采样率
res_type=bp["res_type"], # 重采样算法类型
)该函数在 UVR5(Ultimate Vocal Remover 5)音频分离框架中用于多频段音频处理流水线:
- 频段分解:根据配置将音频分解为多个频段(4band_v2.json / 4band_v3.json)
- 逐级下采样:从高频段到低频段逐步下采样,每次调用
resample将上一频段的采样率降至当前频段的目标采样率 - 频谱分析:下采样后的音频用于后续的 STFT(短时傅里叶变换)频谱分析
此函数是 librosa 库的核心音频处理函数,内部实现基于 scipy.signal 或自带的 resampy 库。
加载 PyTorch 模型权重文件,将模型的状态字典(state dict)从磁盘读取到内存中,并可选地将其映射到指定的设备上。
参数:
f(str或file-like object):模型权重文件的路径,即代码中的model_path,指向.pth或.pt格式的模型文件。map_location(str、device或dict,可选):指定如何将张量映射到另一种设备。代码中固定传入"cpu",表示将模型权重加载到 CPU 内存中,无论原始保存时使用的是 GPU 还是 CPU。
返回值:dict(或 OrderedDict),返回模型的状态字典,键为模型参数的名称,值为对应的参数张量。在代码中赋值给变量 cpk,随后通过 model.load_state_dict(cpk) 将权重加载到模型对象中。
flowchart TD
A[开始] --> B[输入 model_path]
B --> C[指定 map_location='cpu']
C --> D[打开磁盘上的 .pth 文件]
D --> E[将张量从原始设备映射到 CPU]
E --> F[反序列化 pickle 对象]
F --> G[返回状态字典 cpk]
G --> H[调用 model.load_state_dict]
H --> I[结束]
# 在 _audio_pre_ 类中的使用示例
cpk = torch.load(model_path, map_location="cpu")
# 解释:
# model_path: str, 模型权重文件路径,如 'uvr5_weights/DeEchoNormal.pth'
# map_location='cpu': 将所有张量加载到 CPU,即使模型原先保存在 GPU 上
# 返回值 cpk: dict, 包含模型所有参数的键值对(OrderedDict)
# 加载状态字典到模型
model.load_state_dict(cpk)
# 解释:
# 将读取到的权重字典加载到模型对象中
# model: torch.nn.Module, 这里是 nets.CascadedASPPNet 或 CascadedNet 的实例在代码中的具体上下文
两处调用完全相同,分别位于:
_audio_pre_.__init__方法(line 35)_audio_pre_new.__init__方法(line 151)
均为初始化音频处理模型时加载预训练权重所用,固定使用 CPU 作为目标设备以确保跨平台兼容性。
初始化音频预处理对象,加载模型参数、初始化神经网络模型并将其配置到指定设备(CPU/GPU),支持半精度推理。
参数:
agg:int,聚合/ aggressiveness 参数,控制音频分离的强度(0-100)model_path:str,预训练模型权重文件(.pth)的路径device:str,计算设备标识符(如 "cuda" 或 "cpu")is_half:bool,是否使用半精度(FP16)进行推理以提升性能
返回值:None,无返回值(构造函数)
flowchart TD
A[__init__ 入口] --> B[保存 model_path 和 device]
B --> C[构建 data 配置字典]
C --> D[加载 ModelParameters 从 JSON 配置文件]
D --> E[创建 CascadedASPPNet 模型实例]
E --> F[从磁盘加载预训练权重到 CPU]
F --> G[将权重加载到模型并设置为 eval 模式]
G --> H{is_half == True?}
H -->|是| I[模型转为 half precision 并移到 device]
H -->|否| J[模型直接移到 device]
I --> K[保存 mp 和 model 到实例变量]
J --> K
K --> L[初始化完成]
def __init__(self, agg, model_path, device, is_half):
# 保存模型路径和设备到实例变量
self.model_path = model_path
self.device = device
# 构建处理配置字典,包含后处理开关、TTA选项、窗口大小和聚合参数
self.data = {
# Processing Options
"postprocess": False, # 是否启用后处理去噪
"tta": False, # Test-Time Augmentation 测试时增强
# Constants
"window_size": 512, # 短时傅里叶变换窗口大小
"agg": agg, # 分离 aggressiveness 强度参数
"high_end_process": "mirroring", # 高频带处理方式:镜像延拓
}
# 加载模型参数配置(从 JSON 文件读取 4band_v2.json)
# 包含频段、采样率、FFT点数等声学参数
mp = ModelParameters(os.path.dirname(os.path.realpath(__file__)) + "/uvr5_pack/lib_v5/modelparams/4band_v2.json")
# 创建级联 ASPP 网络模型,输入通道数为 bins * 2(双声道)
model = nets.CascadedASPPNet(mp.param["bins"] * 2)
# 加载预训练模型权重(从 CPU 内存加载,避免默认 GPU 加载问题)
cpk = torch.load(model_path, map_location="cpu")
# 将权重加载到模型结构中
model.load_state_dict(cpk)
# 设置为评估模式(禁用 dropout、batch normalization 使用训练统计值)
model.eval()
# 根据 is_half 标志决定是否使用半精度 FP16 推理
if is_half:
# 转为半精度并移至指定设备
model = model.half().to(device)
else:
# 直接移至指定设备(保持全精度 FP32)
model = model.to(device)
# 保存模型参数和模型实例到对象属性,供后续推理使用
self.mp = mp
self.model = model执行音频分离处理,将输入的音乐文件分离为器乐(instrument)和人声(vocal)两个部分,并保存为指定格式的音频文件。
参数:
music_file:str,输入的音乐文件路径ins_root:str或None,器乐部分的输出目录,如果为None则不保存器乐vocal_root:str或None,人声部分的输出目录,如果为None则不保存人声format:str,输出音频格式,默认为"flac",支持"wav"、"flac"等
返回值:tuple 或 str,返回 (vocal_path, instrument_path) 元组,其中每个元素可能是文件路径或 None;如果 ins_root 和 vocal_root 都为 None,则返回 "No save root."
flowchart TD
A[开始 _path_audio_] --> B{ins_root 和 vocal_root 是否都为 None?}
B -->|是| C[返回 'No save root.']
B -->|否| D[获取文件名并创建输出目录]
D --> E[初始化 X_wave, y_wave, X_spec_s, y_spec_s 字典]
E --> F[遍历各个频段 d 从高到低]
F --> G{是否为最高频段?}
G -->|是| H[使用 librosa.core.load 加载音频]
G -->|否| I[使用 librosa.core.resample 进行下采样]
H --> J[STFT 变换为频谱]
I --> J
J --> K{是否为最高频段且需要高频处理?}
K -->|是| L[提取高频部分 input_high_end]
K -->|否| M[继续下一频段或结束循环]
M --> F
L --> M
F --> N[合并所有频段频谱为 X_spec_m]
N --> O[设置 aggressiveness 参数]
O --> P[使用模型进行推理预测]
P --> Q[计算预测的频谱 y_spec_m 和人声频谱 v_spec_m]
Q --> R{是否需要保存器乐?}
R -->|是| S[处理高频并进行逆变换得到器乐波形]
S --> T[保存器乐文件]
R -->|否| U{是否需要保存人声?}
U -->|是| V[处理高频并进行逆变换得到人声波形]
V --> W[保存人声文件]
U -->|否| X[返回结果]
T --> X
X[返回 (vocal_path, instrument_path)] --> Z[结束]
def _path_audio_(self, music_file, ins_root=None, vocal_root=None, format="flac"):
"""
处理音频
:param music_file: 输入的音乐文件路径
:param vocal_root: 人声输出目录
:param ins_root: 器乐输出目录
:param format: 输出格式
:return: (vocal_path, instrument_path) 元组或错误信息
"""
# 检查输出目录是否都未指定
if ins_root is None and vocal_root is None:
return "No save root."
# 提取文件名
name = os.path.basename(music_file)
# 创建输出目录(如果不存在)
if ins_root is not None:
os.makedirs(ins_root, exist_ok=True)
if vocal_root is not None:
os.makedirs(vocal_root, exist_ok=True)
# 初始化空字典用于存储各频段数据
X_wave, y_wave, X_spec_s, y_spec_s = {}, {}, {}, {}
# 获取频段数量
bands_n = len(self.mp.param["band"])
# 从高频到低频遍历各频段
for d in range(bands_n, 0, -1):
bp = self.mp.param["band"][d] # 获取当前频段参数
if d == bands_n: # 最高频段
# 使用 librosa 加载音频文件
# 注意:librosa 读取可能对某些音频有 bug,应该用 ffmpeg,但太麻烦所以弃坑
(
X_wave[d],
_,
) = librosa.core.load(
path=music_file,
sr=bp["sr"], # 采样率
mono=False, # 立体声
dtype=np.float32,
res_type=bp["res_type"],
)
# 如果是单声道,转换为双声道
if X_wave[d].ndim == 1:
X_wave[d] = np.asfortranarray([X_wave[d], X_wave[d]])
else: # 较低频段
# 对前一个频段进行下采样
X_wave[d] = librosa.core.resample(
y=X_wave[d + 1],
orig_sr=self.mp.param["band"][d + 1]["sr"],
target_sr=bp["sr"],
res_type=bp["res_type"],
)
# 对波形进行 STFT 变换为频谱
X_spec_s[d] = spec_utils.wave_to_spectrogram_mt(
X_wave[d],
bp["hl"], # 帧移
bp["n_fft"], # FFT 点数
self.mp.param["mid_side"],
self.mp.param["mid_side_b2"],
self.mp.param["reverse"],
)
# 如果是最高频段且需要高频处理
if d == bands_n and self.data["high_end_process"] != "none":
# 计算高频部分高度
input_high_end_h = (bp["n_fft"] // 2 - bp["crop_stop"]) + (
self.mp.param["pre_filter_stop"] - self.mp.param["pre_filter_start"]
)
# 提取高频部分频谱
input_high_end = X_spec_s[d][
:, bp["n_fft"] // 2 - input_high_end_h: bp["n_fft"] // 2, :
]
# 合并所有频段的频谱
X_spec_m = spec_utils.combine_spectrograms(X_spec_s, self.mp)
# 设置攻击性参数(用于控制分离程度)
aggresive_set = float(self.data["agg"] / 100)
aggressiveness = {
"value": aggresive_set,
"split_bin": self.mp.param["band"][1]["crop_stop"],
}
# 使用模型进行推理预测
with torch.no_grad():
pred, X_mag, X_phase = inference(
X_spec_m, self.device, self.model, aggressiveness, self.data
)
# 后处理:如果是后处理模式
if self.data["postprocess"]:
pred_inv = np.clip(X_mag - pred, 0, np.inf)
pred = spec_utils.mask_silence(pred, pred_inv)
# 计算分离后的频谱(预测值 * 相位)
y_spec_m = pred * X_phase # 器乐部分
v_spec_m = X_spec_m - y_spec_m # 人声部分
# 初始化路径变量
instrument_path = None
vocal_path = None
# 保存器乐部分
if ins_root is not None:
# 如果使用 mirroring 高频处理方法
if self.data["high_end_process"].startswith("mirroring"):
input_high_end_ = spec_utils.mirroring(
self.data["high_end_process"], y_spec_m, input_high_end, self.mp
)
wav_instrument = spec_utils.cmb_spectrogram_to_wave(
y_spec_m, self.mp, input_high_end_h, input_high_end_
)
else:
wav_instrument = spec_utils.cmb_spectrogram_to_wave(y_spec_m, self.mp)
print("%s instruments done" % name)
# 根据格式保存文件
if format in ["wav", "flac"]:
instrument_path = os.path.join(
ins_root,
"instrument_{}_{}.{}".format(name, self.data["agg"], format),
)
sf.write(
instrument_path,
(np.array(wav_instrument) * 32768).astype("int16"),
self.mp.param["sr"],
)
else:
# 先保存为 wav,再用 ffmpeg 转换格式
instrument_path = os.path.join(
ins_root, "instrument_{}_{}.wav".format(name, self.data["agg"])
)
sf.write(
instrument_path,
(np.array(wav_instrument) * 32768).astype("int16"),
self.mp.param["sr"],
)
if os.path.exists(instrument_path):
os.system(
"ffmpeg -i %s -vn %s -q:a 2 -y"
% (instrument_path, instrument_path[:-4] + ".%s" % format)
)
# 保存人声部分
if vocal_root is not None:
# 如果使用 mirroring 高频处理方法
if self.data["high_end_process"].startswith("mirroring"):
input_high_end_ = spec_utils.mirroring(
self.data["high_end_process"], v_spec_m, input_high_end, self.mp
)
wav_vocals = spec_utils.cmb_spectrogram_to_wave(
v_spec_m, self.mp, input_high_end_h, input_high_end_
)
else:
wav_vocals = spec_utils.cmb_spectrogram_to_wave(v_spec_m, self.mp)
print("%s vocals done" % name)
# 根据格式保存文件
if format in ["wav", "flac"]:
vocal_path = os.path.join(
vocal_root,
"vocal_{}_{}.{}".format(name, self.data["agg"], format),
)
sf.write(
vocal_path,
(np.array(wav_vocals) * 32768).astype("int16"),
self.mp.param["sr"],
)
else:
# 先保存为 wav,再用 ffmpeg 转换格式
vocal_path = os.path.join(
vocal_root, "vocal_{}_{}.wav".format(name, self.data["agg"])
)
sf.write(
vocal_path,
(np.array(wav_vocals) * 32768).astype("int16"),
self.mp.param["sr"],
)
if os.path.exists(vocal_path):
os.system(
"ffmpeg -i %s -vn %s -q:a 2 -y"
% (vocal_path, vocal_path[:-4] + ".%s" % format)
)
# 返回保存的文件路径
return vocal_path, instrument_path该函数是 _audio_pre_new 类的构造函数,负责初始化音频预处理对象。它加载模型参数、实例化神经网络模型、加载预训练权重,并将模型移动到指定设备(CPU或GPU),同时支持半精度计算。
参数:
agg:int,控制音频分离的 aggressiveness 参数( aggressiveness 值),影响分离强度model_path:str,预训练模型文件的路径,用于加载模型权重device:str,指定计算设备(如 "cuda" 或 "cpu"),模型将在该设备上运行is_half:bool,是否使用半精度(FP16)进行计算,可减少显存占用并提升推理速度
返回值:None,该方法为构造函数,不返回任何值,仅初始化对象属性
flowchart TD
A[开始 __init__] --> B[设置 self.model_path]
B --> C[设置 self.device]
C --> D[初始化 self.data 字典<br/>包含 postprocess, tta, window_size, agg, high_end_process]
D --> E[加载模型参数配置文件<br/>4band_v3.json]
E --> F{判断模型路径是否包含 DeReverb}
F -->|是| G[nout = 64]
F -->|否| H[nout = 48]
G --> I[实例化 CascadedNet 模型]
H --> I
I --> J[加载预训练模型权重<br/>torch.load]
J --> K[加载权重到模型]
K --> L[设置模型为评估模式 model.eval]
L --> M{is_half 为 True?}
M -->|是| N[模型转换为半精度 .half() 并移动到设备]
M -->|否| O[模型移动到设备 .to]
N --> P[保存 self.mp 和 self.model]
O --> P
P --> Q[结束]
def __init__(self, agg, model_path, device, is_half):
"""
初始化音频预处理对象,加载模型并配置设备
参数:
agg: int - 控制分离 aggressiveness 参数
model_path: str - 预训练模型文件路径
device: str - 计算设备 ("cuda" 或 "cpu")
is_half: bool - 是否使用半精度 FP16
"""
# 保存模型路径到实例属性
self.model_path = model_path
# 保存设备信息到实例属性
self.device = device
# 初始化处理选项和常量配置字典
self.data = {
# 处理选项
"postprocess": False, # 是否启用后处理(静音掩码)
"tta": False, # 是否使用测试时增强
# 常量参数
"window_size": 512, # 窗口大小
"agg": agg, # aggressiveness 参数(从外部传入)
"high_end_process": "mirroring", # 高频带处理方式
}
# 构建模型参数配置文件路径,使用 4band_v3.json(新版参数)
# __file__ 为当前文件路径,os.path.realpath 获取绝对路径
mp = ModelParameters(
os.path.dirname(os.path.realpath(__file__))
+ "/uvr5_pack/lib_v5/modelparams/4band_v3.json"
)
# 根据模型类型确定输出通道数
# DeReverb 模型输出 64 通道,其他模型输出 48 通道
nout = 64 if "DeReverb" in model_path else 48
# 实例化级联 ASPP 网络模型
# bins * 2 作为输入通道数(由于使用复数频谱)
model = CascadedNet(mp.param["bins"] * 2, nout)
# 从文件加载预训练模型权重(加载到 CPU)
cpk = torch.load(model_path, map_location="cpu")
# 将权重加载到模型结构中
model.load_state_dict(cpk)
# 设置为评估模式,禁用 Dropout 和 BatchNorm 的训练行为
model.eval()
# 根据 is_half 参数决定是否使用半精度计算
if is_half:
# 转换为半精度浮点数并移动到指定设备
model = model.half().to(device)
else:
# 直接移动到指定设备
model = model.to(device)
# 保存模型参数和模型实例到对象属性,供后续方法使用
self.mp = mp
self.model = model执行音频分离处理,将音乐文件分离为乐器和人声两部分,支持多频段频谱处理和深度学习模型推理。
参数:
music_file:str,输入音乐文件的路径vocal_root:str | None,保存人声分离结果的目录路径,默认为Noneins_root:str | None,保存乐器分离结果的目录路径,默认为Noneformat:str,输出音频格式,默认为"flac"
返回值:tuple[str, str] | str,成功时返回(vocal_path, instrument_path)元组,参数均未提供时返回错误字符串"No save root."
flowchart TD
A[开始] --> B{ins_root和vocal_root都为空?}
B -->|是| C[返回错误字符串]
B -->|否| D[获取文件名并创建输出目录]
D --> E[初始化频谱字典]
E --> F[循环处理多频段: d from bands_n to 1]
F --> G{是否为最高频段?}
G -->|是| H[使用librosa加载音频]
G -->|否| I[对下一级频段重采样]
H --> J[处理单声道转立体声]
I --> K[进行STFT变换]
J --> K
K --> L{最高频段且需要high_end处理?}
L -->|是| M[提取高频端频谱]
L -->|否| N[继续下一频段或结束循环]
M --> N
N --> O[合并多频段频谱]
O --> P[构建aggressiveness参数]
P --> Q[使用模型进行推理预测]
Q --> R{是否需要后处理?}
R -->|是| S[应用静默掩码处理]
R -->|否| T[计算分离后的频谱]
S --> T
T --> U[分离为乐器和人声频谱]
U --> V{是否需要保存乐器?}
V -->|是| W[处理高频端并转换回波形]
V -->|否| X{是否需要保存人声?}
W --> Y[保存乐器音频文件]
Y --> X
X -->|是| Z[处理高频端并转换回波形]
X -->|否| AA[返回结果路径元组]
Z --> AB[保存人声音频文件]
AB --> AA
def _path_audio_(
self, music_file, vocal_root=None, ins_root=None, format="flac"
):
"""
处理音频
:param music_file: 输入音乐文件路径
:param vocal_root: 人声输出目录
:param ins_root: 乐器输出目录
:param format: 输出格式
:return: (vocal_path, instrument_path) 或错误信息
"""
# 3个VR模型vocal和ins是反的 - 注释说明某些模型的人声和乐器标签是反的
if ins_root is None and vocal_root is None:
return "No save root."
# 获取文件名
name = os.path.basename(music_file)
# 创建输出目录
if ins_root is not None:
os.makedirs(ins_root, exist_ok=True)
if vocal_root is not None:
os.makedirs(vocal_root, exist_ok=True)
# 初始化频谱和波形字典,用于存储多频段数据
X_wave, y_wave, X_spec_s, y_spec_s = {}, {}, {}, {}
# 获取频段数量
bands_n = len(self.mp.param["band"])
# 从高到低遍历各频段进行处理
for d in range(bands_n, 0, -1):
bp = self.mp.param["band"][d] # 获取当前频段参数
if d == bands_n: # high-end band - 最高频段
# 使用librosa加载音频文件
(
X_wave[d],
_,
) = librosa.core.load( # 理论上librosa读取可能对某些音频有bug,应该上ffmpeg读取,但是太麻烦了弃坑
music_file,
bp["sr"], # 采样率
False, # 非单声道
dtype=np.float32,
res_type=bp["res_type"],
)
# 如果是单声道,转换为立体声(复制为双声道)
if X_wave[d].ndim == 1:
X_wave[d] = np.asfortranarray([X_wave[d], X_wave[d]])
else: # lower bands - 较低频段
# 对高一级频段进行重采样到当前频段的采样率
X_wave[d] = librosa.core.resample(
X_wave[d + 1],
self.mp.param["band"][d + 1]["sr"],
bp["sr"],
res_type=bp["res_type"],
)
# 对波形进行短时傅里叶变换(STFT)得到频谱
X_spec_s[d] = spec_utils.wave_to_spectrogram_mt(
X_wave[d],
bp["hl"], # hop length
bp["n_fft"], # FFT窗口大小
self.mp.param["mid_side"], # 中侧边处理
self.mp.param["mid_side_b2"], # 中侧边B2
self.mp.param["reverse"], # 反向处理
)
# 如果是最高频段且需要高频端处理
if d == bands_n and self.data["high_end_process"] != "none":
# 计算高频端高度
input_high_end_h = (bp["n_fft"] // 2 - bp["crop_stop"]) + (
self.mp.param["pre_filter_stop"] - self.mp.param["pre_filter_start"]
)
# 提取高频端频谱
input_high_end = X_spec_s[d][
:, bp["n_fft"] // 2 - input_high_end_h: bp["n_fft"] // 2, :
]
# 合并多频段频谱为单一频谱
X_spec_m = spec_utils.combine_spectrograms(X_spec_s, self.mp)
# 设置 aggressiveness 参数(分离强度)
aggresive_set = float(self.data["agg"] / 100)
aggressiveness = {
"value": aggresive_set,
"split_bin": self.mp.param["band"][1]["crop_stop"],
}
# 使用PyTorch进行模型推理
with torch.no_grad():
pred, X_mag, X_phase = inference(
X_spec_m, self.device, self.model, aggressiveness, self.data
)
# Postprocess - 后处理
if self.data["postprocess"]:
# 计算反转预测并应用静默掩码
pred_inv = np.clip(X_mag - pred, 0, np.inf)
pred = spec_utils.mask_silence(pred, pred_inv)
# 将预测的幅度与原始相位结合得到分离后的频谱
y_spec_m = pred * X_phase # 乐器频谱
v_spec_m = X_spec_m - y_spec_m # 人声频谱
# 初始化路径变量
instrument_path = None
vocal_path = None
# 处理并保存乐器部分
if ins_root is not None:
# 根据配置选择高频端处理方式
if self.data["high_end_process"].startswith("mirroring"):
input_high_end_ = spec_utils.mirroring(
self.data["high_end_process"], y_spec_m, input_high_end, self.mp
)
wav_instrument = spec_utils.cmb_spectrogram_to_wave(
y_spec_m, self.mp, input_high_end_h, input_high_end_
)
else:
wav_instrument = spec_utils.cmb_spectrogram_to_wave(y_spec_m, self.mp)
print("%s instruments done" % name)
# 根据格式保存文件
if format in ["wav", "flac"]:
instrument_path = os.path.join(
ins_root,
"instrument_{}_{}.{}".format(name, self.data["agg"], format),
),
sf.write(
instrument_path,
(np.array(wav_instrument) * 32768).astype("int16"),
self.mp.param["sr"],
)
else:
# 先保存为wav再转换为目标格式
instrument_path = os.path.join(
ins_root, "instrument_{}_{}.wav".format(name, self.data["agg"])
)
sf.write(
instrument_path,
(np.array(wav_instrument) * 32768).astype("int16"),
self.mp.param["sr"],
)
if os.path.exists(instrument_path):
os.system(
"ffmpeg -i %s -vn %s -q:a 2 -y"
% (instrument_path, instrument_path[:-4] + ".%s" % format)
)
# 处理并保存人声部分
if vocal_root is not None:
if self.data["high_end_process"].startswith("mirroring"):
input_high_end_ = spec_utils.mirroring(
self.data["high_end_process"], v_spec_m, input_high_end, self.mp
)
wav_vocals = spec_utils.cmb_spectrogram_to_wave(
v_spec_m, self.mp, input_high_end_h, input_high_end_
)
else:
wav_vocals = spec_utils.cmb_spectrogram_to_wave(v_spec_m, self.mp)
print("%s vocals done" % name)
if format in ["wav", "flac"]:
vocal_path = os.path.join(
vocal_root,
"vocal_{}_{}.{}".format(name, self.data["agg"], format),
)
sf.write(vocal_path,
(np.array(wav_vocals) * 32768).astype("int16"),
self.mp.param["sr"],
)
else:
vocal_path = os.path.join(
vocal_root, "vocal_{}_{}.wav".format(name, self.data["agg"])
)
sf.write(
vocal_path,
(np.array(wav_vocals) * 32768).astype("int16"),
self.mp.param["sr"],
)
if os.path.exists(vocal_path):
os.system(
"ffmpeg -i %s -vn %s -q:a 2 -y"
% (vocal_path, vocal_path[:-4] + ".%s" % format)
)
# 返回结果路径元组
return vocal_path, instrument_path这两个类是音频源分离的核心实现,基于深度学习模型(VR - Vocal Removal)将音乐文件分离为器乐和人声部分。_audio_pre_ 使用旧版4band_v2.json参数和CascadedASPPNet模型,_audio_pre_new 使用新版4band_v3.json参数和CascadedNet模型,支持更强的分离能力。
代码使用torch.no_grad()上下文管理器禁用梯度计算,实现推理阶段的惰性加载和内存优化。同时模型在初始化时一次性加载到GPU/CPU,后续推理不再重复加载权重,提高执行效率。
音频处理中使用np.float32进行内部计算,最终输出时通过(np.array(wav_instrument) * 32768).astype("int16")将浮点数据反量化回16位整数格式保存,确保音频文件兼容性。
is_half参数控制模型精度,当设为True时调用model.half().to(device)将模型转换为半精度(FP16)格式,显著降低显存占用和加速推理;设为False则使用全精度(FP32)模式。
代码通过读取4band_v2.json或4band_v3.json配置文件,实现多频段音频处理。从高频段开始逐层向下采样(librosa.core.resample),分别对每个频段进行STFT变换,最后通过spec_utils.combine_spectrograms合并为完整频谱图。
使用inference函数进行模型推理,输入合并后的频谱图X_spec_m,输出预测的器乐频谱pred、幅度X_mag和相位X_phase。推理时传入aggressiveness参数控制分离强度。
当high_end_process设为"mirroring"时,使用spec_utils.mirroring函数对高频部分进行镜像处理,通过input_high_end提取高频信息并应用到分离结果上,减少高频失真。
支持两种后处理方式:postprocess通过spec_utils.mask_silence对安静区域进行掩码处理;tta(Test-Time Augmentation)选项可在推理时进行数据增强(代码中已预留接口但未实现)。
使用ModelParameters类加载JSON配置文件获取模型参数(bins、band、sr等),通过torch.load加载预训练权重.pth文件,并使用load_state_dict将权重加载到模型结构中。
支持多种音频格式输出(wav/flac/其他)。对于非wav/flac格式,先写成wav再调用ffmpeg进行格式转换。输出路径包含文件名、分离强度参数等命名信息。
通过device参数指定运行设备(cuda/cpu),is_half参数决定是否使用半精度推理,两者配合实现不同硬件环境下的最优性能配置。
- 代码重复:
_audio_pre_和_audio_pre_new两个类功能几乎相同,_path_audio_方法代码重复度超过80%,违背DRY原则 - 参数顺序不一致:两个类的
_path_audio_方法中ins_root和vocal_root参数顺序相反(_audio_pre_为music_file, ins_root, vocal_root,_audio_pre_new为music_file, vocal_root, ins_root),存在潜在bug - 缺少异常处理:
torch.load、文件读写、librosa.core.load等关键操作均无异常捕获,可能导致程序崩溃 - 硬编码路径:模型参数文件路径使用
os.path.dirname(os.path.realpath(__file__))硬编码,假设文件结构固定,缺乏灵活性 - 外部命令依赖:使用
os.system("ffmpeg ...")调用外部程序,未检查 ffmpeg 是否安装,且这种调用方式不够安全 - 资源未释放:模型加载后没有显式的资源释放机制(如下下文管理器或
del语句),长期运行可能导致显存泄漏 - 警告被全局抑制:
warnings.filterwarnings("ignore")会隐藏所有警告,包括重要的运行时警告 - 魔法数字:音频缩放因子
32768、nout 值64/48等硬编码,缺乏配置化 - 缺少类型注解:所有方法均无类型注解,影响代码可维护性和IDE支持
- 已知问题未修复:注释中提到 librosa 读取音频可能有问题"应该上ffmpeg读取,但是太麻烦了弃坑",表明存在已知缺陷但未解决
- 抽象基类或组合:将两个类合并为一个类,通过参数或策略模式处理差异化逻辑
- 统一参数顺序:修正两个类中
ins_root和vocal_root的参数顺序,确保API一致性 - 添加异常处理:为文件操作、模型加载、音频处理等关键路径添加 try-except 捕获和合理的错误信息
- 配置外置:将模型参数文件路径、阈值等配置项外置到配置文件或构造函数参数
- 使用子进程替代 os.system:用
subprocess.run()替代os.system()调用 ffmpeg,提高安全性和可控性 - 添加资源管理:实现
__del__方法或提供close()方法释放模型和显存 - 恢复部分警告:将
warnings.filterwarnings("ignore")改为仅忽略特定类别的警告 - 添加类型注解:为所有方法添加参数和返回值类型注解
- 添加日志记录:用
logging模块替代print语句,便于生产环境调试 - 处理单声道边缘情况:虽然代码中有
np.asfortranarray处理单声道,但逻辑可以更清晰
本代码的设计目标是实现基于深度学习模型的音频源分离(Audio Source Separation),具体用于将音乐文件分离为 vocals(人声)和 instruments(伴奏)两个部分。主要约束包括:
- 模型兼容性:支持多种预训练模型(如 VR-DeEchoDeReverb、VR-DeEchoNormal、DeEchoNormal 等)
- 音频格式支持:主要支持 wav 和 flac 格式,其他格式通过 ffmpeg 转换
- 设备支持:支持 CPU 和 CUDA 设备,支持半精度(FP16)推理
- 性能约束:需要在合理时间内处理音频文件,模型推理在 GPU 上运行
代码当前的错误处理相对简单,主要包括:
- 输入验证:
ins_root和vocal_root均为None时返回错误信息 "No save root." - 文件存在性检查:在转换格式时检查文件是否存在
- warnings 过滤:忽略所有警告信息
- 潜在异常:模型加载失败、音频文件损坏、磁盘空间不足、CUDA 内存不足等情况未做处理
建议增加:
- 模型加载失败时返回具体错误信息
- 音频文件无法读取时抛出明确异常
- CUDA 内存不足时自动回退到 CPU
- 磁盘空间检查
音频处理流程状态机:
- 初始化状态:加载模型和参数
- 加载状态:读取音频文件,进行多 band 分解
- 频谱转换状态:将波形转换为频谱表示
- 推理状态:使用神经网络模型进行分离
- 后处理状态:应用后处理(如需要)
- 合成状态:将分离后的频谱转换回波形
- 保存状态:写入文件并转换格式
数据流:music_file → librosa.load → 多 band 分解 → STFT → combine_spectrograms → model inference → 分离频谱 → ISTFT → 波形文件
主要外部依赖:
- librosa:音频加载和重采样
- numpy:数值计算
- soundfile:音频文件读写
- torch:深度学习框架
- ffmpeg:音频格式转换
- uvr5_pack 包:包含模型定义、频谱处理工具等
接口契约:
_audio_pre_和_audio_pre_new类提供统一的_path_audio_方法- 输入:music_file(音频文件路径)、vocal_root(人声输出目录)、ins_root(伴奏输出目录)、format(输出格式)
- 输出:(vocal_path, instrument_path) 元组
关键配置参数:
agg:分离强度(0-100),影响分离的激进程度is_half:是否使用半精度推理high_end_process:高频处理方式("mirroring" 或 "none")postprocess:是否启用后处理tta:测试时增强(未实现)window_size:窗口大小(512)- 模型路径:指定预训练模型文件
- 批处理支持:当前单文件处理,可考虑批量处理
- 内存优化:对于长音频,可分块处理避免内存溢出
- 缓存机制:模型和参数可缓存复用
- 多线程/多进程:文件 IO 和音频处理可并行
- GPU 利用率:可进一步优化 CUDA 操作
- 路径遍历风险:
os.path.join使用相对安全,但需验证输入路径 - 命令注入风险:
os.system调用 ffmpeg 存在潜在命令注入风险,建议使用 subprocess 或库函数替代 - 文件覆盖:未检查目标文件是否已存在,会直接覆盖
- 模型安全:模型文件来自外部,需验证完整性
- 单元测试:测试各模块独立功能
- 集成测试:测试完整音频分离流程
- 边界测试:测试各种异常输入
- 性能测试:测试不同长度音频的处理时间
- 对比测试:与原版 uvr5 输出对比验证一致性
- Python 3.x
- CUDA 10.2+(如需 GPU 加速)
- 所需 Python 包:torch, librosa, numpy, soundfile
- 磁盘空间:模型文件约 300MB,运行时需要足够空间存储临时文件和输出
_audio_pre_使用 4band_v2.json 配置和nets.CascadedASPPNet_audio_pre_new使用 4band_v3.json 配置和nets_new.CascadedNet_audio_pre_new是更新版本,针对 DeReverb 等新型号优化- 注释提到"3个VR模型vocal和ins是反的",说明某些模型输出需要交换