""" Arpeggiator Controls GUI Control panel for arpeggiator settings including patterns, scales, timing, etc. """ from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QGroupBox, QComboBox, QSlider, QSpinBox, QLabel, QPushButton, QCheckBox, QFrame) from PyQt5.QtCore import Qt, pyqtSlot class ArpeggiatorControls(QWidget): """Control panel for arpeggiator parameters""" def __init__(self, arpeggiator, channel_manager): super().__init__() self.arpeggiator = arpeggiator self.channel_manager = channel_manager # Preset system self.presets = {} self.current_preset = None self.preset_rotation_enabled = False self.preset_rotation_interval = 4 # patterns self.preset_rotation_timer = None self.pattern_count_since_preset_change = 0 self.setup_ui() self.connect_signals() def setup_ui(self): """Set up the user interface with quadrant layout""" layout = QGridLayout(self) layout.setSpacing(5) # Reduced from 15 to 5 layout.setContentsMargins(5, 5, 5, 5) # Minimal margins # Make columns equal width and rows equal height layout.setColumnStretch(0, 1) layout.setColumnStretch(1, 1) layout.setRowStretch(0, 1) layout.setRowStretch(1, 1) # Top-left: Basic Settings basic_group = self.create_basic_settings() layout.addWidget(basic_group, 0, 0) # Top-right: Channel Distribution distribution_group = self.create_distribution_settings() layout.addWidget(distribution_group, 0, 1) # Bottom-left: Pattern Settings pattern_group = self.create_pattern_settings() layout.addWidget(pattern_group, 1, 0) # Bottom-right: Timing Settings timing_group = self.create_timing_settings() layout.addWidget(timing_group, 1, 1) # Add preset rotation controls self.setup_preset_rotation() def create_basic_settings(self) -> QGroupBox: """Create basic arpeggiator settings - no scrollbars, all visible""" group = QGroupBox("Basic Settings") layout = QVBoxLayout(group) layout.setSpacing(2) layout.setContentsMargins(5, 5, 5, 5) # 12 Note buttons - all visible in a compact grid note_frame = QFrame() note_layout = QVBoxLayout(note_frame) note_layout.setSpacing(3) note_layout.addWidget(QLabel("Root Note:")) note_widget = self.create_note_buttons() note_layout.addWidget(note_widget) layout.addWidget(note_frame) # 12 Octave select buttons - all visible in a row octave_frame = QFrame() octave_layout = QVBoxLayout(octave_frame) octave_layout.setSpacing(3) octave_layout.addWidget(QLabel("Octave:")) octave_widget = self.create_octave_buttons() octave_layout.addWidget(octave_widget) layout.addWidget(octave_frame) # Scale buttons - compact grid, all visible scale_frame = QFrame() scale_layout = QVBoxLayout(scale_frame) scale_layout.setSpacing(3) scale_layout.addWidget(QLabel("Scale:")) scale_widget = self.create_scale_buttons() scale_layout.addWidget(scale_widget) layout.addWidget(scale_frame) # Octave Range dropdown octave_frame = QFrame() octave_layout = QVBoxLayout(octave_frame) octave_layout.setSpacing(3) octave_layout.addWidget(QLabel("Octave Range:")) self.octave_range_combo = QComboBox() for i in range(1, 5): self.octave_range_combo.addItem(f"{i} octave{'s' if i > 1 else ''}") self.octave_range_combo.setCurrentIndex(0) # 1 octave octave_layout.addWidget(self.octave_range_combo) layout.addWidget(octave_frame) return group def create_note_buttons(self) -> QWidget: """Create 12 note selection buttons in compact layout""" widget = QWidget() layout = QGridLayout(widget) layout.setSpacing(1) layout.setContentsMargins(0, 0, 0, 0) # Note names for one octave notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] self.root_note_buttons = {} self.current_root_note = 0 # C (index in notes array) self.armed_root_note_button = None # Create 12 note buttons in 2 rows of 6 for i, note in enumerate(notes): button = QPushButton(note) button.setCheckable(True) button.setFixedSize(40, 20) button.clicked.connect(lambda checked, n=i: self.on_root_note_button_clicked(n)) # Set initial state if i == 0: # C button.setChecked(True) self.update_root_note_button_style(button, "active") else: self.update_root_note_button_style(button, "inactive") self.root_note_buttons[i] = button layout.addWidget(button, i // 6, i % 6) return widget def create_octave_buttons(self) -> QWidget: """Create 12 octave select buttons (C0 to C11)""" widget = QWidget() layout = QGridLayout(widget) layout.setSpacing(1) layout.setContentsMargins(0, 0, 0, 0) self.octave_buttons = {} self.current_octave = 4 # C4 self.armed_octave_button = None self.armed_root_note_button = None # Create octave buttons C0 to C11 (12 octaves) for octave in range(12): button = QPushButton(f"C{octave}") button.setCheckable(True) button.setFixedSize(40, 20) button.clicked.connect(lambda checked, o=octave: self.on_octave_button_clicked(o)) # Set initial state if octave == 4: # C4 button.setChecked(True) self.update_octave_button_style(button, "active") else: self.update_octave_button_style(button, "inactive") self.octave_buttons[octave] = button layout.addWidget(button, octave // 6, octave % 6) return widget def create_scale_buttons(self) -> QWidget: """Create scale selection buttons - compact layout""" widget = QWidget() layout = QGridLayout(widget) layout.setSpacing(1) layout.setContentsMargins(0, 0, 0, 0) self.scale_buttons = {} self.current_scale = "major" self.armed_scale_button = None scales = list(self.arpeggiator.SCALES.keys()) # Create scales in a compact grid for i, scale in enumerate(scales): display_name = scale.replace("_", " ").title() if len(display_name) > 10: display_name = display_name[:10] # Truncate long names button = QPushButton(display_name) button.setCheckable(True) button.setFixedSize(70, 20) button.clicked.connect(lambda checked, s=scale: self.on_scale_button_clicked(s)) # Set initial state if scale == "major": button.setChecked(True) self.update_scale_button_style(button, "active") else: self.update_scale_button_style(button, "inactive") self.scale_buttons[scale] = button layout.addWidget(button, i // 4, i % 4) # 4 buttons per row return widget def update_root_note_button_style(self, button, state): """Update button styling based on state""" if state == "active": button.setStyleSheet(""" QPushButton { background-color: #2d5a2d; color: white; border: 2px solid #4a8a4a; font-weight: bold; font-size: 8px; } """) elif state == "armed": button.setStyleSheet(""" QPushButton { background-color: #5a4d2d; color: white; border: 2px solid #8a7a4a; font-weight: bold; font-size: 8px; } """) else: # inactive button.setStyleSheet(""" QPushButton { font-size: 8px; } """) def update_octave_button_style(self, button, state): """Update octave button styling based on state""" if state == "active": button.setStyleSheet(""" QPushButton { background-color: #2d5a2d; color: white; border: 2px solid #4a8a4a; font-weight: bold; font-size: 8px; } """) elif state == "armed": button.setStyleSheet(""" QPushButton { background-color: #5a4d2d; color: white; border: 2px solid #8a7a4a; font-weight: bold; font-size: 8px; } """) else: # inactive button.setStyleSheet(""" QPushButton { font-size: 8px; } """) def update_scale_button_style(self, button, state): """Update scale button styling based on state""" if state == "active": button.setStyleSheet(""" QPushButton { background-color: #2d5a2d; color: white; border: 2px solid #4a8a4a; font-weight: bold; font-size: 8px; } """) elif state == "armed": button.setStyleSheet(""" QPushButton { background-color: #5a4d2d; color: white; border: 2px solid #8a7a4a; font-weight: bold; font-size: 8px; } """) else: # inactive button.setStyleSheet(""" QPushButton { font-size: 8px; } """) def create_pattern_buttons(self) -> QWidget: """Create pattern selection buttons - compact grid""" widget = QWidget() layout = QGridLayout(widget) layout.setSpacing(1) layout.setContentsMargins(0, 0, 0, 0) self.pattern_buttons = {} self.current_pattern = "up" self.armed_pattern_button = None patterns = self.arpeggiator.PATTERN_TYPES # Create patterns in a compact grid for i, pattern in enumerate(patterns): display_name = pattern.replace("_", " ").title() if len(display_name) > 10: display_name = display_name[:10] # Truncate long names button = QPushButton(display_name) button.setCheckable(True) button.setFixedSize(70, 20) button.clicked.connect(lambda checked, p=pattern: self.on_pattern_button_clicked(p)) # Set initial state if pattern == "up": button.setChecked(True) self.update_pattern_button_style(button, "active") else: self.update_pattern_button_style(button, "inactive") self.pattern_buttons[pattern] = button layout.addWidget(button, i // 3, i % 3) # 3 buttons per row return widget def update_pattern_button_style(self, button, state): """Update pattern button styling based on state""" if state == "active": button.setStyleSheet(""" QPushButton { background-color: #2d5a2d; color: white; border: 2px solid #4a8a4a; font-weight: bold; font-size: 8px; } """) elif state == "armed": button.setStyleSheet(""" QPushButton { background-color: #5a4d2d; color: white; border: 2px solid #8a7a4a; font-weight: bold; font-size: 8px; } """) else: # inactive button.setStyleSheet(""" QPushButton { font-size: 8px; } """) def create_distribution_buttons(self) -> QWidget: """Create distribution selection buttons - compact grid""" widget = QWidget() layout = QGridLayout(widget) layout.setSpacing(1) layout.setContentsMargins(0, 0, 0, 0) self.distribution_buttons = {} self.current_distribution = "up" self.armed_distribution_button = None patterns = self.arpeggiator.CHANNEL_DISTRIBUTION_PATTERNS # Create distributions in a compact grid for i, pattern in enumerate(patterns): display_name = pattern.replace("_", " ").title() if len(display_name) > 12: display_name = display_name[:12] # Truncate long names button = QPushButton(display_name) button.setCheckable(True) button.setFixedSize(80, 20) button.clicked.connect(lambda checked, p=pattern: self.on_distribution_button_clicked(p)) # Set initial state if pattern == "up": button.setChecked(True) self.update_distribution_button_style(button, "active") else: self.update_distribution_button_style(button, "inactive") self.distribution_buttons[pattern] = button layout.addWidget(button, i // 3, i % 3) # 3 buttons per row return widget def update_distribution_button_style(self, button, state): """Update distribution button styling based on state""" if state == "active": button.setStyleSheet(""" QPushButton { background-color: #2d5a2d; color: white; border: 2px solid #4a8a4a; font-weight: bold; font-size: 8px; } """) elif state == "armed": button.setStyleSheet(""" QPushButton { background-color: #5a4d2d; color: white; border: 2px solid #8a7a4a; font-weight: bold; font-size: 8px; } """) else: # inactive button.setStyleSheet(""" QPushButton { font-size: 8px; } """) def create_pattern_settings(self) -> QGroupBox: """Create pattern settings controls - no scrollbars""" group = QGroupBox("Pattern Settings") layout = QVBoxLayout(group) layout.setSpacing(2) layout.setContentsMargins(5, 5, 5, 5) # Pattern Type Buttons - all visible pattern_frame = QFrame() pattern_layout = QVBoxLayout(pattern_frame) pattern_layout.addWidget(QLabel("Pattern:")) pattern_widget = self.create_pattern_buttons() pattern_layout.addWidget(pattern_widget) layout.addWidget(pattern_frame) return group def create_distribution_settings(self) -> QGroupBox: """Create channel distribution settings - no scrollbars""" group = QGroupBox("Channel Distribution") layout = QVBoxLayout(group) layout.setSpacing(2) layout.setContentsMargins(5, 5, 5, 5) # Channel Distribution Pattern Buttons - all visible dist_frame = QFrame() dist_layout = QVBoxLayout(dist_frame) dist_layout.addWidget(QLabel("Distribution:")) distribution_widget = self.create_distribution_buttons() dist_layout.addWidget(distribution_widget) layout.addWidget(dist_frame) # Description label self.distribution_description = QLabel("Channels: 1 → 2 → 3 → 4 → 5 → 6...") self.distribution_description.setStyleSheet("color: #888888; font-style: italic;") self.distribution_description.setWordWrap(True) layout.addWidget(self.distribution_description) return group def create_timing_settings(self) -> QGroupBox: """Create timing controls""" group = QGroupBox("Timing Settings") layout = QGridLayout(group) # Tempo layout.addWidget(QLabel("Tempo:"), 0, 0) self.tempo_spin = QSpinBox() self.tempo_spin.setRange(40, 200) self.tempo_spin.setValue(120) self.tempo_spin.setSuffix(" BPM") layout.addWidget(self.tempo_spin, 0, 1) # Note Speed layout.addWidget(QLabel("Note Speed:"), 1, 0) self.speed_combo = QComboBox() speeds = list(self.arpeggiator.NOTE_SPEEDS.keys()) for speed in speeds: self.speed_combo.addItem(speed) self.speed_combo.setCurrentText("1/8") layout.addWidget(self.speed_combo, 1, 1) # Gate (Note Length) layout.addWidget(QLabel("Gate:"), 2, 0) gate_layout = QHBoxLayout() self.gate_slider = QSlider(Qt.Horizontal) self.gate_slider.setRange(10, 200) # 10% to 200% self.gate_slider.setValue(100) self.gate_label = QLabel("100%") self.gate_label.setFixedWidth(40) gate_layout.addWidget(self.gate_slider) gate_layout.addWidget(self.gate_label) layout.addLayout(gate_layout, 2, 1) # Swing layout.addWidget(QLabel("Swing:"), 3, 0) swing_layout = QHBoxLayout() self.swing_slider = QSlider(Qt.Horizontal) self.swing_slider.setRange(-100, 100) self.swing_slider.setValue(0) self.swing_label = QLabel("0%") self.swing_label.setFixedWidth(40) swing_layout.addWidget(self.swing_slider) swing_layout.addWidget(self.swing_label) layout.addLayout(swing_layout, 3, 1) # Base Velocity layout.addWidget(QLabel("Velocity:"), 4, 0) velocity_layout = QHBoxLayout() self.velocity_slider = QSlider(Qt.Horizontal) self.velocity_slider.setRange(1, 127) self.velocity_slider.setValue(80) self.velocity_label = QLabel("80") self.velocity_label.setFixedWidth(40) velocity_layout.addWidget(self.velocity_slider) velocity_layout.addWidget(self.velocity_label) layout.addLayout(velocity_layout, 4, 1) return group def setup_preset_rotation(self): """Setup preset rotation controls""" # Find the timing settings group that was just created timing_groups = self.findChildren(QGroupBox) timing_group = None for group in timing_groups: if group.title() == "Timing Settings": timing_group = group break if timing_group and hasattr(timing_group, 'layout') and timing_group.layout(): layout = timing_group.layout() # Preset rotation controls layout.addWidget(QLabel("Preset Rotation:"), 6, 0) preset_layout = QVBoxLayout() # Enable checkbox self.preset_rotation_checkbox = QPushButton("Enable Presets") self.preset_rotation_checkbox.setCheckable(True) self.preset_rotation_checkbox.setFixedSize(100, 25) preset_layout.addWidget(self.preset_rotation_checkbox) # Interval control interval_layout = QHBoxLayout() interval_layout.addWidget(QLabel("Every:")) self.preset_interval_spin = QSpinBox() self.preset_interval_spin.setRange(1, 16) self.preset_interval_spin.setValue(4) self.preset_interval_spin.setSuffix(" loops") self.preset_interval_spin.setFixedSize(80, 25) interval_layout.addWidget(self.preset_interval_spin) preset_layout.addLayout(interval_layout) # Preset buttons preset_button_layout = QHBoxLayout() self.save_preset_button = QPushButton("Save") self.save_preset_button.setFixedSize(50, 25) preset_button_layout.addWidget(self.save_preset_button) self.load_preset_button = QPushButton("Load") self.load_preset_button.setFixedSize(50, 25) preset_button_layout.addWidget(self.load_preset_button) preset_layout.addLayout(preset_button_layout) layout.addLayout(preset_layout, 6, 1) def connect_signals(self): """Connect GUI controls to arpeggiator""" # Basic settings self.octave_range_combo.currentIndexChanged.connect(self.on_octave_range_changed) # Connect new button signals for note_index, button in self.root_note_buttons.items(): button.clicked.connect(lambda checked, n=note_index: self.on_root_note_button_clicked(n)) for octave, button in self.octave_buttons.items(): button.clicked.connect(lambda checked, o=octave: self.on_octave_button_clicked(o)) # Timing settings self.tempo_spin.valueChanged.connect(self.on_tempo_changed) self.speed_combo.currentTextChanged.connect(self.on_speed_changed) self.gate_slider.valueChanged.connect(self.on_gate_changed) self.swing_slider.valueChanged.connect(self.on_swing_changed) self.velocity_slider.valueChanged.connect(self.on_velocity_changed) # Arpeggiator state changes self.arpeggiator.armed_state_changed.connect(self.update_armed_states) def on_root_note_button_clicked(self, note_index): """Handle root note button click - uses note index (0-11) and octave""" midi_note = self.current_octave * 12 + note_index # If arpeggiator is playing, arm the change if self.arpeggiator.is_playing: # Clear previous armed state if self.armed_root_note_button: self.update_root_note_button_style(self.armed_root_note_button, "inactive") # Set new armed state (but keep current active button green) button = self.root_note_buttons[note_index] self.armed_root_note_button = button self.update_root_note_button_style(button, "armed") self.arpeggiator.arm_root_note(midi_note) else: # Apply immediately self.set_active_root_note(note_index) self.arpeggiator.set_root_note(midi_note) def on_scale_button_clicked(self, scale): """Handle scale button click - FIXED armed state logic""" # If arpeggiator is playing, arm the change if self.arpeggiator.is_playing: # Clear previous armed state if self.armed_scale_button: self.update_scale_button_style(self.armed_scale_button, "inactive") # Set new armed state (but keep current active button green) button = self.scale_buttons[scale] self.armed_scale_button = button self.update_scale_button_style(button, "armed") self.arpeggiator.arm_scale(scale) else: # Apply immediately self.set_active_scale(scale) self.arpeggiator.set_scale(scale) def on_pattern_button_clicked(self, pattern): """Handle pattern button click - FIXED armed state logic""" # If arpeggiator is playing, arm the change if self.arpeggiator.is_playing: # Clear previous armed state if self.armed_pattern_button: self.update_pattern_button_style(self.armed_pattern_button, "inactive") # Set new armed state (but keep current active button green) button = self.pattern_buttons[pattern] self.armed_pattern_button = button self.update_pattern_button_style(button, "armed") self.arpeggiator.arm_pattern_type(pattern) else: # Apply immediately self.set_active_pattern(pattern) self.arpeggiator.set_pattern_type(pattern) def on_distribution_button_clicked(self, distribution): """Handle distribution button click - FIXED armed state logic""" # If arpeggiator is playing, arm the change if self.arpeggiator.is_playing: # Clear previous armed state if self.armed_distribution_button: self.update_distribution_button_style(self.armed_distribution_button, "inactive") # Set new armed state (but keep current active button green) button = self.distribution_buttons[distribution] self.armed_distribution_button = button self.update_distribution_button_style(button, "armed") self.arpeggiator.arm_channel_distribution(distribution) else: # Apply immediately self.set_active_distribution(distribution) self.arpeggiator.set_channel_distribution(distribution) def on_octave_button_clicked(self, octave): """Handle octave button click""" # If arpeggiator is playing, arm the change if self.arpeggiator.is_playing: # Clear previous armed state if self.armed_octave_button: self.update_octave_button_style(self.armed_octave_button, "inactive") # Set new armed state (but keep current active button green) button = self.octave_buttons[octave] self.armed_octave_button = button self.update_octave_button_style(button, "armed") # Arm the new MIDI note midi_note = octave * 12 + self.current_root_note self.arpeggiator.arm_root_note(midi_note) else: # Apply immediately self.set_active_octave(octave) midi_note = octave * 12 + self.current_root_note self.arpeggiator.set_root_note(midi_note) def set_active_root_note(self, note_index): """Set active root note button""" # Clear current active state if self.current_root_note in self.root_note_buttons: self.update_root_note_button_style(self.root_note_buttons[self.current_root_note], "inactive") # Clear armed state if it's the same note if self.armed_root_note_button and self.armed_root_note_button == self.root_note_buttons.get(note_index): self.armed_root_note_button = None # Set new active state self.current_root_note = note_index if note_index in self.root_note_buttons: self.update_root_note_button_style(self.root_note_buttons[note_index], "active") def set_active_octave(self, octave): """Set active octave button""" # Clear current active state if self.current_octave in self.octave_buttons: self.update_octave_button_style(self.octave_buttons[self.current_octave], "inactive") # Clear armed state if it's the same octave if self.armed_octave_button and self.armed_octave_button == self.octave_buttons.get(octave): self.armed_octave_button = None # Set new active state self.current_octave = octave if octave in self.octave_buttons: self.update_octave_button_style(self.octave_buttons[octave], "active") def set_active_scale(self, scale): """Set active scale button - FIXED to clear previous active""" # Clear current active state if self.current_scale in self.scale_buttons: self.update_scale_button_style(self.scale_buttons[self.current_scale], "inactive") # Clear armed state if it's the same scale if self.armed_scale_button and self.armed_scale_button == self.scale_buttons.get(scale): self.armed_scale_button = None # Set new active state self.current_scale = scale if scale in self.scale_buttons: self.update_scale_button_style(self.scale_buttons[scale], "active") def set_active_pattern(self, pattern): """Set active pattern button - FIXED to clear previous active""" # Clear current active state if self.current_pattern in self.pattern_buttons: self.update_pattern_button_style(self.pattern_buttons[self.current_pattern], "inactive") # Clear armed state if it's the same pattern if self.armed_pattern_button and self.armed_pattern_button == self.pattern_buttons.get(pattern): self.armed_pattern_button = None # Set new active state self.current_pattern = pattern if pattern in self.pattern_buttons: self.update_pattern_button_style(self.pattern_buttons[pattern], "active") def set_active_distribution(self, distribution): """Set active distribution button - FIXED to clear previous active""" # Clear current active state if self.current_distribution in self.distribution_buttons: self.update_distribution_button_style(self.distribution_buttons[self.current_distribution], "inactive") # Clear armed state if it's the same distribution if self.armed_distribution_button and self.armed_distribution_button == self.distribution_buttons.get(distribution): self.armed_distribution_button = None # Set new active state self.current_distribution = distribution if distribution in self.distribution_buttons: self.update_distribution_button_style(self.distribution_buttons[distribution], "active") self.update_distribution_description(distribution) @pyqtSlot() def update_armed_states(self): """Update armed states when arpeggiator state changes - FIXED logic""" # This is called when armed states are applied at pattern end if self.armed_root_note_button and self.arpeggiator.armed_root_note is None: # Armed root note was applied, move to active note_index = None for n, btn in self.root_note_buttons.items(): if btn == self.armed_root_note_button: note_index = n break if note_index is not None: self.set_active_root_note(note_index) if self.armed_octave_button and self.arpeggiator.armed_root_note is None: # Armed octave was applied, move to active octave = None for o, btn in self.octave_buttons.items(): if btn == self.armed_octave_button: octave = o break if octave is not None: self.set_active_octave(octave) if self.armed_scale_button and self.arpeggiator.armed_scale is None: # Armed scale was applied, move to active scale = None for s, btn in self.scale_buttons.items(): if btn == self.armed_scale_button: scale = s break if scale: self.set_active_scale(scale) if self.armed_pattern_button and self.arpeggiator.armed_pattern_type is None: # Armed pattern was applied, move to active pattern = None for p, btn in self.pattern_buttons.items(): if btn == self.armed_pattern_button: pattern = p break if pattern: self.set_active_pattern(pattern) if self.armed_distribution_button and self.arpeggiator.armed_channel_distribution is None: # Armed distribution was applied, move to active distribution = None for d, btn in self.distribution_buttons.items(): if btn == self.armed_distribution_button: distribution = d break if distribution: self.set_active_distribution(distribution) def update_distribution_description(self, distribution: str): """Update distribution pattern description""" descriptions = { "up": "Channels: 1 → 2 → 3 → 4 → 5 → 6...", "down": "Channels: 6 → 5 → 4 → 3 → 2 → 1...", "up_down": "Channels: 1 → 2 → 3 → 4 → 5 → 6 → 5 → 4 → 3 → 2 → 1...", "bounce": "Channels: 1 → 2 → 3 → 4 → 5 → 6 → 5 → 4 → 3 → 2 → 1...", "random": "Channels: Random selection each note", "cycle": "Channels: 1 → 2 → 3 → 4 → 5 → 6 → 1 → 2...", "alternating": "Channels: 1 → 6 → 2 → 5 → 3 → 4...", "single_channel": "Channels: All notes on channel 1" } description = descriptions.get(distribution, "Unknown pattern") self.distribution_description.setText(description) @pyqtSlot(int) def on_tempo_changed(self, tempo): """Handle tempo change""" self.arpeggiator.set_tempo(float(tempo)) @pyqtSlot(str) def on_speed_changed(self, speed): """Handle note speed change""" self.arpeggiator.set_note_speed(speed) @pyqtSlot(int) def on_gate_changed(self, gate_percent): """Handle gate change""" gate_value = gate_percent / 100.0 self.arpeggiator.set_gate(gate_value) self.gate_label.setText(f"{gate_percent}%") @pyqtSlot(int) def on_swing_changed(self, swing_percent): """Handle swing change""" swing_value = swing_percent / 100.0 self.arpeggiator.set_swing(swing_value) self.swing_label.setText(f"{swing_percent}%") @pyqtSlot(int) def on_octave_range_changed(self, index): """Handle octave range change""" octaves = index + 1 # Convert 0-based index to 1-4 range self.arpeggiator.set_octave_range(octaves) @pyqtSlot(int) def on_velocity_changed(self, velocity): """Handle velocity change""" self.arpeggiator.set_velocity(velocity) self.velocity_label.setText(str(velocity)) @pyqtSlot(bool) def on_preset_rotation_toggled(self, enabled): """Handle preset rotation enable/disable""" self.preset_rotation_enabled = enabled print(f"Preset rotation {'enabled' if enabled else 'disabled'}") @pyqtSlot(int) def on_preset_interval_changed(self, interval): """Handle preset rotation interval change""" self.preset_rotation_interval = interval print(f"Preset interval set to {interval} patterns") def save_current_preset(self): """Save current settings as a preset""" preset_name = f"Preset_{len(self.presets) + 1}" preset = { 'root_note': self.current_root_note, 'octave': self.current_octave, 'scale': self.current_scale, 'pattern': self.current_pattern, 'distribution': self.current_distribution, 'octave_range': self.octave_range_combo.currentIndex() if hasattr(self, 'octave_range_combo') else 0, 'tempo': self.tempo_spin.value(), 'speed': self.speed_combo.currentText(), 'gate': self.gate_slider.value(), 'swing': self.swing_slider.value(), 'velocity': self.velocity_slider.value() } self.presets[preset_name] = preset print(f"Saved {preset_name}") def load_preset_dialog(self): """Show preset selection dialog""" if not self.presets: print("No presets saved") return # For now, cycle through presets preset_names = list(self.presets.keys()) if self.current_preset in preset_names: current_index = preset_names.index(self.current_preset) next_index = (current_index + 1) % len(preset_names) else: next_index = 0 next_preset = preset_names[next_index] self.load_preset(next_preset) def load_preset(self, preset_name: str): """Load a specific preset""" if preset_name not in self.presets: return preset = self.presets[preset_name] self.current_preset = preset_name # Apply preset settings if self.arpeggiator.is_playing: # Arm changes for pattern-end application midi_note = preset['octave'] * 12 + preset['root_note'] self.arpeggiator.arm_root_note(midi_note) self.arpeggiator.arm_scale(preset['scale']) self.arpeggiator.arm_pattern_type(preset['pattern']) self.arpeggiator.arm_channel_distribution(preset['distribution']) else: # Apply immediately self.set_active_root_note(preset['root_note']) self.set_active_octave(preset['octave']) self.set_active_scale(preset['scale']) self.set_active_pattern(preset['pattern']) self.set_active_distribution(preset['distribution']) midi_note = preset['octave'] * 12 + preset['root_note'] self.arpeggiator.set_root_note(midi_note) self.arpeggiator.set_scale(preset['scale']) self.arpeggiator.set_pattern_type(preset['pattern']) self.arpeggiator.set_channel_distribution(preset['distribution']) # Apply other settings immediately if hasattr(self, 'octave_range_combo'): self.octave_range_combo.setCurrentIndex(preset['octave_range']) self.tempo_spin.setValue(preset['tempo']) self.speed_combo.setCurrentText(preset['speed']) self.gate_slider.setValue(preset['gate']) self.swing_slider.setValue(preset['swing']) self.velocity_slider.setValue(preset['velocity']) print(f"Loaded {preset_name}")