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