From 190992c26eb7c87d2d816f8460341a55642fcc0d Mon Sep 17 00:00:00 2001 From: melancholytron Date: Wed, 10 Sep 2025 13:29:45 -0500 Subject: [PATCH] Add random scale note selection feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add "Random" button option to scale note section that selects different random scale notes each pattern loop - Update arpeggiator engine to support "random" as scale_note_start value alongside integers - Add helper method _get_actual_scale_note_start() to handle random selection on each pattern generation - Update GUI to properly style and handle "Random" button selection with orange coloring - Support both immediate and armed state changes for random scale note selection 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- core/arpeggiator_engine.py | 36 ++++++++++++++++++++++++-------- gui/arpeggiator_controls.py | 41 +++++++++++++++++++++++++++++++++---- 2 files changed, 64 insertions(+), 13 deletions(-) diff --git a/core/arpeggiator_engine.py b/core/arpeggiator_engine.py index 49573df..c6d802e 100644 --- a/core/arpeggiator_engine.py +++ b/core/arpeggiator_engine.py @@ -8,7 +8,8 @@ Generates arpeggio patterns, handles timing, and integrates with routing and vol import time import math import threading -from typing import Dict, List, Optional, Tuple, Set +import random +from typing import Dict, List, Optional, Tuple, Set, Union from PyQt5.QtCore import QObject, pyqtSignal, QTimer from .midi_channel_manager import MIDIChannelManager @@ -237,14 +238,20 @@ class ArpeggiatorEngine(QObject): """Set base velocity 0-127""" self.velocity = max(0, min(127, velocity)) - def set_scale_note_start(self, scale_note_index: int): - """Set which scale note to start the arpeggio from""" - self.scale_note_start = max(0, scale_note_index) + def set_scale_note_start(self, scale_note_index: Union[int, str]): + """Set which scale note to start the arpeggio from (integer index or 'random')""" + if scale_note_index == "random": + self.scale_note_start = "random" + else: + self.scale_note_start = max(0, scale_note_index) self.regenerate_pattern() - def arm_scale_note_start(self, scale_note_index: int): + def arm_scale_note_start(self, scale_note_index: Union[int, str]): """Arm a scale note start position to change at pattern end""" - self.armed_scale_note_start = max(0, scale_note_index) + if scale_note_index == "random": + self.armed_scale_note_start = "random" + else: + self.armed_scale_note_start = max(0, scale_note_index) self.armed_state_changed.emit() def clear_armed_scale_note_start(self): @@ -618,7 +625,8 @@ class ArpeggiatorEngine(QObject): root_in_octave = self.root_note % 12 # Get the interval for the selected scale degree - start_degree = self.scale_note_start % len(scale_intervals) + actual_start = self._get_actual_scale_note_start() + start_degree = actual_start % len(scale_intervals) start_interval = scale_intervals[start_degree] starting_note = base_octave * 12 + root_in_octave + start_interval @@ -651,7 +659,8 @@ class ArpeggiatorEngine(QObject): root_in_octave = self.root_note % 12 # Get the interval for the selected scale degree - start_degree = self.scale_note_start % len(scale_intervals) + actual_start = self._get_actual_scale_note_start() + start_degree = actual_start % len(scale_intervals) start_interval = scale_intervals[start_degree] starting_note = base_octave * 12 + root_in_octave + start_interval @@ -695,7 +704,8 @@ class ArpeggiatorEngine(QObject): root_in_octave = self.root_note % 12 # Get the interval for the selected scale degree - start_degree = self.scale_note_start % len(scale_intervals) + actual_start = self._get_actual_scale_note_start() + start_degree = actual_start % len(scale_intervals) # Generate only the number of notes specified by note_limit for i in range(self.note_limit): @@ -713,6 +723,14 @@ class ArpeggiatorEngine(QObject): return notes + def _get_actual_scale_note_start(self) -> int: + """Get the actual scale note start index to use, handling random selection""" + if self.scale_note_start == "random": + scale_intervals = self.SCALES[self.scale] + return random.randint(0, len(scale_intervals) - 1) + else: + return self.scale_note_start + 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 diff --git a/gui/arpeggiator_controls.py b/gui/arpeggiator_controls.py index 8484cd1..cfc29f4 100644 --- a/gui/arpeggiator_controls.py +++ b/gui/arpeggiator_controls.py @@ -1346,6 +1346,21 @@ class ArpeggiatorControls(QWidget): self.scale_notes_buttons[i] = btn self.scale_notes_layout.addWidget(btn, 0, i) + + # Add Random button as an additional option + random_btn = QPushButton("Random") + random_btn.setFixedSize(60, 25) + random_btn.setCheckable(True) + random_btn.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #555555;") + random_btn.clicked.connect(lambda checked: self.on_scale_note_clicked("random")) + + # Check if random is currently selected + if self.current_scale_note_index == "random": + random_btn.setChecked(True) + random_btn.setStyleSheet("background: #ff8800; color: white; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #ffaa00;") + + self.scale_notes_buttons["random"] = random_btn + self.scale_notes_layout.addWidget(random_btn, 0, len(scale_notes)) def on_scale_note_clicked(self, scale_note_index): """Handle scale note selection with armed state support""" @@ -1360,8 +1375,11 @@ class ArpeggiatorControls(QWidget): 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;") + # It was the current active note, restore its proper color + if old_armed_index == "random": + self.armed_scale_note_button.setStyleSheet("background: #ff8800; color: white; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #ffaa00;") + else: + 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;") @@ -1385,7 +1403,10 @@ class ArpeggiatorControls(QWidget): 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;") + if scale_note_index == "random": + self.scale_notes_buttons[scale_note_index].setStyleSheet("background: #ff8800; color: white; font-size: 11px; font-weight: bold; padding: 0px; border: 1px solid #ffaa00;") + else: + 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() @@ -1393,4 +1414,16 @@ class ArpeggiatorControls(QWidget): 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) \ No newline at end of file + self.arpeggiator.set_scale_note_start(self.current_scale_note_index) + + def on_random_scale_note_clicked(self): + """Select a random scale note""" + if self.scale_notes_buttons: + import random + # Get all available scale note indices + available_indices = list(self.scale_notes_buttons.keys()) + if available_indices: + # Choose a random index + random_index = random.choice(available_indices) + # Trigger the scale note selection + self.on_scale_note_clicked(random_index) \ No newline at end of file