diff --git a/core/__pycache__/arpeggiator_engine.cpython-310.pyc b/core/__pycache__/arpeggiator_engine.cpython-310.pyc index 6b12e36..ecb5a27 100644 Binary files a/core/__pycache__/arpeggiator_engine.cpython-310.pyc and b/core/__pycache__/arpeggiator_engine.cpython-310.pyc differ diff --git a/core/arpeggiator_engine.py b/core/arpeggiator_engine.py index 2c4352b..24ee0c1 100644 --- a/core/arpeggiator_engine.py +++ b/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: diff --git a/gui/__pycache__/arpeggiator_controls.cpython-310.pyc b/gui/__pycache__/arpeggiator_controls.cpython-310.pyc index db87535..0f89ca4 100644 Binary files a/gui/__pycache__/arpeggiator_controls.cpython-310.pyc and b/gui/__pycache__/arpeggiator_controls.cpython-310.pyc differ diff --git a/gui/__pycache__/main_window.cpython-310.pyc b/gui/__pycache__/main_window.cpython-310.pyc index bfb78bc..6145652 100644 Binary files a/gui/__pycache__/main_window.cpython-310.pyc and b/gui/__pycache__/main_window.cpython-310.pyc differ diff --git a/gui/__pycache__/preset_controls.cpython-310.pyc b/gui/__pycache__/preset_controls.cpython-310.pyc index a5450d3..966631a 100644 Binary files a/gui/__pycache__/preset_controls.cpython-310.pyc and b/gui/__pycache__/preset_controls.cpython-310.pyc differ diff --git a/gui/arpeggiator_controls.py b/gui/arpeggiator_controls.py index d2ba99d..22e567c 100644 --- a/gui/arpeggiator_controls.py +++ b/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'): diff --git a/gui/main_window.py b/gui/main_window.py index 149e5cf..7ca2559 100644 --- a/gui/main_window.py +++ b/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) diff --git a/gui/preset_controls.py b/gui/preset_controls.py index 3c7ca31..fe7110a 100644 --- a/gui/preset_controls.py +++ b/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"))