From ff10903a05cf89512061652f998295078aa20bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=A4usler?= Date: Mon, 30 Jun 2025 02:26:59 +0200 Subject: [PATCH] feat: add righ deck alternating functionality, DRY'd --- animations.py | 292 +++++++++++++++++++++++++++++++++++++++++++++++++- main.py | 12 +-- 2 files changed, 296 insertions(+), 8 deletions(-) diff --git a/animations.py b/animations.py index 5ea8f98..cd66139 100644 --- a/animations.py +++ b/animations.py @@ -21,6 +21,7 @@ class Animation: self.name = name self.running = False self.thread = None + self.on_complete_callback = None def start(self) -> None: """Start the animation in a separate thread.""" @@ -41,6 +42,22 @@ class Animation: def _run(self) -> None: """Main animation loop. To be implemented by subclasses.""" raise NotImplementedError("Subclasses must implement _run method") + + def set_on_complete_callback(self, callback: Callable) -> None: + """ + Set a callback to be called when this animation completes. + + Args: + callback: Function to call when animation completes + """ + self.on_complete_callback = callback + + def notify_complete(self) -> None: + """ + Notify that this animation has completed by calling the callback. + """ + if self.on_complete_callback: + self.on_complete_callback() class MidiLedAnimation(Animation): @@ -79,10 +96,15 @@ class MidiLedAnimation(Animation): # Pattern functions mapped to names self.pattern_functions = { "chase": self._chase_pattern, + "reverse_chase": self._reverse_chase_pattern, "pulse": self._pulse_pattern, "alternate": self._alternate_pattern, "random": self._random_pattern } + + # Animation parameters + self.repeat_count = None # None means infinite, otherwise number of repeats + self.current_repeat = 0 def _run(self) -> None: """Run the animation loop based on the selected pattern.""" @@ -96,9 +118,10 @@ class MidiLedAnimation(Animation): def _chase_pattern(self) -> None: """ Chase pattern: turn on each LED in sequence, turning off the previous one. - Like a "running light" effect. + Like a "running light" effect going forward through the sequence. """ current_idx = 0 + completed_repeats = 0 while self.running: # Turn off all LEDs @@ -115,6 +138,50 @@ class MidiLedAnimation(Animation): # Move to next position current_idx = (current_idx + 1) % len(self.led_sequence) + + # Check for completion of a full cycle + if current_idx == 0: + completed_repeats += 1 + if self.repeat_count and completed_repeats >= self.repeat_count: + self.running = False + break + + # Notify completion if callback is set + self.notify_complete() + + def _reverse_chase_pattern(self) -> None: + """ + Reverse chase pattern: turn on each LED in reverse sequence. + Like a "running light" effect going backward through the sequence. + """ + current_idx = 0 + completed_repeats = 0 + + while self.running: + # Turn off all LEDs + for i, (channel, note) in enumerate(self.led_sequence): + if i != current_idx: # Skip the current position + self._send_midi_note(channel, note, self.off_velocity) + + # Turn on current LED + channel, note = self.led_sequence[current_idx] + self._send_midi_note(channel, note, self.on_velocity) + + # Wait interval + time.sleep(self.interval) + + # Move to previous position (reverse direction) + current_idx = (current_idx - 1) % len(self.led_sequence) + + # Check for completion of a full cycle + if current_idx == len(self.led_sequence) - 1: + completed_repeats += 1 + if self.repeat_count and completed_repeats >= self.repeat_count: + self.running = False + break + + # Notify completion if callback is set + self.notify_complete() def _pulse_pattern(self) -> None: """ @@ -184,6 +251,146 @@ class MidiLedAnimation(Animation): print(f"Error sending MIDI message: {e}") +class AnimationSequence(Animation): + """ + Animation that consists of a sequence of other animations. + Runs animations one after another in sequence. + """ + + def __init__(self, name: str, animations: List[Animation]): + """ + Initialize animation sequence. + + Args: + name: Name of the sequence + animations: List of animation instances to run in sequence + """ + super().__init__(name) + self.animations = animations + self.current_index = 0 + + # Set up callbacks for chaining animations + for i in range(len(animations) - 1): + animations[i].set_on_complete_callback(self._next_animation) + + def _run(self) -> None: + """ + Run animations in sequence. + """ + if not self.animations: + return + + # Start first animation + self.current_index = 0 + self.animations[0].start() + + # Wait for all animations to complete + while self.running and self.current_index < len(self.animations): + # Just wait - callbacks handle progression + time.sleep(0.1) + + # Check if current animation is still running + if self.animations[self.current_index].thread and not self.animations[self.current_index].thread.is_alive(): + # If for some reason the callback wasn't triggered, move to next + self._next_animation() + + # Notify completion + self.notify_complete() + + def _next_animation(self, *args) -> None: + """ + Start the next animation in sequence. + Called when current animation completes. + """ + if not self.running: + return + + # Move to next animation + self.current_index += 1 + + # If we have more animations, start the next one + if self.current_index < len(self.animations): + self.animations[self.current_index].start() + + def stop(self) -> None: + """ + Stop the animation sequence and all contained animations. + """ + super().stop() + for animation in self.animations: + animation.stop() + + +class AnimationCycle(Animation): + """ + Repeatedly cycles through a sequence of animations. + """ + + def __init__(self, name: str, animations: List[Animation], repeat_count: int = None): + """ + Initialize animation cycle. + + Args: + name: Name of the cycle + animations: List of animation instances to cycle through + repeat_count: Number of times to repeat the entire cycle (None = infinite) + """ + super().__init__(name) + self.animations = animations + self.repeat_count = repeat_count + self.current_animation_index = 0 + self.cycle_count = 0 + + # Set up callbacks for chaining animations + for animation in animations: + animation.set_on_complete_callback(self._next_animation) + + def _run(self) -> None: + """ + Run the animation cycle. + """ + if not self.animations: + return + + # Start first animation + self.current_animation_index = 0 + self.cycle_count = 0 + self.animations[0].start() + + # Wait for cycle to complete + while self.running and (self.repeat_count is None or self.cycle_count < self.repeat_count): + time.sleep(0.1) + + def _next_animation(self, *args) -> None: + """ + Start the next animation in the cycle. + """ + if not self.running: + return + + # Move to next animation + self.current_animation_index = (self.current_animation_index + 1) % len(self.animations) + + # Check if we've completed a full cycle + if self.current_animation_index == 0: + self.cycle_count += 1 + if self.repeat_count and self.cycle_count >= self.repeat_count: + self.running = False + self.notify_complete() + return + + # Start next animation + self.animations[self.current_animation_index].start() + + def stop(self) -> None: + """ + Stop the animation cycle and all contained animations. + """ + super().stop() + for animation in self.animations: + animation.stop() + + class AnimationManager: """Manages multiple animations.""" @@ -243,7 +450,8 @@ class AnimationManager: pattern: str = "chase", interval: float = 0.1, on_velocity: int = 127, - off_velocity: int = 0 + off_velocity: int = 0, + repeat_count: int = None ) -> MidiLedAnimation: """ Create a deck animation with the appropriate LED sequence. @@ -285,6 +493,86 @@ class AnimationManager: off_velocity=off_velocity ) + # Set repeat count if specified + animation.repeat_count = repeat_count + # Auto-add to manager self.add_animation(animation) return animation + + def create_alternating_chase_animation( + self, + name: str, + midi_controller, + deck_side: str, + interval: float = 0.1, + cycles_per_direction: int = 1, + on_velocity: int = 127, + off_velocity: int = 0 + ) -> AnimationCycle: + """ + Create an animation that alternates between chase and reverse chase patterns. + + Args: + name: Animation name + midi_controller: MidiController instance + deck_side: Either "left" or "right" + interval: Time between steps in seconds + cycles_per_direction: Number of complete cycles in each direction + on_velocity: Velocity for "on" state + off_velocity: Velocity for "off" state + + Returns: + AnimationCycle instance + """ + # Define deck LED sequences - channel/note pairs for each deck + deck_sequences = { + "left": [ + (1, 3), (1, 4), (1, 5), (1, 6), + (1, 10), (1, 9), (1, 8), (1, 7) + ], + "right": [ + (3, 3), (3, 4), (3, 5), (3, 6), + (3, 10), (3, 9), (3, 8), (3, 7) + ] + } + + if deck_side not in deck_sequences: + raise ValueError(f"Unknown deck side: {deck_side}. Use 'left' or 'right'.") + + # Create forward chaser animation + forward_animation = MidiLedAnimation( + name=f"{name}_forward", + midi_controller=midi_controller, + led_sequence=deck_sequences[deck_side], + pattern="chase", + interval=interval, + on_velocity=on_velocity, + off_velocity=off_velocity + ) + forward_animation.repeat_count = cycles_per_direction + + # Create reverse chaser animation + reverse_animation = MidiLedAnimation( + name=f"{name}_reverse", + midi_controller=midi_controller, + led_sequence=deck_sequences[deck_side], + pattern="reverse_chase", + interval=interval, + on_velocity=on_velocity, + off_velocity=off_velocity + ) + reverse_animation.repeat_count = cycles_per_direction + + # Create cycle that alternates between forward and reverse + cycle = AnimationCycle( + name=name, + animations=[forward_animation, reverse_animation] + ) + + # Add all animations to manager + self.add_animation(forward_animation) + self.add_animation(reverse_animation) + self.add_animation(cycle) + + return cycle diff --git a/main.py b/main.py index 7401c06..6eae141 100644 --- a/main.py +++ b/main.py @@ -71,14 +71,14 @@ def main(): config_manager.get_mappings() ) - # Setup right deck animation - print("Setting up right deck animation...") - right_deck_animation = animation_manager.create_deck_animation( - name="right_deck_chase", + # Setup right deck animation with alternating chase directions + print("Setting up right deck animation sequence...") + right_deck_animation = animation_manager.create_alternating_chase_animation( + name="right_deck_alternating", midi_controller=midi_controller, deck_side="right", - pattern="chase", - interval=0.15 # Animation speed in seconds between steps + interval=0.12, # Animation speed in seconds between steps + cycles_per_direction=1 # Number of full cycles in each direction before changing ) # Open MIDI port