@ -22,6 +22,7 @@ class ArpeggiatorControls(QWidget):
self . root_note_buttons = { }
self . octave_buttons = { }
self . scale_buttons = { }
self . scale_notes_buttons = { }
self . pattern_buttons = { }
self . distribution_buttons = { }
self . speed_buttons = { }
@ -40,6 +41,7 @@ class ArpeggiatorControls(QWidget):
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
@ -148,10 +150,22 @@ class ArpeggiatorControls(QWidget):
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 ( 20 )
self . octave_range_combo . setFixedHeight ( 3 0)
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 )
@ -292,7 +306,7 @@ class ArpeggiatorControls(QWidget):
self . tempo_spin . setRange ( 40 , 200 )
self . tempo_spin . setValue ( 120 )
self . tempo_spin . setSuffix ( " BPM " )
self . tempo_spin . setFixedHeight ( 2 0)
self . tempo_spin . setFixedHeight ( 3 0)
tempo_layout . addWidget ( self . tempo_spin )
layout . addLayout ( tempo_layout )
@ -327,7 +341,7 @@ class ArpeggiatorControls(QWidget):
self . gate_slider = QSlider ( Qt . Horizontal )
self . gate_slider . setRange ( 10 , 200 )
self . gate_slider . setValue ( 100 )
self . gate_slider . setFixedHeight ( 20 )
self . gate_slider . setFixedHeight ( 25 )
gate_layout . addWidget ( self . gate_slider )
self . gate_label = QLabel ( " 100 % " )
self . gate_label . setFixedWidth ( 40 )
@ -340,7 +354,7 @@ class ArpeggiatorControls(QWidget):
self . swing_slider = QSlider ( Qt . Horizontal )
self . swing_slider . setRange ( - 100 , 100 )
self . swing_slider . setValue ( 0 )
self . swing_slider . setFixedHeight ( 20 )
self . swing_slider . setFixedHeight ( 25 )
swing_layout . addWidget ( self . swing_slider )
self . swing_label = QLabel ( " 0 % " )
self . swing_label . setFixedWidth ( 40 )
@ -353,7 +367,7 @@ class ArpeggiatorControls(QWidget):
self . velocity_slider = QSlider ( Qt . Horizontal )
self . velocity_slider . setRange ( 1 , 127 )
self . velocity_slider . setValue ( 80 )
self . velocity_slider . setFixedHeight ( 20 )
self . velocity_slider . setFixedHeight ( 25 )
velocity_layout . addWidget ( self . velocity_slider )
self . velocity_label = QLabel ( " 80 " )
self . velocity_label . setFixedWidth ( 40 )
@ -383,7 +397,7 @@ class ArpeggiatorControls(QWidget):
self . delay_length_spin . setRange ( 0 , 8 )
self . delay_length_spin . setValue ( 3 )
self . delay_length_spin . setSuffix ( " repeats " )
self . delay_length_spin . setFixedHeight ( 2 0)
self . delay_length_spin . setFixedHeight ( 3 0)
self . delay_length_spin . setEnabled ( False )
delay_length_layout . addWidget ( self . delay_length_spin )
delay_layout . addLayout ( delay_length_layout )
@ -432,7 +446,7 @@ class ArpeggiatorControls(QWidget):
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 . setFixedHeight ( 25 )
self . delay_fade_slider . setEnabled ( False )
delay_fade_layout . addWidget ( self . delay_fade_slider )
@ -472,6 +486,7 @@ class ArpeggiatorControls(QWidget):
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 ) :
@ -495,9 +510,15 @@ class ArpeggiatorControls(QWidget):
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
@ -519,9 +540,15 @@ class ArpeggiatorControls(QWidget):
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
@ -541,9 +568,16 @@ class ArpeggiatorControls(QWidget):
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
@ -740,6 +774,9 @@ class ArpeggiatorControls(QWidget):
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
@ -751,6 +788,9 @@ class ArpeggiatorControls(QWidget):
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
@ -762,6 +802,10 @@ class ArpeggiatorControls(QWidget):
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
@ -786,4 +830,181 @@ class ArpeggiatorControls(QWidget):
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 )