added basic for for pad synth
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# ComfyUI Signal Processing
|
||||
|
||||
This repo contains 3 nodes for [ComfyUI](https://github.com/comfyanonymous/ComfyUI) that allows for audio filtering
|
||||
This repo contains 4 nodes for [ComfyUI](https://github.com/comfyanonymous/ComfyUI) that allows for audio filtering
|
||||
|
||||
## Nodes
|
||||
|
||||
@@ -11,6 +11,13 @@ fantastic algorithm of by by Nasca Octavian PAUL, his repository here : https://
|
||||
- **stretch_factor**: the stretch factor from 0 to 100
|
||||
- **window_size_seconds**: window length ins seconds. higher value create smoother stretching effect.
|
||||
|
||||
### SignalProcessingPadSynth :
|
||||
This node is a synthesiser "PadSynth" based on an algorithm by Nasca Octavian PAUL
|
||||
Original Source Code Here: https://zynaddsubfx.sourceforge.io/doc/PADsynth/PADsynth.htm
|
||||
- **audio**: input audio
|
||||
- **stretch_factor**: the stretch factor from 0 to 100
|
||||
- **window_size_seconds**: window length ins seconds. higher value create smoother stretching effect.
|
||||
|
||||
### SignalProcessingFilter :
|
||||
This node lets you stretch audio to about 100x it's original speed whil mainting pitch. it's great for making pad sounds:
|
||||
- **audio**: input audio
|
||||
|
||||
@@ -41,7 +41,6 @@ class SignalProcessingLoadAudio():
|
||||
|
||||
audio_file_path = folder_paths.get_annotated_filepath(audio_file)
|
||||
|
||||
metadata = torchaudio.info(audio_file_path)
|
||||
waveform, sample_rate = torchaudio.load(audio_file_path)
|
||||
waveform = waveform.unsqueeze(0).contiguous()
|
||||
return ({'waveform': waveform, 'sample_rate': sample_rate},)
|
||||
@@ -310,14 +309,156 @@ class SignalProcessingPaulStretch:
|
||||
return orig_n
|
||||
|
||||
|
||||
class SignalProcessingPadSynth:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> Dict:
|
||||
return {
|
||||
"required": {
|
||||
"samplerate": ("INT", {"default": 44100, "min": 8000, "max": 96000, "step": 1}),
|
||||
"fundamental_freq": ("FLOAT", {"default": 261.0, "min": 20.0, "max": 2000.0, "step": 1.0}),
|
||||
"bandwidth_cents": ("FLOAT", {"default": 40.0, "min": 10.0, "max": 100.0, "step": 1.0}),
|
||||
"number_harmonics": ("INT", {"default": 64, "min": 1, "max": 128, "step": 1}),
|
||||
"amplitude_per_harmonic": ("STRING", {"default": "", "multiline": False, "hint": "Provide a JSON array of amplitudes per harmonic, e.g., [0.0, 1.0, 0.5, ...] or leave empty for default."})
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("AUDIO",)
|
||||
RETURN_NAMES = ("audio",)
|
||||
CATEGORY = "Signal Processing"
|
||||
FUNCTION = "process"
|
||||
|
||||
def process(
|
||||
self,
|
||||
samplerate: int,
|
||||
fundamental_freq: float,
|
||||
bandwidth_cents: float,
|
||||
number_harmonics: int,
|
||||
amplitude_per_harmonic: str,
|
||||
) -> Tuple[Dict[str, torch.Tensor]]:
|
||||
"""
|
||||
Apply PADsynth algorithm to generate audio.
|
||||
|
||||
Parameters:
|
||||
samplerate (int): Sampling rate in Hz.
|
||||
fundamental_freq (float): Fundamental frequency in Hz.
|
||||
bandwidth_cents (float): Bandwidth in cents for Gaussian profile.
|
||||
number_harmonics (int): Number of harmonics to generate.
|
||||
amplitude_per_harmonic (str): JSON-formatted string specifying amplitudes per harmonic. Leave empty for defaults.
|
||||
|
||||
Returns:
|
||||
Tuple[Dict[str, torch.Tensor]]: Generated audio with waveform and sample rate.
|
||||
"""
|
||||
|
||||
import json
|
||||
# Initialize logging
|
||||
|
||||
# Define FFT size
|
||||
N = 262144 # As per C++ code
|
||||
|
||||
# Initialize amplitude array A
|
||||
if not amplitude_per_harmonic.strip():
|
||||
# Use default amplitude distribution
|
||||
A = torch.zeros(number_harmonics, dtype=torch.double)
|
||||
A[0] = 0.0 # A[0] is not used
|
||||
for i in range(1, number_harmonics):
|
||||
A[i] = 1.0 / i
|
||||
if (i % 2) == 0:
|
||||
A[i] *= 2.0
|
||||
else:
|
||||
# Parse JSON string to list
|
||||
try:
|
||||
amplitude_list = json.loads(amplitude_per_harmonic)
|
||||
if not isinstance(amplitude_list, list):
|
||||
raise ValueError("amplitude_per_harmonic must be a JSON array.")
|
||||
if len(amplitude_list) != number_harmonics:
|
||||
raise ValueError("Length of amplitude_per_harmonic must match number_harmonics.")
|
||||
# Convert to torch tensor
|
||||
A = torch.tensor(amplitude_list, dtype=torch.double)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError("amplitude_per_harmonic must be a valid JSON array.") from e
|
||||
except Exception as e:
|
||||
raise
|
||||
|
||||
# Initialize frequency amplitude and phase arrays
|
||||
freq_amp = torch.zeros(N // 2, dtype=torch.double)
|
||||
freq_phase = torch.rand(N // 2, dtype=torch.double) * 2.0 * math.pi # Random phases between 0 and 2pi
|
||||
|
||||
# Define Gaussian profile function
|
||||
def profile(fi: torch.Tensor, bwi: torch.Tensor) -> torch.Tensor:
|
||||
x = fi / bwi
|
||||
x_sq = x ** 2
|
||||
# Avoid computing exp(-x^2) for x_sq > 14.71280603
|
||||
mask = x_sq <= 14.71280603
|
||||
result = torch.zeros_like(x_sq)
|
||||
result[mask] = torch.exp(-x_sq[mask]) / bwi[mask]
|
||||
return result
|
||||
|
||||
# Convert bandwidth from cents to Hz
|
||||
# bw_Hz = (2^(bw/1200) -1) * f * nh
|
||||
# Convert bandwidth_cents to multiplier
|
||||
bw_multiplier = 2.0 ** (bandwidth_cents / 1200.0) - 1.0
|
||||
|
||||
# Populate frequency amplitude array
|
||||
for nh in range(1, number_harmonics):
|
||||
f_nh = fundamental_freq * nh
|
||||
bw_Hz = bw_multiplier * f_nh
|
||||
bwi = bw_Hz / (2.0 * samplerate)
|
||||
fi = f_nh / samplerate # Normalized frequency
|
||||
|
||||
# Create tensors for frequency bins
|
||||
i = torch.arange(N // 2, dtype=torch.double)
|
||||
# Normalized frequency for each bin
|
||||
normalized_freq = i / N # Equivalent to i * (samplerate / N) / samplerate = i / N
|
||||
|
||||
# Compute profile
|
||||
fi_tensor = torch.full_like(i, fi)
|
||||
bwi_tensor = torch.full_like(i, bwi)
|
||||
profile_values = profile(normalized_freq - fi_tensor, bwi_tensor)
|
||||
|
||||
# Update frequency amplitude
|
||||
freq_amp += profile_values * A[nh]
|
||||
|
||||
# Construct complex frequency domain tensor
|
||||
real = freq_amp * torch.cos(freq_phase)
|
||||
imag = freq_amp * torch.sin(freq_phase)
|
||||
freq_complex = torch.complex(real, imag) # Shape: (N//2,)
|
||||
|
||||
# Perform IFFT using torch.fft.irfft
|
||||
smp = torch.fft.irfft(freq_complex, n=N) # Shape: (N,)
|
||||
|
||||
# Normalize the signal to prevent clipping
|
||||
max_val = torch.max(torch.abs(smp))
|
||||
if max_val < 1e-5:
|
||||
max_val = 1e-5 # Prevent division by zero
|
||||
smp = smp / (max_val * math.sqrt(2)) # Normalize to 1/sqrt(2) as in C++ code
|
||||
|
||||
# Convert to float32 for saving
|
||||
smp = smp.float()
|
||||
|
||||
# Prepare waveform tensor: (C, N)
|
||||
waveform_out = smp.unsqueeze(0) # Mono audio
|
||||
|
||||
# Reshape waveform_out to include batch dimension: (1, C, N)
|
||||
waveform_out = waveform_out.unsqueeze(0) # Shape: (1, C, N)
|
||||
|
||||
# Return the synthesized audio
|
||||
return ({'waveform': waveform_out, 'sample_rate': samplerate},)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"SignalProcessingLoadAudio": SignalProcessingLoadAudio,
|
||||
"SignalProcessingFilter": SignalProcessingFilter,
|
||||
"SignalProcessingPaulStretch": SignalProcessingPaulStretch
|
||||
"SignalProcessingPaulStretch": SignalProcessingPaulStretch,
|
||||
"SignalProcessingPadSynth": SignalProcessingPadSynth
|
||||
}
|
||||
|
||||
print('NODE_CLASS_MAPPINGS',NODE_CLASS_MAPPINGS)
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SignalProcessingLoadAudio": "(SP) Load Audio",
|
||||
"SignalProcessingFilter": "(SP) Filter",
|
||||
"SignalProcessingPaulStretch" : "(SP) PaulStretch"
|
||||
"SignalProcessingPaulStretch" : "(SP) PaulStretch",
|
||||
"SignalProcessingPadSynth": "(SP) PadSynth"
|
||||
}
|
||||
|
||||
print('NODE_DISPLAY_NAME_MAPPINGS',NODE_DISPLAY_NAME_MAPPINGS)
|
||||
|
||||
Reference in New Issue
Block a user