You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

332 lines
13 KiB

"""
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()
}