Browse Source

Link volume patterns to pattern length and add triplet timing divisions

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 <noreply@anthropic.com>
master
melancholytron 2 months ago
parent
commit
53b29f8b72
  1. 10
      core/arpeggiator_engine.py
  2. 35
      core/volume_pattern_engine.py
  3. 15
      gui/arpeggiator_controls.py

10
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):

35
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

15
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

Loading…
Cancel
Save