Browse Source

Add parameter override checkboxes and fix spacebar emergency stop

- Add checkboxes next to all parameter labels to prevent preset changes during group cycling
- Enable selective parameter locking for live manipulation without preset interference
- Fix spacebar emergency stop with global event filter to work when text boxes are focused
- Remove keyboard note functionality (AWSDFGTGHYUJ keys) as requested
- Fix up_down and down_up pattern algorithms for proper note sequence reversals
- Improve debug logging for arpeggiator state changes

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
master
melancholytron 2 months ago
parent
commit
451aeec5fa
  1. 80
      core/arpeggiator_engine.py
  2. 91
      gui/arpeggiator_controls.py
  3. 101
      gui/main_window.py
  4. 79
      gui/preset_controls.py

80
core/arpeggiator_engine.py

@ -482,10 +482,15 @@ class ArpeggiatorEngine(QObject):
def stop(self): def stop(self):
"""Stop arpeggiator playback""" """Stop arpeggiator playback"""
print(f"DEBUG: stop() called, is_playing = {self.is_playing}")
if self.is_playing: if self.is_playing:
print("DEBUG: Stopping playback")
self.is_playing = False self.is_playing = False
self.all_notes_off() self.all_notes_off()
self.playing_state_changed.emit(False) self.playing_state_changed.emit(False)
print("DEBUG: Playback stopped, emitted playing_state_changed(False)")
else:
print("DEBUG: Already stopped, nothing to do")
def all_notes_off(self): def all_notes_off(self):
"""Send note off for all active notes""" """Send note off for all active notes"""
@ -526,32 +531,69 @@ class ArpeggiatorEngine(QObject):
if self.current_pattern: if self.current_pattern:
original_pattern = self.current_pattern.copy() original_pattern = self.current_pattern.copy()
# For directional patterns, adapt the pattern to fit the length
# For directional patterns, create proper up/down sequences
if self.pattern_type in ["up_down", "down_up"] and self.user_pattern_length >= 4: if self.pattern_type in ["up_down", "down_up"] and self.user_pattern_length >= 4:
# For up_down with 4 steps: take first half up, second half down
scale_notes = self._get_all_scale_notes() scale_notes = self._get_all_scale_notes()
half_length = self.user_pattern_length // 2 half_length = self.user_pattern_length // 2
if self.pattern_type == "up_down": if self.pattern_type == "up_down":
# First half: up progression
up_part = scale_notes[:half_length] if len(scale_notes) >= half_length else scale_notes * ((half_length // len(scale_notes)) + 1)
up_part = up_part[:half_length]
# Second half: down progression
down_part = list(reversed(scale_notes))[:half_length] if len(scale_notes) >= half_length else list(reversed(scale_notes)) * ((half_length // len(scale_notes)) + 1)
down_part = down_part[:half_length]
# Create up_down pattern: go up then reverse direction
pattern = []
# First generate an up pattern for the full length
up_pattern = []
for i in range(self.user_pattern_length):
note_idx = i % len(scale_notes)
up_pattern.append(scale_notes[note_idx])
# Now create the actual pattern: up for half, then down for half
for i in range(self.user_pattern_length):
if i < half_length:
# First half: go up
pattern.append(up_pattern[i])
else:
# Second half: go down from where we were
# Start going backwards from the last up note
reverse_idx = half_length - 1 - (i - half_length)
if reverse_idx < 0:
# If we've gone past the beginning, continue the pattern
reverse_idx = (i - half_length) % len(scale_notes)
pattern.append(scale_notes[reverse_idx])
else:
pattern.append(up_pattern[reverse_idx])
self.current_pattern = pattern
self.current_pattern = up_part + down_part
elif self.pattern_type == "down_up": elif self.pattern_type == "down_up":
# First half: down progression
down_part = list(reversed(scale_notes))[:half_length] if len(scale_notes) >= half_length else list(reversed(scale_notes)) * ((half_length // len(scale_notes)) + 1)
down_part = down_part[:half_length]
# Second half: up progression
up_part = scale_notes[:half_length] if len(scale_notes) >= half_length else scale_notes * ((half_length // len(scale_notes)) + 1)
up_part = up_part[:half_length]
self.current_pattern = down_part + up_part
# Create down_up pattern: start high, go down then reverse direction
pattern = []
# First generate a down pattern starting from the highest note
down_pattern = []
for i in range(self.user_pattern_length):
# Start from highest note and go down
note_idx = (len(scale_notes) - 1 - i) % len(scale_notes)
if note_idx < 0:
note_idx = len(scale_notes) + note_idx
down_pattern.append(scale_notes[note_idx])
# Now create the actual pattern: down for half, then up for half
for i in range(self.user_pattern_length):
if i < half_length:
# First half: go down from highest
pattern.append(down_pattern[i])
else:
# Second half: go up from where we were
# Start going backwards from the last down note
reverse_idx = half_length - 1 - (i - half_length)
if reverse_idx < 0:
# If we've gone past the beginning, continue the pattern
reverse_idx = (i - half_length) % len(scale_notes)
pattern.append(scale_notes[reverse_idx])
else:
pattern.append(down_pattern[reverse_idx])
self.current_pattern = pattern
else: else:
# For other patterns, use the original logic # For other patterns, use the original logic
if len(self.current_pattern) > self.user_pattern_length: if len(self.current_pattern) > self.user_pattern_length:

91
gui/arpeggiator_controls.py

@ -54,6 +54,30 @@ class ArpeggiatorControls(QWidget):
self.armed_note_limit_button = None self.armed_note_limit_button = None
# Speed changes apply immediately - no armed state needed # Speed changes apply immediately - no armed state needed
# Parameter override tracking - when checked, preset changes ignore these parameters
self.parameter_overrides = {
'root_note': False,
'octave': False,
'scale': False,
'scale_note_start': False,
'pattern_type': False,
'pattern_length': False,
'note_limit': False,
'channel_distribution': False,
'note_speed': False,
'gate': False,
'swing': False,
'velocity': False,
'tempo': False,
'delay_enabled': False,
'delay_length': False,
'delay_timing': False,
'delay_fade': False
}
# Override checkbox widgets
self.override_checkboxes = {}
self.setup_ui() self.setup_ui()
self.connect_signals() self.connect_signals()
@ -169,6 +193,39 @@ class ArpeggiatorControls(QWidget):
else: else:
self.apply_button_style(button, 12, "normal") self.apply_button_style(button, 12, "normal")
def create_parameter_label_with_override(self, text, param_name):
"""Create a label with an override checkbox"""
from PyQt5.QtWidgets import QCheckBox
container = QWidget()
layout = QHBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(8)
# Create override checkbox
checkbox = QCheckBox()
checkbox.setFixedSize(16, 16)
checkbox.setToolTip(f"Override {text} - when checked, presets won't change this parameter")
checkbox.stateChanged.connect(lambda state, param=param_name: self.on_parameter_override_changed(param, state == 2))
# Create label
label = QLabel(text)
# Add to layout
layout.addWidget(checkbox)
layout.addWidget(label)
layout.addStretch() # Push everything to the left
# Store checkbox reference
self.override_checkboxes[param_name] = checkbox
return container
def on_parameter_override_changed(self, param_name, is_overridden):
"""Handle parameter override checkbox changes"""
self.parameter_overrides[param_name] = is_overridden
print(f"DEBUG: Parameter {param_name} override: {is_overridden}")
def setup_ui(self): def setup_ui(self):
"""Clean quadrant layout with readable buttons""" """Clean quadrant layout with readable buttons"""
# Main grid with better spacing and expansion # Main grid with better spacing and expansion
@ -207,7 +264,7 @@ class ArpeggiatorControls(QWidget):
layout.setContentsMargins(8, 8, 8, 8) layout.setContentsMargins(8, 8, 8, 8)
# Root notes - 12 buttons in horizontal row, NO spacing between buttons # Root notes - 12 buttons in horizontal row, NO spacing between buttons
layout.addWidget(QLabel("Root Note:"))
layout.addWidget(self.create_parameter_label_with_override("Root Note:", "root_note"))
notes_widget = QWidget() notes_widget = QWidget()
notes_layout = QHBoxLayout(notes_widget) notes_layout = QHBoxLayout(notes_widget)
notes_layout.setSpacing(0) # NO spacing between buttons notes_layout.setSpacing(0) # NO spacing between buttons
@ -228,7 +285,7 @@ class ArpeggiatorControls(QWidget):
layout.addWidget(notes_widget) layout.addWidget(notes_widget)
# Octaves - 6 buttons in horizontal row, NO spacing between buttons # Octaves - 6 buttons in horizontal row, NO spacing between buttons
layout.addWidget(QLabel("Octave:"))
layout.addWidget(self.create_parameter_label_with_override("Octave:", "octave"))
octave_widget = QWidget() octave_widget = QWidget()
octave_layout = QHBoxLayout(octave_widget) octave_layout = QHBoxLayout(octave_widget)
octave_layout.setSpacing(0) # NO spacing between buttons octave_layout.setSpacing(0) # NO spacing between buttons
@ -248,7 +305,7 @@ class ArpeggiatorControls(QWidget):
layout.addWidget(octave_widget) layout.addWidget(octave_widget)
# Scales - 2 rows of 4, minimal vertical spacing # Scales - 2 rows of 4, minimal vertical spacing
layout.addWidget(QLabel("Scale:"))
layout.addWidget(self.create_parameter_label_with_override("Scale:", "scale"))
scales_widget = QWidget() scales_widget = QWidget()
scales_layout = QGridLayout(scales_widget) scales_layout = QGridLayout(scales_widget)
scales_layout.setSpacing(0) # NO horizontal spacing scales_layout.setSpacing(0) # NO horizontal spacing
@ -274,7 +331,7 @@ class ArpeggiatorControls(QWidget):
layout.addWidget(scales_widget) layout.addWidget(scales_widget)
# Scale notes selection # Scale notes selection
layout.addWidget(QLabel("Scale Notes:"))
layout.addWidget(self.create_parameter_label_with_override("Scale Notes:", "scale_note_start"))
scale_notes_widget = QWidget() scale_notes_widget = QWidget()
self.scale_notes_layout = QGridLayout(scale_notes_widget) self.scale_notes_layout = QGridLayout(scale_notes_widget)
self.scale_notes_layout.setSpacing(2) self.scale_notes_layout.setSpacing(2)
@ -302,7 +359,7 @@ class ArpeggiatorControls(QWidget):
layout.setSpacing(6) layout.setSpacing(6)
layout.setContentsMargins(8, 8, 8, 8) layout.setContentsMargins(8, 8, 8, 8)
layout.addWidget(QLabel("Distribution Pattern:"))
layout.addWidget(self.create_parameter_label_with_override("Distribution Pattern:", "channel_distribution"))
# 2 rows of 4 distribution buttons # 2 rows of 4 distribution buttons
dist_widget = QWidget() dist_widget = QWidget()
@ -358,7 +415,7 @@ class ArpeggiatorControls(QWidget):
layout.setSpacing(6) layout.setSpacing(6)
layout.setContentsMargins(8, 8, 8, 8) layout.setContentsMargins(8, 8, 8, 8)
layout.addWidget(QLabel("Arpeggio Pattern:"))
layout.addWidget(self.create_parameter_label_with_override("Arpeggio Pattern:", "pattern_type"))
# 2 rows of 4 pattern buttons # 2 rows of 4 pattern buttons
pattern_widget = QWidget() pattern_widget = QWidget()
@ -389,7 +446,7 @@ class ArpeggiatorControls(QWidget):
layout.addWidget(pattern_widget) layout.addWidget(pattern_widget)
# Pattern length buttons # Pattern length buttons
layout.addWidget(QLabel("Pattern Length:"))
layout.addWidget(self.create_parameter_label_with_override("Pattern Length:", "pattern_length"))
length_widget = QWidget() length_widget = QWidget()
length_layout = QGridLayout(length_widget) length_layout = QGridLayout(length_widget)
length_layout.setSpacing(0) # NO horizontal spacing length_layout.setSpacing(0) # NO horizontal spacing
@ -414,7 +471,7 @@ class ArpeggiatorControls(QWidget):
layout.addWidget(length_widget) layout.addWidget(length_widget)
# Note limit buttons # Note limit buttons
layout.addWidget(QLabel("Note Limit:"))
layout.addWidget(self.create_parameter_label_with_override("Note Limit:", "note_limit"))
note_limit_widget = QWidget() note_limit_widget = QWidget()
note_limit_layout = QGridLayout(note_limit_widget) note_limit_layout = QGridLayout(note_limit_widget)
note_limit_layout.setSpacing(0) # NO horizontal spacing note_limit_layout.setSpacing(0) # NO horizontal spacing
@ -446,7 +503,7 @@ class ArpeggiatorControls(QWidget):
# Tempo # Tempo
tempo_layout = QHBoxLayout() tempo_layout = QHBoxLayout()
tempo_layout.addWidget(QLabel("Tempo:"))
tempo_layout.addWidget(self.create_parameter_label_with_override("Tempo:", "tempo"))
self.tempo_spin = QSpinBox() self.tempo_spin = QSpinBox()
self.tempo_spin.setRange(40, 200) self.tempo_spin.setRange(40, 200)
self.tempo_spin.setValue(120) self.tempo_spin.setValue(120)
@ -456,7 +513,7 @@ class ArpeggiatorControls(QWidget):
layout.addLayout(tempo_layout) layout.addLayout(tempo_layout)
# Speed buttons # Speed buttons
layout.addWidget(QLabel("Note Speed:"))
layout.addWidget(self.create_parameter_label_with_override("Note Speed:", "note_speed"))
speed_widget = QWidget() speed_widget = QWidget()
speed_layout = QHBoxLayout(speed_widget) speed_layout = QHBoxLayout(speed_widget)
speed_layout.setSpacing(0) # NO spacing between buttons speed_layout.setSpacing(0) # NO spacing between buttons
@ -482,7 +539,7 @@ class ArpeggiatorControls(QWidget):
# Gate # Gate
gate_layout = QHBoxLayout() gate_layout = QHBoxLayout()
gate_layout.addWidget(QLabel("Gate:"))
gate_layout.addWidget(self.create_parameter_label_with_override("Gate:", "gate"))
self.gate_slider = QSlider(Qt.Horizontal) self.gate_slider = QSlider(Qt.Horizontal)
self.gate_slider.setRange(10, 200) self.gate_slider.setRange(10, 200)
self.gate_slider.setValue(100) self.gate_slider.setValue(100)
@ -495,7 +552,7 @@ class ArpeggiatorControls(QWidget):
# Swing # Swing
swing_layout = QHBoxLayout() swing_layout = QHBoxLayout()
swing_layout.addWidget(QLabel("Swing:"))
swing_layout.addWidget(self.create_parameter_label_with_override("Swing:", "swing"))
self.swing_slider = QSlider(Qt.Horizontal) self.swing_slider = QSlider(Qt.Horizontal)
self.swing_slider.setRange(-100, 100) self.swing_slider.setRange(-100, 100)
self.swing_slider.setValue(0) self.swing_slider.setValue(0)
@ -508,7 +565,7 @@ class ArpeggiatorControls(QWidget):
# Velocity # Velocity
velocity_layout = QHBoxLayout() velocity_layout = QHBoxLayout()
velocity_layout.addWidget(QLabel("Velocity:"))
velocity_layout.addWidget(self.create_parameter_label_with_override("Velocity:", "velocity"))
self.velocity_slider = QSlider(Qt.Horizontal) self.velocity_slider = QSlider(Qt.Horizontal)
self.velocity_slider.setRange(1, 127) self.velocity_slider.setRange(1, 127)
self.velocity_slider.setValue(80) self.velocity_slider.setValue(80)
@ -525,7 +582,7 @@ class ArpeggiatorControls(QWidget):
# Delay toggle # Delay toggle
self.delay_enabled = False self.delay_enabled = False
delay_toggle_layout = QHBoxLayout() delay_toggle_layout = QHBoxLayout()
delay_toggle_layout.addWidget(QLabel("Delay/Echo:"))
delay_toggle_layout.addWidget(self.create_parameter_label_with_override("Delay/Echo:", "delay_enabled"))
self.delay_toggle = QPushButton("OFF") self.delay_toggle = QPushButton("OFF")
self.delay_toggle.setFixedSize(50, 20) self.delay_toggle.setFixedSize(50, 20)
self.delay_toggle.setCheckable(True) self.delay_toggle.setCheckable(True)
@ -537,7 +594,7 @@ class ArpeggiatorControls(QWidget):
# Delay length (0-8 repeats) # Delay length (0-8 repeats)
delay_length_layout = QHBoxLayout() delay_length_layout = QHBoxLayout()
delay_length_layout.addWidget(QLabel("Delay Length:"))
delay_length_layout.addWidget(self.create_parameter_label_with_override("Delay Length:", "delay_length"))
self.delay_length_spin = QSpinBox() self.delay_length_spin = QSpinBox()
self.delay_length_spin.setRange(0, 8) self.delay_length_spin.setRange(0, 8)
self.delay_length_spin.setValue(3) self.delay_length_spin.setValue(3)
@ -548,7 +605,7 @@ class ArpeggiatorControls(QWidget):
delay_layout.addLayout(delay_length_layout) delay_layout.addLayout(delay_length_layout)
# Delay timing buttons (same as note speed) # Delay timing buttons (same as note speed)
delay_timing_label = QLabel("Delay Timing:")
delay_timing_label = self.create_parameter_label_with_override("Delay Timing:", "delay_timing")
delay_timing_label.setEnabled(False) delay_timing_label.setEnabled(False)
delay_layout.addWidget(delay_timing_label) delay_layout.addWidget(delay_timing_label)
self.delay_timing_label = delay_timing_label self.delay_timing_label = delay_timing_label
@ -583,7 +640,7 @@ class ArpeggiatorControls(QWidget):
# Delay fade slider (percentage) # Delay fade slider (percentage)
delay_fade_layout = QHBoxLayout() delay_fade_layout = QHBoxLayout()
delay_fade_label = QLabel("Delay Fade:")
delay_fade_label = self.create_parameter_label_with_override("Delay Fade:", "delay_fade")
delay_fade_label.setEnabled(False) delay_fade_label.setEnabled(False)
delay_fade_layout.addWidget(delay_fade_label) delay_fade_layout.addWidget(delay_fade_label)
self.delay_fade_label = delay_fade_label self.delay_fade_label = delay_fade_label

101
gui/main_window.py

@ -8,7 +8,7 @@ Integrates all GUI components into a cohesive interface.
from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QGridLayout, QPushButton, QLabel, QSlider, QComboBox, QGridLayout, QPushButton, QLabel, QSlider, QComboBox,
QSpinBox, QGroupBox, QTabWidget, QSplitter, QFrame, QSpinBox, QGroupBox, QTabWidget, QSplitter, QFrame,
QSizePolicy)
QSizePolicy, QShortcut)
from PyQt5.QtCore import Qt, QTimer, pyqtSlot, QSize from PyQt5.QtCore import Qt, QTimer, pyqtSlot, QSize
from PyQt5.QtGui import QFont, QPalette, QColor, QKeySequence from PyQt5.QtGui import QFont, QPalette, QColor, QKeySequence
@ -102,27 +102,15 @@ class MainWindow(QMainWindow):
self.scaling_enabled = True self.scaling_enabled = True
# Keyboard note mapping # Keyboard note mapping
self.keyboard_notes = {
Qt.Key_A: 60, # C
Qt.Key_W: 61, # C#
Qt.Key_S: 62, # D
Qt.Key_E: 63, # D#
Qt.Key_D: 64, # E
Qt.Key_F: 65, # F
Qt.Key_T: 66, # F#
Qt.Key_G: 67, # G
Qt.Key_Y: 68, # G#
Qt.Key_H: 69, # A
Qt.Key_U: 70, # A#
Qt.Key_J: 71, # B
Qt.Key_K: 72, # C (next octave)
}
self.held_keys = set()
# No keyboard note input - removed per user request
self.setup_ui() self.setup_ui()
self.setup_connections() self.setup_connections()
self.apply_dark_theme() self.apply_dark_theme()
# Set up global keyboard shortcut for emergency stop
self.setup_emergency_stop()
def setup_ui(self): def setup_ui(self):
"""Initialize the user interface""" """Initialize the user interface"""
central_widget = QWidget() central_widget = QWidget()
@ -161,7 +149,7 @@ class MainWindow(QMainWindow):
# Simulator display now integrated into arpeggiator tab - removed standalone tab # Simulator display now integrated into arpeggiator tab - removed standalone tab
# Presets tab # Presets tab
self.preset_controls = PresetControls(self.arpeggiator, self.channel_manager, self.volume_engine)
self.preset_controls = PresetControls(self.arpeggiator, self.channel_manager, self.volume_engine, self.arp_controls)
tab_widget.addTab(self.preset_controls, "Presets") tab_widget.addTab(self.preset_controls, "Presets")
# Set up preset callback for armed preset system # Set up preset callback for armed preset system
@ -172,11 +160,46 @@ class MainWindow(QMainWindow):
main_layout.addWidget(status_frame) main_layout.addWidget(status_frame)
# Create status bar # Create status bar
self.statusBar().showMessage("Ready - Use keyboard (AWSDFGTGHYUJ) to play notes, SPACE for emergency stop")
self.statusBar().showMessage("Ready - SPACE for emergency stop")
# Create menu bar # Create menu bar
self.create_menu_bar() self.create_menu_bar()
def setup_emergency_stop(self):
"""Set up global emergency stop that works even with text boxes focused"""
# Install event filter on the application to catch spacebar before any widget gets it
from PyQt5.QtWidgets import QApplication
app = QApplication.instance()
app.installEventFilter(self)
print("DEBUG: Emergency stop event filter installed")
def emergency_stop(self):
"""Emergency stop triggered by spacebar shortcut"""
print(f"DEBUG: Emergency stop activated, arpeggiator.is_playing = {self.arpeggiator.is_playing}")
if self.arpeggiator.is_playing:
print("DEBUG: Calling arpeggiator.stop()")
self.arpeggiator.stop()
self.statusBar().showMessage("Emergency stop activated", 1000)
print(f"DEBUG: After stop, is_playing = {self.arpeggiator.is_playing}")
else:
print("DEBUG: Arpeggiator not playing, ignoring emergency stop")
def eventFilter(self, source, event):
"""Global event filter to catch spacebar for emergency stop"""
from PyQt5.QtCore import QEvent
from PyQt5.QtGui import QKeyEvent
# Only handle key press events
if event.type() == QEvent.KeyPress:
key_event = event
if key_event.key() == Qt.Key_Space and not key_event.isAutoRepeat():
print(f"DEBUG: Spacebar intercepted from {source.__class__.__name__}")
self.emergency_stop()
return True # Consume the event to prevent text boxes from getting it
# Let other events pass through normally
return super().eventFilter(source, event)
# Removed create_control_panel and create_display_panel methods - now using direct tab layout # Removed create_control_panel and create_display_panel methods - now using direct tab layout
def create_transport_controls(self) -> QFrame: def create_transport_controls(self) -> QFrame:
@ -648,45 +671,7 @@ class MainWindow(QMainWindow):
"• Built-in simulator mode\n" "• Built-in simulator mode\n"
"• Native Instruments Maschine integration") "• Native Instruments Maschine integration")
def keyPressEvent(self, event):
"""Handle key press for note input"""
key = event.key()
# Avoid key repeat
if event.isAutoRepeat():
return
if key in self.keyboard_notes:
note = self.keyboard_notes[key]
if note not in self.held_keys:
self.held_keys.add(note)
self.arpeggiator.note_on(note)
self.statusBar().showMessage(f"Note ON: {note}", 500)
elif key == Qt.Key_Space:
# Spacebar emergency stop only (does not start playback)
if self.arpeggiator.is_playing:
self.on_stop_clicked()
self.statusBar().showMessage("Emergency stop activated", 1000)
super().keyPressEvent(event)
def keyReleaseEvent(self, event):
"""Handle key release for note input"""
key = event.key()
# Avoid key repeat
if event.isAutoRepeat():
return
if key in self.keyboard_notes:
note = self.keyboard_notes[key]
if note in self.held_keys:
self.held_keys.remove(note)
self.arpeggiator.note_off(note)
self.statusBar().showMessage(f"Note OFF: {note}", 500)
super().keyReleaseEvent(event)
# Keyboard event handling removed - using QShortcut for emergency stop instead
def resizeEvent(self, event): def resizeEvent(self, event):
"""Handle window resize for dynamic scaling""" """Handle window resize for dynamic scaling"""

79
gui/preset_controls.py

@ -17,11 +17,12 @@ import random
class PresetControls(QWidget): class PresetControls(QWidget):
"""Control panel for preset management""" """Control panel for preset management"""
def __init__(self, arpeggiator, channel_manager, volume_engine):
def __init__(self, arpeggiator, channel_manager, volume_engine, arpeggiator_controls=None):
super().__init__() super().__init__()
self.arpeggiator = arpeggiator self.arpeggiator = arpeggiator
self.channel_manager = channel_manager self.channel_manager = channel_manager
self.volume_engine = volume_engine self.volume_engine = volume_engine
self.arpeggiator_controls = arpeggiator_controls
# Preset storage # Preset storage
self.presets = {} self.presets = {}
@ -370,39 +371,58 @@ class PresetControls(QWidget):
if data == preset: if data == preset:
preset_name = name preset_name = name
break break
# Apply arpeggiator settings
# Apply arpeggiator settings (check for overrides first)
arp_settings = preset.get("arpeggiator", {}) arp_settings = preset.get("arpeggiator", {})
self.arpeggiator.set_root_note(arp_settings.get("root_note", 60))
self.arpeggiator.set_scale(arp_settings.get("scale", "major"))
self.arpeggiator.set_scale_note_start(arp_settings.get("scale_note_start", 0))
self.arpeggiator.set_pattern_type(arp_settings.get("pattern_type", "up"))
self.arpeggiator.set_octave_range(arp_settings.get("octave_range", 1))
self.arpeggiator.set_note_speed(arp_settings.get("note_speed", "1/8"))
self.arpeggiator.set_gate(arp_settings.get("gate", 1.0))
self.arpeggiator.set_swing(arp_settings.get("swing", 0.0))
self.arpeggiator.set_velocity(arp_settings.get("velocity", 80))
self.arpeggiator.set_tempo(arp_settings.get("tempo", 120.0))
# Only apply settings that aren't overridden
if not self._is_parameter_overridden('root_note'):
self.arpeggiator.set_root_note(arp_settings.get("root_note", 60))
if not self._is_parameter_overridden('scale'):
self.arpeggiator.set_scale(arp_settings.get("scale", "major"))
if not self._is_parameter_overridden('scale_note_start'):
self.arpeggiator.set_scale_note_start(arp_settings.get("scale_note_start", 0))
if not self._is_parameter_overridden('pattern_type'):
self.arpeggiator.set_pattern_type(arp_settings.get("pattern_type", "up"))
if not self._is_parameter_overridden('octave'):
self.arpeggiator.set_octave_range(arp_settings.get("octave_range", 1))
if not self._is_parameter_overridden('note_speed'):
self.arpeggiator.set_note_speed(arp_settings.get("note_speed", "1/8"))
if not self._is_parameter_overridden('gate'):
self.arpeggiator.set_gate(arp_settings.get("gate", 1.0))
if not self._is_parameter_overridden('swing'):
self.arpeggiator.set_swing(arp_settings.get("swing", 0.0))
if not self._is_parameter_overridden('velocity'):
self.arpeggiator.set_velocity(arp_settings.get("velocity", 80))
if not self._is_parameter_overridden('tempo'):
self.arpeggiator.set_tempo(arp_settings.get("tempo", 120.0))
# Apply user pattern length (check both old and new names for compatibility) # Apply user pattern length (check both old and new names for compatibility)
pattern_length = arp_settings.get("user_pattern_length") or arp_settings.get("pattern_length", 8)
if hasattr(self.arpeggiator, 'set_user_pattern_length'):
self.arpeggiator.set_user_pattern_length(pattern_length)
elif hasattr(self.arpeggiator, 'set_pattern_length'):
self.arpeggiator.set_pattern_length(pattern_length)
if not self._is_parameter_overridden('pattern_length'):
pattern_length = arp_settings.get("user_pattern_length") or arp_settings.get("pattern_length", 8)
if hasattr(self.arpeggiator, 'set_user_pattern_length'):
self.arpeggiator.set_user_pattern_length(pattern_length)
elif hasattr(self.arpeggiator, 'set_pattern_length'):
self.arpeggiator.set_pattern_length(pattern_length)
# Apply note limit # Apply note limit
note_limit = arp_settings.get("note_limit", 7)
if hasattr(self.arpeggiator, 'set_note_limit'):
self.arpeggiator.set_note_limit(note_limit)
if not self._is_parameter_overridden('note_limit'):
note_limit = arp_settings.get("note_limit", 7)
if hasattr(self.arpeggiator, 'set_note_limit'):
self.arpeggiator.set_note_limit(note_limit)
# Apply channel distribution # Apply channel distribution
self.arpeggiator.set_channel_distribution(arp_settings.get("channel_distribution", "up"))
if not self._is_parameter_overridden('channel_distribution'):
self.arpeggiator.set_channel_distribution(arp_settings.get("channel_distribution", "up"))
# Apply delay settings # Apply delay settings
self.arpeggiator.set_delay_enabled(arp_settings.get("delay_enabled", False))
self.arpeggiator.set_delay_length(arp_settings.get("delay_length", 3))
self.arpeggiator.set_delay_timing(arp_settings.get("delay_timing", "1/4"))
self.arpeggiator.set_delay_fade(arp_settings.get("delay_fade", 0.3))
if not self._is_parameter_overridden('delay_enabled'):
self.arpeggiator.set_delay_enabled(arp_settings.get("delay_enabled", False))
if not self._is_parameter_overridden('delay_length'):
self.arpeggiator.set_delay_length(arp_settings.get("delay_length", 3))
if not self._is_parameter_overridden('delay_timing'):
self.arpeggiator.set_delay_timing(arp_settings.get("delay_timing", "1/4"))
if not self._is_parameter_overridden('delay_fade'):
self.arpeggiator.set_delay_fade(arp_settings.get("delay_fade", 0.3))
# Apply channel settings # Apply channel settings
channel_settings = preset.get("channels", {}) channel_settings = preset.get("channels", {})
@ -702,6 +722,15 @@ class PresetControls(QWidget):
except Exception as e: except Exception as e:
QMessageBox.critical(self, "Export Error", f"Error exporting preset: {str(e)}") QMessageBox.critical(self, "Export Error", f"Error exporting preset: {str(e)}")
def _is_parameter_overridden(self, param_name):
"""Check if a parameter is overridden (checkbox checked)"""
if self.arpeggiator_controls and hasattr(self.arpeggiator_controls, 'parameter_overrides'):
is_overridden = self.arpeggiator_controls.parameter_overrides.get(param_name, False)
if is_overridden:
print(f"DEBUG: Skipping {param_name} - parameter is overridden")
return is_overridden
return False
def load_presets_from_directory(self): def load_presets_from_directory(self):
"""Load all presets from the presets directory""" """Load all presets from the presets directory"""
if not os.path.exists(self.presets_directory): if not os.path.exists(self.presets_directory):

Loading…
Cancel
Save