diff --git a/__init__.py b/__init__.py index 7126e84..e615e38 100644 --- a/__init__.py +++ b/__init__.py @@ -8,6 +8,9 @@ from .nodes.masks.temporal_masks import ( from .nodes.audio.audio_nodes import ( AudioSeparator, AudioFeatureVisualizer, + FrequencyFilterCustom, + FrequencyFilterPreset, + AudioFilter, ) from .nodes.masks.audio_masks import ( AudioMaskMorph, @@ -66,7 +69,10 @@ NODE_CLASS_MAPPINGS = { "AudioMaskTransform":AudioMaskTransform, "AudioMaskMath": AudioMaskMath, "AudioSeparator": AudioSeparator, - "AudioFeatureVisualizer":AudioFeatureVisualizer, + #"AudioFeatureVisualizer":AudioFeatureVisualizer, + "FrequencyFilterCustom": FrequencyFilterCustom, + "FrequencyFilterPreset": FrequencyFilterPreset, + "AudioFilter":AudioFilter, "MovingShape": MovingShape, @@ -79,7 +85,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "MaskTransform":"Temporal Mask Transform | RyanOnTheInside", "MaskMath":"Temporal Mask Math | RyanOnTheInside", "MaskRings":"Temporal Mask Rings | RyanOnTheInside", - "MaskWarp":"Mask Warp | RyanOnTheInside", + "MaskWarp":"Temporal Mask Warp | RyanOnTheInside", "OpticalFlowMaskModulation": "Optical Flow Mask Modulation | RyanOnTheInside", @@ -102,7 +108,10 @@ NODE_DISPLAY_NAME_MAPPINGS = { "AudioMaskTransform":"Audio Mask Transform | RyanOnTheInside", "AudioMaskMath": "Audio Mask Math | RyanOnTheInside", "AudioSeparator": "Audio Separator | RyanOnTheInside", - "AudioFeatureVisualizer": "Audio Feature Visualizer | RyanOnTheInside" , + #"AudioFeatureVisualizer": "Audio Feature Visualizer | RyanOnTheInside" , + "Frequency Filter Custom": "Frequency Filter Custom | RyanOnTheInside", + "Frequency Filter Preset": "Frequency Filter Preset | RyanOnTheInside", + "AudioFilter": "Audio Filter | RyanOnTheInside", "MovingShape": "Moving Shape | RyanOnTheInside", "TextMaskNode":"Text Mask Node | RyanOnTheInside" diff --git a/nodes/audio/audio_nodes.py b/nodes/audio/audio_nodes.py index ee24475..cbea72a 100644 --- a/nodes/audio/audio_nodes.py +++ b/nodes/audio/audio_nodes.py @@ -6,6 +6,9 @@ import matplotlib.pyplot as plt from scipy.ndimage import zoom import cv2 from .audio_utils import AudioVisualizer +from scipy import signal +import logging + class AudioSeparator: @classmethod @@ -22,6 +25,7 @@ class AudioSeparator: RETURN_NAMES = ("audio", "video_frames","drums_audio", "vocals_audio", "bass_audio", "other_audio") FUNCTION = "process_audio" + def __init__(self): self.separator = openunmix.umxl(targets=['drums', 'vocals', 'bass', 'other'], device='cpu') @@ -29,32 +33,24 @@ class AudioSeparator: waveform = audio['waveform'] sample_rate = audio['sample_rate'] - # Extract dimensions from video_frames num_frames, height, width, _ = video_frames.shape - print(f"Video frames shape: {video_frames.shape}") - # Ensure the waveform is in the correct shape (nb_channels, nb_samples) if waveform.dim() == 3: waveform = waveform.squeeze(0) # Remove batch dimension if present if waveform.dim() == 1: waveform = waveform.unsqueeze(0) # Add channel dimension if mono if waveform.shape[0] != 2: waveform = waveform.repeat(2, 1) # Duplicate mono to stereo if necessary - print(f"Waveform shape after preprocessing: {waveform.shape}") - - # Add a batch dimension + waveform = waveform.unsqueeze(0) - print(f"Waveform shape before separation: {waveform.shape}") # Separate the audio using Open-Unmix estimates = self.separator(waveform) - print(f"Estimates shape: {estimates.shape}") # Create isolated audio objects for each target isolated_audio = {} for i, target in enumerate(['drums', 'vocals', 'bass', 'other']): target_waveform = estimates[:, i, :, :] # Shape: (1, 2, num_samples) - print(f"{target} audio shape: {target_waveform.shape}") isolated_audio[target] = { 'waveform': target_waveform, @@ -70,7 +66,167 @@ class AudioSeparator: isolated_audio['bass'], isolated_audio['other'], ) - + +class AudioFilter: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "audio": ("AUDIO",), + "filters": ("FREQUENCY_FILTER",), + }, + } + + RETURN_TYPES = ("AUDIO",) + FUNCTION = "apply_filters" + + def apply_filters(self, audio, filters): + audio_np = audio['waveform'].cpu().numpy().squeeze(0) + sample_rate = audio['sample_rate'] + + filtered_channels = [] + for channel in audio_np: + filtered_channel = channel + for filter_params in filters: + filtered_channel = self.apply_single_filter(filtered_channel, filter_params, sample_rate) + filtered_channels.append(filtered_channel) + + filtered_audio = np.stack(filtered_channels) + + max_val = np.max(np.abs(filtered_audio)) + if max_val > 1.0: + filtered_audio = filtered_audio / max_val + + filtered_tensor = torch.from_numpy(filtered_audio).unsqueeze(0).float() + + return ({"waveform": filtered_tensor, "sample_rate": sample_rate},) + + def apply_single_filter(self, audio, filter_params, sample_rate): + filter_type = filter_params['type'] + order = filter_params['order'] + cutoff = filter_params['cutoff'] + + if filter_type == 'lowpass': + b, a = signal.butter(order, cutoff, btype='low', fs=sample_rate) + elif filter_type == 'highpass': + b, a = signal.butter(order, cutoff, btype='high', fs=sample_rate) + elif filter_type == 'bandpass': + low = cutoff - 50 + high = cutoff + 50 + b, a = signal.butter(order, [low, high], btype='band', fs=sample_rate) + else: + raise ValueError(f"Unsupported filter type: {filter_type}") + + return signal.lfilter(b, a, audio) + +class FrequencyFilterPreset: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "preset": ([ + "isolate_kick_drum", + "isolate_snare_drum", + "isolate_hi_hat", + "isolate_bass", + "isolate_vocals", + "remove_rumble", + "brighten_mix", + "remove_hiss" + ],), + }, + "optional": { + "previous_filter": ("FREQUENCY_FILTER",), + }, + } + + RETURN_TYPES = ("FREQUENCY_FILTER",) + FUNCTION = "create_preset_filter_chain" + + def create_preset_filter_chain(self, preset, previous_filter=None): + new_filters = self.get_preset_filters(preset) + + if previous_filter: + if isinstance(previous_filter, list): + return (previous_filter + new_filters,) + else: + return ([previous_filter] + new_filters,) + else: + return (new_filters,) + + def get_preset_filters(self, preset): + if preset == "isolate_kick_drum": + return [ + {"type": "highpass", "order": 2, "cutoff": 30}, + {"type": "lowpass", "order": 4, "cutoff": 200} + ] + elif preset == "isolate_snare_drum": + return [ + {"type": "bandpass", "order": 4, "cutoff": 200}, + {"type": "bandpass", "order": 4, "cutoff": 5000} + ] + elif preset == "isolate_hi_hat": + return [ + {"type": "highpass", "order": 4, "cutoff": 8000} + ] + elif preset == "isolate_bass": + return [ + {"type": "lowpass", "order": 4, "cutoff": 250} + ] + elif preset == "isolate_vocals": + return [ + {"type": "bandpass", "order": 2, "cutoff": 300}, + {"type": "bandpass", "order": 2, "cutoff": 4000} + ] + elif preset == "remove_rumble": + return [ + {"type": "highpass", "order": 4, "cutoff": 40} + ] + elif preset == "brighten_mix": + return [ + {"type": "highpass", "order": 2, "cutoff": 6000} + ] + elif preset == "remove_hiss": + return [ + {"type": "lowpass", "order": 6, "cutoff": 7500} + ] + else: + raise ValueError(f"Unknown preset: {preset}") + +class FrequencyFilterCustom: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "filter_type": (["lowpass", "highpass", "bandpass"],), + "order": ("INT", {"default": 4, "min": 1, "max": 10, "step": 1}), + "cutoff": ("FLOAT", {"default": 1000, "min": 20, "max": 20000, "step": 1}), + }, + "optional": { + "previous_filter": ("FREQUENCY_FILTER",), + }, + } + + RETURN_TYPES = ("FREQUENCY_FILTER",) + FUNCTION = "create_filter" + + def create_filter(self, filter_type, order, cutoff, previous_filter=None): + filter_params = { + "type": filter_type, + "order": order, + "cutoff": cutoff, + } + + if previous_filter: + if isinstance(previous_filter, list): + previous_filter.append(filter_params) + return (previous_filter,) + else: + return ([previous_filter, filter_params],) + else: + return ([filter_params],) + + class AudioFeatureVisualizer: @classmethod def INPUT_TYPES(cls): @@ -95,9 +251,7 @@ class AudioFeatureVisualizer: CATEGORY = "audio" def visualize_audio_feature(self, audio, video_frames, visualization_type, frame_rate): - # Extract dimensions from video_frames num_frames, height, width, _ = video_frames.shape - print(f"Video frames shape: {video_frames.shape}") visualizer = AudioVisualizer(audio, num_frames, height, width, frame_rate) @@ -116,6 +270,5 @@ class AudioFeatureVisualizer: else: raise ValueError(f"Unsupported visualization type: {visualization_type}") - print(f"Output mask shape: {mask.shape}") return (mask,) \ No newline at end of file diff --git a/nodes/audio/audio_utils.py b/nodes/audio/audio_utils.py index b2e0ea7..ef2bf12 100644 --- a/nodes/audio/audio_utils.py +++ b/nodes/audio/audio_utils.py @@ -32,7 +32,7 @@ class BaseAudioProcessor: end_sample = int(end_time * self.sample_rate) return self.audio[start_sample:end_sample] -class AudioVisualizer(BaseAudioProcessor): +class AudioVisualizer(BaseAudioProcessor): def create_spectrogram(self): print(f"create_spectrogram input shapes: audio={self.audio.shape}, num_frames={self.num_frames}, height={self.height}, width={self.width}") diff --git a/nodes/masks/audio_masks.py b/nodes/masks/audio_masks.py index 8be3392..e43e5d9 100644 --- a/nodes/masks/audio_masks.py +++ b/nodes/masks/audio_masks.py @@ -1,74 +1,8 @@ -from .mask_base import MaskBase -from .mask_utils import morph_mask, normalize_array, warp_mask, transform_mask, combine_masks -from ..audio.audio_utils import AudioFeatureExtractor +from .mask_base import AudioMaskBase +from .mask_utils import morph_mask, warp_mask, transform_mask, combine_masks import numpy as np import torch -class AudioMaskBase(MaskBase): - @classmethod - def INPUT_TYPES(cls): - return { - **super().INPUT_TYPES(), - "required": { - **super().INPUT_TYPES()["required"], - "audio": ("AUDIO",), - "video_frames": ("IMAGE",), - "audio_feature": (["amplitude_envelope", "rms_energy", "spectral_centroid", "onset_detection", "chroma_features"],), - "feature_threshold": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}), - "frame_rate": ("FLOAT", {"default": 30, "min": 0.1, "max": 120, "step": 0.1}), - } - } - - RETURN_TYPES = ("MASK","AUDIO") - FUNCTION = "main_function" - - def __init__(self): - super().__init__() - - def initialize(self, audio, audio_feature, num_frames, frame_rate): - self.audio_feature_extractor = AudioFeatureExtractor(audio, num_frames, frame_rate, feature_type=audio_feature) - - def process_mask(self, mask: np.ndarray, strength: float, audio: dict, video_frames: torch.Tensor, audio_feature: str, feature_threshold: float, frame_rate: float, **kwargs) -> np.ndarray: - # Extract audio feature - feature = self.audio_feature_extractor.extract() - - # Normalize feature to [0, 1] range - normalized_feature = normalize_array(feature) - - # Ensure the feature length matches the number of video frames - num_frames = mask.shape[0] - if len(normalized_feature) != num_frames: - normalized_feature = np.interp(np.linspace(0, 1, num_frames), np.linspace(0, 1, len(normalized_feature)), normalized_feature) - - # Apply the audio-driven effect to each frame - processed_masks = [] - for i in range(num_frames): - if normalized_feature[i] < feature_threshold: - processed_masks.append(mask[i]) - else: - frame_strength = strength * (1 + normalized_feature[i]) - processed_mask = self.process_single_frame(mask[i], frame_strength, frame_index=i, **kwargs) - processed_masks.append(processed_mask) - - return np.stack(processed_masks) - - def process_single_frame(self, mask: np.ndarray, strength: float, frame_index: int = None, **kwargs) -> np.ndarray: - """ - Process a single frame of the mask. - This method should be implemented by subclasses. - """ - raise NotImplementedError("Subclasses must implement process_single_frame method") - - def apply_audio_driven_mask(self, masks, audio, video_frames, strength, audio_feature, feature_threshold, frame_rate, **kwargs): - processed_masks = self.process_mask(masks.numpy(), strength, audio, video_frames, audio_feature, feature_threshold, frame_rate, **kwargs) - return self.apply_mask_operation(torch.from_numpy(processed_masks), masks, strength, **kwargs) - - def main_function(self, masks, audio, video_frames, strength, audio_feature, feature_threshold, frame_rate, **kwargs): - num_frames = masks.shape[0] - self.initialize(audio, audio_feature, num_frames, frame_rate) - - return self.apply_audio_driven_mask(masks, audio, video_frames, strength, audio_feature, feature_threshold, frame_rate, **kwargs), audio - class AudioMaskMorph(AudioMaskBase): @classmethod def INPUT_TYPES(cls): diff --git a/nodes/masks/mask_base.py b/nodes/masks/mask_base.py index 578c1c3..ba728b4 100644 --- a/nodes/masks/mask_base.py +++ b/nodes/masks/mask_base.py @@ -18,7 +18,7 @@ import random from typing import List, Tuple import pymunk import cv2 - +from ..audio.audio_utils import AudioFeatureExtractor class MaskBase(ABC): @classmethod @@ -33,7 +33,7 @@ class MaskBase(ABC): } } - CATEGORY = "/RyanOnTheInside/masks/" + CATEGORY="RyanOnTheInside/Masks" def __init__(self): self.pre_processors = [] @@ -150,7 +150,9 @@ class TemporalMaskBase(MaskBase, ABC): "palindrome": ("BOOLEAN", {"default": False}), } } - + + CATEGORY="RyanOnTheInside/TemporalMasks" + def __init__(self): super().__init__() @@ -235,7 +237,7 @@ class ParticleSystemMaskBase(MaskBase, ABC): RETURN_TYPES = ("MASK", "IMAGE") FUNCTION = "main_function" - CATEGORY = "/RyanOnTheInside/masks/" + CATEGORY="RyanOnTheInside/ParticleSystemMasks" def __init__(self): super().__init__() @@ -812,7 +814,7 @@ class OpticalFlowMaskBase(MaskBase, ABC): } } - CATEGORY = "/RyanOnTheInside/masks/" + CATEGORY="RyanOnTheInside/OpticalFlowMasks" def __init__(self): super().__init__() @@ -860,5 +862,67 @@ class OpticalFlowMaskBase(MaskBase, ABC): processed_masks = np.stack(result) return self.apply_mask_operation(processed_masks, masks, strength, **kwargs) +class AudioMaskBase(MaskBase): + @classmethod + def INPUT_TYPES(cls): + return { + **super().INPUT_TYPES(), + "required": { + "frame_rate": ("FLOAT", {"default": 30, "min": 0.1, "max": 120, "step": 0.1}), + **super().INPUT_TYPES()["required"], + "audio": ("AUDIO",), + "video_frames": ("IMAGE",), + "audio_feature": (["amplitude_envelope", "rms_energy", "spectral_centroid", "onset_detection", "chroma_features"],), + "feature_threshold": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}), + + } + } + + RETURN_TYPES = ("MASK","AUDIO") + FUNCTION = "main_function" + CATEGORY="RyanOnTheInside/AudioMasks" + + def __init__(self): + super().__init__() + + def initialize(self, audio, audio_feature, num_frames, frame_rate): + self.audio_feature_extractor = AudioFeatureExtractor(audio, num_frames, frame_rate, feature_type=audio_feature) + + def process_mask(self, mask: np.ndarray, strength: float, audio: dict, video_frames: torch.Tensor, audio_feature: str, feature_threshold: float, frame_rate: float, **kwargs) -> np.ndarray: + feature = self.audio_feature_extractor.extract() + + normalized_feature = normalize_array(feature) + + num_frames = mask.shape[0] + if len(normalized_feature) != num_frames: + normalized_feature = np.interp(np.linspace(0, 1, num_frames), np.linspace(0, 1, len(normalized_feature)), normalized_feature) + + processed_masks = [] + for i in range(num_frames): + if normalized_feature[i] < feature_threshold: + processed_masks.append(mask[i]) + else: + frame_strength = strength * (1 + normalized_feature[i]) + processed_mask = self.process_single_frame(mask[i], frame_strength, frame_index=i, **kwargs) + processed_masks.append(processed_mask) + + return np.stack(processed_masks) + + def process_single_frame(self, mask: np.ndarray, strength: float, frame_index: int = None, **kwargs) -> np.ndarray: + """ + Process a single frame of the mask. + This method should be implemented by subclasses. + """ + raise NotImplementedError("Subclasses must implement process_single_frame method") + + def apply_audio_driven_mask(self, masks, audio, video_frames, strength, audio_feature, feature_threshold, frame_rate, **kwargs): + processed_masks = self.process_mask(masks.numpy(), strength, audio, video_frames, audio_feature, feature_threshold, frame_rate, **kwargs) + return self.apply_mask_operation(torch.from_numpy(processed_masks), masks, strength, **kwargs) + + def main_function(self, masks, audio, video_frames, strength, audio_feature, feature_threshold, frame_rate, **kwargs): + num_frames = masks.shape[0] + self.initialize(audio, audio_feature, num_frames, frame_rate) + + return self.apply_audio_driven_mask(masks, audio, video_frames, strength, audio_feature, feature_threshold, frame_rate, **kwargs), audio diff --git a/nodes/masks/particle_system_masks.py b/nodes/masks/particle_system_masks.py index 88a3b69..37dee2b 100644 --- a/nodes/masks/particle_system_masks.py +++ b/nodes/masks/particle_system_masks.py @@ -22,7 +22,6 @@ class ParticleEmissionMask(ParticleSystemMaskBase): RETURN_TYPES = ("MASK","IMAGE") FUNCTION = "main_function" - CATEGORY = "Masks/Particle Systems" def process_single_mask(self, mask: np.ndarray, frame_index: int, emission_strength: float, **kwargs) -> Tuple[np.ndarray, np.ndarray]: @@ -78,7 +77,6 @@ class Vortex: RETURN_TYPES = ("VORTEX",) FUNCTION = "create_vortex" - CATEGORY = "RyanOnTheInside/ParticleSystems" def create_vortex(self, x, y, strength, radius, inward_factor, movement_speed, color, draw, previous_vortex=None): vortex = { @@ -119,7 +117,6 @@ class GravityWell: RETURN_TYPES = ("GRAVITY_WELL",) FUNCTION = "create_gravity_well" - CATEGORY = "RyanOnTheInside/ParticleSystems" def create_gravity_well(self, x, y, strength, radius, type, color, draw, previous_well=None): well = { @@ -164,7 +161,6 @@ class ParticleEmitter: RETURN_TYPES = ("PARTICLE_EMITTER",) FUNCTION = "create_emitter" - CATEGORY = "RyanOnTheInside/ParticleSystems" def create_emitter(self, emitter_x, emitter_y, particle_direction, particle_spread, particle_size, particle_speed, emission_rate, color, initial_plume, @@ -211,7 +207,6 @@ class SpringJointSetting: RETURN_TYPES = ("SPRING_JOINT_SETTING",) FUNCTION = "create_setting" - CATEGORY = "RyanOnTheInside/ParticleSystems" def create_setting(self, stiffness, damping, rest_length, max_distance): return ({ @@ -237,7 +232,6 @@ class EmitterMovement: RETURN_TYPES = ("EMITTER_MOVEMENT",) FUNCTION = "create_movement" - CATEGORY = "RyanOnTheInside/ParticleSystems" def create_movement(self, emitter_x_frequency, emitter_y_frequency, direction_frequency, emitter_x_amplitude, emitter_y_amplitude, direction_amplitude): @@ -274,7 +268,6 @@ class StaticBody: RETURN_TYPES = ("STATIC_BODY",) FUNCTION = "create_static_body" - CATEGORY = "RyanOnTheInside/ParticleSystems" def create_static_body(self, shape_type, x1, y1, x2, y2, elasticity, friction, draw, color, previous_body=None): body = { @@ -313,7 +306,6 @@ class ParticleModulationBase: } } - CATEGORY = "RyanOnTheInside/ParticleSystems" def create_modulation(self, start_frame, end_frame, effect_duration, temporal_easing, palindrome, previous_modulation=None): modulation = { diff --git a/pyproject.toml b/pyproject.toml index 7e64a35..eed5c21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,9 @@ [project] name = "comfyui_ryanonyheinside" -description = "Custom nodes introducing particle simulations, optical flow, and temporal masks" -version = "1.1.0" +description = "Custom nodes introducing particle simulations, optical flow, audio manipulation & reactivity, and temporal masks" +version = "1.2.0" license = {file = "LICENSE"} -dependencies = ["opencv-python==4.10.0","scipy","pillow","librosa","pymunk==6.8.1","matplotlib"] +dependencies = ["opencv-python==4.10.0","scipy","pillow","librosa","pymunk==6.8.1","matplotlib","openunmix"] [project.urls] Repository = "https://github.com/ryanontheinside/ComfyUI_RyanOnTheInside" diff --git a/readme.md b/readme.md index 9aab703..d47813d 100644 --- a/readme.md +++ b/readme.md @@ -1,10 +1,11 @@ # ComfyUI RyanOnTheInside Node Pack ## Overview -RyanOnTheInside node pack introduces the following (so far): -- Particle Systems -- Optical Flow -- Temporal Masks +RyanOnTheInside node pack introduces the following: +- Particle Systems - dynamic particle systems in Comfy! +- Audio Masks - Create reactive masks from audio! Even separate vocals, drums, bass, etc! +- Optical Flow - Masks generated by the motion in a video! +- Temporal Masks - Apply effects to masks over tiiiiime!
+
- *A few examples showcasing object-aware particles, initial plume effects, vortex interactions, gravity wells, and directional emitters.*
+*Examples showcasing object-aware particles, initial plume effects, vortex interactions, gravity wells, and directional emitters.*
#### General Parameters for particle system
These settings apply to all particle system mask nodes, and generally affect the simulation space level.
@@ -197,18 +212,110 @@ These are optional inputs for a simulation space. They can be chained together t
- `color`: Color of the vortex visualization (RGB tuple)
- `draw`: Visibility of the vortex (0.0 to 1.0)
-## Optical Flow Masks
+
+
+*Examples of audio-driven mask effects: morphing, warping, transformation, and mathematical operations*
+
+### Common Parameters for all AudioMask nodes
+- `masks`: Input mask sequence to be processed
+- `audio`: Input audio for driving the mask generation
+- `video_frames`: Corresponding video frames
+- `strength`: Overall strength of the effect (0.0 to 1.0)
+- `audio_feature`: Audio feature to use for mask modulation (amplitude_envelope, rms_energy, spectral_centroid, onset_detection, chroma_features)
+- `feature_threshold`: Threshold for audio feature detection (0.0 to 1.0)
+- `frame_rate`: Frame rate of the video (0.1 to 120.0 fps)
+
+### Audio Mask Nodes
+
+#### AudioMaskMorph
+Applies morphological operations to masks based on audio features:
+- `morph_type`: Type of morphological operation ("erode", "dilate", "open", "close")
+- `max_kernel_size`: Maximum size of the morphological kernel (3 to 21, odd numbers only)
+- `max_iterations`: Maximum number of times to apply the operation (1 to 50)
+
+#### AudioMaskWarp
+Applies warping effects to masks driven by audio:
+- `warp_type`: Type of warping effect ("perlin", "radial", "swirl")
+- `frequency`: Frequency of the warping effect (0.01 to 1.0)
+- `max_amplitude`: Maximum amplitude of the warping effect (0.1 to 500.0)
+- `octaves`: Number of octaves for noise-based warps (1 to 8)
+
+#### AudioMaskTransform
+Applies geometric transformations to masks based on audio features:
+- `transform_type`: Type of transformation ("translate", "rotate", "scale")
+- `max_x_value`: Maximum X-axis transformation value (-1000.0 to 1000.0)
+- `max_y_value`: Maximum Y-axis transformation value (-1000.0 to 1000.0)
+
+#### AudioMaskMath
+Performs mathematical operations between two masks, modulated by audio:
+- `mask_b`: Second mask to combine with the input mask
+- `combination_method`: Mathematical operation to apply ("add", "subtract", "multiply", "minimum", "maximum")
+
+### Audio Processing and Visualization Nodes
+
+#### AudioSeparator
+Separates audio into different components:
+- `audio`: Input audio to be processed
+- `video_frames`: Corresponding video frames
+- `frame_rate`: Frame rate of the video
+
+Outputs: original audio, video frames, drums audio, vocals audio, bass audio, other audio
+
+#### AudioFilter
+Applies frequency filters to audio:
+- `audio`: Input audio to be filtered
+- `filters`: Frequency filters to be applied (FREQUENCY_FILTER type)
+
+#### FrequencyFilterPreset
+Creates preset filter chains for common audio processing tasks:
+- `preset`: Preset to use (e.g., "isolate_kick_drum", "isolate_vocals", "remove_rumble")
+- `previous_filter`: (Optional) Previous filter chain to append to
+
+#### FrequencyFilterCustom
+Creates custom frequency filters:
+- `filter_type`: Type of filter ("lowpass", "highpass", "bandpass")
+- `order`: Filter order (1 to 10)
+- `cutoff`: Cutoff frequency (20 to 20000 Hz)
+- `previous_filter`: (Optional) Previous filter chain to append to
+
+#### AudioFeatureVisualizer
+Visualizes various audio features:
+- `audio`: Input audio to visualize
+- `video_frames`: Corresponding video frames
+- `visualization_type`: Type of visualization ("waveform", "spectrogram", "mfcc", "chroma", "tonnetz", "spectral_centroid")
+- `frame_rate`: Frame rate of the video
+
+These audio processing and visualization nodes enable complex audio analysis, filtering, and feature extraction, allowing for creative audio-driven effects and visualizations within the ComfyUI environment.
+
+
+
- *Motion generated trails*
+*Motion generated trails using optical flow analysis*
### Optical Flow Mask Nodes
@@ -258,9 +365,12 @@ This node will generate a particle system animation driven by optical flow, crea
- `Initial Velocity`: Starting speed of newly emitted particles (0.1 to 5.0)
-->
-## Temporal Masks
+
+
- *Examples of various combinations of temporal mask effects including morphing, transformations, math , and warping.*
+*Examples of various combinations of temporal mask effects including morphing, transformations, math, and warping.*
- `Start Frame`: The frame number where the effect begins (0 to 1000).
@@ -321,8 +431,12 @@ Applies various warping effects to the mask.
All these nodes inherit the common parameters from MaskBase and TemporalMaskBase, allowing for precise control over the timing and strength of the effects across multiple frames.
+