feat: add righ deck alternating functionality, DRY'd
This commit is contained in:
parent
5b48ee7f36
commit
ff10903a05
2 changed files with 296 additions and 8 deletions
292
animations.py
292
animations.py
|
|
@ -21,6 +21,7 @@ class Animation:
|
||||||
self.name = name
|
self.name = name
|
||||||
self.running = False
|
self.running = False
|
||||||
self.thread = None
|
self.thread = None
|
||||||
|
self.on_complete_callback = None
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
"""Start the animation in a separate thread."""
|
"""Start the animation in a separate thread."""
|
||||||
|
|
@ -42,6 +43,22 @@ class Animation:
|
||||||
"""Main animation loop. To be implemented by subclasses."""
|
"""Main animation loop. To be implemented by subclasses."""
|
||||||
raise NotImplementedError("Subclasses must implement _run method")
|
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):
|
class MidiLedAnimation(Animation):
|
||||||
"""Animation for MIDI controller LEDs."""
|
"""Animation for MIDI controller LEDs."""
|
||||||
|
|
@ -79,11 +96,16 @@ class MidiLedAnimation(Animation):
|
||||||
# Pattern functions mapped to names
|
# Pattern functions mapped to names
|
||||||
self.pattern_functions = {
|
self.pattern_functions = {
|
||||||
"chase": self._chase_pattern,
|
"chase": self._chase_pattern,
|
||||||
|
"reverse_chase": self._reverse_chase_pattern,
|
||||||
"pulse": self._pulse_pattern,
|
"pulse": self._pulse_pattern,
|
||||||
"alternate": self._alternate_pattern,
|
"alternate": self._alternate_pattern,
|
||||||
"random": self._random_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:
|
def _run(self) -> None:
|
||||||
"""Run the animation loop based on the selected pattern."""
|
"""Run the animation loop based on the selected pattern."""
|
||||||
if self.pattern in self.pattern_functions:
|
if self.pattern in self.pattern_functions:
|
||||||
|
|
@ -96,9 +118,10 @@ class MidiLedAnimation(Animation):
|
||||||
def _chase_pattern(self) -> None:
|
def _chase_pattern(self) -> None:
|
||||||
"""
|
"""
|
||||||
Chase pattern: turn on each LED in sequence, turning off the previous one.
|
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
|
current_idx = 0
|
||||||
|
completed_repeats = 0
|
||||||
|
|
||||||
while self.running:
|
while self.running:
|
||||||
# Turn off all LEDs
|
# Turn off all LEDs
|
||||||
|
|
@ -116,6 +139,50 @@ class MidiLedAnimation(Animation):
|
||||||
# Move to next position
|
# Move to next position
|
||||||
current_idx = (current_idx + 1) % len(self.led_sequence)
|
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:
|
def _pulse_pattern(self) -> None:
|
||||||
"""
|
"""
|
||||||
Pulse pattern: all LEDs turn on and off together.
|
Pulse pattern: all LEDs turn on and off together.
|
||||||
|
|
@ -184,6 +251,146 @@ class MidiLedAnimation(Animation):
|
||||||
print(f"Error sending MIDI message: {e}")
|
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:
|
class AnimationManager:
|
||||||
"""Manages multiple animations."""
|
"""Manages multiple animations."""
|
||||||
|
|
||||||
|
|
@ -243,7 +450,8 @@ class AnimationManager:
|
||||||
pattern: str = "chase",
|
pattern: str = "chase",
|
||||||
interval: float = 0.1,
|
interval: float = 0.1,
|
||||||
on_velocity: int = 127,
|
on_velocity: int = 127,
|
||||||
off_velocity: int = 0
|
off_velocity: int = 0,
|
||||||
|
repeat_count: int = None
|
||||||
) -> MidiLedAnimation:
|
) -> MidiLedAnimation:
|
||||||
"""
|
"""
|
||||||
Create a deck animation with the appropriate LED sequence.
|
Create a deck animation with the appropriate LED sequence.
|
||||||
|
|
@ -285,6 +493,86 @@ class AnimationManager:
|
||||||
off_velocity=off_velocity
|
off_velocity=off_velocity
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Set repeat count if specified
|
||||||
|
animation.repeat_count = repeat_count
|
||||||
|
|
||||||
# Auto-add to manager
|
# Auto-add to manager
|
||||||
self.add_animation(animation)
|
self.add_animation(animation)
|
||||||
return 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
|
||||||
|
|
|
||||||
12
main.py
12
main.py
|
|
@ -71,14 +71,14 @@ def main():
|
||||||
config_manager.get_mappings()
|
config_manager.get_mappings()
|
||||||
)
|
)
|
||||||
|
|
||||||
# Setup right deck animation
|
# Setup right deck animation with alternating chase directions
|
||||||
print("Setting up right deck animation...")
|
print("Setting up right deck animation sequence...")
|
||||||
right_deck_animation = animation_manager.create_deck_animation(
|
right_deck_animation = animation_manager.create_alternating_chase_animation(
|
||||||
name="right_deck_chase",
|
name="right_deck_alternating",
|
||||||
midi_controller=midi_controller,
|
midi_controller=midi_controller,
|
||||||
deck_side="right",
|
deck_side="right",
|
||||||
pattern="chase",
|
interval=0.12, # Animation speed in seconds between steps
|
||||||
interval=0.15 # Animation speed in seconds between steps
|
cycles_per_direction=1 # Number of full cycles in each direction before changing
|
||||||
)
|
)
|
||||||
|
|
||||||
# Open MIDI port
|
# Open MIDI port
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue