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.
1294 lines
62 KiB
1294 lines
62 KiB
"""
|
|
Arpeggiator Controls - READABLE BUTTONS WITH PROPER SIZING
|
|
"""
|
|
|
|
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
|
QGroupBox, QComboBox, QSlider, QSpinBox, QLabel,
|
|
QPushButton, QFrame, QSizePolicy)
|
|
from PyQt5.QtCore import Qt, pyqtSlot
|
|
|
|
class ArpeggiatorControls(QWidget):
|
|
"""Readable arpeggiator controls with properly sized buttons"""
|
|
|
|
def __init__(self, arpeggiator, channel_manager, simulator=None):
|
|
super().__init__()
|
|
self.arpeggiator = arpeggiator
|
|
self.channel_manager = channel_manager
|
|
self.simulator = simulator
|
|
|
|
# State tracking
|
|
self.presets = {}
|
|
self.current_preset = None
|
|
self.root_note_buttons = {}
|
|
self.octave_buttons = {}
|
|
self.scale_buttons = {}
|
|
self.scale_notes_buttons = {}
|
|
self.pattern_buttons = {}
|
|
self.distribution_buttons = {}
|
|
self.speed_buttons = {}
|
|
self.pattern_length_buttons = {}
|
|
self.note_limit_buttons = {}
|
|
|
|
# Scaling support
|
|
self.scale_factor = 1.0
|
|
self.all_buttons = [] # Track all buttons for scaling updates
|
|
|
|
self.current_root_note = 0
|
|
self.current_octave = 4
|
|
self.current_scale = "major"
|
|
self.current_pattern = "up"
|
|
self.current_distribution = "up"
|
|
self.current_speed = "1/8"
|
|
self.current_pattern_length = 8
|
|
self.current_note_limit = 7 # Default to 7 (full scale)
|
|
self.current_delay_timing = "1/4"
|
|
|
|
# Armed state tracking
|
|
self.armed_root_note_button = None
|
|
self.armed_octave_button = None
|
|
self.armed_scale_button = None
|
|
self.armed_scale_note_button = None
|
|
self.armed_pattern_button = None
|
|
self.armed_distribution_button = None
|
|
self.armed_pattern_length_button = None
|
|
self.armed_note_limit_button = None
|
|
# 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.connect_signals()
|
|
|
|
def create_scalable_button(self, text, base_width=40, base_height=22, base_font_size=12,
|
|
checkable=False, style_type="normal"):
|
|
"""Create a button with scalable sizing and styling"""
|
|
button = QPushButton(text)
|
|
button.setCheckable(checkable)
|
|
|
|
# Set size policy for expansion
|
|
button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
|
|
# Set minimum size based on scaled values
|
|
min_width = max(30, int(base_width * self.scale_factor))
|
|
min_height = max(18, int(base_height * self.scale_factor))
|
|
button.setMinimumSize(min_width, min_height)
|
|
|
|
# Apply initial styling
|
|
self.apply_button_style(button, base_font_size, style_type)
|
|
|
|
# Track button for scaling updates
|
|
self.all_buttons.append(button)
|
|
|
|
return button
|
|
|
|
def apply_button_style(self, button, base_font_size=12, style_type="normal"):
|
|
"""Apply scalable styling to a button"""
|
|
font_size = max(8, int(base_font_size * self.scale_factor))
|
|
padding = max(1, int(3 * self.scale_factor))
|
|
|
|
if style_type == "active":
|
|
button.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background: #00aa44;
|
|
color: white;
|
|
font-size: {font_size}px;
|
|
font-weight: bold;
|
|
padding: {padding}px;
|
|
border: 1px solid #00cc55;
|
|
}}
|
|
QPushButton:hover {{
|
|
background: #00cc66;
|
|
border: 1px solid #00ee77;
|
|
}}
|
|
""")
|
|
elif style_type == "orange":
|
|
button.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background: #cc6600;
|
|
color: white;
|
|
font-size: {font_size}px;
|
|
font-weight: bold;
|
|
padding: {padding}px;
|
|
border: 1px solid #ee8800;
|
|
}}
|
|
QPushButton:hover {{
|
|
background: #ee7700;
|
|
border: 1px solid #ffaa00;
|
|
}}
|
|
""")
|
|
elif style_type == "blue":
|
|
button.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background: #0066cc;
|
|
color: white;
|
|
font-size: {font_size}px;
|
|
font-weight: bold;
|
|
padding: {padding}px;
|
|
border: 1px solid #0088ee;
|
|
}}
|
|
QPushButton:hover {{
|
|
background: #0088ee;
|
|
border: 1px solid #00aaff;
|
|
}}
|
|
""")
|
|
elif style_type == "purple":
|
|
button.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background: #9933cc;
|
|
color: white;
|
|
font-size: {font_size}px;
|
|
font-weight: bold;
|
|
padding: {padding}px;
|
|
border: 1px solid #bb55ee;
|
|
}}
|
|
QPushButton:hover {{
|
|
background: #aa44dd;
|
|
border: 1px solid #cc66ff;
|
|
}}
|
|
""")
|
|
elif style_type == "disabled":
|
|
button.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background: #2a2a2a;
|
|
color: #666666;
|
|
font-size: {font_size}px;
|
|
font-weight: bold;
|
|
padding: {padding}px;
|
|
border: 1px solid #333333;
|
|
}}
|
|
""")
|
|
else: # normal
|
|
button.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background: #3a3a3a;
|
|
color: #ffffff;
|
|
font-size: {font_size}px;
|
|
font-weight: bold;
|
|
padding: {padding}px;
|
|
border: 1px solid #555555;
|
|
}}
|
|
QPushButton:hover {{
|
|
background: #505050;
|
|
border: 1px solid #777777;
|
|
}}
|
|
""")
|
|
|
|
def apply_scaling(self, scale_factor):
|
|
"""Apply new scaling factor to all buttons"""
|
|
self.scale_factor = scale_factor
|
|
|
|
# Update all tracked buttons
|
|
for button in self.all_buttons:
|
|
# Update minimum size
|
|
current_size = button.minimumSize()
|
|
new_width = max(30, int(40 * scale_factor)) # Base width 40
|
|
new_height = max(18, int(22 * scale_factor)) # Base height 22
|
|
button.setMinimumSize(new_width, new_height)
|
|
|
|
# Re-apply styling with new scale
|
|
# Determine button type by checking current style
|
|
current_style = button.styleSheet()
|
|
if "#00aa44" in current_style:
|
|
self.apply_button_style(button, 12, "active")
|
|
elif "#cc6600" in current_style:
|
|
self.apply_button_style(button, 12, "orange")
|
|
elif "#0066cc" in current_style:
|
|
self.apply_button_style(button, 12, "blue")
|
|
elif "#9933cc" in current_style:
|
|
self.apply_button_style(button, 12, "purple")
|
|
elif "#2a2a2a" in current_style:
|
|
self.apply_button_style(button, 12, "disabled")
|
|
else:
|
|
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):
|
|
"""Clean quadrant layout with readable buttons"""
|
|
# Main grid with better spacing and expansion
|
|
main = QGridLayout(self)
|
|
main.setSpacing(12)
|
|
main.setContentsMargins(12, 12, 12, 12)
|
|
|
|
# Equal quadrants with size policies
|
|
basic_quad = self.basic_quadrant()
|
|
basic_quad.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
main.addWidget(basic_quad, 0, 0)
|
|
|
|
dist_quad = self.distribution_quadrant()
|
|
dist_quad.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
main.addWidget(dist_quad, 0, 1)
|
|
|
|
pattern_quad = self.pattern_quadrant()
|
|
pattern_quad.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
main.addWidget(pattern_quad, 1, 0)
|
|
|
|
timing_quad = self.timing_quadrant()
|
|
timing_quad.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
main.addWidget(timing_quad, 1, 1)
|
|
|
|
# Equal stretch for all quadrants
|
|
main.setRowStretch(0, 1)
|
|
main.setRowStretch(1, 1)
|
|
main.setColumnStretch(0, 1)
|
|
main.setColumnStretch(1, 1)
|
|
|
|
def basic_quadrant(self):
|
|
"""Basic settings with readable buttons"""
|
|
group = QGroupBox("Basic Settings")
|
|
layout = QVBoxLayout(group)
|
|
layout.setSpacing(6)
|
|
layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
# Root notes - 12 buttons in horizontal row, NO spacing between buttons
|
|
layout.addWidget(self.create_parameter_label_with_override("Root Note:", "root_note"))
|
|
notes_widget = QWidget()
|
|
notes_layout = QHBoxLayout(notes_widget)
|
|
notes_layout.setSpacing(0) # NO spacing between buttons
|
|
notes_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
|
|
for i, note in enumerate(notes):
|
|
btn = self.create_scalable_button(note, 40, 22, 12, checkable=True,
|
|
style_type="active" if i == 0 else "normal")
|
|
btn.clicked.connect(lambda checked, n=i: self.on_root_note_clicked(n))
|
|
|
|
if i == 0:
|
|
btn.setChecked(True)
|
|
|
|
self.root_note_buttons[i] = btn
|
|
notes_layout.addWidget(btn)
|
|
|
|
layout.addWidget(notes_widget)
|
|
|
|
# Octaves - 6 buttons in horizontal row, NO spacing between buttons
|
|
layout.addWidget(self.create_parameter_label_with_override("Octave:", "octave"))
|
|
octave_widget = QWidget()
|
|
octave_layout = QHBoxLayout(octave_widget)
|
|
octave_layout.setSpacing(0) # NO spacing between buttons
|
|
octave_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
for octave in range(3, 9): # C3 to C8
|
|
btn = self.create_scalable_button(f"C{octave}", 50, 22, 12, checkable=True,
|
|
style_type="active" if octave == 4 else "normal")
|
|
btn.clicked.connect(lambda checked, o=octave: self.on_octave_clicked(o))
|
|
|
|
if octave == 4:
|
|
btn.setChecked(True)
|
|
|
|
self.octave_buttons[octave] = btn
|
|
octave_layout.addWidget(btn)
|
|
|
|
layout.addWidget(octave_widget)
|
|
|
|
# Scales - 2 rows of 4, minimal vertical spacing
|
|
layout.addWidget(self.create_parameter_label_with_override("Scale:", "scale"))
|
|
scales_widget = QWidget()
|
|
scales_layout = QGridLayout(scales_widget)
|
|
scales_layout.setSpacing(0) # NO horizontal spacing
|
|
scales_layout.setVerticalSpacing(2) # Minimal vertical spacing
|
|
scales_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
main_scales = ["major", "minor", "dorian", "phrygian", "lydian", "mixolydian", "pentatonic_major", "pentatonic_minor"]
|
|
for i, scale in enumerate(main_scales):
|
|
display_name = scale.replace("_", " ").title()
|
|
if len(display_name) > 10:
|
|
display_name = display_name[:10]
|
|
|
|
btn = self.create_scalable_button(display_name, 120, 22, 12, checkable=True,
|
|
style_type="active" if scale == "major" else "normal")
|
|
btn.clicked.connect(lambda checked, s=scale: self.on_scale_clicked(s))
|
|
|
|
if scale == "major":
|
|
btn.setChecked(True)
|
|
|
|
self.scale_buttons[scale] = btn
|
|
scales_layout.addWidget(btn, i // 4, i % 4)
|
|
|
|
layout.addWidget(scales_widget)
|
|
|
|
# Scale notes selection
|
|
layout.addWidget(self.create_parameter_label_with_override("Scale Notes:", "scale_note_start"))
|
|
scale_notes_widget = QWidget()
|
|
self.scale_notes_layout = QGridLayout(scale_notes_widget)
|
|
self.scale_notes_layout.setSpacing(2)
|
|
self.scale_notes_buttons = {}
|
|
self.current_scale_note_index = 0 # Start from root by default
|
|
|
|
# Initially populate with major scale notes
|
|
self.update_scale_notes_display()
|
|
layout.addWidget(scale_notes_widget)
|
|
|
|
# Octave range dropdown
|
|
layout.addWidget(QLabel("Octave Range:"))
|
|
self.octave_range_combo = QComboBox()
|
|
self.octave_range_combo.setFixedHeight(30)
|
|
for i in range(1, 5):
|
|
self.octave_range_combo.addItem(f"{i} octave{'s' if i > 1 else ''}")
|
|
layout.addWidget(self.octave_range_combo)
|
|
|
|
return group
|
|
|
|
def distribution_quadrant(self):
|
|
"""Distribution with readable buttons and simulator display"""
|
|
group = QGroupBox("Channel Distribution")
|
|
layout = QVBoxLayout(group)
|
|
layout.setSpacing(6)
|
|
layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
layout.addWidget(self.create_parameter_label_with_override("Distribution Pattern:", "channel_distribution"))
|
|
|
|
# 2 rows of 4 distribution buttons
|
|
dist_widget = QWidget()
|
|
dist_layout = QGridLayout(dist_widget)
|
|
dist_layout.setSpacing(0) # NO horizontal spacing
|
|
dist_layout.setVerticalSpacing(2) # Minimal vertical spacing
|
|
dist_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
patterns = ["up", "down", "up_down", "bounce", "random", "cycle", "alternating", "single_channel"]
|
|
for i, pattern in enumerate(patterns):
|
|
display_name = pattern.replace("_", " ").title()
|
|
if len(display_name) > 12:
|
|
display_name = display_name[:12]
|
|
|
|
btn = QPushButton(display_name)
|
|
btn.setFixedSize(120, 22) # Taller buttons for better readability
|
|
btn.setCheckable(True)
|
|
btn.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;")
|
|
btn.clicked.connect(lambda checked, p=pattern: self.on_distribution_clicked(p))
|
|
|
|
if pattern == "up":
|
|
btn.setChecked(True)
|
|
btn.setStyleSheet("background: #0066cc; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #0088ee;")
|
|
|
|
self.distribution_buttons[pattern] = btn
|
|
dist_layout.addWidget(btn, i // 4, i % 4)
|
|
|
|
layout.addWidget(dist_widget)
|
|
|
|
# Description
|
|
self.dist_desc = QLabel("Channels: 1 → 2 → 3 → 4 → 5 → 6...")
|
|
self.dist_desc.setStyleSheet("font-size: 10px; color: gray;")
|
|
layout.addWidget(self.dist_desc)
|
|
|
|
# Simulator display
|
|
if self.simulator:
|
|
from .simulator_display import SimulatorDisplay
|
|
self.simulator_display = SimulatorDisplay(self.simulator, self.channel_manager)
|
|
layout.addWidget(self.simulator_display)
|
|
else:
|
|
# Create placeholder for now
|
|
placeholder = QLabel("Simulator display will appear here")
|
|
placeholder.setStyleSheet("font-size: 10px; color: gray; text-align: center;")
|
|
placeholder.setAlignment(Qt.AlignCenter)
|
|
layout.addWidget(placeholder)
|
|
|
|
return group
|
|
|
|
def pattern_quadrant(self):
|
|
"""Pattern with readable buttons"""
|
|
group = QGroupBox("Pattern Settings")
|
|
layout = QVBoxLayout(group)
|
|
layout.setSpacing(6)
|
|
layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
layout.addWidget(self.create_parameter_label_with_override("Arpeggio Pattern:", "pattern_type"))
|
|
|
|
# 2 rows of 4 pattern buttons
|
|
pattern_widget = QWidget()
|
|
pattern_layout = QGridLayout(pattern_widget)
|
|
pattern_layout.setSpacing(0) # NO horizontal spacing
|
|
pattern_layout.setVerticalSpacing(2) # Minimal vertical spacing
|
|
pattern_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
patterns = ["up", "down", "up_down", "down_up", "random", "chord", "note_order", "custom"]
|
|
for i, pattern in enumerate(patterns):
|
|
display_name = pattern.replace("_", " ").title()
|
|
if len(display_name) > 12:
|
|
display_name = display_name[:12]
|
|
|
|
btn = QPushButton(display_name)
|
|
btn.setFixedSize(120, 22) # Taller buttons for better readability
|
|
btn.setCheckable(True)
|
|
btn.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;")
|
|
btn.clicked.connect(lambda checked, p=pattern: self.on_pattern_clicked(p))
|
|
|
|
if pattern == "up":
|
|
btn.setChecked(True)
|
|
btn.setStyleSheet("background: #cc6600; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #ee8800;")
|
|
|
|
self.pattern_buttons[pattern] = btn
|
|
pattern_layout.addWidget(btn, i // 4, i % 4)
|
|
|
|
layout.addWidget(pattern_widget)
|
|
|
|
# Pattern length buttons
|
|
layout.addWidget(self.create_parameter_label_with_override("Pattern Length:", "pattern_length"))
|
|
length_widget = QWidget()
|
|
length_layout = QGridLayout(length_widget)
|
|
length_layout.setSpacing(0) # NO horizontal spacing
|
|
length_layout.setVerticalSpacing(2) # Minimal vertical spacing
|
|
length_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
self.pattern_length_buttons = {}
|
|
for i in range(1, 17): # 1-16 pattern lengths
|
|
btn = QPushButton(str(i))
|
|
btn.setFixedSize(30, 22) # Smaller buttons for numbers
|
|
btn.setCheckable(True)
|
|
btn.clicked.connect(lambda checked, length=i: self.on_pattern_length_clicked(length))
|
|
|
|
if i == 8: # Default to 8
|
|
btn.setChecked(True)
|
|
self.apply_button_style(btn, 12, "orange")
|
|
else:
|
|
self.apply_button_style(btn, 12, "normal")
|
|
|
|
self.all_buttons.append(btn)
|
|
self.pattern_length_buttons[i] = btn
|
|
length_layout.addWidget(btn, (i-1) // 8, (i-1) % 8) # 2 rows of 8
|
|
|
|
layout.addWidget(length_widget)
|
|
|
|
# Note limit buttons
|
|
layout.addWidget(self.create_parameter_label_with_override("Note Limit:", "note_limit"))
|
|
note_limit_widget = QWidget()
|
|
note_limit_layout = QGridLayout(note_limit_widget)
|
|
note_limit_layout.setSpacing(0) # NO horizontal spacing
|
|
note_limit_layout.setVerticalSpacing(2) # Minimal vertical spacing
|
|
note_limit_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
self.note_limit_buttons = {}
|
|
for i in range(1, 8): # 1-7 note limits (1-7 notes from scale)
|
|
btn = self.create_scalable_button(str(i), 30, 22, 12, checkable=True,
|
|
style_type="blue" if i == 7 else "normal")
|
|
btn.clicked.connect(lambda checked, limit=i: self.on_note_limit_clicked(limit))
|
|
|
|
if i == 7: # Default to 7 (full scale)
|
|
btn.setChecked(True)
|
|
|
|
self.note_limit_buttons[i] = btn
|
|
note_limit_layout.addWidget(btn, 0, i-1) # Single row
|
|
|
|
layout.addWidget(note_limit_widget)
|
|
|
|
return group
|
|
|
|
def timing_quadrant(self):
|
|
"""Timing with readable controls"""
|
|
group = QGroupBox("Timing Settings")
|
|
layout = QVBoxLayout(group)
|
|
layout.setSpacing(6)
|
|
layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
# Tempo
|
|
tempo_layout = QHBoxLayout()
|
|
tempo_layout.addWidget(self.create_parameter_label_with_override("Tempo:", "tempo"))
|
|
self.tempo_spin = QSpinBox()
|
|
self.tempo_spin.setRange(40, 200)
|
|
self.tempo_spin.setValue(120)
|
|
self.tempo_spin.setSuffix(" BPM")
|
|
self.tempo_spin.setFixedHeight(30)
|
|
tempo_layout.addWidget(self.tempo_spin)
|
|
layout.addLayout(tempo_layout)
|
|
|
|
# Speed buttons
|
|
layout.addWidget(self.create_parameter_label_with_override("Note Speed:", "note_speed"))
|
|
speed_widget = QWidget()
|
|
speed_layout = QHBoxLayout(speed_widget)
|
|
speed_layout.setSpacing(0) # NO spacing between buttons
|
|
speed_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
self.speed_buttons = {}
|
|
speeds = ["1/32", "1/16", "1/8", "1/4", "1/2", "1/1"]
|
|
for speed in speeds:
|
|
btn = QPushButton(speed)
|
|
btn.setFixedSize(50, 22) # Taller buttons for better readability
|
|
btn.setCheckable(True)
|
|
btn.clicked.connect(lambda checked, s=speed: self.on_speed_clicked(s))
|
|
|
|
if speed == "1/8":
|
|
btn.setChecked(True)
|
|
self.apply_button_style(btn, 12, "purple")
|
|
else:
|
|
self.apply_button_style(btn, 12, "normal")
|
|
|
|
self.all_buttons.append(btn)
|
|
self.speed_buttons[speed] = btn
|
|
speed_layout.addWidget(btn)
|
|
|
|
layout.addWidget(speed_widget)
|
|
|
|
# Gate
|
|
gate_layout = QHBoxLayout()
|
|
gate_layout.addWidget(self.create_parameter_label_with_override("Gate:", "gate"))
|
|
self.gate_slider = QSlider(Qt.Horizontal)
|
|
self.gate_slider.setRange(10, 200)
|
|
self.gate_slider.setValue(100)
|
|
self.gate_slider.setFixedHeight(25)
|
|
gate_layout.addWidget(self.gate_slider)
|
|
self.gate_label = QLabel("100%")
|
|
self.gate_label.setFixedWidth(40)
|
|
gate_layout.addWidget(self.gate_label)
|
|
layout.addLayout(gate_layout)
|
|
|
|
# Swing
|
|
swing_layout = QHBoxLayout()
|
|
swing_layout.addWidget(self.create_parameter_label_with_override("Swing:", "swing"))
|
|
self.swing_slider = QSlider(Qt.Horizontal)
|
|
self.swing_slider.setRange(-100, 100)
|
|
self.swing_slider.setValue(0)
|
|
self.swing_slider.setFixedHeight(25)
|
|
swing_layout.addWidget(self.swing_slider)
|
|
self.swing_label = QLabel("0%")
|
|
self.swing_label.setFixedWidth(40)
|
|
swing_layout.addWidget(self.swing_label)
|
|
layout.addLayout(swing_layout)
|
|
|
|
# Velocity
|
|
velocity_layout = QHBoxLayout()
|
|
velocity_layout.addWidget(self.create_parameter_label_with_override("Velocity:", "velocity"))
|
|
self.velocity_slider = QSlider(Qt.Horizontal)
|
|
self.velocity_slider.setRange(1, 127)
|
|
self.velocity_slider.setValue(80)
|
|
self.velocity_slider.setFixedHeight(25)
|
|
velocity_layout.addWidget(self.velocity_slider)
|
|
self.velocity_label = QLabel("80")
|
|
self.velocity_label.setFixedWidth(40)
|
|
velocity_layout.addWidget(self.velocity_label)
|
|
layout.addLayout(velocity_layout)
|
|
|
|
# Delay/Echo controls
|
|
delay_layout = QVBoxLayout()
|
|
|
|
# Delay toggle
|
|
self.delay_enabled = False
|
|
delay_toggle_layout = QHBoxLayout()
|
|
delay_toggle_layout.addWidget(self.create_parameter_label_with_override("Delay/Echo:", "delay_enabled"))
|
|
self.delay_toggle = QPushButton("OFF")
|
|
self.delay_toggle.setFixedSize(50, 20)
|
|
self.delay_toggle.setCheckable(True)
|
|
self.delay_toggle.setStyleSheet("background: #5a2d2d; color: white; font-size: 10px; font-weight: bold;")
|
|
self.delay_toggle.clicked.connect(self.on_delay_toggle)
|
|
delay_toggle_layout.addWidget(self.delay_toggle)
|
|
delay_toggle_layout.addStretch()
|
|
delay_layout.addLayout(delay_toggle_layout)
|
|
|
|
# Delay length (0-8 repeats)
|
|
delay_length_layout = QHBoxLayout()
|
|
delay_length_layout.addWidget(self.create_parameter_label_with_override("Delay Length:", "delay_length"))
|
|
self.delay_length_spin = QSpinBox()
|
|
self.delay_length_spin.setRange(0, 8)
|
|
self.delay_length_spin.setValue(3)
|
|
self.delay_length_spin.setSuffix(" repeats")
|
|
self.delay_length_spin.setFixedHeight(30)
|
|
self.delay_length_spin.setEnabled(False)
|
|
delay_length_layout.addWidget(self.delay_length_spin)
|
|
delay_layout.addLayout(delay_length_layout)
|
|
|
|
# Delay timing buttons - single row
|
|
delay_timing_label = self.create_parameter_label_with_override("Delay Timing:", "delay_timing")
|
|
delay_timing_label.setEnabled(False)
|
|
delay_layout.addWidget(delay_timing_label)
|
|
self.delay_timing_label = delay_timing_label
|
|
|
|
delay_timing_widget = QWidget()
|
|
delay_timing_layout = QHBoxLayout(delay_timing_widget)
|
|
delay_timing_layout.setSpacing(0) # NO spacing between buttons
|
|
delay_timing_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
self.delay_timing_buttons = {}
|
|
delay_speeds = ["1/8", "1/8T", "1/4", "1/4T", "1/2", "1/2T", "1/1", "2/1", "2/1T", "4/1", "4/1T"]
|
|
for speed in delay_speeds:
|
|
btn = QPushButton(speed)
|
|
btn.setFixedSize(35, 22) # Consistent height with other buttons
|
|
btn.setCheckable(True)
|
|
btn.setEnabled(False)
|
|
btn.clicked.connect(lambda checked, s=speed: self.on_delay_timing_clicked(s))
|
|
|
|
if speed == "1/4":
|
|
btn.setChecked(True)
|
|
self.apply_button_style(btn, 10, "purple")
|
|
else:
|
|
self.apply_button_style(btn, 10, "disabled")
|
|
|
|
self.all_buttons.append(btn)
|
|
self.delay_timing_buttons[speed] = btn
|
|
delay_timing_layout.addWidget(btn)
|
|
|
|
delay_timing_widget.setEnabled(False)
|
|
self.delay_timing_widget = delay_timing_widget
|
|
delay_layout.addWidget(delay_timing_widget)
|
|
|
|
# Delay fade slider (percentage)
|
|
delay_fade_layout = QHBoxLayout()
|
|
delay_fade_label = self.create_parameter_label_with_override("Delay Fade:", "delay_fade")
|
|
delay_fade_label.setEnabled(False)
|
|
delay_fade_layout.addWidget(delay_fade_label)
|
|
self.delay_fade_label = delay_fade_label
|
|
|
|
self.delay_fade_slider = QSlider(Qt.Horizontal)
|
|
self.delay_fade_slider.setRange(10, 90)
|
|
self.delay_fade_slider.setValue(30) # 30% fade per repeat
|
|
self.delay_fade_slider.setFixedHeight(25)
|
|
self.delay_fade_slider.setEnabled(False)
|
|
delay_fade_layout.addWidget(self.delay_fade_slider)
|
|
|
|
self.delay_fade_value = QLabel("30%")
|
|
self.delay_fade_value.setFixedWidth(40)
|
|
self.delay_fade_value.setEnabled(False)
|
|
delay_fade_layout.addWidget(self.delay_fade_value)
|
|
delay_layout.addLayout(delay_fade_layout)
|
|
|
|
layout.addLayout(delay_layout)
|
|
|
|
# Presets
|
|
preset_layout = QHBoxLayout()
|
|
self.save_btn = QPushButton("Save Preset")
|
|
self.save_btn.setFixedSize(80, 20)
|
|
self.load_btn = QPushButton("Load Preset")
|
|
self.load_btn.setFixedSize(80, 20)
|
|
preset_layout.addWidget(self.save_btn)
|
|
preset_layout.addWidget(self.load_btn)
|
|
preset_layout.addStretch()
|
|
layout.addLayout(preset_layout)
|
|
|
|
return group
|
|
|
|
def connect_signals(self):
|
|
"""Connect all signals"""
|
|
self.tempo_spin.valueChanged.connect(self.on_tempo_changed)
|
|
# Speed is now handled by individual button click handlers
|
|
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)
|
|
self.delay_length_spin.valueChanged.connect(self.on_delay_length_changed)
|
|
self.delay_fade_slider.valueChanged.connect(self.on_delay_fade_changed)
|
|
self.octave_range_combo.currentIndexChanged.connect(self.on_octave_range_changed)
|
|
self.save_btn.clicked.connect(self.save_preset)
|
|
self.load_btn.clicked.connect(self.load_preset)
|
|
|
|
if hasattr(self.arpeggiator, 'armed_state_changed'):
|
|
self.arpeggiator.armed_state_changed.connect(self.update_armed_states)
|
|
self.arpeggiator.settings_changed.connect(self.update_gui_from_engine)
|
|
|
|
# Event handlers
|
|
def on_root_note_clicked(self, note_index):
|
|
midi_note = self.current_octave * 12 + note_index
|
|
|
|
if hasattr(self.arpeggiator, 'is_playing') and self.arpeggiator.is_playing:
|
|
# ARMED STATE - button turns orange, waits for pattern end
|
|
if self.armed_root_note_button:
|
|
self.armed_root_note_button.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;")
|
|
|
|
self.armed_root_note_button = self.root_note_buttons[note_index]
|
|
self.root_note_buttons[note_index].setStyleSheet("background: #ff8800; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #ffaa00;")
|
|
|
|
if hasattr(self.arpeggiator, 'arm_root_note'):
|
|
self.arpeggiator.arm_root_note(midi_note)
|
|
else:
|
|
# IMMEDIATE CHANGE - apply right away
|
|
if self.current_root_note in self.root_note_buttons:
|
|
self.root_note_buttons[self.current_root_note].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;")
|
|
|
|
self.current_root_note = note_index
|
|
self.root_note_buttons[note_index].setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;")
|
|
|
|
# Update scale notes display when root note changes
|
|
self.update_scale_notes_display()
|
|
|
|
if hasattr(self.arpeggiator, 'set_root_note'):
|
|
self.arpeggiator.set_root_note(midi_note)
|
|
|
|
# Update starting scale note position
|
|
self.update_arpeggiator_scale_note()
|
|
|
|
def on_octave_clicked(self, octave):
|
|
midi_note = octave * 12 + self.current_root_note
|
|
|
|
if hasattr(self.arpeggiator, 'is_playing') and self.arpeggiator.is_playing:
|
|
# ARMED STATE - button turns orange
|
|
if self.armed_octave_button:
|
|
self.armed_octave_button.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;")
|
|
|
|
self.armed_octave_button = self.octave_buttons[octave]
|
|
self.octave_buttons[octave].setStyleSheet("background: #ff8800; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #ffaa00;")
|
|
|
|
if hasattr(self.arpeggiator, 'arm_root_note'):
|
|
self.arpeggiator.arm_root_note(midi_note)
|
|
else:
|
|
# IMMEDIATE CHANGE
|
|
if self.current_octave in self.octave_buttons:
|
|
self.octave_buttons[self.current_octave].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;")
|
|
|
|
self.current_octave = octave
|
|
self.octave_buttons[octave].setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;")
|
|
|
|
# Update scale notes display when octave changes
|
|
self.update_scale_notes_display()
|
|
|
|
if hasattr(self.arpeggiator, 'set_root_note'):
|
|
self.arpeggiator.set_root_note(midi_note)
|
|
|
|
# Update starting scale note position
|
|
self.update_arpeggiator_scale_note()
|
|
|
|
def on_scale_clicked(self, scale):
|
|
if hasattr(self.arpeggiator, 'is_playing') and self.arpeggiator.is_playing:
|
|
# ARMED STATE - button turns orange
|
|
if self.armed_scale_button:
|
|
self.armed_scale_button.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;")
|
|
|
|
self.armed_scale_button = self.scale_buttons[scale]
|
|
self.scale_buttons[scale].setStyleSheet("background: #ff8800; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #ffaa00;")
|
|
|
|
if hasattr(self.arpeggiator, 'arm_scale'):
|
|
self.arpeggiator.arm_scale(scale)
|
|
else:
|
|
# IMMEDIATE CHANGE
|
|
if self.current_scale in self.scale_buttons:
|
|
self.scale_buttons[self.current_scale].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;")
|
|
|
|
self.current_scale = scale
|
|
self.scale_buttons[scale].setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;")
|
|
|
|
# Update scale notes display when scale changes
|
|
self.current_scale_note_index = 0 # Reset to root when scale changes
|
|
self.update_scale_notes_display()
|
|
|
|
if hasattr(self.arpeggiator, 'set_scale'):
|
|
self.arpeggiator.set_scale(scale)
|
|
|
|
# Update starting scale note position
|
|
self.update_arpeggiator_scale_note()
|
|
|
|
def on_pattern_clicked(self, pattern):
|
|
if hasattr(self.arpeggiator, 'is_playing') and self.arpeggiator.is_playing:
|
|
# ARMED STATE - button turns orange
|
|
if self.armed_pattern_button:
|
|
self.armed_pattern_button.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;")
|
|
|
|
self.armed_pattern_button = self.pattern_buttons[pattern]
|
|
self.pattern_buttons[pattern].setStyleSheet("background: #ff8800; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #ffaa00;")
|
|
|
|
if hasattr(self.arpeggiator, 'arm_pattern_type'):
|
|
self.arpeggiator.arm_pattern_type(pattern)
|
|
else:
|
|
# IMMEDIATE CHANGE
|
|
if self.current_pattern in self.pattern_buttons:
|
|
self.pattern_buttons[self.current_pattern].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;")
|
|
|
|
self.current_pattern = pattern
|
|
self.pattern_buttons[pattern].setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;")
|
|
|
|
if hasattr(self.arpeggiator, 'set_pattern_type'):
|
|
self.arpeggiator.set_pattern_type(pattern)
|
|
|
|
def on_distribution_clicked(self, pattern):
|
|
if hasattr(self.arpeggiator, 'is_playing') and self.arpeggiator.is_playing:
|
|
# ARMED STATE - button turns orange
|
|
if self.armed_distribution_button:
|
|
self.armed_distribution_button.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;")
|
|
|
|
self.armed_distribution_button = self.distribution_buttons[pattern]
|
|
self.distribution_buttons[pattern].setStyleSheet("background: #ff8800; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #ffaa00;")
|
|
|
|
if hasattr(self.arpeggiator, 'arm_channel_distribution'):
|
|
self.arpeggiator.arm_channel_distribution(pattern)
|
|
else:
|
|
# IMMEDIATE CHANGE
|
|
if self.current_distribution in self.distribution_buttons:
|
|
self.distribution_buttons[self.current_distribution].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;")
|
|
|
|
self.current_distribution = pattern
|
|
self.distribution_buttons[pattern].setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;")
|
|
|
|
if hasattr(self.arpeggiator, 'set_channel_distribution'):
|
|
self.arpeggiator.set_channel_distribution(pattern)
|
|
|
|
def on_speed_clicked(self, speed):
|
|
# Speed changes apply immediately (no armed state needed for timing)
|
|
if self.current_speed in self.speed_buttons:
|
|
self.apply_button_style(self.speed_buttons[self.current_speed], 12, "normal")
|
|
|
|
self.current_speed = speed
|
|
self.apply_button_style(self.speed_buttons[speed], 12, "purple")
|
|
|
|
if hasattr(self.arpeggiator, 'set_note_speed'):
|
|
self.arpeggiator.set_note_speed(speed)
|
|
|
|
def on_pattern_length_clicked(self, length):
|
|
# Pattern length changes are armed (applied at pattern end)
|
|
if self.armed_pattern_length_button:
|
|
self.apply_button_style(self.armed_pattern_length_button, 12, "normal")
|
|
|
|
self.armed_pattern_length_button = self.pattern_length_buttons[length]
|
|
self.apply_button_style(self.pattern_length_buttons[length], 12, "orange")
|
|
|
|
if hasattr(self.arpeggiator, 'set_pattern_length'):
|
|
self.arpeggiator.set_pattern_length(length)
|
|
|
|
def on_note_limit_clicked(self, limit):
|
|
"""Handle note limit button clicks (armed - applied at pattern end)"""
|
|
if self.armed_note_limit_button:
|
|
self.apply_button_style(self.armed_note_limit_button, 12, "normal")
|
|
|
|
self.armed_note_limit_button = self.note_limit_buttons[limit]
|
|
self.apply_button_style(self.note_limit_buttons[limit], 12, "orange")
|
|
|
|
if hasattr(self.arpeggiator, 'set_note_limit'):
|
|
self.arpeggiator.set_note_limit(limit)
|
|
|
|
def on_delay_toggle(self):
|
|
"""Handle delay on/off toggle"""
|
|
self.delay_enabled = self.delay_toggle.isChecked()
|
|
|
|
if self.delay_enabled:
|
|
self.delay_toggle.setText("ON")
|
|
self.delay_toggle.setStyleSheet("background: #2d5a2d; color: white; font-size: 10px; font-weight: bold;")
|
|
# Enable all delay controls
|
|
self.delay_length_spin.setEnabled(True)
|
|
self.delay_timing_label.setEnabled(True)
|
|
self.delay_timing_widget.setEnabled(True)
|
|
self.delay_fade_label.setEnabled(True)
|
|
self.delay_fade_slider.setEnabled(True)
|
|
self.delay_fade_value.setEnabled(True)
|
|
|
|
# Enable timing buttons and update their style
|
|
for btn in self.delay_timing_buttons.values():
|
|
btn.setEnabled(True)
|
|
if btn.isChecked():
|
|
btn.setStyleSheet("background: #9933cc; color: white; font-size: 10px; font-weight: bold; padding: 0px; border: 1px solid #bb55ee;")
|
|
else:
|
|
btn.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 10px; font-weight: bold; padding: 0px; border: 1px solid #555555;")
|
|
else:
|
|
self.delay_toggle.setText("OFF")
|
|
self.delay_toggle.setStyleSheet("background: #5a2d2d; color: white; font-size: 10px; font-weight: bold;")
|
|
# Disable all delay controls
|
|
self.delay_length_spin.setEnabled(False)
|
|
self.delay_timing_label.setEnabled(False)
|
|
self.delay_timing_widget.setEnabled(False)
|
|
self.delay_fade_label.setEnabled(False)
|
|
self.delay_fade_slider.setEnabled(False)
|
|
self.delay_fade_value.setEnabled(False)
|
|
|
|
# Disable timing buttons and dim their style
|
|
for btn in self.delay_timing_buttons.values():
|
|
btn.setEnabled(False)
|
|
btn.setStyleSheet("background: #2a2a2a; color: #666666; font-size: 10px; font-weight: bold; padding: 0px; border: 1px solid #333333;")
|
|
|
|
if hasattr(self.arpeggiator, 'set_delay_enabled'):
|
|
self.arpeggiator.set_delay_enabled(self.delay_enabled)
|
|
|
|
def on_delay_timing_clicked(self, timing):
|
|
"""Handle delay timing button clicks"""
|
|
if self.current_delay_timing in self.delay_timing_buttons:
|
|
self.apply_button_style(self.delay_timing_buttons[self.current_delay_timing], 10, "disabled")
|
|
|
|
self.current_delay_timing = timing
|
|
self.apply_button_style(self.delay_timing_buttons[timing], 10, "purple")
|
|
|
|
if hasattr(self.arpeggiator, 'set_delay_timing'):
|
|
self.arpeggiator.set_delay_timing(timing)
|
|
|
|
def on_delay_length_changed(self, length):
|
|
"""Handle delay length changes"""
|
|
if hasattr(self.arpeggiator, 'set_delay_length'):
|
|
self.arpeggiator.set_delay_length(length)
|
|
|
|
def on_delay_fade_changed(self, fade_percent):
|
|
"""Handle delay fade changes"""
|
|
self.delay_fade_value.setText(f"{fade_percent}%")
|
|
if hasattr(self.arpeggiator, 'set_delay_fade'):
|
|
self.arpeggiator.set_delay_fade(fade_percent / 100.0) # Convert to 0-1 range
|
|
|
|
@pyqtSlot(int)
|
|
def on_tempo_changed(self, tempo):
|
|
if hasattr(self.arpeggiator, 'set_tempo'):
|
|
self.arpeggiator.set_tempo(float(tempo))
|
|
|
|
# on_speed_changed removed - now using on_speed_clicked with buttons
|
|
|
|
@pyqtSlot(int)
|
|
def on_gate_changed(self, value):
|
|
self.gate_label.setText(f"{value}%")
|
|
if hasattr(self.arpeggiator, 'set_gate'):
|
|
self.arpeggiator.set_gate(value / 100.0)
|
|
|
|
@pyqtSlot(int)
|
|
def on_swing_changed(self, value):
|
|
self.swing_label.setText(f"{value}%")
|
|
if hasattr(self.arpeggiator, 'set_swing'):
|
|
self.arpeggiator.set_swing(value / 100.0)
|
|
|
|
@pyqtSlot(int)
|
|
def on_velocity_changed(self, value):
|
|
self.velocity_label.setText(str(value))
|
|
if hasattr(self.arpeggiator, 'set_velocity'):
|
|
self.arpeggiator.set_velocity(value)
|
|
|
|
@pyqtSlot(int)
|
|
def on_octave_range_changed(self, index):
|
|
if hasattr(self.arpeggiator, 'set_octave_range'):
|
|
self.arpeggiator.set_octave_range(index + 1)
|
|
|
|
def save_preset(self):
|
|
preset_name = f"Preset_{len(self.presets) + 1}"
|
|
self.presets[preset_name] = {
|
|
'root_note': self.current_root_note,
|
|
'octave': self.current_octave,
|
|
'scale': self.current_scale,
|
|
'pattern': self.current_pattern,
|
|
'distribution': self.current_distribution
|
|
}
|
|
print(f"Saved {preset_name}")
|
|
|
|
def load_preset(self):
|
|
if not self.presets:
|
|
print("No presets saved")
|
|
return
|
|
preset_name = list(self.presets.keys())[0]
|
|
preset = self.presets[preset_name]
|
|
# Apply preset logic here
|
|
print(f"Loaded {preset_name}")
|
|
|
|
@pyqtSlot()
|
|
def update_armed_states(self):
|
|
"""Handle armed state updates - orange buttons become green at pattern end"""
|
|
# Check if armed states were applied (armed values become None when applied)
|
|
|
|
# Root note armed -> active
|
|
if self.armed_root_note_button and hasattr(self.arpeggiator, 'armed_root_note') and self.arpeggiator.armed_root_note is None:
|
|
# Find which note this was
|
|
for note_index, btn in self.root_note_buttons.items():
|
|
if btn == self.armed_root_note_button:
|
|
# Clear old active
|
|
if self.current_root_note in self.root_note_buttons:
|
|
self.root_note_buttons[self.current_root_note].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;")
|
|
# Set new active (orange -> green)
|
|
self.current_root_note = note_index
|
|
btn.setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;")
|
|
self.armed_root_note_button = None
|
|
# Update scale notes display when root note changes
|
|
self.update_scale_notes_display()
|
|
self.update_arpeggiator_scale_note() # Sync with engine
|
|
break
|
|
|
|
# Octave armed -> active
|
|
if self.armed_octave_button and hasattr(self.arpeggiator, 'armed_root_note') and self.arpeggiator.armed_root_note is None:
|
|
for octave, btn in self.octave_buttons.items():
|
|
if btn == self.armed_octave_button:
|
|
if self.current_octave in self.octave_buttons:
|
|
self.octave_buttons[self.current_octave].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;")
|
|
self.current_octave = octave
|
|
btn.setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;")
|
|
self.armed_octave_button = None
|
|
# Update scale notes display when octave changes
|
|
self.update_scale_notes_display()
|
|
self.update_arpeggiator_scale_note() # Sync with engine
|
|
break
|
|
|
|
# Scale armed -> active
|
|
if self.armed_scale_button and hasattr(self.arpeggiator, 'armed_scale') and self.arpeggiator.armed_scale is None:
|
|
for scale, btn in self.scale_buttons.items():
|
|
if btn == self.armed_scale_button:
|
|
if self.current_scale in self.scale_buttons:
|
|
self.scale_buttons[self.current_scale].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;")
|
|
self.current_scale = scale
|
|
btn.setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;")
|
|
self.armed_scale_button = None
|
|
# Update scale notes display when scale changes
|
|
self.current_scale_note_index = 0 # Reset to root when scale changes
|
|
self.update_scale_notes_display()
|
|
self.update_arpeggiator_scale_note() # Sync with engine
|
|
break
|
|
|
|
# Pattern armed -> active
|
|
if self.armed_pattern_button and hasattr(self.arpeggiator, 'armed_pattern_type') and self.arpeggiator.armed_pattern_type is None:
|
|
for pattern, btn in self.pattern_buttons.items():
|
|
if btn == self.armed_pattern_button:
|
|
if self.current_pattern in self.pattern_buttons:
|
|
self.pattern_buttons[self.current_pattern].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;")
|
|
self.current_pattern = pattern
|
|
btn.setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;")
|
|
self.armed_pattern_button = None
|
|
break
|
|
|
|
# Distribution armed -> active
|
|
if self.armed_distribution_button and hasattr(self.arpeggiator, 'armed_channel_distribution') and self.arpeggiator.armed_channel_distribution is None:
|
|
for distribution, btn in self.distribution_buttons.items():
|
|
if btn == self.armed_distribution_button:
|
|
if self.current_distribution in self.distribution_buttons:
|
|
self.distribution_buttons[self.current_distribution].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;")
|
|
self.current_distribution = distribution
|
|
btn.setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;")
|
|
self.armed_distribution_button = None
|
|
break
|
|
|
|
# Scale note armed -> active
|
|
if self.armed_scale_note_button and hasattr(self.arpeggiator, 'armed_scale_note_start') and self.arpeggiator.armed_scale_note_start is None:
|
|
for scale_note_index, btn in self.scale_notes_buttons.items():
|
|
if btn == self.armed_scale_note_button:
|
|
# Clear old active scale note
|
|
if self.current_scale_note_index in self.scale_notes_buttons:
|
|
self.scale_notes_buttons[self.current_scale_note_index].setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #555555;")
|
|
# Set new active scale note (orange -> blue)
|
|
self.current_scale_note_index = scale_note_index
|
|
btn.setStyleSheet("background: #0099ff; color: white; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #00bbff;")
|
|
self.armed_scale_note_button = None
|
|
break
|
|
|
|
# Pattern length armed -> active
|
|
if self.armed_pattern_length_button and hasattr(self.arpeggiator, 'armed_pattern_length') and self.arpeggiator.armed_pattern_length is None:
|
|
for length, btn in self.pattern_length_buttons.items():
|
|
if btn == self.armed_pattern_length_button:
|
|
# Clear old active pattern length
|
|
if self.current_pattern_length in self.pattern_length_buttons:
|
|
self.apply_button_style(self.pattern_length_buttons[self.current_pattern_length], 12, "normal")
|
|
# Set new active pattern length (orange -> orange)
|
|
self.current_pattern_length = length
|
|
self.apply_button_style(btn, 12, "orange")
|
|
self.armed_pattern_length_button = None
|
|
break
|
|
|
|
# Note limit armed -> active
|
|
if self.armed_note_limit_button and hasattr(self.arpeggiator, 'armed_note_limit') and self.arpeggiator.armed_note_limit is None:
|
|
for limit, btn in self.note_limit_buttons.items():
|
|
if btn == self.armed_note_limit_button:
|
|
# Clear old active note limit
|
|
if self.current_note_limit in self.note_limit_buttons:
|
|
self.apply_button_style(self.note_limit_buttons[self.current_note_limit], 12, "normal")
|
|
# Set new active note limit (orange -> blue)
|
|
self.current_note_limit = limit
|
|
self.apply_button_style(btn, 12, "blue")
|
|
self.armed_note_limit_button = None
|
|
break
|
|
|
|
# Speed changes apply immediately - no armed state needed
|
|
|
|
def update_gui_from_engine(self):
|
|
"""Update all GUI controls to match engine settings"""
|
|
try:
|
|
# Update scale buttons
|
|
if hasattr(self, 'scale_buttons'):
|
|
# Clear current scale styling
|
|
if hasattr(self, 'current_scale') and self.current_scale in self.scale_buttons:
|
|
self.scale_buttons[self.current_scale].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;")
|
|
# Set new active scale
|
|
self.current_scale = self.arpeggiator.scale
|
|
if self.current_scale in self.scale_buttons:
|
|
self.scale_buttons[self.current_scale].setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;")
|
|
|
|
# Update pattern buttons
|
|
if hasattr(self, 'pattern_buttons'):
|
|
# Clear current pattern styling
|
|
if hasattr(self, 'current_pattern') and self.current_pattern in self.pattern_buttons:
|
|
self.pattern_buttons[self.current_pattern].setStyleSheet("font-size: 12px; font-weight: bold; padding: 0px;")
|
|
# Set new active pattern
|
|
self.current_pattern = self.arpeggiator.pattern_type
|
|
if self.current_pattern in self.pattern_buttons:
|
|
self.pattern_buttons[self.current_pattern].setStyleSheet("background: #4CAF50; color: white; font-size: 12px; font-weight: bold; padding: 0px;")
|
|
|
|
# Update scale note buttons
|
|
if hasattr(self, 'scale_notes_buttons'):
|
|
# Clear current scale note styling
|
|
if hasattr(self, 'current_scale_note_index') and self.current_scale_note_index in self.scale_notes_buttons:
|
|
self.scale_notes_buttons[self.current_scale_note_index].setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #555555;")
|
|
# Set new active scale note
|
|
self.current_scale_note_index = getattr(self.arpeggiator, 'scale_note_start', 0)
|
|
if self.current_scale_note_index in self.scale_notes_buttons:
|
|
self.scale_notes_buttons[self.current_scale_note_index].setStyleSheet("background: #0099ff; color: white; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #00bbff;")
|
|
|
|
# Update note speed buttons
|
|
if hasattr(self, 'speed_buttons'):
|
|
# Clear current speed styling
|
|
if hasattr(self, 'current_speed') and self.current_speed in self.speed_buttons:
|
|
self.speed_buttons[self.current_speed].setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;")
|
|
# Set new active speed
|
|
self.current_speed = self.arpeggiator.note_speed
|
|
if self.current_speed in self.speed_buttons:
|
|
self.speed_buttons[self.current_speed].setStyleSheet("background: #9933cc; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #bb55ee;")
|
|
|
|
# Update pattern length buttons
|
|
if hasattr(self, 'pattern_length_buttons'):
|
|
# Clear current pattern length styling
|
|
if hasattr(self, 'current_pattern_length') and self.current_pattern_length in self.pattern_length_buttons:
|
|
self.apply_button_style(self.pattern_length_buttons[self.current_pattern_length], 12, "normal")
|
|
# Set new active pattern length
|
|
if hasattr(self.arpeggiator, 'user_pattern_length'):
|
|
self.current_pattern_length = self.arpeggiator.user_pattern_length
|
|
if self.current_pattern_length in self.pattern_length_buttons:
|
|
self.apply_button_style(self.pattern_length_buttons[self.current_pattern_length], 12, "orange")
|
|
|
|
# Update note limit buttons
|
|
if hasattr(self, 'note_limit_buttons'):
|
|
# Clear current note limit styling
|
|
if hasattr(self, 'current_note_limit') and self.current_note_limit in self.note_limit_buttons:
|
|
self.apply_button_style(self.note_limit_buttons[self.current_note_limit], 12, "normal")
|
|
# Set new active note limit
|
|
if hasattr(self.arpeggiator, 'note_limit'):
|
|
self.current_note_limit = self.arpeggiator.note_limit
|
|
if self.current_note_limit in self.note_limit_buttons:
|
|
self.apply_button_style(self.note_limit_buttons[self.current_note_limit], 12, "blue")
|
|
|
|
# Update delay controls
|
|
if hasattr(self, 'delay_enabled_checkbox'):
|
|
self.delay_enabled_checkbox.setChecked(self.arpeggiator.delay_enabled)
|
|
if hasattr(self, 'delay_length_spin'):
|
|
self.delay_length_spin.setValue(self.arpeggiator.delay_length)
|
|
if hasattr(self, 'delay_fade_slider'):
|
|
self.delay_fade_slider.setValue(int(self.arpeggiator.delay_fade * 100))
|
|
|
|
# Update sliders and spinboxes
|
|
if hasattr(self, 'gate_slider'):
|
|
self.gate_slider.setValue(int(self.arpeggiator.gate * 100))
|
|
if hasattr(self, 'swing_slider'):
|
|
self.swing_slider.setValue(int(self.arpeggiator.swing * 100))
|
|
if hasattr(self, 'velocity_slider'):
|
|
self.velocity_slider.setValue(self.arpeggiator.velocity)
|
|
if hasattr(self, 'octave_range_combo'):
|
|
self.octave_range_combo.setCurrentIndex(self.arpeggiator.octave_range - 1)
|
|
if hasattr(self, 'tempo_spin'):
|
|
self.tempo_spin.setValue(int(self.arpeggiator.tempo))
|
|
|
|
except Exception as e:
|
|
print(f"Error updating GUI from engine: {e}")
|
|
|
|
def update_scale_notes_display(self):
|
|
"""Update the scale notes buttons based on current root note and scale"""
|
|
# Clear existing buttons
|
|
for button in self.scale_notes_buttons.values():
|
|
button.deleteLater()
|
|
self.scale_notes_buttons.clear()
|
|
|
|
# Get the scale definition
|
|
scale_intervals = self.arpeggiator.SCALES.get(self.current_scale, [0, 2, 4, 5, 7, 9, 11])
|
|
|
|
# Note names for display
|
|
note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
|
|
|
|
# Calculate the actual MIDI notes for this scale
|
|
root_midi = self.current_octave * 12 + self.current_root_note
|
|
scale_notes = []
|
|
for interval in scale_intervals:
|
|
scale_notes.append(root_midi + interval)
|
|
|
|
# Create buttons for each scale note
|
|
for i, midi_note in enumerate(scale_notes):
|
|
note_name = note_names[midi_note % 12]
|
|
octave = midi_note // 12
|
|
display_text = f"{note_name}{octave}"
|
|
|
|
btn = QPushButton(display_text)
|
|
btn.setFixedSize(50, 25)
|
|
btn.setCheckable(True)
|
|
btn.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #555555;")
|
|
btn.clicked.connect(lambda checked, idx=i: self.on_scale_note_clicked(idx))
|
|
|
|
# Set first note (root) as selected by default
|
|
if i == self.current_scale_note_index:
|
|
btn.setChecked(True)
|
|
btn.setStyleSheet("background: #0099ff; color: white; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #00bbff;")
|
|
|
|
self.scale_notes_buttons[i] = btn
|
|
self.scale_notes_layout.addWidget(btn, 0, i)
|
|
|
|
def on_scale_note_clicked(self, scale_note_index):
|
|
"""Handle scale note selection with armed state support"""
|
|
if hasattr(self.arpeggiator, 'is_playing') and self.arpeggiator.is_playing:
|
|
# ARMED STATE - button turns orange, waits for pattern end
|
|
if self.armed_scale_note_button:
|
|
# Reset previous armed button
|
|
old_armed_index = None
|
|
for idx, btn in self.scale_notes_buttons.items():
|
|
if btn == self.armed_scale_note_button:
|
|
old_armed_index = idx
|
|
break
|
|
if old_armed_index is not None:
|
|
if old_armed_index == self.current_scale_note_index:
|
|
# It was the current active note, make it blue again
|
|
self.armed_scale_note_button.setStyleSheet("background: #0099ff; color: white; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #00bbff;")
|
|
else:
|
|
# It was just armed, make it gray again
|
|
self.armed_scale_note_button.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #555555;")
|
|
|
|
# Set new armed button to orange
|
|
self.armed_scale_note_button = self.scale_notes_buttons[scale_note_index]
|
|
self.scale_notes_buttons[scale_note_index].setStyleSheet("background: #ff8800; color: white; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #ffaa00;")
|
|
|
|
# Arm the scale note change in the engine
|
|
if hasattr(self.arpeggiator, 'arm_scale_note_start'):
|
|
self.arpeggiator.arm_scale_note_start(scale_note_index)
|
|
else:
|
|
# IMMEDIATE CHANGE - apply right away
|
|
old_index = self.current_scale_note_index
|
|
self.current_scale_note_index = scale_note_index
|
|
|
|
# Update button styling
|
|
if old_index in self.scale_notes_buttons:
|
|
self.scale_notes_buttons[old_index].setChecked(False)
|
|
self.scale_notes_buttons[old_index].setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #555555;")
|
|
|
|
if scale_note_index in self.scale_notes_buttons:
|
|
self.scale_notes_buttons[scale_note_index].setChecked(True)
|
|
self.scale_notes_buttons[scale_note_index].setStyleSheet("background: #0099ff; color: white; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #00bbff;")
|
|
|
|
# Update arpeggiator engine with new starting scale note
|
|
self.update_arpeggiator_scale_note()
|
|
|
|
def update_arpeggiator_scale_note(self):
|
|
"""Update the arpeggiator engine with the selected scale note starting position"""
|
|
if hasattr(self.arpeggiator, 'set_scale_note_start'):
|
|
self.arpeggiator.set_scale_note_start(self.current_scale_note_index)
|