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.
 

1129 lines
55 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 = {}
# 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_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
# Speed changes apply immediately - no armed state needed
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;
}}
""")
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")
else:
self.apply_button_style(button, 12, "normal")
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(QLabel("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(QLabel("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(QLabel("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(QLabel("Scale Notes:"))
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(QLabel("Distribution Pattern:"))
# 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(QLabel("Arpeggio Pattern:"))
# 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(QLabel("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.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;")
btn.clicked.connect(lambda checked, length=i: self.on_pattern_length_clicked(length))
if i == 8: # Default to 8
btn.setChecked(True)
btn.setStyleSheet("background: #cc6600; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #ee8800;")
self.pattern_length_buttons[i] = btn
length_layout.addWidget(btn, i // 8, i % 8) # 2 rows of 8
layout.addWidget(length_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(QLabel("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(QLabel("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.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;")
btn.clicked.connect(lambda checked, s=speed: self.on_speed_clicked(s))
if speed == "1/8":
btn.setChecked(True)
btn.setStyleSheet("background: #9933cc; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #bb55ee;")
self.speed_buttons[speed] = btn
speed_layout.addWidget(btn)
layout.addWidget(speed_widget)
# Gate
gate_layout = QHBoxLayout()
gate_layout.addWidget(QLabel("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(QLabel("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(QLabel("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(QLabel("Delay/Echo:"))
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(QLabel("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 (same as note speed)
delay_timing_label = QLabel("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 = QGridLayout(delay_timing_widget)
delay_timing_layout.setSpacing(0)
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 i, speed in enumerate(delay_speeds):
btn = QPushButton(speed)
btn.setFixedSize(35, 18) # Slightly smaller to fit more
btn.setCheckable(True)
btn.setStyleSheet("background: #2a2a2a; color: #666666; font-size: 9px; font-weight: bold; padding: 0px; border: 1px solid #333333;")
btn.setEnabled(False)
btn.clicked.connect(lambda checked, s=speed: self.on_delay_timing_clicked(s))
if speed == "1/4":
btn.setChecked(True)
self.delay_timing_buttons[speed] = btn
# Arrange in 2 rows (6 on top, 5 on bottom)
row = i // 6
col = i % 6
delay_timing_layout.addWidget(btn, row, col)
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 = QLabel("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.speed_buttons[self.current_speed].setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;")
self.current_speed = speed
self.speed_buttons[speed].setStyleSheet("background: #9933cc; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #bb55ee;")
if hasattr(self.arpeggiator, 'set_note_speed'):
self.arpeggiator.set_note_speed(speed)
def on_pattern_length_clicked(self, length):
# Pattern length changes apply immediately
if self.current_pattern_length in self.pattern_length_buttons:
self.pattern_length_buttons[self.current_pattern_length].setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;")
self.current_pattern_length = length
self.pattern_length_buttons[length].setStyleSheet("background: #cc6600; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #ee8800;")
if hasattr(self.arpeggiator, 'set_pattern_length'):
self.arpeggiator.set_pattern_length(length)
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.delay_timing_buttons[self.current_delay_timing].setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 10px; font-weight: bold; padding: 0px; border: 1px solid #555555;")
self.current_delay_timing = timing
self.delay_timing_buttons[timing].setStyleSheet("background: #9933cc; color: white; font-size: 10px; font-weight: bold; padding: 0px; border: 1px solid #bb55ee;")
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
# 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.pattern_length_buttons[self.current_pattern_length].setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;")
# 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.pattern_length_buttons[self.current_pattern_length].setStyleSheet("background: #cc6600; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #ee8800;")
# 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)