391 lines
11 KiB
Python
391 lines
11 KiB
Python
import sys
|
|
import warnings
|
|
|
|
import torch
|
|
import torchaudio
|
|
|
|
from .node_def import BASE_NODE_CATEGORY, MAX_SAFE_INT, MAX_SAMPLE_RATE, AudioData
|
|
|
|
NODE_CATEGORY = BASE_NODE_CATEGORY + "/edit"
|
|
|
|
|
|
class CutAudio:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"audio": ("AUDIO",),
|
|
"start_second": ("FLOAT", {"default": 0.0, "step": 0.001}),
|
|
"end_second": ("FLOAT", {"default": 0.0, "step": 0.001}),
|
|
}
|
|
}
|
|
|
|
CATEGORY = NODE_CATEGORY
|
|
|
|
RETURN_TYPES = ("AUDIO",)
|
|
RETURN_NAMES = ("audio",)
|
|
FUNCTION = "cut_audio"
|
|
|
|
def cut_audio(
|
|
self,
|
|
audio: AudioData,
|
|
start_second: float,
|
|
end_second: float,
|
|
):
|
|
start_sample = max(0, int(start_second * audio["sample_rate"]) - 1)
|
|
end_sample = max(0, int(end_second * audio["sample_rate"]) - 1)
|
|
if start_sample == end_sample:
|
|
warnings.warn("start_sample and end_sample have the same value.")
|
|
return (audio,)
|
|
|
|
view1 = audio["waveform"][..., :start_sample]
|
|
view2 = audio["waveform"][..., end_sample:]
|
|
new_audio = {
|
|
"waveform": torch.concat([view1, view2], dim=-1),
|
|
"sample_rate": audio["sample_rate"],
|
|
}
|
|
return (new_audio,)
|
|
|
|
|
|
class TrimAudio:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"audio": ("AUDIO",),
|
|
"start_second": ("FLOAT", {"default": 0.0, "step": 0.001}),
|
|
"end_second": ("FLOAT", {"default": 0.0, "step": 0.001}),
|
|
}
|
|
}
|
|
|
|
CATEGORY = NODE_CATEGORY
|
|
|
|
RETURN_TYPES = ("AUDIO",)
|
|
RETURN_NAMES = ("audio",)
|
|
FUNCTION = "trim_audio"
|
|
|
|
def trim_audio(
|
|
self,
|
|
audio: AudioData,
|
|
start_second: float,
|
|
end_second: float,
|
|
):
|
|
start_sample = max(0, int(start_second * audio["sample_rate"]) - 1)
|
|
end_sample = max(0, int(end_second * audio["sample_rate"]) - 1)
|
|
if start_sample == end_sample:
|
|
warnings.warn("start_sample and end_sample have the same value.")
|
|
return (audio,)
|
|
|
|
view = audio["waveform"][..., start_sample:end_sample]
|
|
new_audio = {
|
|
"waveform": view.detach().clone(),
|
|
"sample_rate": audio["sample_rate"],
|
|
}
|
|
return (new_audio,)
|
|
|
|
|
|
class TrimAudioBySample:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"audio": ("AUDIO",),
|
|
"start_sample": ("INT", {"default": 0, "min": 0, "max": MAX_SAFE_INT}),
|
|
"end_sample": ("INT", {"default": 0, "min": 0, "max": MAX_SAFE_INT}),
|
|
}
|
|
}
|
|
|
|
CATEGORY = NODE_CATEGORY
|
|
|
|
RETURN_TYPES = ("AUDIO",)
|
|
RETURN_NAMES = ("audio",)
|
|
FUNCTION = "trim_audio"
|
|
|
|
def trim_audio(
|
|
self,
|
|
audio: AudioData,
|
|
start_sample: int,
|
|
end_sample: int,
|
|
):
|
|
if start_sample == end_sample:
|
|
warnings.warn("start_sample and end_sample have the same value.")
|
|
return (audio,)
|
|
|
|
view = audio["waveform"][..., start_sample:end_sample]
|
|
new_audio = {
|
|
"waveform": view.detach().clone(),
|
|
"sample_rate": audio["sample_rate"],
|
|
}
|
|
return (new_audio,)
|
|
|
|
|
|
class SilenceAudio:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"audio": ("AUDIO",),
|
|
"start_second": ("FLOAT", {"default": 0.0, "step": 0.001}),
|
|
"end_second": ("FLOAT", {"default": 0.0, "step": 0.001}),
|
|
}
|
|
}
|
|
|
|
CATEGORY = NODE_CATEGORY
|
|
|
|
RETURN_TYPES = ("AUDIO",)
|
|
RETURN_NAMES = ("audio",)
|
|
FUNCTION = "silence_audio"
|
|
|
|
def silence_audio(
|
|
self,
|
|
audio: AudioData,
|
|
start_second: float,
|
|
end_second: float,
|
|
):
|
|
start_sample = max(0, int(start_second * audio["sample_rate"]) - 1)
|
|
end_sample = max(0, int(end_second * audio["sample_rate"]) - 1)
|
|
if start_sample == end_sample:
|
|
warnings.warn("start_sample and end_sample have the same value.")
|
|
return (audio,)
|
|
|
|
copy_waveform = audio["waveform"].detach().clone()
|
|
copy_waveform[..., start_sample:end_sample] = 0
|
|
new_audio = {"waveform": copy_waveform, "sample_rate": audio["sample_rate"]}
|
|
return (new_audio,)
|
|
|
|
|
|
class MakeSilenceAudio:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"second": ("FLOAT", {"default": 0.0, "step": 0.001}),
|
|
"sample_rate": ("INT", {"default": 16000, "min": 1, "max": MAX_SAMPLE_RATE}),
|
|
"channel": (["monaural", "stereo"],),
|
|
"batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}),
|
|
}
|
|
}
|
|
|
|
CATEGORY = NODE_CATEGORY
|
|
|
|
RETURN_TYPES = ("AUDIO",)
|
|
RETURN_NAMES = ("audio",)
|
|
FUNCTION = "make_silence_audio"
|
|
|
|
def make_silence_audio(
|
|
self,
|
|
second: float,
|
|
sample_rate: int,
|
|
channel: str,
|
|
batch_size: int,
|
|
):
|
|
ch_dim = 1 if channel == "monaural" else 2
|
|
samples = int(second * sample_rate)
|
|
wave = torch.zeros((batch_size, ch_dim, samples))
|
|
new_audio = {"waveform": wave, "sample_rate": sample_rate}
|
|
return (new_audio,)
|
|
|
|
|
|
class SplitAudio:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"audio": ("AUDIO",),
|
|
"second": ("FLOAT", {"default": 0.0, "step": 0.001}),
|
|
}
|
|
}
|
|
|
|
CATEGORY = NODE_CATEGORY
|
|
|
|
RETURN_TYPES = ("AUDIO", "AUDIO")
|
|
RETURN_NAMES = ("audio1", "audio2")
|
|
FUNCTION = "split_audio"
|
|
|
|
def split_audio(self, audio: AudioData, second: float):
|
|
sample = max(0, int(second * audio["sample_rate"]) - 1)
|
|
view1 = audio["waveform"][..., :sample]
|
|
view2 = audio["waveform"][..., sample:]
|
|
audio1 = {
|
|
"waveform": view1.detach().clone(),
|
|
"sample_rate": audio["sample_rate"],
|
|
}
|
|
audio2 = {
|
|
"waveform": view2.detach().clone(),
|
|
"sample_rate": audio["sample_rate"],
|
|
}
|
|
return (audio1, audio2)
|
|
|
|
|
|
class JoinAudio:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"audios": ("AUDIO",),
|
|
"silent_interval": ("FLOAT", {"default": 0.0, "step": 0.001}),
|
|
}
|
|
}
|
|
|
|
CATEGORY = NODE_CATEGORY
|
|
INPUT_IS_LIST = True
|
|
RETURN_TYPES = ("AUDIO",)
|
|
RETURN_NAMES = ("audio",)
|
|
FUNCTION = "join_audio"
|
|
|
|
def join_audio(self, audios: list[AudioData], silent_interval: float):
|
|
if len(audios) == 0:
|
|
raise ValueError("Audios size is zero.")
|
|
if not all(
|
|
[audio["sample_rate"] == audios[0]["sample_rate"] for audio in audios]
|
|
):
|
|
raise ValueError("sample_rate must be the same.")
|
|
if not all(
|
|
[
|
|
audio["waveform"].size(1) == audios[0]["waveform"].size(1)
|
|
for audio in audios
|
|
]
|
|
):
|
|
raise ValueError("All audio must be either stereo or monaural.")
|
|
|
|
samples = int(silent_interval * audios[0]["sample_rate"])
|
|
interval = torch.zeros((audios[0]["waveform"].size(1), samples))
|
|
new_tensors = []
|
|
for audio in audios:
|
|
new_tensors.append(audio)
|
|
new_tensors.append(interval)
|
|
|
|
new_tensors.pop()
|
|
|
|
new_waveforms = torch.concat(new_tensors, dim=-1)
|
|
new_audio = {"waveform": new_waveforms, "sample_rate": audios[0]["sample_rate"]}
|
|
return (new_audio,)
|
|
|
|
|
|
class ConcatAudio:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"audio1": ("AUDIO",),
|
|
"audio2": ("AUDIO",),
|
|
"silent_interval": ("FLOAT", {"default": 0.0, "step": 0.001}),
|
|
}
|
|
}
|
|
|
|
CATEGORY = NODE_CATEGORY
|
|
|
|
RETURN_TYPES = ("AUDIO",)
|
|
RETURN_NAMES = ("audio",)
|
|
FUNCTION = "concat_audio"
|
|
|
|
def concat_audio(
|
|
self, audio1: AudioData, audio2: AudioData, silent_interval: float
|
|
):
|
|
if audio1["sample_rate"] != audio2["sample_rate"]:
|
|
raise ValueError(
|
|
f"sample_rate is different.\naudio1: {0}\naudio2: {1}".format(
|
|
audio1["sample_rate"], audio2["sample_rate"]
|
|
)
|
|
)
|
|
|
|
audio1_ch = "stereo" if audio1["waveform"].size(1) > 1 else "monaural"
|
|
audio2_ch = "stereo" if audio2["waveform"].size(1) > 1 else "monaural"
|
|
if not audio1_ch == audio2_ch:
|
|
raise ValueError(f"naudio1 is {audio1_ch} but audio2 is {audio2_ch}")
|
|
|
|
samples = int(silent_interval * audio1["sample_rate"])
|
|
interval = torch.zeros(
|
|
(audio1["waveform"].size(0), audio1["waveform"].size(1), samples)
|
|
)
|
|
new_waveform = torch.concat(
|
|
[audio1["waveform"], interval, audio2["waveform"]], dim=-1
|
|
)
|
|
new_audio = {"waveform": new_waveform, "sample_rate": audio1["sample_rate"]}
|
|
return (new_audio,)
|
|
|
|
|
|
class ResampleAudio:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"audio": ("AUDIO",),
|
|
"new_freq": ("INT", {"default": 32000, "min": 1, "max": MAX_SAMPLE_RATE}),
|
|
"resampling_method": (["sinc_interp_hann", "sinc_interp_kaiser"],),
|
|
"lowpass_filter_width": (
|
|
"INT",
|
|
{"default": 6, "min": 0, "max": MAX_SAFE_INT},
|
|
),
|
|
"rolloff": (
|
|
"FLOAT",
|
|
{
|
|
"default": 0.99,
|
|
"min": 0,
|
|
"max": sys.float_info.max,
|
|
},
|
|
),
|
|
},
|
|
"optional": {
|
|
"beta": (
|
|
"FLOAT",
|
|
{
|
|
"default": 14.769656459379492,
|
|
"min": 0.0,
|
|
"max": sys.float_info.max,
|
|
},
|
|
),
|
|
},
|
|
}
|
|
|
|
CATEGORY = NODE_CATEGORY
|
|
|
|
RETURN_TYPES = ("AUDIO",)
|
|
RETURN_NAMES = ("audio",)
|
|
FUNCTION = "resample"
|
|
|
|
def resample(
|
|
self,
|
|
audio: AudioData,
|
|
new_freq: int,
|
|
resampling_method: str,
|
|
lowpass_filter_width: int,
|
|
rolloff: float,
|
|
beta=None,
|
|
):
|
|
transform = torchaudio.transforms.Resample(
|
|
orig_freq=audio["sample_rate"],
|
|
new_freq=new_freq,
|
|
resampling_method=resampling_method,
|
|
lowpass_filter_width=lowpass_filter_width,
|
|
rolloff=rolloff,
|
|
beta=beta,
|
|
)
|
|
new_audio = {"waveform": transform(audio["waveform"]), "sample_rate": new_freq}
|
|
return (new_audio,)
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"SDT_CutAudio": CutAudio,
|
|
"SDT_TrimAudio": TrimAudio,
|
|
"SDT_TrimAudioBySample": TrimAudioBySample,
|
|
"SDT_SilenceAudio": SilenceAudio,
|
|
"SDT_MakeSilenceAudio": MakeSilenceAudio,
|
|
"SDT_SplitAudio": SplitAudio,
|
|
"SDT_JoinAudio": JoinAudio,
|
|
"SDT_ConcatAudio": ConcatAudio,
|
|
"SDT_ResampleAudio": ResampleAudio,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"SDT_CutAudio": "Cut Audio",
|
|
"SDT_TrimAudio": "Trim Audio",
|
|
"SDT_TrimAudioBySample": "Trim Audio By Sample",
|
|
"SDT_SilenceAudio": "Silence Audio",
|
|
"SDT_MakeSilenceAudio": "Make Silence Audio",
|
|
"SDT_SplitAudio": "Split Audio",
|
|
"SDT_JoinAudio": "Join Audio",
|
|
"SDT_ConcatAudio": "Concat Audio",
|
|
"SDT_ResampleAudio": "Resample Audio",
|
|
}
|