@ -25,6 +25,7 @@ class ArpeggiatorControls(QWidget):
self . pattern_buttons = { }
self . distribution_buttons = { }
self . speed_buttons = { }
self . pattern_length_buttons = { }
self . current_root_note = 0
self . current_octave = 4
@ -32,6 +33,8 @@ class ArpeggiatorControls(QWidget):
self . current_pattern = " up "
self . current_distribution = " up "
self . current_speed = " 1/8 "
self . current_pattern_length = 8
self . current_delay_timing = " 1/8 "
# Armed state tracking
self . armed_root_note_button = None
@ -248,6 +251,31 @@ class ArpeggiatorControls(QWidget):
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 ) :
@ -332,6 +360,87 @@ class ArpeggiatorControls(QWidget):
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 ( 20 )
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 = QHBoxLayout ( delay_timing_widget )
delay_timing_layout . setSpacing ( 0 )
delay_timing_layout . setContentsMargins ( 0 , 0 , 0 , 0 )
self . delay_timing_buttons = { }
delay_speeds = [ " 1/32 " , " 1/16 " , " 1/8 " , " 1/4 " , " 1/2 " , " 1/1 " ]
for speed in delay_speeds :
btn = QPushButton ( speed )
btn . setFixedSize ( 40 , 18 )
btn . setCheckable ( True )
btn . setStyleSheet ( " background: #2a2a2a; color: #666666; font-size: 10px; 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/8 " :
btn . setChecked ( True )
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 = 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 ( 20 )
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 " )
@ -352,6 +461,8 @@ class ArpeggiatorControls(QWidget):
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 )
@ -485,6 +596,80 @@ class ArpeggiatorControls(QWidget):
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 ' ) :