feat: add animations utils and right deck chaser as first animation
This commit is contained in:
parent
4cfd750afc
commit
5b48ee7f36
2 changed files with 332 additions and 2 deletions
290
animations.py
Normal file
290
animations.py
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
"""
|
||||
Animation module for MIDI-to-Hue application.
|
||||
Handles threaded animations for MIDI controller LEDs.
|
||||
"""
|
||||
import threading
|
||||
import time
|
||||
import mido
|
||||
from typing import List, Dict, Any, Callable, Optional, Tuple, Union
|
||||
|
||||
|
||||
class Animation:
|
||||
"""Base class for animations that can be applied to different LED sets."""
|
||||
|
||||
def __init__(self, name: str):
|
||||
"""
|
||||
Initialize animation base class.
|
||||
|
||||
Args:
|
||||
name: Name of the animation for identification
|
||||
"""
|
||||
self.name = name
|
||||
self.running = False
|
||||
self.thread = None
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start the animation in a separate thread."""
|
||||
if self.thread and self.thread.is_alive():
|
||||
return
|
||||
|
||||
self.running = True
|
||||
self.thread = threading.Thread(target=self._run)
|
||||
self.thread.daemon = True
|
||||
self.thread.start()
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop the animation thread."""
|
||||
self.running = False
|
||||
if self.thread:
|
||||
self.thread.join(1.0) # Wait up to 1 second for thread to finish
|
||||
|
||||
def _run(self) -> None:
|
||||
"""Main animation loop. To be implemented by subclasses."""
|
||||
raise NotImplementedError("Subclasses must implement _run method")
|
||||
|
||||
|
||||
class MidiLedAnimation(Animation):
|
||||
"""Animation for MIDI controller LEDs."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
midi_controller,
|
||||
led_sequence: List[Tuple[int, int]], # List of (channel, note) tuples
|
||||
interval: float = 0.1,
|
||||
pattern: str = "chase",
|
||||
on_velocity: int = 127,
|
||||
off_velocity: int = 0
|
||||
):
|
||||
"""
|
||||
Initialize MIDI LED animation.
|
||||
|
||||
Args:
|
||||
name: Animation name
|
||||
midi_controller: MidiController instance
|
||||
led_sequence: List of (channel, note) tuples defining the LEDs to animate
|
||||
interval: Time between steps in seconds
|
||||
pattern: Animation pattern name ("chase", "pulse", "alternate", etc.)
|
||||
on_velocity: MIDI velocity for "on" state (0-127)
|
||||
off_velocity: MIDI velocity for "off" state (0-127)
|
||||
"""
|
||||
super().__init__(name)
|
||||
self.midi_controller = midi_controller
|
||||
self.led_sequence = led_sequence
|
||||
self.interval = interval
|
||||
self.pattern = pattern
|
||||
self.on_velocity = on_velocity
|
||||
self.off_velocity = off_velocity
|
||||
|
||||
# Pattern functions mapped to names
|
||||
self.pattern_functions = {
|
||||
"chase": self._chase_pattern,
|
||||
"pulse": self._pulse_pattern,
|
||||
"alternate": self._alternate_pattern,
|
||||
"random": self._random_pattern
|
||||
}
|
||||
|
||||
def _run(self) -> None:
|
||||
"""Run the animation loop based on the selected pattern."""
|
||||
if self.pattern in self.pattern_functions:
|
||||
# Call the appropriate pattern function
|
||||
self.pattern_functions[self.pattern]()
|
||||
else:
|
||||
# Default to chase pattern if unknown pattern specified
|
||||
self._chase_pattern()
|
||||
|
||||
def _chase_pattern(self) -> None:
|
||||
"""
|
||||
Chase pattern: turn on each LED in sequence, turning off the previous one.
|
||||
Like a "running light" effect.
|
||||
"""
|
||||
current_idx = 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 next position
|
||||
current_idx = (current_idx + 1) % len(self.led_sequence)
|
||||
|
||||
def _pulse_pattern(self) -> None:
|
||||
"""
|
||||
Pulse pattern: all LEDs turn on and off together.
|
||||
"""
|
||||
state = True # Start with all on
|
||||
|
||||
while self.running:
|
||||
velocity = self.on_velocity if state else self.off_velocity
|
||||
|
||||
# Set all LEDs to current state
|
||||
for channel, note in self.led_sequence:
|
||||
self._send_midi_note(channel, note, velocity)
|
||||
|
||||
# Wait interval
|
||||
time.sleep(self.interval)
|
||||
|
||||
# Toggle state
|
||||
state = not state
|
||||
|
||||
def _alternate_pattern(self) -> None:
|
||||
"""
|
||||
Alternate pattern: alternate between odd and even LEDs.
|
||||
"""
|
||||
state = True # True = evens on, odds off; False = odds on, evens off
|
||||
|
||||
while self.running:
|
||||
for i, (channel, note) in enumerate(self.led_sequence):
|
||||
is_even = i % 2 == 0
|
||||
velocity = self.on_velocity if is_even == state else self.off_velocity
|
||||
self._send_midi_note(channel, note, velocity)
|
||||
|
||||
# Wait interval
|
||||
time.sleep(self.interval)
|
||||
|
||||
# Toggle state
|
||||
state = not state
|
||||
|
||||
def _random_pattern(self) -> None:
|
||||
"""
|
||||
Random pattern: randomized blinking of LEDs.
|
||||
"""
|
||||
import random
|
||||
|
||||
while self.running:
|
||||
# Randomly select LEDs to turn on
|
||||
for channel, note in self.led_sequence:
|
||||
velocity = self.on_velocity if random.random() > 0.5 else self.off_velocity
|
||||
self._send_midi_note(channel, note, velocity)
|
||||
|
||||
# Wait interval
|
||||
time.sleep(self.interval)
|
||||
|
||||
def _send_midi_note(self, channel: int, note: int, velocity: int) -> None:
|
||||
"""
|
||||
Send MIDI note message to control an LED.
|
||||
|
||||
Args:
|
||||
channel: MIDI channel
|
||||
note: Note number
|
||||
velocity: Note velocity (0-127)
|
||||
"""
|
||||
try:
|
||||
msg = mido.Message('note_on', note=note, velocity=velocity, channel=channel)
|
||||
self.midi_controller.send_message(msg)
|
||||
except Exception as e:
|
||||
print(f"Error sending MIDI message: {e}")
|
||||
|
||||
|
||||
class AnimationManager:
|
||||
"""Manages multiple animations."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the animation manager."""
|
||||
self.animations = {}
|
||||
|
||||
def add_animation(self, animation: Animation) -> None:
|
||||
"""
|
||||
Add an animation to the manager.
|
||||
|
||||
Args:
|
||||
animation: Animation instance
|
||||
"""
|
||||
self.animations[animation.name] = animation
|
||||
|
||||
def start_animation(self, name: str) -> bool:
|
||||
"""
|
||||
Start a specific animation.
|
||||
|
||||
Args:
|
||||
name: Name of the animation to start
|
||||
|
||||
Returns:
|
||||
True if animation was found and started, False otherwise
|
||||
"""
|
||||
if name in self.animations:
|
||||
self.animations[name].start()
|
||||
return True
|
||||
return False
|
||||
|
||||
def stop_animation(self, name: str) -> bool:
|
||||
"""
|
||||
Stop a specific animation.
|
||||
|
||||
Args:
|
||||
name: Name of the animation to stop
|
||||
|
||||
Returns:
|
||||
True if animation was found and stopped, False otherwise
|
||||
"""
|
||||
if name in self.animations:
|
||||
self.animations[name].stop()
|
||||
return True
|
||||
return False
|
||||
|
||||
def stop_all(self) -> None:
|
||||
"""Stop all running animations."""
|
||||
for animation in self.animations.values():
|
||||
animation.stop()
|
||||
|
||||
def create_deck_animation(
|
||||
self,
|
||||
name: str,
|
||||
midi_controller,
|
||||
deck_side: str,
|
||||
pattern: str = "chase",
|
||||
interval: float = 0.1,
|
||||
on_velocity: int = 127,
|
||||
off_velocity: int = 0
|
||||
) -> MidiLedAnimation:
|
||||
"""
|
||||
Create a deck animation with the appropriate LED sequence.
|
||||
|
||||
Args:
|
||||
name: Animation name
|
||||
midi_controller: MidiController instance
|
||||
deck_side: Either "left" or "right"
|
||||
pattern: Animation pattern
|
||||
interval: Time between steps
|
||||
on_velocity: Velocity for "on" state
|
||||
off_velocity: Velocity for "off" state
|
||||
|
||||
Returns:
|
||||
Created MidiLedAnimation 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'.")
|
||||
|
||||
animation = MidiLedAnimation(
|
||||
name=name,
|
||||
midi_controller=midi_controller,
|
||||
led_sequence=deck_sequences[deck_side],
|
||||
pattern=pattern,
|
||||
interval=interval,
|
||||
on_velocity=on_velocity,
|
||||
off_velocity=off_velocity
|
||||
)
|
||||
|
||||
# Auto-add to manager
|
||||
self.add_animation(animation)
|
||||
return animation
|
||||
44
main.py
44
main.py
|
|
@ -1,19 +1,25 @@
|
|||
"""
|
||||
Main module for MIDI-to-Hue application.
|
||||
Ties together the config, MIDI controller, Hue controller, and mapper.
|
||||
Ties together the config, MIDI controller, Hue controller, mapper, and animations.
|
||||
"""
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
import signal
|
||||
from config import ConfigManager
|
||||
from hue_controller import HueController
|
||||
from midi_controller import MidiController, DeviceMappingManager
|
||||
from mapper import MidiToHueMapper
|
||||
from animations import AnimationManager, MidiLedAnimation
|
||||
|
||||
def main():
|
||||
"""Main application entry point."""
|
||||
# Load configuration
|
||||
config_manager = ConfigManager()
|
||||
|
||||
# Create animation manager
|
||||
animation_manager = AnimationManager()
|
||||
|
||||
# Initialize Hue controller
|
||||
try:
|
||||
hue_controller = HueController(
|
||||
|
|
@ -40,6 +46,14 @@ def main():
|
|||
"note_on/1/8": 'left_deck_6',
|
||||
"note_on/1/9": 'left_deck_7',
|
||||
"note_on/1/10": 'left_deck_8',
|
||||
"note_on/3/3": 'right_deck_1',
|
||||
"note_on/3/4": 'right_deck_2',
|
||||
"note_on/3/5": 'right_deck_3',
|
||||
"note_on/3/6": 'right_deck_4',
|
||||
"note_on/3/7": 'right_deck_5',
|
||||
"note_on/3/8": 'right_deck_6',
|
||||
"note_on/3/9": 'right_deck_7',
|
||||
"note_on/3/10": 'right_deck_8',
|
||||
"control_change/5/1": 'left_volume_slider',
|
||||
"control_change/6/1": 'right_volume_slider',
|
||||
}
|
||||
|
|
@ -57,16 +71,42 @@ def main():
|
|||
config_manager.get_mappings()
|
||||
)
|
||||
|
||||
# Open MIDI port and process messages
|
||||
# Setup right deck animation
|
||||
print("Setting up right deck animation...")
|
||||
right_deck_animation = animation_manager.create_deck_animation(
|
||||
name="right_deck_chase",
|
||||
midi_controller=midi_controller,
|
||||
deck_side="right",
|
||||
pattern="chase",
|
||||
interval=0.15 # Animation speed in seconds between steps
|
||||
)
|
||||
|
||||
# Open MIDI port
|
||||
if not midi_controller.open():
|
||||
print("Failed to open MIDI port.")
|
||||
return 1
|
||||
|
||||
# Start the deck animation
|
||||
print("Starting right deck animation...")
|
||||
animation_manager.start_animation("right_deck_chase")
|
||||
|
||||
# Handle graceful shutdown
|
||||
def signal_handler(sig, frame):
|
||||
print("\nStopping animations and exiting...")
|
||||
animation_manager.stop_all()
|
||||
midi_controller.close()
|
||||
sys.exit(0)
|
||||
|
||||
# Register signal handler for Ctrl+C
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
try:
|
||||
# Process MIDI messages in main thread
|
||||
midi_controller.process_messages()
|
||||
except KeyboardInterrupt:
|
||||
print("\nExiting program...")
|
||||
finally:
|
||||
animation_manager.stop_all()
|
||||
midi_controller.close()
|
||||
|
||||
return 0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue