""" Maschine Controller Module High-level controller that integrates Maschine hardware with the arpeggiator application. Handles control mappings and state synchronization. """ from PyQt5.QtCore import QObject, pyqtSlot from .maschine_interface import MaschineInterface class MaschineController(QObject): """ High-level controller for Maschine integration. Bridges Maschine hardware controls with application functionality. """ def __init__(self, arpeggiator, channel_manager, volume_engine, synth_router, output_manager): super().__init__() self.arpeggiator = arpeggiator self.channel_manager = channel_manager self.volume_engine = volume_engine self.synth_router = synth_router self.output_manager = output_manager # Maschine interface self.maschine = MaschineInterface() # State tracking self.held_notes = set() self.shift_held = False self.current_preset_bank = 0 # Pattern cycling state self.pattern_index = 0 self.volume_pattern_index = 0 self.routing_pattern_index = 0 self.setup_connections() def setup_connections(self): """Setup signal connections""" # Maschine signals self.maschine.pad_pressed.connect(self.on_pad_pressed) self.maschine.pad_released.connect(self.on_pad_released) self.maschine.encoder_changed.connect(self.on_encoder_changed) self.maschine.button_pressed.connect(self.on_button_pressed) # Application signals for LED feedback self.arpeggiator.playing_state_changed.connect(self.on_playing_state_changed) self.arpeggiator.note_triggered.connect(self.on_note_triggered) self.channel_manager.active_synth_count_changed.connect(self.update_channel_leds) self.volume_engine.pattern_changed.connect(self.update_pattern_leds) def connect_maschine(self, input_device: str = None, output_device: str = None) -> bool: """Connect to Maschine hardware""" success = self.maschine.connect(input_device, output_device) if success: self.update_all_leds() return success def disconnect_maschine(self): """Disconnect from Maschine hardware""" self.maschine.disconnect() @pyqtSlot(int, int) def on_pad_pressed(self, pad_number: int, velocity: int): """Handle pad press events""" mapping = self.maschine.get_pad_mapping(pad_number) if not mapping: return if mapping['type'] == 'note_trigger': note = mapping['note'] self.held_notes.add(note) # Send note to arpeggiator self.arpeggiator.note_on(note) # Visual feedback self.maschine.send_feedback_pulse(pad_number) # If this is the first note and arpeggiator isn't playing, start it if len(self.held_notes) == 1 and not self.arpeggiator.is_playing: self.arpeggiator.start() @pyqtSlot(int) def on_pad_released(self, pad_number: int): """Handle pad release events""" mapping = self.maschine.get_pad_mapping(pad_number) if not mapping: return if mapping['type'] == 'note_trigger': note = mapping['note'] if note in self.held_notes: self.held_notes.remove(note) # Send note off to arpeggiator self.arpeggiator.note_off(note) @pyqtSlot(int, int) def on_encoder_changed(self, encoder_number: int, delta: int): """Handle encoder changes""" mapping = self.maschine.get_encoder_mapping(encoder_number) if not mapping: return mapping_type = mapping['type'] if mapping_type == 'tempo': current_tempo = self.arpeggiator.tempo new_tempo = current_tempo + (delta * mapping['step']) new_tempo = max(mapping['min'], min(mapping['max'], new_tempo)) self.arpeggiator.set_tempo(new_tempo) elif mapping_type == 'swing': current_swing = self.arpeggiator.swing * 100 # Convert to percentage new_swing = current_swing + (delta * mapping['step']) new_swing = max(mapping['min'], min(mapping['max'], new_swing)) self.arpeggiator.set_swing(new_swing / 100.0) elif mapping_type == 'gate': current_gate = self.arpeggiator.gate new_gate = current_gate + (delta * mapping['step']) new_gate = max(mapping['min'], min(mapping['max'], new_gate)) self.arpeggiator.set_gate(new_gate) elif mapping_type == 'root_note': current_note = self.arpeggiator.root_note new_note = current_note + delta new_note = max(mapping['min'], min(mapping['max'], new_note)) self.arpeggiator.set_root_note(new_note) elif mapping_type == 'pattern_select': if delta != 0: patterns = mapping['values'] self.pattern_index = (self.pattern_index + (1 if delta > 0 else -1)) % len(patterns) self.arpeggiator.set_pattern_type(patterns[self.pattern_index]) elif mapping_type == 'scale_select': if delta != 0: scales = mapping['values'] current_index = scales.index(self.arpeggiator.scale) if self.arpeggiator.scale in scales else 0 new_index = (current_index + (1 if delta > 0 else -1)) % len(scales) self.arpeggiator.set_scale(scales[new_index]) elif mapping_type == 'volume_pattern': if delta != 0: patterns = mapping['values'] self.volume_pattern_index = (self.volume_pattern_index + (1 if delta > 0 else -1)) % len(patterns) self.volume_engine.set_pattern(patterns[self.volume_pattern_index]) elif mapping_type == 'pattern_speed': current_speed = self.volume_engine.pattern_speed new_speed = current_speed + (delta * mapping['step']) new_speed = max(mapping['min'], min(mapping['max'], new_speed)) self.volume_engine.set_pattern_speed(new_speed) @pyqtSlot(int) def on_button_pressed(self, button_number: int): """Handle button press events""" # Find button name from number button_name = None for name, num in self.maschine.MASCHINE_BUTTONS.items(): if num == button_number: button_name = name break if not button_name: return mapping = self.maschine.button_mappings.get(button_name) if not mapping: return mapping_type = mapping['type'] if mapping_type == 'preset': # Load preset (would need preset system integration) preset_number = mapping['preset_number'] # self.load_preset(preset_number) elif mapping_type == 'output_mode_toggle': current_mode = self.output_manager.current_mode new_mode = "hardware" if current_mode == "simulator" else "simulator" self.output_manager.set_mode(new_mode) elif mapping_type == 'routing_pattern_cycle': patterns = self.synth_router.ROUTING_PATTERNS self.routing_pattern_index = (self.routing_pattern_index + 1) % len(patterns) self.synth_router.set_routing_pattern(patterns[self.routing_pattern_index]) elif mapping_type == 'volume_pattern_cycle': patterns = self.volume_engine.PATTERN_TYPES self.volume_pattern_index = (self.volume_pattern_index + 1) % len(patterns) self.volume_engine.set_pattern(patterns[self.volume_pattern_index]) elif mapping_type == 'panic': self.output_manager.send_panic() self.all_notes_off() elif mapping_type == 'restart_pattern': self.arpeggiator.stop() if self.held_notes: self.arpeggiator.start() @pyqtSlot(bool) def on_playing_state_changed(self, is_playing: bool): """Update LEDs based on playing state""" if is_playing: # Pulse active pads when playing self.pulse_active_pads() else: # Update static display self.update_note_leds() @pyqtSlot(int, int, int, float) def on_note_triggered(self, channel: int, note: int, velocity: int, duration: float): """Provide visual feedback when notes are triggered""" # Flash corresponding pad if the note maps to one for pad, mapping in self.maschine.pad_mappings.items(): if mapping.get('type') == 'note_trigger' and mapping.get('note') == note: brightness = velocity / 127.0 # Calculate color based on channel hue = (channel - 1) / 16.0 self.maschine.set_pad_color_brightness(pad, brightness, hue) break def update_all_leds(self): """Update all LED displays""" self.update_note_leds() self.update_channel_leds() self.update_pattern_leds() def update_note_leds(self): """Update pad LEDs based on held notes""" for pad, mapping in self.maschine.pad_mappings.items(): if mapping.get('type') == 'note_trigger': note = mapping.get('note') if note in self.held_notes: self.maschine.set_pad_led(pad, 100, "green") else: self.maschine.set_pad_led(pad, 20, "white") def update_channel_leds(self): """Update LEDs based on active channels""" active_count = self.channel_manager.active_synth_count # Use first N pads to indicate active channels for i in range(1, 17): if i <= active_count: # Get channel status voices = self.channel_manager.get_voice_count(i) if voices > 0: brightness = min(100 + voices * 20, 127) # Brighter with more voices self.maschine.set_pad_led(i, brightness, "blue") else: self.maschine.set_pad_led(i, 30, "blue") else: self.maschine.set_pad_led(i, 0) def update_pattern_leds(self): """Update LEDs based on current patterns""" # This could use different pad modes or encoder LEDs pass def pulse_active_pads(self): """Pulse pads that correspond to held notes""" for pad, mapping in self.maschine.pad_mappings.items(): if mapping.get('type') == 'note_trigger': note = mapping.get('note') if note in self.held_notes: self.maschine.flash_pad(pad, "yellow", 100) def all_notes_off(self): """Stop all notes and clear held notes""" self.held_notes.clear() for note in range(128): self.arpeggiator.note_off(note) self.update_note_leds() def set_pad_mode(self, mode: str): """Set pad display mode (notes, channels, patterns, etc.)""" if mode == "notes": self.setup_note_mappings() elif mode == "channels": self.setup_channel_mappings() elif mode == "patterns": self.setup_pattern_mappings() self.update_all_leds() def setup_note_mappings(self): """Setup pads for note triggering""" for i in range(1, 17): self.maschine.set_pad_mapping(i, { 'type': 'note_trigger', 'note': 48 + i, # C3 and up 'function': None }) def setup_channel_mappings(self): """Setup pads for channel control""" for i in range(1, 17): self.maschine.set_pad_mapping(i, { 'type': 'channel_select', 'channel': i, 'function': None }) def setup_pattern_mappings(self): """Setup pads for pattern selection""" patterns = self.arpeggiator.PATTERN_TYPES for i, pattern in enumerate(patterns[:16], 1): self.maschine.set_pad_mapping(i, { 'type': 'pattern_select', 'pattern': pattern, 'function': None }) def is_connected(self) -> bool: """Check if Maschine is connected""" return self.maschine.is_connected() def get_status(self) -> dict: """Get comprehensive status""" return { 'connected': self.is_connected(), 'held_notes': list(self.held_notes), 'maschine_status': self.maschine.get_status() }