Files
2024-12-28 11:12:31 -08:00

169 lines
6.1 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Author: C0ffymachyne
License: GPLv3
Version: 1.0.0
Description:
Pad Synth Choir Synthesizer port of code from this article
https://zynaddsubfx.sourceforge.io/doc/PADsynth/PADsynth.htm#c_implementation
"""
import torch
import math
from typing import Tuple, List, Dict, Any, Union
class SignalProcessingPadSynthChoir:
@classmethod
def INPUT_TYPES(cls) -> Dict[str, Any]:
return {
"required": {
"samplerate": (
"INT",
{"default": 44100, "min": 8000, "max": 96000, "step": 1},
),
"base_freq": (
"FLOAT",
{"default": 130.81, "min": 20.0, "max": 2000.0, "step": 1.0},
),
"step_size": ("INT", {"default": 4, "min": 1, "max": 24, "step": 1}),
"num_notes": ("INT", {"default": 7, "min": 1, "max": 24, "step": 1}),
"bandwidth_cents": (
"FLOAT",
{"default": 60.0, "min": 10.0, "max": 100.0, "step": 1.0},
),
"number_harmonics": (
"INT",
{"default": 64, "min": 1, "max": 128, "step": 1},
),
}
}
RETURN_TYPES = ("AUDIO_LIST", "INT")
RETURN_NAMES = ("audios", "sample_rate")
CATEGORY = "Signal Processing"
FUNCTION = "process"
def process(
self,
samplerate: int,
base_freq: float,
step_size: int,
num_notes: int,
bandwidth_cents: float,
number_harmonics: int,
) -> Tuple[List[Dict[str, Union[torch.Tensor, int]]], int]:
"""
Apply PADsynth choir algorithm to generate multiple audio files.
Parameters:
samplerate (int): Sampling rate in Hz.
base_freq (float): Base frequency in Hz.
step_size (int): Step size in semitones between notes.
num_notes (int): Number of notes to generate.
bandwidth_cents (float): Bandwidth in cents for Gaussian profile.
number_harmonics (int): Number of harmonics to generate.
Returns:
Tuple[List[Dict[str, torch.Tensor]]]:
List of generated audios with waveform and sample rate.
"""
# Define FFT size
N = 262144 # As per C++ code
audios = []
for note_index in range(num_notes):
note_semitones = step_size * note_index
f1 = base_freq * (2.0 ** (note_semitones / 12.0))
# Compute amplitude_per_harmonic with formants
A = torch.zeros(number_harmonics, dtype=torch.double)
A[0] = 0.0 # A[0] is not used
for i in range(1, number_harmonics):
# Calculate formants based on the C++ choir implementation
formants = (
math.exp(-(((i * f1 - 600.0) / 150.0) ** 2))
+ math.exp(-(((i * f1 - 900.0) / 250.0) ** 2))
+ math.exp(-(((i * f1 - 2200.0) / 200.0) ** 2))
+ math.exp(-(((i * f1 - 2600.0) / 250.0) ** 2))
+ math.exp(-(((i * f1) / 3000.0) ** 2)) * 0.1
)
A[i] = (1.0 / i) * formants
# Optionally, you can debug amplitude values
# logger.debug(f"Harmonic {i}: A[{i}]={A[i]:.4f}")
# 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
bw_multiplier = 2.0 ** (bandwidth_cents / 1200.0) - 1.0
# Create tensors for frequency bins
i = torch.arange(N // 2, dtype=torch.double)
normalized_freq = i / N # Equivalent to i / N
# Compute and accumulate frequency amplitudes for each harmonic
for nh in range(1, number_harmonics):
f_nh = f1 * nh
bw_Hz = bw_multiplier * f_nh
bwi = bw_Hz / (2.0 * samplerate)
fi = f_nh / samplerate # Normalized frequency
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)
# Append to audios list
audios.append({"waveform": waveform_out, "sample_rate": samplerate})
# Return the list of generated audios
return audios, samplerate