From 53b29f8b72a257ed7937b2937e49fe69be7cc338 Mon Sep 17 00:00:00 2001 From: melancholytron Date: Mon, 8 Sep 2025 13:11:06 -0500 Subject: [PATCH] Link volume patterns to pattern length and add triplet timing divisions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Volume Pattern Improvements: - Volume patterns now use pattern length as "bar length" for timing - Swell pattern: completes one full cycle per pattern length - Breathing pattern: completes two breath cycles per pattern length - Build pattern: builds over pattern length, fades over 2x pattern length - Fade pattern: fades from max to min over pattern length - Pulse pattern: creates 4 pulses per pattern length - Alternating pattern: alternates based on pattern step position - All patterns now scale dynamically with user-selected pattern length Delay Timing Enhancements: - Add triplet note divisions: 1/32T, 1/16T, 1/8T, 1/4T, 1/2T - Triplet calculations: 1/4T = 1/6 beat, 1/8T = 1/12 beat, etc. - Arrange delay timing buttons in 2-row grid layout for better fit - Smaller button size (35px) with 9px font for compact display - Support for more complex rhythmic delay patterns Technical Implementation: - Add set_bar_length() method to volume pattern engine - Pattern length changes automatically update volume engine bar length - Mathematical timing calculations adjusted for bar-relative patterns - Triplet note values added to NOTE_SPEEDS dictionary - Grid layout for delay timing controls with proper spacing Result: Volume patterns now properly sync to the selected pattern length, creating musically coherent lighting that matches the arpeggio structure. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- core/arpeggiator_engine.py | 10 ++++++++-- core/volume_pattern_engine.py | 35 +++++++++++++++++++++++------------ gui/arpeggiator_controls.py | 15 +++++++++------ 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/core/arpeggiator_engine.py b/core/arpeggiator_engine.py index b64b902..d460f5e 100644 --- a/core/arpeggiator_engine.py +++ b/core/arpeggiator_engine.py @@ -60,8 +60,9 @@ class ArpeggiatorEngine(QObject): # Note speeds (as fractions of a beat) NOTE_SPEEDS = { - "1/32": 1/32, "1/16": 1/16, "1/8": 1/8, "1/4": 1/4, - "1/2": 1/2, "1/1": 1, "2/1": 2 + "1/32": 1/32, "1/32T": 1/48, "1/16": 1/16, "1/16T": 1/24, + "1/8": 1/8, "1/8T": 1/12, "1/4": 1/4, "1/4T": 1/6, + "1/2": 1/2, "1/2T": 1/3, "1/1": 1, "2/1": 2 } def __init__(self, channel_manager: MIDIChannelManager, synth_router: SynthRouter, @@ -136,6 +137,9 @@ class ArpeggiatorEngine(QObject): self.calculate_step_duration() self.calculate_delay_step_duration() + # Initialize volume engine with pattern length + self.volume_engine.set_bar_length(self.user_pattern_length) + # Setup update timer self.update_timer = QTimer() self.update_timer.timeout.connect(self.update) @@ -248,6 +252,8 @@ class ArpeggiatorEngine(QObject): """Set user-defined pattern length""" 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() def set_delay_enabled(self, enabled: bool): diff --git a/core/volume_pattern_engine.py b/core/volume_pattern_engine.py index caf9708..c9a6d2e 100644 --- a/core/volume_pattern_engine.py +++ b/core/volume_pattern_engine.py @@ -38,6 +38,7 @@ class VolumePatternEngine(QObject): # Position tracking for patterns self.pattern_position = 0.0 self.pattern_direction = 1 # 1 for forward, -1 for reverse + self.bar_length = 8 # Pattern length defines a "bar" for volume patterns # Volume ranges per channel {channel: (min, max)} self.channel_volume_ranges: Dict[int, Tuple[float, float]] = {} @@ -72,6 +73,10 @@ class VolumePatternEngine(QObject): """Set pattern intensity multiplier (0.0 to 2.0)""" self.pattern_intensity = max(0.0, min(2.0, intensity)) + def set_bar_length(self, length: int): + """Set bar length (pattern length) for volume patterns""" + self.bar_length = max(1, min(16, length)) + def set_channel_volume_range(self, channel: int, min_vol: float, max_vol: float): """Set volume range for a specific channel (0.0 to 1.0)""" min_vol = max(0.0, min(1.0, min_vol)) @@ -147,13 +152,15 @@ class VolumePatternEngine(QObject): return 1.0 elif self.current_pattern == "swell": - # Gradual swell up and down - cycle = math.sin(self.pattern_position * 0.5) * 0.5 + 0.5 + # Gradual swell up and down - completes one cycle per bar + cycle_rate = (2 * math.pi) / self.bar_length # Complete cycle over bar length + cycle = math.sin(self.pattern_position * cycle_rate) * 0.5 + 0.5 return cycle elif self.current_pattern == "breathing": - # Smooth breathing rhythm - cycle = math.sin(self.pattern_position) * 0.5 + 0.5 + # Smooth breathing rhythm - 2 cycles per bar + breathing_rate = (4 * math.pi) / self.bar_length # Two breaths per bar + cycle = math.sin(self.pattern_position * breathing_rate) * 0.5 + 0.5 return 0.3 + cycle * 0.7 # Keep minimum at 30% elif self.current_pattern == "wave": @@ -163,25 +170,29 @@ class VolumePatternEngine(QObject): return wave elif self.current_pattern == "build": - # Gradual crescendo - build_progress = (self.pattern_position * 0.1) % 2.0 + # Gradual crescendo over bar length, then fade back + build_cycle_length = self.bar_length * 2 # Build up and fade down over 2 bars + build_progress = (self.pattern_position % build_cycle_length) / build_cycle_length * 2.0 if build_progress > 1.0: build_progress = 2.0 - build_progress # Fade back down return build_progress elif self.current_pattern == "fade": - # Gradual diminuendo - fade_progress = 1.0 - ((self.pattern_position * 0.1) % 1.0) + # Gradual diminuendo over bar length + fade_cycle_progress = (self.pattern_position % self.bar_length) / self.bar_length + fade_progress = 1.0 - fade_cycle_progress return fade_progress elif self.current_pattern == "pulse": - # Sharp rhythmic pulses - pulse = math.sin(self.pattern_position * 2) + # Sharp rhythmic pulses - 4 pulses per bar + pulse_rate = (8 * math.pi) / self.bar_length # 4 complete cycles per bar + pulse = math.sin(self.pattern_position * pulse_rate) return 1.0 if pulse > 0.8 else 0.3 elif self.current_pattern == "alternating": - # Alternate between high and low - return 1.0 if int(self.pattern_position) % 2 == 0 else 0.3 + # Alternate between high and low based on bar subdivisions + step_within_bar = int(self.pattern_position) % self.bar_length + return 1.0 if step_within_bar % 2 == 0 else 0.3 elif self.current_pattern == "stutter": # Rapid volume changes diff --git a/gui/arpeggiator_controls.py b/gui/arpeggiator_controls.py index 57346c7..01ef27f 100644 --- a/gui/arpeggiator_controls.py +++ b/gui/arpeggiator_controls.py @@ -395,17 +395,17 @@ class ArpeggiatorControls(QWidget): self.delay_timing_label = delay_timing_label delay_timing_widget = QWidget() - delay_timing_layout = QHBoxLayout(delay_timing_widget) + delay_timing_layout = QGridLayout(delay_timing_widget) delay_timing_layout.setSpacing(0) delay_timing_layout.setContentsMargins(0, 0, 0, 0) self.delay_timing_buttons = {} - delay_speeds = ["1/32", "1/16", "1/8", "1/4", "1/2", "1/1"] - for speed in delay_speeds: + delay_speeds = ["1/32", "1/32T", "1/16", "1/16T", "1/8", "1/8T", "1/4", "1/4T", "1/2", "1/2T", "1/1"] + for i, speed in enumerate(delay_speeds): btn = QPushButton(speed) - btn.setFixedSize(40, 18) + btn.setFixedSize(35, 18) # Slightly smaller to fit more btn.setCheckable(True) - btn.setStyleSheet("background: #2a2a2a; color: #666666; font-size: 10px; font-weight: bold; padding: 0px; border: 1px solid #333333;") + btn.setStyleSheet("background: #2a2a2a; color: #666666; font-size: 9px; font-weight: bold; padding: 0px; border: 1px solid #333333;") btn.setEnabled(False) btn.clicked.connect(lambda checked, s=speed: self.on_delay_timing_clicked(s)) @@ -413,7 +413,10 @@ class ArpeggiatorControls(QWidget): btn.setChecked(True) self.delay_timing_buttons[speed] = btn - delay_timing_layout.addWidget(btn) + # Arrange in 2 rows + row = i // 6 + col = i % 6 + delay_timing_layout.addWidget(btn, row, col) delay_timing_widget.setEnabled(False) self.delay_timing_widget = delay_timing_widget