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.
 

964 lines
38 KiB

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