Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c87923faeb | ||
|
|
64070adf77 | ||
|
|
b8ee1856c5 | ||
|
|
f973796f07 | ||
|
|
9a1297beaf | ||
|
|
93856c88eb | ||
|
|
75ed37ed3b | ||
|
|
08fb457a9d | ||
|
|
dfcce31318 | ||
|
|
499aac5e80 | ||
|
|
6760256cd7 |
+2
-2
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "audio-separation"
|
||||
description = "Audio separation (aka demixing) nodes, for Vocals, Instruments, Bass, Drums and Others. Using MDX-Net, no extra dependencies, support for batch and resample."
|
||||
version = "1.0.0"
|
||||
description = "Audio separation (aka demixing) nodes, for Vocals, Instruments, Bass, Drums and Others (experimental Piano and Guitar). Using MDX-Net and Demucs, no extra dependencies, support for batch and resample. Choose between High Quality and Speed. All safetensor models (No ONNX, No PyTorch)"
|
||||
version = "1.1.0"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = []
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ import typing as tp
|
||||
import torch
|
||||
from torch import nn
|
||||
from torch.nn import functional as F
|
||||
import torchaudio
|
||||
|
||||
from .resample_frac import resample_frac # From julius
|
||||
from .demucs_code import capture_init, center_trim, unfold
|
||||
from .CrossTransformerEncoder import LayerScale
|
||||
|
||||
@@ -373,6 +373,8 @@ class Demucs(nn.Module):
|
||||
if rescale:
|
||||
rescale_module(self, reference=rescale)
|
||||
|
||||
self.upsampler = self.downsampler = None
|
||||
|
||||
def valid_length(self, length):
|
||||
"""
|
||||
Return the nearest valid length to use with the model so that
|
||||
@@ -413,7 +415,16 @@ class Demucs(nn.Module):
|
||||
x = F.pad(x, (delta // 2, delta - delta // 2))
|
||||
|
||||
if self.resample:
|
||||
x = resample_frac(x, 1, 2)
|
||||
if self.upsampler is None:
|
||||
# Create the resamplers as instance attributes.
|
||||
# They will be automatically moved to the correct device when model.to(device) is called.
|
||||
self.upsampler = torchaudio.transforms.Resample(orig_freq=self.samplerate,
|
||||
new_freq=2 * self.samplerate,
|
||||
lowpass_filter_width=24).to(x.device)
|
||||
self.downsampler = torchaudio.transforms.Resample(orig_freq=2 * self.samplerate,
|
||||
new_freq=self.samplerate,
|
||||
lowpass_filter_width=24).to(x.device)
|
||||
x = self.upsampler(x)
|
||||
|
||||
saved = []
|
||||
for encode in self.encoder:
|
||||
@@ -429,7 +440,7 @@ class Demucs(nn.Module):
|
||||
x = decode(x + skip)
|
||||
|
||||
if self.resample:
|
||||
x = resample_frac(x, 2, 1)
|
||||
x = self.downsampler(x)
|
||||
x = x * std + mean
|
||||
x = center_trim(x, length)
|
||||
x = x.view(x.size(0), len(self.sources), self.audio_channels, x.size(-1))
|
||||
|
||||
@@ -265,19 +265,23 @@ class DemixerDemucs(DemixerGeneric):
|
||||
output_dict = {
|
||||
"waveform": output_tensor,
|
||||
"sample_rate": model.samplerate,
|
||||
"stem": stem_name.capitalize()
|
||||
"stem": stem_name.capitalize(),
|
||||
"generated": True,
|
||||
}
|
||||
final_outputs.append(output_dict)
|
||||
else:
|
||||
logger.debug(f"Stem '{stem_name}' not produced by model. Returning silence.")
|
||||
# Create a silent tensor with the correct shape, device, and dtype
|
||||
silent_waveform = torch.zeros(ref_batch_size, ref_channels, ref_samples, dtype=torch.float32)
|
||||
if not input_was_batched:
|
||||
silent_waveform = silent_waveform.squeeze(0)
|
||||
|
||||
# Wrap the silent tensor in the ComfyUI AUDIO dictionary format
|
||||
silent_dict = {
|
||||
"waveform": silent_waveform,
|
||||
"sample_rate": model.samplerate,
|
||||
"stem": stem_name.capitalize()
|
||||
"stem": stem_name.capitalize(),
|
||||
"generated": False,
|
||||
}
|
||||
final_outputs.append(silent_dict)
|
||||
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
from fractions import Fraction
|
||||
import logging
|
||||
from ..utils.misc import NODES_NAME
|
||||
|
||||
rlogger = logging.getLogger(f"{NODES_NAME}.demucs_log")
|
||||
|
||||
|
||||
class DemucsModelInfo(object):
|
||||
def __init__(self, index, klass_name: str, kwargs: dict, logger, weights, extra=False, sig=None):
|
||||
super().__init__()
|
||||
self.index = index
|
||||
self.kwargs = kwargs
|
||||
self.logger = logger
|
||||
self.extra = extra
|
||||
self.extra_indent = ""
|
||||
self.weights = weights
|
||||
self.signature = sig
|
||||
|
||||
# Always log these fundamental parameters
|
||||
sr = kwargs.get('samplerate', 44100)
|
||||
if sr != 44100:
|
||||
rlogger.warning("Model not configured for 44.1 kHz sample rate")
|
||||
a_ch = kwargs.get('audio_channels', 2)
|
||||
if a_ch != 2:
|
||||
rlogger.warning("Model not configured for stereo")
|
||||
|
||||
if klass_name == "HTDemucs":
|
||||
self.htdemucs()
|
||||
elif klass_name == "HDemucs":
|
||||
self.hdemucs()
|
||||
elif klass_name == "Demucs":
|
||||
self.demucs()
|
||||
else:
|
||||
logger.warning(f"No specific logger for model class: {klass_name}. "
|
||||
"Displaying raw kwargs.")
|
||||
for key, value in kwargs.items():
|
||||
logger(f" - {key}: {value}")
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self.kwargs.get(key, default)
|
||||
|
||||
def log_type(self, name):
|
||||
start = "" if self.index < 0 else f"{self.index+1}. "
|
||||
msg = f" {start}Type: {name}"
|
||||
if self.signature:
|
||||
msg += f" [{self.signature}]"
|
||||
self.logger(msg)
|
||||
|
||||
def _log_param(self, param_name, default, description="", unit="", indent=" ", can_skip=False):
|
||||
"""
|
||||
Logs a parameter if its value is different from the default, or if it's a key parameter.
|
||||
|
||||
Args:
|
||||
param_name (str): The name of the parameter to check.
|
||||
default: The default value for this parameter.
|
||||
description (str): A user-friendly description of the parameter.
|
||||
unit (str): An optional unit to display after the value (e.g., 'Hz').
|
||||
indent (str): The indentation string for the log message.
|
||||
"""
|
||||
value = self.get(param_name, default)
|
||||
|
||||
# We log if the value is not the default, or if it's a fundamental parameter.
|
||||
is_default = (value == default)
|
||||
is_important = param_name in ['sources', 'segment']
|
||||
|
||||
if is_default and not is_important and not self.extra:
|
||||
return None
|
||||
if can_skip and is_default:
|
||||
return None
|
||||
|
||||
if param_name == 'sources' and self.weights:
|
||||
value = [s if w == 1.0 else ('' if not w else f'{w}*{s}') for s, w in zip(value, self.weights)]
|
||||
|
||||
if unit == '%':
|
||||
value *= 100
|
||||
unit_str = f" {unit}" if unit else ""
|
||||
desc_str = description if description else param_name.capitalize().replace('_', ' ')
|
||||
indent += self.extra_indent
|
||||
value_str = f"{value.numerator}/{value.denominator}" if isinstance(value, Fraction) else str(value)
|
||||
n = (39 - len(desc_str) - len(value_str) - len(unit_str))*" "
|
||||
return f" {indent}- {desc_str}: {value_str}{unit_str} {n}({param_name})"
|
||||
|
||||
def log_param(self, param_name, default, description="", unit="", indent=" ", can_skip=False):
|
||||
res = self._log_param(param_name, default, description, unit, indent, can_skip)
|
||||
if res is not None:
|
||||
self.logger(res)
|
||||
|
||||
def add(self, param_name, default, description="", unit="", indent=" ", can_skip=False):
|
||||
res = self._log_param(param_name, default, description, unit, indent, can_skip)
|
||||
if res is not None:
|
||||
self.params.append(res)
|
||||
|
||||
def reset(self):
|
||||
self.params = []
|
||||
|
||||
def sub_section(self, name):
|
||||
self.logger(f" {name}:")
|
||||
|
||||
def section(self, name):
|
||||
self.logger(" " + "-" * 40)
|
||||
self.sub_section(name)
|
||||
|
||||
def flush(self, name, is_sub=False):
|
||||
if self.params:
|
||||
if is_sub:
|
||||
self.sub_section(self.extra_indent + name)
|
||||
else:
|
||||
self.section(name)
|
||||
for p in self.params:
|
||||
self.logger(p)
|
||||
|
||||
def structure(self, ch=64, depth=6, with_lstm=False, with_ch_tm=False):
|
||||
self.reset()
|
||||
self.add('channels', ch, "Initial hidden channels")
|
||||
self.add('depth', depth, "Number of U-Net layers")
|
||||
self.add('growth', 2.0, "Channel growth factor per layer")
|
||||
self.add('rewrite', True, "Use 1x1 convolutions in blocks")
|
||||
if with_lstm:
|
||||
self.add('lstm_layers', 0, "Number of main LSTM layers", can_skip=True)
|
||||
if with_ch_tm:
|
||||
self.add('channels_time', None, "Specific channels for time branch", can_skip=True)
|
||||
self.flush("Structure")
|
||||
|
||||
def convolutions(self, advanced=False):
|
||||
self.reset()
|
||||
self.add('kernel_size', 8)
|
||||
self.add('stride', 4)
|
||||
if advanced:
|
||||
self.add('time_stride', 2, "Final time layer stride")
|
||||
self.add('context', 1, "Decoder context window size")
|
||||
if advanced:
|
||||
self.add('context_enc', 0, "Encoder context window size")
|
||||
self.flush("Convolutions")
|
||||
|
||||
def normalization(self):
|
||||
self.reset()
|
||||
self.add('norm_starts', 4, "Start at layer")
|
||||
self.add('norm_groups', 4, "Number of groups")
|
||||
self.flush("Normalization")
|
||||
|
||||
def dconv(self, full=True):
|
||||
if self.get('dconv_mode', 1) <= 0:
|
||||
return
|
||||
self.reset()
|
||||
where = ['', 'In encoder', 'In decoder', 'In encoder and decoder'][self.get('dconv_mode', 1)]
|
||||
self.add('dconv_mode', 1, where)
|
||||
self.add('dconv_depth', 2, "Number of layers in DConv branch")
|
||||
if full:
|
||||
comp = 4
|
||||
init = 1e-4
|
||||
else:
|
||||
comp = 8
|
||||
init = 1e-3
|
||||
self.add('dconv_comp', comp, "Channel compression factor")
|
||||
self.add('dconv_init', init, "Initial scale")
|
||||
if full:
|
||||
self.add('dconv_attn', 4, "Layer to start attention in DConv")
|
||||
self.add('dconv_lstm', 4, "Layer to start LSTM in DConv")
|
||||
self.flush("DConv Residual Branch")
|
||||
|
||||
def stft(self):
|
||||
self.reset()
|
||||
self.add('nfft', 4096, "Frequency Bins")
|
||||
# Decode the method
|
||||
cac = self.get('cac')
|
||||
niters = self.get('wiener_iters', 0)
|
||||
if cac:
|
||||
zout = "Complex as Channels (CaC)"
|
||||
elif niters >= 0:
|
||||
zout = "Wiener filtering"
|
||||
else:
|
||||
zout = "Naive iSTFT from masking"
|
||||
self.add('___', zout, "Framework")
|
||||
self.add('cac', True, "Use Complex as Channels")
|
||||
if not self.get('cac', True):
|
||||
self.add('wiener_iters', 0, "Wiener filter iterations")
|
||||
self.flush("STFT")
|
||||
|
||||
def freq_branch(self):
|
||||
self.reset()
|
||||
def_ratio = None
|
||||
if self.get('multi_freqs') == []:
|
||||
def_ratio = []
|
||||
self.add('multi_freqs', def_ratio, "Ratios for frequency band splitting")
|
||||
if self.get('multi_freqs'):
|
||||
self.add('multi_freqs_depth', 2, "Layers to apply frequency splitting")
|
||||
self.add('freq_emb', 0.2, "Frequency embedding weight")
|
||||
if self.get('freq_emb'):
|
||||
indent = " "
|
||||
self.add('emb_scale', 10, "Scale", indent=indent)
|
||||
self.add('emb_smooth', True, "Smooth", indent=indent)
|
||||
self.flush("Frequency Branch")
|
||||
|
||||
def demucs(self):
|
||||
"""Logs the parameters for the original Demucs class."""
|
||||
self.log_type("Classic Waveform Demucs (Demucs)")
|
||||
self.log_param('sources', [], "Target source names")
|
||||
self.log_param('segment', 40, "Segment size", unit="s")
|
||||
# --- Structure & Channels ---
|
||||
self.structure(ch=64, depth=6, with_lstm=True)
|
||||
# --- Convolutions ---
|
||||
self.convolutions()
|
||||
|
||||
self.reset()
|
||||
self.add('gelu', True, "GeLU (not ReLU)")
|
||||
if self.get('rewrite', True):
|
||||
self.add('glu', True, "GLU in 1x1 rewrite (not ReLU)")
|
||||
self.flush("Activations")
|
||||
# --- Normalization ---
|
||||
self.normalization()
|
||||
# --- DConv Residual Branch ---
|
||||
self.dconv()
|
||||
# --- Pre/Post Processing ---
|
||||
self.reset()
|
||||
self.add('resample', True, "Use 2x resampling")
|
||||
self.add('normalize', True, "Normalize audio on-the-fly")
|
||||
self.flush("Processing")
|
||||
|
||||
def hdemucs(self):
|
||||
"""Logs the parameters for the HDemucs (Hybrid Spectrogram/Waveform) class."""
|
||||
self.log_type("Hybrid Demucs (Spectrogram + Waveform) (HDemucs)")
|
||||
self.log_param('sources', [], "Target source names")
|
||||
self.log_param('segment', 40, "Segment size", unit="s")
|
||||
# --- Structure & Channels ---
|
||||
self.structure(ch=48, depth=6, with_ch_tm=True)
|
||||
# --- STFT & Spectrogram ---
|
||||
self.stft()
|
||||
# --- Frequency Branch ---
|
||||
self.freq_branch()
|
||||
# --- Convolutions ---
|
||||
self.convolutions(advanced=True)
|
||||
# --- Normalization ---
|
||||
self.normalization()
|
||||
# --- DConv Residual Branch (defaults are different from Demucs) ---
|
||||
self.dconv()
|
||||
|
||||
def htdemucs(self):
|
||||
"""Logs the parameters for the HTDemucs (Hybrid Transformer) class."""
|
||||
self.log_type("Hybrid Transformer Demucs (HTDemucs)")
|
||||
self.log_param('sources', [], "Target source names")
|
||||
self.log_param('segment', 10, "Segment size", unit="s")
|
||||
# --- Structure & Channels (defaults are different from HDemucs) ---
|
||||
self.structure(ch=48, depth=4)
|
||||
# --- STFT & Spectrogram ---
|
||||
self.stft()
|
||||
# --- Frequency Branch ---
|
||||
self.freq_branch()
|
||||
# --- Convolutions ---
|
||||
self.convolutions(advanced=True)
|
||||
# --- Normalization ---
|
||||
self.normalization()
|
||||
# --- DConv (defaults are different) ---
|
||||
self.dconv(full=False)
|
||||
# --- Transformer Block ---
|
||||
if self.get('t_layers', 5) > 0:
|
||||
self.extra_indent = " "
|
||||
# --- Main Transformer ---
|
||||
self.reset()
|
||||
if self.get('bottom_channels', 0):
|
||||
self.add('bottom_channels', 0, "Channels forced to")
|
||||
self.add('t_hidden_scale', 4.0, "Hidden scale")
|
||||
self.add('t_layers', 5, "Number of transformer layers")
|
||||
self.add('t_heads', 8, "Number of attention heads")
|
||||
self.add('t_dropout', 0.0, "Dropout")
|
||||
self.flush("Transformer")
|
||||
# --- Positional Embeddings ---
|
||||
self.reset()
|
||||
self.add('t_emb', 'sin', "Type")
|
||||
self.add('t_weight_pos_embed', 1.0, "Weight", can_skip=True)
|
||||
t_emb = self.get('t_emb', 'sin')
|
||||
if t_emb == 'scaled':
|
||||
self.add('t_max_positions', 10000, "Max positions")
|
||||
elif t_emb == 'sin':
|
||||
self.add('t_max_period', 10000.0, "Max period")
|
||||
self.add('t_sin_random_shift', 0, "Random shift", can_skip=True)
|
||||
elif t_emb == 'cape':
|
||||
self.add('t_cape_mean_normalize', True, "Cape normalize")
|
||||
self.add('t_cape_glob_loc_scale', [5000.0, 1.0, 1.4], "Cape params")
|
||||
if self.get('t_cape_augment', True):
|
||||
rlogger.warning("t_cape_augment is True in loaded model, should be False for inference.")
|
||||
self.flush("Positional Embeddings", is_sub=True)
|
||||
# --- Transformer Normalization ---
|
||||
self.reset()
|
||||
self.add('t_norm_first', True, "Before attention/FFN")
|
||||
self.add('t_norm_in', True, "Before pos. embedding")
|
||||
if self.get('t_norm_in', True):
|
||||
self.add('t_norm_in_group', False, "On all timesteps")
|
||||
self.add('t_group_norm', False, "Of encoder on all timesteps")
|
||||
self.add('t_norm_out', True, "GroupNorm at end of layers")
|
||||
self.flush("Normalization", is_sub=True)
|
||||
# --- Transformer Misc ---
|
||||
self.reset()
|
||||
self.add('t_cross_first', False, "Cross-attention is the first layer")
|
||||
self.add('t_layer_scale', True, "Layer scale")
|
||||
self.add('t_gelu', True, "GeLU (not ReLU)")
|
||||
self.flush("Various", is_sub=True)
|
||||
# --- Sparsity ---
|
||||
# Log sparsity details only if sparse attention is enabled
|
||||
self.reset()
|
||||
is_sparse = self.get('t_sparse_self_attn', False)
|
||||
self.add('t_sparse_self_attn', False, "Use sparse self-attention")
|
||||
if is_sparse:
|
||||
self.add('t_sparse_cross_attn', False, "Sparse cross-attention")
|
||||
self.add('t_auto_sparsity', False, "Automatic sparsity")
|
||||
auto_sparsity = self.get('t_auto_sparsity', False)
|
||||
if not auto_sparsity:
|
||||
self.add('t_mask_type', 'diag', "Masking pattern")
|
||||
self.add('t_mask_random_seed', 42, "Mask seed")
|
||||
mask_t = self.get('t_mask_type', 'diag')
|
||||
if 'diag' in mask_t:
|
||||
self.add('t_sparse_attn_window', 500, "Window size")
|
||||
if 'global' in mask_t:
|
||||
self.add('t_global_window', 100, "Window size")
|
||||
if 'random' in mask_t:
|
||||
self.add('t_sparsity', 0.95, "Sparsity for random mask", unit="%")
|
||||
self.flush("Sparsity", is_sub=True)
|
||||
# Training only
|
||||
# self.add(logger, kwargs, 't_weight_decay', 0.0, "Weight decay", extra=False)
|
||||
# self.add(logger, kwargs, 't_lr', None, "Learning rate", extra=False)
|
||||
# self.add(logger, kwargs, 't_cape_augment', True, "Learning rate", extra=False)
|
||||
# self.add(logger, kwargs, 'rescale', 0.1, "Rescale trick", extra=False)
|
||||
self.extra_indent = ""
|
||||
@@ -9,14 +9,15 @@ import json
|
||||
import logging
|
||||
from safetensors import safe_open
|
||||
from .MDX_Net import MDX_Net
|
||||
from ..utils.misc import NODES_NAME, json_object_hook
|
||||
from ..utils.misc import NODES_NAME, json_object_hook, get_debug_level
|
||||
# Demucs class imports
|
||||
from .demucs_api import BagOfModels
|
||||
from .demucs_log_helper import DemucsModelInfo
|
||||
|
||||
logger = logging.getLogger(f"{NODES_NAME}.get_model")
|
||||
|
||||
|
||||
def get_metadata(file_path, d):
|
||||
def get_metadata(file_path, d=None):
|
||||
""" Read the metadata from a safetensors file """
|
||||
logger.debug(f"Reading metadata from {file_path}")
|
||||
metadata = {}
|
||||
@@ -25,6 +26,8 @@ def get_metadata(file_path, d):
|
||||
if not metadata:
|
||||
raise ValueError(f"Could not read metadata from safetensors file: {file_path}")
|
||||
|
||||
if d is None:
|
||||
return metadata
|
||||
# Is this a child model?
|
||||
parent = d.get('parent')
|
||||
if parent:
|
||||
@@ -57,7 +60,7 @@ def get_hyperparameter(metadata, parameter, as_type, default=None, warn_diff=Tru
|
||||
def get_mdx_model(d):
|
||||
""" Create an MDX_Net object with the specified parameters """
|
||||
# Check the file is consistent we our data base
|
||||
metadata = get_metadata(d['model_path'])
|
||||
metadata = get_metadata(d['model_path'], d)
|
||||
dim_f = get_hyperparameter(metadata, 'mdx_dim_f_set', "int", d['mdx_dim_f_set'])
|
||||
channels = get_hyperparameter(metadata, 'channels', "int", d['channels'])
|
||||
stages = get_hyperparameter(metadata, 'stages', "int", d['stages'])
|
||||
@@ -102,8 +105,11 @@ def get_demucs_model(d):
|
||||
logger.debug(f" - Redirecting class: {class_module} -> {local_class_module}")
|
||||
module = importlib.import_module(local_class_module)
|
||||
klass = getattr(module, class_name)
|
||||
instance = klass(*args, **kwargs)
|
||||
instance._signature = sig
|
||||
instance._metadata = model_meta
|
||||
|
||||
sub_models.append({'sig': sig, 'model': klass(*args, **kwargs)})
|
||||
sub_models.append({'sig': sig, 'model': instance})
|
||||
|
||||
# Create a mapping from signature to model instance
|
||||
model_map = {m['sig']: m['model'] for m in sub_models}
|
||||
@@ -113,17 +119,37 @@ def get_demucs_model(d):
|
||||
segment = float(metadata.get('segment', '0'))
|
||||
|
||||
if not is_bag:
|
||||
weights = None
|
||||
final_model = ordered_models[0]
|
||||
final_model.signatures = signatures
|
||||
else:
|
||||
logger.debug("Rebuilding BagOfModels container...")
|
||||
weights = json.loads(metadata.get('weights', 'null'))
|
||||
|
||||
final_model = BagOfModels(ordered_models, weights=weights, segment=segment)
|
||||
final_model.signatures = signatures
|
||||
|
||||
final_model.config_segment = segment
|
||||
|
||||
debug_level = get_debug_level(logger)
|
||||
if debug_level >= 1:
|
||||
# Show some information of the resulting model
|
||||
logger.debug("Model information:")
|
||||
logger.debug(f"- Total models {len(ordered_models)}")
|
||||
if weights is not None and len(weights) != len(ordered_models):
|
||||
raise ValueError(f"Invalid weights for {len(ordered_models)} models: {weights}")
|
||||
for n, m in enumerate(ordered_models):
|
||||
tp = m.__class__.__name__
|
||||
w = weights[n] if weights else None
|
||||
if n == 0:
|
||||
ref_sources = m.sources
|
||||
else:
|
||||
if m.sources != ref_sources:
|
||||
logger.error("The sub-model outputs doesn't match")
|
||||
if w and len(m.sources) != len(w):
|
||||
raise ValueError(f"Invalid {w} weights for {m.sources} sources")
|
||||
num = -1 if len(ordered_models) == 1 else n
|
||||
DemucsModelInfo(num, tp, m._metadata['kwargs'], logger.debug, w, extra=debug_level > 1, sig=m._signature)
|
||||
|
||||
return final_model
|
||||
|
||||
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
# File under the MIT license, see https://github.com/adefossez/julius/LICENSE for details.
|
||||
# Author: adefossez, 2020
|
||||
"""
|
||||
Differentiable, Pytorch based resampling.
|
||||
Implementation of Julius O. Smith algorithm for resampling.
|
||||
See https://ccrma.stanford.edu/~jos/resample/ for details.
|
||||
This implementation is specially optimized for when new_sr / old_sr is a fraction
|
||||
with a small numerator and denominator when removing the gcd (e.g. new_sr = 700, old_sr = 500).
|
||||
|
||||
Very similar to [bmcfee/resampy](https://github.com/bmcfee/resampy) except this implementation
|
||||
is optimized for the case mentioned before, while resampy is slower but more general.
|
||||
|
||||
"""
|
||||
import math
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
from torch.nn import functional as F
|
||||
|
||||
|
||||
def sinc(x: torch.Tensor):
|
||||
"""
|
||||
Implementation of sinc, i.e. sin(x) / x
|
||||
|
||||
__Warning__: the input is not multiplied by `pi`!
|
||||
"""
|
||||
return torch.where(x == 0, torch.tensor(1., device=x.device, dtype=x.dtype), torch.sin(x) / x)
|
||||
|
||||
|
||||
class ResampleFrac(torch.nn.Module):
|
||||
"""
|
||||
Resampling from the sample rate `old_sr` to `new_sr`.
|
||||
"""
|
||||
def __init__(self, old_sr: int, new_sr: int, zeros: int = 24, rolloff: float = 0.945):
|
||||
"""
|
||||
Args:
|
||||
old_sr (int): sample rate of the input signal x.
|
||||
new_sr (int): sample rate of the output.
|
||||
zeros (int): number of zero crossing to keep in the sinc filter.
|
||||
rolloff (float): use a lowpass filter that is `rolloff * new_sr / 2`,
|
||||
to ensure sufficient margin due to the imperfection of the FIR filter used.
|
||||
Lowering this value will reduce anti-aliasing, but will reduce some of the
|
||||
highest frequencies.
|
||||
|
||||
Shape:
|
||||
|
||||
- Input: `[*, T]`
|
||||
- Output: `[*, T']` with `T' = int(new_sr * T / old_sr)
|
||||
|
||||
|
||||
.. caution::
|
||||
After dividing `old_sr` and `new_sr` by their GCD, both should be small
|
||||
for this implementation to be fast.
|
||||
|
||||
>>> import torch
|
||||
>>> resample = ResampleFrac(4, 5)
|
||||
>>> x = torch.randn(1000)
|
||||
>>> print(len(resample(x)))
|
||||
1250
|
||||
"""
|
||||
super().__init__()
|
||||
if not isinstance(old_sr, int) or not isinstance(new_sr, int):
|
||||
raise ValueError("old_sr and new_sr should be integers")
|
||||
gcd = math.gcd(old_sr, new_sr)
|
||||
self.old_sr = old_sr // gcd
|
||||
self.new_sr = new_sr // gcd
|
||||
self.zeros = zeros
|
||||
self.rolloff = rolloff
|
||||
|
||||
self._init_kernels()
|
||||
|
||||
def _init_kernels(self):
|
||||
if self.old_sr == self.new_sr:
|
||||
return
|
||||
|
||||
kernels = []
|
||||
sr = min(self.new_sr, self.old_sr)
|
||||
# rolloff will perform antialiasing filtering by removing the highest frequencies.
|
||||
# At first I thought I only needed this when downsampling, but when upsampling
|
||||
# you will get edge artifacts without this, the edge is equivalent to zero padding,
|
||||
# which will add high freq artifacts.
|
||||
sr *= self.rolloff
|
||||
|
||||
# The key idea of the algorithm is that x(t) can be exactly reconstructed from x[i] (tensor)
|
||||
# using the sinc interpolation formula:
|
||||
# x(t) = sum_i x[i] sinc(pi * old_sr * (i / old_sr - t))
|
||||
# We can then sample the function x(t) with a different sample rate:
|
||||
# y[j] = x(j / new_sr)
|
||||
# or,
|
||||
# y[j] = sum_i x[i] sinc(pi * old_sr * (i / old_sr - j / new_sr))
|
||||
|
||||
# We see here that y[j] is the convolution of x[i] with a specific filter, for which
|
||||
# we take an FIR approximation, stopping when we see at least `zeros` zeros crossing.
|
||||
# But y[j+1] is going to have a different set of weights and so on, until y[j + new_sr].
|
||||
# Indeed:
|
||||
# y[j + new_sr] = sum_i x[i] sinc(pi * old_sr * ((i / old_sr - (j + new_sr) / new_sr))
|
||||
# = sum_i x[i] sinc(pi * old_sr * ((i - old_sr) / old_sr - j / new_sr))
|
||||
# = sum_i x[i + old_sr] sinc(pi * old_sr * (i / old_sr - j / new_sr))
|
||||
# so y[j+new_sr] uses the same filter as y[j], but on a shifted version of x by `old_sr`.
|
||||
# This will explain the F.conv1d after, with a stride of old_sr.
|
||||
self._width = math.ceil(self.zeros * self.old_sr / sr)
|
||||
# If old_sr is still big after GCD reduction, most filters will be very unbalanced, i.e.,
|
||||
# they will have a lot of almost zero values to the left or to the right...
|
||||
# There is probably a way to evaluate those filters more efficiently, but this is kept for
|
||||
# future work.
|
||||
idx = torch.arange(-self._width, self._width + self.old_sr).float()
|
||||
for i in range(self.new_sr):
|
||||
t = (-i/self.new_sr + idx/self.old_sr) * sr
|
||||
t = t.clamp_(-self.zeros, self.zeros)
|
||||
t *= math.pi
|
||||
window = torch.cos(t/self.zeros/2)**2
|
||||
kernel = sinc(t) * window
|
||||
# Renormalize kernel to ensure a constant signal is preserved.
|
||||
kernel.div_(kernel.sum())
|
||||
kernels.append(kernel)
|
||||
|
||||
self.register_buffer("kernel", torch.stack(kernels).view(self.new_sr, 1, -1))
|
||||
|
||||
def forward(self, x: torch.Tensor, output_length: Optional[int] = None, full: bool = False):
|
||||
"""
|
||||
Resample x.
|
||||
Args:
|
||||
x (Tensor): signal to resample, time should be the last dimension
|
||||
output_length (None or int): This can be set to the desired output length
|
||||
(last dimension). Allowed values are between 0 and
|
||||
ceil(length * new_sr / old_sr). When None (default) is specified, the
|
||||
floored output length will be used. In order to select the largest possible
|
||||
size, use the `full` argument.
|
||||
full (bool): return the longest possible output from the input. This can be useful
|
||||
if you chain resampling operations, and want to give the `output_length` only
|
||||
for the last one, while passing `full=True` to all the other ones.
|
||||
"""
|
||||
if self.old_sr == self.new_sr:
|
||||
return x
|
||||
shape = x.shape
|
||||
length = x.shape[-1]
|
||||
x = x.reshape(-1, length)
|
||||
x = F.pad(x[:, None], (self._width, self._width + self.old_sr), mode='replicate')
|
||||
ys = F.conv1d(x, self.kernel, stride=self.old_sr) # type: ignore
|
||||
y = ys.transpose(1, 2).reshape(list(shape[:-1]) + [-1])
|
||||
|
||||
float_output_length = torch.as_tensor(self.new_sr * length / self.old_sr)
|
||||
max_output_length = torch.ceil(float_output_length).long()
|
||||
default_output_length = torch.floor(float_output_length).long()
|
||||
|
||||
if output_length is None:
|
||||
applied_output_length = max_output_length if full else default_output_length
|
||||
elif output_length < 0 or output_length > max_output_length:
|
||||
raise ValueError(f"output_length must be between 0 and {max_output_length}")
|
||||
else:
|
||||
applied_output_length = torch.tensor(output_length)
|
||||
if full:
|
||||
raise ValueError("You cannot pass both full=True and output_length")
|
||||
return y[..., :applied_output_length] # type: ignore
|
||||
|
||||
|
||||
def resample_frac(x: torch.Tensor, old_sr: int, new_sr: int,
|
||||
zeros: int = 24, rolloff: float = 0.945,
|
||||
output_length: Optional[int] = None, full: bool = False):
|
||||
"""
|
||||
Functional version of `ResampleFrac`, refer to its documentation for more information.
|
||||
|
||||
..warning::
|
||||
If you call repeatidly this functions with the same sample rates, then the
|
||||
resampling kernel will be recomputed every time. For best performance, you should use
|
||||
and cache an instance of `ResampleFrac`.
|
||||
"""
|
||||
return ResampleFrac(old_sr, new_sr, zeros, rolloff).to(x)(x, output_length, full)
|
||||
|
||||
|
||||
# Easier implementations for downsampling and upsampling by a factor of 2
|
||||
# Kept for testing and reference
|
||||
|
||||
def _kernel_upsample2_downsample2(zeros):
|
||||
# Kernel for upsampling and downsampling by a factor of 2. Interestingly,
|
||||
# it is the same kernel used for both.
|
||||
win = torch.hann_window(4 * zeros + 1, periodic=False)
|
||||
winodd = win[1::2]
|
||||
t = torch.linspace(-zeros + 0.5, zeros - 0.5, 2 * zeros)
|
||||
t *= math.pi
|
||||
kernel = (sinc(t) * winodd).view(1, 1, -1)
|
||||
return kernel
|
||||
|
||||
|
||||
def _upsample2(x, zeros=24):
|
||||
"""
|
||||
Upsample x by a factor of two. The output will be exactly twice as long as the input.
|
||||
Args:
|
||||
x (Tensor): signal to upsample, time should be the last dimension
|
||||
zeros (int): number of zero crossing to keep in the sinc filter.
|
||||
|
||||
This function is kept only for reference, you should use the more generic `resample_frac`
|
||||
one. This function does not perform anti-aliasing filtering.
|
||||
"""
|
||||
*other, time = x.shape
|
||||
kernel = _kernel_upsample2_downsample2(zeros).to(x)
|
||||
out = F.conv1d(x.view(-1, 1, time), kernel, padding=zeros)[..., 1:].view(*other, time)
|
||||
y = torch.stack([x, out], dim=-1)
|
||||
return y.view(*other, -1)
|
||||
|
||||
|
||||
def _downsample2(x, zeros=24):
|
||||
"""
|
||||
Downsample x by a factor of two. The output length is half of the input, ceiled.
|
||||
Args:
|
||||
x (Tensor): signal to downsample, time should be the last dimension
|
||||
zeros (int): number of zero crossing to keep in the sinc filter.
|
||||
|
||||
This function is kept only for reference, you should use the more generic `resample_frac`
|
||||
one. This function does not perform anti-aliasing filtering.
|
||||
"""
|
||||
if x.shape[-1] % 2 != 0:
|
||||
x = F.pad(x, (0, 1))
|
||||
xeven = x[..., ::2]
|
||||
xodd = x[..., 1::2]
|
||||
*other, time = xodd.shape
|
||||
kernel = _kernel_upsample2_downsample2(zeros).to(x)
|
||||
out = xeven + F.conv1d(xodd.view(-1, 1, time), kernel, padding=zeros)[..., :-1].view(
|
||||
*other, time)
|
||||
return out.view(*other, -1).mul(0.5)
|
||||
@@ -10,8 +10,12 @@ NODES_NAME = "AudioSeparation"
|
||||
NODES_DEBUG_VAR = NODES_NAME.upper() + "_NODES_DEBUG"
|
||||
|
||||
|
||||
def get_debug_level(logger):
|
||||
return logging.DEBUG - logger.getEffectiveLevel() + 1
|
||||
|
||||
|
||||
def debugl(logger, level, msg):
|
||||
if logger.getEffectiveLevel() <= logging.DEBUG - (level - 1):
|
||||
if get_debug_level(logger) >= level:
|
||||
logger.debug(msg)
|
||||
|
||||
|
||||
|
||||
Regular → Executable
+1
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2025 Salvador E. Tropea
|
||||
# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial
|
||||
# License: GPLv3
|
||||
|
||||
Regular → Executable
+2
-1
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2025 Salvador E. Tropea
|
||||
# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial
|
||||
# License: GPLv3
|
||||
@@ -47,7 +48,7 @@ def demix(d, args):
|
||||
|
||||
if args.save_complement:
|
||||
for wav in wavs[1:]:
|
||||
if wav is None:
|
||||
if wav is None or not wav['generated']:
|
||||
continue
|
||||
out_path = f"{args.out_base or base}_{wav['stem']}{out_ext}"
|
||||
save_audio(wav['waveform'], wav['sample_rate'], out_path, out_format)
|
||||
|
||||
Regular → Executable
+1
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2025 Salvador E. Tropea
|
||||
# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial
|
||||
# License: GPLv3
|
||||
|
||||
Regular → Executable
+1
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2025 Salvador E. Tropea
|
||||
# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial
|
||||
# License: GPLv3
|
||||
|
||||
Regular → Executable
+1
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2025 Salvador E. Tropea
|
||||
# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial
|
||||
# License: GPLv3
|
||||
|
||||
Regular → Executable
+1
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2025 Salvador E. Tropea
|
||||
# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial
|
||||
# License: GPLv3
|
||||
|
||||
Regular → Executable
+1
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2025 Salvador E. Tropea
|
||||
# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial
|
||||
# License: GPLv3
|
||||
|
||||
Regular → Executable
+17
-1
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2025 Salvador E. Tropea
|
||||
# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial
|
||||
# License: GPLv3
|
||||
@@ -13,6 +14,7 @@ import bootstrap # noqa: F401
|
||||
from source.utils.logger import main_logger, logger_set_standalone
|
||||
from source.utils.misc import cli_add_verbose, json_object_hook
|
||||
from source.inference.get_model import get_metadata
|
||||
from source.inference.demucs_log_helper import DemucsModelInfo
|
||||
|
||||
|
||||
def show_metadata(args):
|
||||
@@ -23,11 +25,25 @@ def show_metadata(args):
|
||||
expanded = {k: json.loads(v, object_hook=json_object_hook) if v[0] in '{[' else v for k, v in d.items()}
|
||||
pprint(expanded)
|
||||
|
||||
sigs = expanded.get('signatures')
|
||||
if sigs:
|
||||
single = len(sigs) == 1
|
||||
print("Demucs model")
|
||||
if not single:
|
||||
print(f"Composed by {len(sigs)} submodels")
|
||||
weights = expanded.get('weights')
|
||||
# Demucs model
|
||||
for n, s in enumerate(sigs):
|
||||
d = expanded[s]
|
||||
num = -1 if single else n
|
||||
DemucsModelInfo(num, d['class_name'], d['kwargs'], print, weights[n] if weights else None, extra=args.verbose,
|
||||
sig=s)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Shows the safetensors metadata",
|
||||
formatter_class=argparse.RawTextHelpFormatter)
|
||||
parser.add_argument('input_file', type=str, help="Path to the input ONNX model file.")
|
||||
parser.add_argument('input_file', type=str, help="Path to the input safetensors model file.")
|
||||
cli_add_verbose(parser)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
Regular → Executable
+1
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2025 Salvador E. Tropea
|
||||
# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial
|
||||
# License: GPLv3
|
||||
|
||||
Regular → Executable
Reference in New Issue
Block a user