172 lines
6.2 KiB
Python
172 lines
6.2 KiB
Python
import torch
|
|
import numpy as np
|
|
from scipy.signal import butter, sosfiltfilt
|
|
|
|
class AudioPostProcessor:
|
|
"""
|
|
Advanced Audio Post-Processing Node for ComfyUI
|
|
Applies professional-grade effects chain to audio
|
|
"""
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"audio": ("AUDIO",),
|
|
"effect": ([
|
|
"gospel_choir",
|
|
"trap_808",
|
|
"vocal_doubler",
|
|
"vinyl_emulation",
|
|
"radio_effect",
|
|
"hall_reverb",
|
|
"tape_saturation"
|
|
], {"default": "gospel_choir"}),
|
|
"intensity": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.05}),
|
|
"wet_dry": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 1.0, "step": 0.05}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("AUDIO",)
|
|
RETURN_NAMES = ("processed_audio",)
|
|
FUNCTION = "process"
|
|
CATEGORY = "audio/effects"
|
|
|
|
def process(self, audio, effect, intensity, wet_dry):
|
|
# Convert to numpy array
|
|
if audio.dim() == 3:
|
|
audio = audio.squeeze(0)
|
|
audio_np = audio.numpy()
|
|
sample_rate = 48000
|
|
|
|
# Process each channel
|
|
processed = np.zeros_like(audio_np)
|
|
for c in range(audio_np.shape[0]):
|
|
dry_signal = audio_np[c]
|
|
wet_signal = self.apply_effect(dry_signal, sample_rate, effect, intensity)
|
|
|
|
# Apply wet/dry mix
|
|
processed[c] = (1 - wet_dry) * dry_signal + wet_dry * wet_signal
|
|
|
|
return (processed.unsqueeze(0),)
|
|
|
|
def apply_effect(self, audio, sample_rate, effect, intensity):
|
|
"""Apply selected effect to audio"""
|
|
if effect == "gospel_choir":
|
|
return self.gospel_choir(audio, sample_rate, intensity)
|
|
elif effect == "trap_808":
|
|
return self.trap_808(audio, sample_rate, intensity)
|
|
elif effect == "vocal_doubler":
|
|
return self.vocal_doubler(audio, sample_rate, intensity)
|
|
elif effect == "vinyl_emulation":
|
|
return self.vinyl_emulation(audio, sample_rate, intensity)
|
|
elif effect == "radio_effect":
|
|
return self.radio_effect(audio, sample_rate, intensity)
|
|
elif effect == "hall_reverb":
|
|
return self.hall_reverb(audio, sample_rate, intensity)
|
|
elif effect == "tape_saturation":
|
|
return self.tape_saturation(audio, sample_rate, intensity)
|
|
return audio
|
|
|
|
def gospel_choir(self, audio, sample_rate, intensity):
|
|
"""Add gospel choir harmonies"""
|
|
# Create harmonies
|
|
harmony1 = self.pitch_shift(audio, sample_rate, 3) # Minor third
|
|
harmony2 = self.pitch_shift(audio, sample_rate, 7) # Fifth
|
|
harmony3 = self.pitch_shift(audio, sample_rate, 10) # Minor seventh
|
|
|
|
# Blend harmonies
|
|
choir = 0.5 * harmony1 + 0.3 * harmony2 + 0.2 * harmony3
|
|
|
|
# Apply church reverb
|
|
choir = self.apply_reverb(choir, sample_rate, 2.5, 0.6)
|
|
|
|
return (1 - intensity) * audio + intensity * choir
|
|
|
|
def trap_808(self, audio, sample_rate, intensity):
|
|
"""Add trap-style 808 bass"""
|
|
# Detect fundamental frequency
|
|
fundamental = self.detect_fundamental(audio, sample_rate)
|
|
|
|
# Generate 808 sine wave
|
|
t = np.arange(len(audio)) / sample_rate
|
|
sine_wave = np.sin(2 * np.pi * fundamental * t)
|
|
|
|
# Add pitch slide
|
|
slide = np.exp(-t * 2) * fundamental * 0.5
|
|
sine_wave = np.sin(2 * np.pi * (fundamental + slide) * t)
|
|
|
|
# Apply distortion
|
|
sine_wave = np.tanh(sine_wave * 3)
|
|
|
|
# Mix with original
|
|
return audio + intensity * sine_wave * 0.3
|
|
|
|
def vocal_doubler(self, audio, sample_rate, intensity):
|
|
"""Create vocal doubling effect"""
|
|
# Short delay
|
|
delay_samples = int(0.03 * sample_rate) # 30ms delay
|
|
delayed = np.roll(audio, delay_samples)
|
|
delayed[:delay_samples] = 0
|
|
|
|
# Pitch modulation
|
|
modulated = self.pitch_shift(delayed, sample_rate, 0.5 * intensity)
|
|
|
|
# Stereo spread
|
|
left = 0.7 * audio + 0.3 * modulated
|
|
right = 0.3 * audio + 0.7 * modulated
|
|
|
|
return np.stack([left, right]) if audio.ndim > 1 else left
|
|
|
|
def vinyl_emulation(self, audio, sample_rate, intensity):
|
|
"""Add vinyl record characteristics"""
|
|
# Apply low-pass filter
|
|
sos = butter(2, 10000, 'lowpass', fs=sample_rate, output='sos')
|
|
audio = sosfiltfilt(sos, audio)
|
|
|
|
# Add crackle
|
|
if intensity > 0.3:
|
|
crackle = np.random.uniform(-0.02, 0.02, len(audio))
|
|
audio += intensity * crackle * 0.5
|
|
|
|
# Add wow & flutter
|
|
phase = np.cumsum(1 + 0.005 * np.sin(2 * np.pi * 0.5 * np.arange(len(audio)) / sample_rate))
|
|
audio = np.interp(np.arange(len(audio)), phase, audio)
|
|
|
|
return audio
|
|
|
|
def pitch_shift(self, audio, sample_rate, semitones):
|
|
"""Simple pitch shifting"""
|
|
# Using Fourier method for simplicity (real implementations use phase vocoding)
|
|
n = len(audio)
|
|
freqs = np.fft.rfftfreq(n, d=1/sample_rate)
|
|
fft = np.fft.rfft(audio)
|
|
shifted_freqs = freqs * (2 ** (semitones / 12))
|
|
|
|
# Interpolate complex spectrum
|
|
new_fft = np.interp(freqs, shifted_freqs, fft, left=0, right=0)
|
|
return np.fft.irfft(new_fft, n=n).real
|
|
|
|
def detect_fundamental(self, audio, sample_rate):
|
|
"""Detect fundamental frequency using autocorrelation"""
|
|
# Normalize audio
|
|
audio = audio - np.mean(audio)
|
|
audio = audio / np.max(np.abs(audio))
|
|
|
|
# Autocorrelation
|
|
corr = np.correlate(audio, audio, mode='full')
|
|
corr = corr[len(corr)//2:]
|
|
|
|
# Find first peak after zero
|
|
peaks = np.where((corr[1:-1] > corr[:-2]) & (corr[1:-1] > corr[2:]))[0] + 1
|
|
if len(peaks) > 0:
|
|
return sample_rate / peaks[0]
|
|
return 100 # Default to 100Hz
|
|
|
|
# For ComfyUI registration
|
|
NODE_CLASS_MAPPINGS = {
|
|
"AudioPostProcessor": AudioPostProcessor
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"AudioPostProcessor": "🎚️ Audio Post-Processor"
|
|
} |