Files
2025-06-17 10:37:11 +09:00

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",
}