Browse Source

Add Note Limit feature and fix armed system for pattern settings

Major updates:
- Add Note Limit (1-7) to pattern settings - restricts which notes from scale are used
- Fix pattern length and note limit to use proper armed system
- Both settings now arm (orange) and apply at pattern end without interference
- Add spacebar emergency stop (stop-only, doesn't start playback)
- Pattern generation respects note limit for all pattern types
- Note limit included in preset save/load system
- Updated status bar to reflect emergency stop functionality

Example: Scale=C major, Note Limit=3, Pattern Length=8
Result: C,D,E,C,D,E,C,D (then pattern repeats)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
master
melancholytron 2 months ago
parent
commit
6f2517f653
  1. BIN
      core/__pycache__/arpeggiator_engine.cpython-310.pyc
  2. 82
      core/arpeggiator_engine.py
  3. BIN
      gui/__pycache__/arpeggiator_controls.cpython-310.pyc
  4. BIN
      gui/__pycache__/main_window.cpython-310.pyc
  5. BIN
      gui/__pycache__/preset_controls.cpython-310.pyc
  6. 88
      gui/arpeggiator_controls.py
  7. 7
      gui/main_window.py
  8. 6
      gui/preset_controls.py

BIN
core/__pycache__/arpeggiator_engine.cpython-310.pyc

82
core/arpeggiator_engine.py

@ -96,6 +96,8 @@ class ArpeggiatorEngine(QObject):
self.armed_pattern_type = None
self.armed_channel_distribution = None
self.armed_scale_note_start = None
self.armed_pattern_length = None
self.armed_note_limit = None
self.armed_preset_data = None
self.preset_apply_callback = None # Callback function for applying presets
@ -116,6 +118,7 @@ class ArpeggiatorEngine(QObject):
self.current_step = 0
self.pattern_length = 0
self.user_pattern_length = 8 # User-settable pattern length (1-16)
self.note_limit = 7 # Limit how many notes from the scale to use (1-7)
# Delay/Echo settings
self.delay_enabled = False
@ -291,6 +294,8 @@ class ArpeggiatorEngine(QObject):
self.armed_pattern_type is not None,
self.armed_channel_distribution is not None,
self.armed_scale_note_start is not None,
self.armed_pattern_length is not None,
self.armed_note_limit is not None,
self.armed_preset_data is not None
])
@ -310,6 +315,8 @@ class ArpeggiatorEngine(QObject):
self.armed_pattern_type = None
self.armed_channel_distribution = None
self.armed_scale_note_start = None
self.armed_pattern_length = None
self.armed_note_limit = None
self.armed_preset_data = None
self.armed_timeout.stop() # Stop timeout timer
self.armed_state_changed.emit()
@ -328,12 +335,16 @@ class ArpeggiatorEngine(QObject):
self.tempo_changed.emit(bpm)
def set_pattern_length(self, length: int):
"""Set user-defined pattern length"""
"""Set user-defined pattern length (armed - applies at pattern end)"""
if 1 <= length <= 16:
self.user_pattern_length = length
# Update volume engine bar length to match pattern length
self.volume_engine.set_bar_length(length)
self.regenerate_pattern()
self.armed_pattern_length = length
self.armed_state_changed.emit()
def set_note_limit(self, limit: int):
"""Set the note limit (1-7) - how many notes from the scale to use (armed - applies at pattern end)"""
if 1 <= limit <= 7:
self.armed_note_limit = limit
self.armed_state_changed.emit()
def set_delay_enabled(self, enabled: bool):
"""Enable or disable delay/echo"""
@ -632,8 +643,41 @@ class ArpeggiatorEngine(QObject):
return notes
def _get_limited_scale_notes(self) -> List[int]:
"""Get scale notes limited by note_limit setting"""
scale_intervals = self.SCALES[self.scale]
notes = []
# Calculate the starting note based on root note and scale_note_start
base_octave = self.root_note // 12
root_in_octave = self.root_note % 12
# Get the interval for the selected scale degree
start_degree = self.scale_note_start % len(scale_intervals)
# Generate only the number of notes specified by note_limit
for i in range(self.note_limit):
degree = (start_degree + i) % len(scale_intervals)
# If we wrapped around, go to next octave
if i > 0 and degree < (start_degree + i - 1) % len(scale_intervals):
base_octave += 1
interval = scale_intervals[degree]
note = base_octave * 12 + root_in_octave + interval
if 0 <= note <= 127:
notes.append(note)
return notes
def _get_all_scale_notes(self) -> List[int]:
"""Get all available scale notes in both directions for patterns that need full range"""
# Use limited scale notes if note_limit is less than full scale
if self.note_limit < 7:
return self._get_limited_scale_notes()
# Original behavior for full scale (note_limit = 7)
up_notes = self._generate_scale_notes_up()
down_notes = self._generate_scale_notes_down()
@ -657,14 +701,24 @@ class ArpeggiatorEngine(QObject):
def _generate_up_pattern(self) -> List[int]:
"""Generate ascending arpeggio pattern"""
if self.note_limit < 7:
return self._get_limited_scale_notes()
return self._generate_scale_notes_up()
def _generate_down_pattern(self) -> List[int]:
"""Generate descending arpeggio pattern"""
if self.note_limit < 7:
limited_notes = self._get_limited_scale_notes()
return list(reversed(limited_notes))
return self._generate_scale_notes_down()
def _generate_up_down_pattern(self) -> List[int]:
"""Generate up then down pattern"""
if self.note_limit < 7:
limited_notes = self._get_limited_scale_notes()
# Up, then down (avoiding duplicate at starting note)
return limited_notes + list(reversed(limited_notes))[1:]
up_notes = self._generate_scale_notes_up()
down_notes = self._generate_scale_notes_down()
# Up, then down (avoiding duplicate at starting note)
@ -672,6 +726,11 @@ class ArpeggiatorEngine(QObject):
def _generate_down_up_pattern(self) -> List[int]:
"""Generate down then up pattern"""
if self.note_limit < 7:
limited_notes = self._get_limited_scale_notes()
# Down, then up (avoiding duplicate at starting note)
return list(reversed(limited_notes)) + limited_notes[1:]
down_notes = self._generate_scale_notes_down()
up_notes = self._generate_scale_notes_up()
# Down, then up (avoiding duplicate at starting note)
@ -940,6 +999,19 @@ class ArpeggiatorEngine(QObject):
self.armed_scale_note_start = None
changes_applied = True
# Apply armed pattern length
if self.armed_pattern_length is not None:
self.user_pattern_length = self.armed_pattern_length
self.volume_engine.set_bar_length(self.armed_pattern_length)
self.armed_pattern_length = None
changes_applied = True
# Apply armed note limit
if self.armed_note_limit is not None:
self.note_limit = self.armed_note_limit
self.armed_note_limit = None
changes_applied = True
# Apply armed preset
if self.armed_preset_data is not None and self.preset_apply_callback:
try:

