Browse Source

Add random scale note selection feature

- 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 <noreply@anthropic.com>
master
melancholytron 1 month ago
parent
commit
190992c26e
  1. 36
      core/arpeggiator_engine.py
  2. 41
      gui/arpeggiator_controls.py

36
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

41
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)
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)
Loading…
Cancel
Save