BIN
gui/__pycache__/arpeggiator_controls.cpython-310.pyc

BIN
gui/__pycache__/main_window.cpython-310.pyc

BIN
gui/__pycache__/preset_controls.cpython-310.pyc

88
gui/arpeggiator_controls.py

@ -27,6 +27,7 @@ class ArpeggiatorControls(QWidget):
self.distribution_buttons = {}
self.speed_buttons = {}
self.pattern_length_buttons = {}
self.note_limit_buttons = {}
# Scaling support
self.scale_factor = 1.0
@ -39,6 +40,7 @@ class ArpeggiatorControls(QWidget):
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
@ -48,6 +50,8 @@ class ArpeggiatorControls(QWidget):
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
self.setup_ui()
@ -409,6 +413,28 @@ class ArpeggiatorControls(QWidget):
layout.addWidget(length_widget)
# Note limit buttons
layout.addWidget(QLabel("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):
@ -753,16 +779,27 @@ class ArpeggiatorControls(QWidget):
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;")
# 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.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;")
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()
@ -962,6 +999,32 @@ class ArpeggiatorControls(QWidget):
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):
@ -1011,12 +1074,23 @@ class ArpeggiatorControls(QWidget):
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;")
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.pattern_length_buttons[self.current_pattern_length].setStyleSheet("background: #cc6600; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #ee8800;")
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'):

7
gui/main_window.py

@ -172,7 +172,7 @@ class MainWindow(QMainWindow):
main_layout.addWidget(status_frame)
# Create status bar
self.statusBar().showMessage("Ready - Use keyboard (AWSDFGTGHYUJ) to play notes, SPACE to start/stop")
self.statusBar().showMessage("Ready - Use keyboard (AWSDFGTGHYUJ) to play notes, SPACE for emergency stop")
# Create menu bar
self.create_menu_bar()
@ -664,11 +664,10 @@ class MainWindow(QMainWindow):
self.statusBar().showMessage(f"Note ON: {note}", 500)
elif key == Qt.Key_Space:
# Spacebar starts/stops arpeggiator
# Spacebar emergency stop only (does not start playback)
if self.arpeggiator.is_playing:
self.on_stop_clicked()
else:
self.on_play_clicked()
self.statusBar().showMessage("Emergency stop activated", 1000)
super().keyPressEvent(event)

6
gui/preset_controls.py

@ -333,6 +333,7 @@ class PresetControls(QWidget):
"velocity": self.arpeggiator.velocity,
"tempo": self.arpeggiator.tempo,
"user_pattern_length": getattr(self.arpeggiator, 'user_pattern_length', 8),
"note_limit": getattr(self.arpeggiator, 'note_limit', 7),
"channel_distribution": self.arpeggiator.channel_distribution,
"delay_enabled": self.arpeggiator.delay_enabled,
"delay_length": self.arpeggiator.delay_length,
@ -389,6 +390,11 @@ class PresetControls(QWidget):
elif hasattr(self.arpeggiator, 'set_pattern_length'):
self.arpeggiator.set_pattern_length(pattern_length)
# Apply note limit
note_limit = arp_settings.get("note_limit", 7)
if hasattr(self.arpeggiator, 'set_note_limit'):
self.arpeggiator.set_note_limit(note_limit)
# Apply channel distribution
self.arpeggiator.set_channel_distribution(arp_settings.get("channel_distribution", "up"))

Loading…
Cancel
Save