You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
691 lines
27 KiB
691 lines
27 KiB
"""
|
|
Arpeggiator Controls GUI - Complete Redesign
|
|
|
|
Clean quadrant layout with no overlapping buttons, guaranteed spacing.
|
|
"""
|
|
|
|
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
|
QGroupBox, QComboBox, QSlider, QSpinBox, QLabel,
|
|
QPushButton, QCheckBox, QFrame, QScrollArea, QSizePolicy)
|
|
from PyQt5.QtCore import Qt, pyqtSlot
|
|
|
|
class ArpeggiatorControls(QWidget):
|
|
"""Control panel for arpeggiator parameters - redesigned for no overlaps"""
|
|
|
|
def __init__(self, arpeggiator, channel_manager):
|
|
super().__init__()
|
|
self.arpeggiator = arpeggiator
|
|
self.channel_manager = channel_manager
|
|
|
|
# Preset system
|
|
self.presets = {}
|
|
self.current_preset = None
|
|
self.preset_rotation_enabled = False
|
|
self.preset_rotation_interval = 4
|
|
self.preset_rotation_timer = None
|
|
self.pattern_count_since_preset_change = 0
|
|
|
|
# Button tracking
|
|
self.root_note_buttons = {}
|
|
self.octave_buttons = {}
|
|
self.scale_buttons = {}
|
|
self.pattern_buttons = {}
|
|
self.distribution_buttons = {}
|
|
|
|
# Current states
|
|
self.current_root_note = 0 # C
|
|
self.current_octave = 4 # C4
|
|
self.current_scale = "major"
|
|
self.current_pattern = "up"
|
|
self.current_distribution = "up"
|
|
|
|
# Armed states
|
|
self.armed_root_note_button = None
|
|
self.armed_octave_button = None
|
|
self.armed_scale_button = None
|
|
self.armed_pattern_button = None
|
|
self.armed_distribution_button = None
|
|
|
|
self.setup_ui()
|
|
self.connect_signals()
|
|
|
|
def setup_ui(self):
|
|
"""Set up clean quadrant layout"""
|
|
# Main layout - fixed size quadrants
|
|
main_layout = QGridLayout(self)
|
|
main_layout.setSpacing(10)
|
|
main_layout.setContentsMargins(10, 10, 10, 10)
|
|
|
|
# Create four equal quadrants
|
|
basic_quad = self.create_basic_quadrant()
|
|
distribution_quad = self.create_distribution_quadrant()
|
|
pattern_quad = self.create_pattern_quadrant()
|
|
timing_quad = self.create_timing_quadrant()
|
|
|
|
# Add to grid with equal sizing
|
|
main_layout.addWidget(basic_quad, 0, 0)
|
|
main_layout.addWidget(distribution_quad, 0, 1)
|
|
main_layout.addWidget(pattern_quad, 1, 0)
|
|
main_layout.addWidget(timing_quad, 1, 1)
|
|
|
|
# Make all quadrants equal
|
|
main_layout.setRowStretch(0, 1)
|
|
main_layout.setRowStretch(1, 1)
|
|
main_layout.setColumnStretch(0, 1)
|
|
main_layout.setColumnStretch(1, 1)
|
|
|
|
def create_basic_quadrant(self):
|
|
"""Create basic settings quadrant - guaranteed no overlaps"""
|
|
group = QGroupBox("Basic Settings")
|
|
layout = QVBoxLayout(group)
|
|
layout.setSpacing(8)
|
|
layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
# Root Note - 12 buttons in 3 rows of 4
|
|
root_label = QLabel("Root Note:")
|
|
layout.addWidget(root_label)
|
|
|
|
root_container = QWidget()
|
|
root_layout = QGridLayout(root_container)
|
|
root_layout.setSpacing(4)
|
|
root_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
|
|
for i, note in enumerate(notes):
|
|
btn = QPushButton(note)
|
|
btn.setFixedSize(35, 25)
|
|
btn.setCheckable(True)
|
|
btn.clicked.connect(lambda checked, n=i: self.on_root_note_clicked(n))
|
|
|
|
if i == 0: # C is default
|
|
btn.setChecked(True)
|
|
self.set_button_style(btn, "active")
|
|
else:
|
|
self.set_button_style(btn, "inactive")
|
|
|
|
self.root_note_buttons[i] = btn
|
|
root_layout.addWidget(btn, i // 4, i % 4)
|
|
|
|
layout.addWidget(root_container)
|
|
|
|
# Octave - 6 buttons in 2 rows of 3
|
|
octave_label = QLabel("Octave:")
|
|
layout.addWidget(octave_label)
|
|
|
|
octave_container = QWidget()
|
|
octave_layout = QGridLayout(octave_container)
|
|
octave_layout.setSpacing(4)
|
|
octave_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
for i in range(6): # C3 to C8
|
|
octave = i + 3
|
|
btn = QPushButton(f"C{octave}")
|
|
btn.setFixedSize(35, 25)
|
|
btn.setCheckable(True)
|
|
btn.clicked.connect(lambda checked, o=octave: self.on_octave_clicked(o))
|
|
|
|
if octave == 4: # C4 is default
|
|
btn.setChecked(True)
|
|
self.set_button_style(btn, "active")
|
|
else:
|
|
self.set_button_style(btn, "inactive")
|
|
|
|
self.octave_buttons[octave] = btn
|
|
octave_layout.addWidget(btn, i // 3, i % 3)
|
|
|
|
layout.addWidget(octave_container)
|
|
|
|
# Scales - 8 main scales in 4 rows of 2
|
|
scale_label = QLabel("Scale:")
|
|
layout.addWidget(scale_label)
|
|
|
|
scale_container = QWidget()
|
|
scale_layout = QGridLayout(scale_container)
|
|
scale_layout.setSpacing(4)
|
|
scale_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
main_scales = ["major", "minor", "dorian", "phrygian", "lydian", "mixolydian", "pentatonic_major", "pentatonic_minor"]
|
|
for i, scale in enumerate(main_scales):
|
|
display_name = scale.replace("_", " ").title()
|
|
if len(display_name) > 8:
|
|
display_name = display_name[:8]
|
|
|
|
btn = QPushButton(display_name)
|
|
btn.setFixedSize(75, 25)
|
|
btn.setCheckable(True)
|
|
btn.clicked.connect(lambda checked, s=scale: self.on_scale_clicked(s))
|
|
|
|
if scale == "major": # Major is default
|
|
btn.setChecked(True)
|
|
self.set_button_style(btn, "active")
|
|
else:
|
|
self.set_button_style(btn, "inactive")
|
|
|
|
self.scale_buttons[scale] = btn
|
|
scale_layout.addWidget(btn, i // 2, i % 2)
|
|
|
|
layout.addWidget(scale_container)
|
|
|
|
# Octave Range dropdown
|
|
range_label = QLabel("Octave Range:")
|
|
layout.addWidget(range_label)
|
|
|
|
self.octave_range_combo = QComboBox()
|
|
for i in range(1, 5):
|
|
self.octave_range_combo.addItem(f"{i} octave{'s' if i > 1 else ''}")
|
|
self.octave_range_combo.setCurrentIndex(0)
|
|
layout.addWidget(self.octave_range_combo)
|
|
|
|
return group
|
|
|
|
def create_distribution_quadrant(self):
|
|
"""Create channel distribution quadrant"""
|
|
group = QGroupBox("Channel Distribution")
|
|
layout = QVBoxLayout(group)
|
|
layout.setSpacing(8)
|
|
layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
# Distribution patterns - 8 patterns in 4 rows of 2
|
|
dist_label = QLabel("Distribution Pattern:")
|
|
layout.addWidget(dist_label)
|
|
|
|
dist_container = QWidget()
|
|
dist_layout = QGridLayout(dist_container)
|
|
dist_layout.setSpacing(4)
|
|
dist_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
patterns = ["up", "down", "up_down", "bounce", "random", "cycle", "alternating", "single_channel"]
|
|
for i, pattern in enumerate(patterns):
|
|
display_name = pattern.replace("_", " ").title()
|
|
if len(display_name) > 10:
|
|
display_name = display_name[:10]
|
|
|
|
btn = QPushButton(display_name)
|
|
btn.setFixedSize(85, 25)
|
|
btn.setCheckable(True)
|
|
btn.clicked.connect(lambda checked, p=pattern: self.on_distribution_clicked(p))
|
|
|
|
if pattern == "up": # Up is default
|
|
btn.setChecked(True)
|
|
self.set_button_style(btn, "active")
|
|
else:
|
|
self.set_button_style(btn, "inactive")
|
|
|
|
self.distribution_buttons[pattern] = btn
|
|
dist_layout.addWidget(btn, i // 2, i % 2)
|
|
|
|
layout.addWidget(dist_container)
|
|
|
|
# Description
|
|
self.distribution_description = QLabel("Channels: 1 → 2 → 3 → 4 → 5 → 6...")
|
|
self.distribution_description.setStyleSheet("color: #666; font-style: italic; font-size: 10px;")
|
|
self.distribution_description.setWordWrap(True)
|
|
layout.addWidget(self.distribution_description)
|
|
|
|
return group
|
|
|
|
def create_pattern_quadrant(self):
|
|
"""Create pattern settings quadrant"""
|
|
group = QGroupBox("Pattern Settings")
|
|
layout = QVBoxLayout(group)
|
|
layout.setSpacing(8)
|
|
layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
# Arpeggio patterns - 8 patterns in 4 rows of 2
|
|
pattern_label = QLabel("Arpeggio Pattern:")
|
|
layout.addWidget(pattern_label)
|
|
|
|
pattern_container = QWidget()
|
|
pattern_layout = QGridLayout(pattern_container)
|
|
pattern_layout.setSpacing(4)
|
|
pattern_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
patterns = self.arpeggiator.PATTERN_TYPES[:8] # First 8 patterns
|
|
for i, pattern in enumerate(patterns):
|
|
display_name = pattern.replace("_", " ").title()
|
|
if len(display_name) > 10:
|
|
display_name = display_name[:10]
|
|
|
|
btn = QPushButton(display_name)
|
|
btn.setFixedSize(85, 25)
|
|
btn.setCheckable(True)
|
|
btn.clicked.connect(lambda checked, p=pattern: self.on_pattern_clicked(p))
|
|
|
|
if pattern == "up": # Up is default
|
|
btn.setChecked(True)
|
|
self.set_button_style(btn, "active")
|
|
else:
|
|
self.set_button_style(btn, "inactive")
|
|
|
|
self.pattern_buttons[pattern] = btn
|
|
pattern_layout.addWidget(btn, i // 2, i % 2)
|
|
|
|
layout.addWidget(pattern_container)
|
|
|
|
return group
|
|
|
|
def create_timing_quadrant(self):
|
|
"""Create timing settings quadrant"""
|
|
group = QGroupBox("Timing Settings")
|
|
layout = QVBoxLayout(group)
|
|
layout.setSpacing(8)
|
|
layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
# Tempo
|
|
tempo_layout = QHBoxLayout()
|
|
tempo_layout.addWidget(QLabel("Tempo:"))
|
|
self.tempo_spin = QSpinBox()
|
|
self.tempo_spin.setRange(40, 200)
|
|
self.tempo_spin.setValue(120)
|
|
self.tempo_spin.setSuffix(" BPM")
|
|
tempo_layout.addWidget(self.tempo_spin)
|
|
layout.addLayout(tempo_layout)
|
|
|
|
# Note Speed
|
|
speed_layout = QHBoxLayout()
|
|
speed_layout.addWidget(QLabel("Speed:"))
|
|
self.speed_combo = QComboBox()
|
|
speeds = list(self.arpeggiator.NOTE_SPEEDS.keys())
|
|
for speed in speeds:
|
|
self.speed_combo.addItem(speed)
|
|
self.speed_combo.setCurrentText("1/8")
|
|
speed_layout.addWidget(self.speed_combo)
|
|
layout.addLayout(speed_layout)
|
|
|
|
# Gate
|
|
gate_layout = QHBoxLayout()
|
|
gate_layout.addWidget(QLabel("Gate:"))
|
|
self.gate_slider = QSlider(Qt.Horizontal)
|
|
self.gate_slider.setRange(10, 200)
|
|
self.gate_slider.setValue(100)
|
|
gate_layout.addWidget(self.gate_slider)
|
|
self.gate_label = QLabel("100%")
|
|
self.gate_label.setFixedWidth(35)
|
|
gate_layout.addWidget(self.gate_label)
|
|
layout.addLayout(gate_layout)
|
|
|
|
# Swing
|
|
swing_layout = QHBoxLayout()
|
|
swing_layout.addWidget(QLabel("Swing:"))
|
|
self.swing_slider = QSlider(Qt.Horizontal)
|
|
self.swing_slider.setRange(-100, 100)
|
|
self.swing_slider.setValue(0)
|
|
swing_layout.addWidget(self.swing_slider)
|
|
self.swing_label = QLabel("0%")
|
|
self.swing_label.setFixedWidth(35)
|
|
swing_layout.addWidget(self.swing_label)
|
|
layout.addLayout(swing_layout)
|
|
|
|
# Velocity
|
|
velocity_layout = QHBoxLayout()
|
|
velocity_layout.addWidget(QLabel("Velocity:"))
|
|
self.velocity_slider = QSlider(Qt.Horizontal)
|
|
self.velocity_slider.setRange(1, 127)
|
|
self.velocity_slider.setValue(80)
|
|
velocity_layout.addWidget(self.velocity_slider)
|
|
self.velocity_label = QLabel("80")
|
|
self.velocity_label.setFixedWidth(35)
|
|
velocity_layout.addWidget(self.velocity_label)
|
|
layout.addLayout(velocity_layout)
|
|
|
|
# Preset controls
|
|
preset_layout = QHBoxLayout()
|
|
self.save_preset_btn = QPushButton("Save")
|
|
self.save_preset_btn.setFixedSize(50, 25)
|
|
self.load_preset_btn = QPushButton("Load")
|
|
self.load_preset_btn.setFixedSize(50, 25)
|
|
preset_layout.addWidget(self.save_preset_btn)
|
|
preset_layout.addWidget(self.load_preset_btn)
|
|
preset_layout.addStretch()
|
|
layout.addLayout(preset_layout)
|
|
|
|
return group
|
|
|
|
def set_button_style(self, button, state):
|
|
"""Set button style based on state"""
|
|
if state == "active":
|
|
button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #2d5a2d;
|
|
color: white;
|
|
border: 2px solid #4a8a4a;
|
|
font-weight: bold;
|
|
font-size: 10px;
|
|
}
|
|
""")
|
|
elif state == "armed":
|
|
button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #5a4d2d;
|
|
color: white;
|
|
border: 2px solid #8a7a4a;
|
|
font-weight: bold;
|
|
font-size: 10px;
|
|
}
|
|
""")
|
|
else: # inactive
|
|
button.setStyleSheet("""
|
|
QPushButton {
|
|
font-size: 10px;
|
|
}
|
|
""")
|
|
|
|
def connect_signals(self):
|
|
"""Connect all signals"""
|
|
# Timing controls
|
|
self.tempo_spin.valueChanged.connect(self.on_tempo_changed)
|
|
self.speed_combo.currentTextChanged.connect(self.on_speed_changed)
|
|
self.gate_slider.valueChanged.connect(self.on_gate_changed)
|
|
self.swing_slider.valueChanged.connect(self.on_swing_changed)
|
|
self.velocity_slider.valueChanged.connect(self.on_velocity_changed)
|
|
self.octave_range_combo.currentIndexChanged.connect(self.on_octave_range_changed)
|
|
|
|
# Preset controls
|
|
self.save_preset_btn.clicked.connect(self.save_current_preset)
|
|
self.load_preset_btn.clicked.connect(self.load_preset_dialog)
|
|
|
|
# Arpeggiator signals
|
|
self.arpeggiator.armed_state_changed.connect(self.update_armed_states)
|
|
|
|
# Event handlers
|
|
def on_root_note_clicked(self, note_index):
|
|
"""Handle root note button click"""
|
|
midi_note = self.current_octave * 12 + note_index
|
|
|
|
if self.arpeggiator.is_playing:
|
|
self.arm_root_note(note_index)
|
|
self.arpeggiator.arm_root_note(midi_note)
|
|
else:
|
|
self.set_active_root_note(note_index)
|
|
self.arpeggiator.set_root_note(midi_note)
|
|
|
|
def on_octave_clicked(self, octave):
|
|
"""Handle octave button click"""
|
|
midi_note = octave * 12 + self.current_root_note
|
|
|
|
if self.arpeggiator.is_playing:
|
|
self.arm_octave(octave)
|
|
self.arpeggiator.arm_root_note(midi_note)
|
|
else:
|
|
self.set_active_octave(octave)
|
|
self.arpeggiator.set_root_note(midi_note)
|
|
|
|
def on_scale_clicked(self, scale):
|
|
"""Handle scale button click"""
|
|
if self.arpeggiator.is_playing:
|
|
self.arm_scale(scale)
|
|
self.arpeggiator.arm_scale(scale)
|
|
else:
|
|
self.set_active_scale(scale)
|
|
self.arpeggiator.set_scale(scale)
|
|
|
|
def on_pattern_clicked(self, pattern):
|
|
"""Handle pattern button click"""
|
|
if self.arpeggiator.is_playing:
|
|
self.arm_pattern(pattern)
|
|
self.arpeggiator.arm_pattern_type(pattern)
|
|
else:
|
|
self.set_active_pattern(pattern)
|
|
self.arpeggiator.set_pattern_type(pattern)
|
|
|
|
def on_distribution_clicked(self, distribution):
|
|
"""Handle distribution button click"""
|
|
if self.arpeggiator.is_playing:
|
|
self.arm_distribution(distribution)
|
|
self.arpeggiator.arm_channel_distribution(distribution)
|
|
else:
|
|
self.set_active_distribution(distribution)
|
|
self.arpeggiator.set_channel_distribution(distribution)
|
|
|
|
# State management
|
|
def set_active_root_note(self, note_index):
|
|
"""Set active root note"""
|
|
if self.current_root_note in self.root_note_buttons:
|
|
self.set_button_style(self.root_note_buttons[self.current_root_note], "inactive")
|
|
|
|
self.current_root_note = note_index
|
|
if note_index in self.root_note_buttons:
|
|
self.set_button_style(self.root_note_buttons[note_index], "active")
|
|
|
|
def set_active_octave(self, octave):
|
|
"""Set active octave"""
|
|
if self.current_octave in self.octave_buttons:
|
|
self.set_button_style(self.octave_buttons[self.current_octave], "inactive")
|
|
|
|
self.current_octave = octave
|
|
if octave in self.octave_buttons:
|
|
self.set_button_style(self.octave_buttons[octave], "active")
|
|
|
|
def set_active_scale(self, scale):
|
|
"""Set active scale"""
|
|
if self.current_scale in self.scale_buttons:
|
|
self.set_button_style(self.scale_buttons[self.current_scale], "inactive")
|
|
|
|
self.current_scale = scale
|
|
if scale in self.scale_buttons:
|
|
self.set_button_style(self.scale_buttons[scale], "active")
|
|
|
|
def set_active_pattern(self, pattern):
|
|
"""Set active pattern"""
|
|
if self.current_pattern in self.pattern_buttons:
|
|
self.set_button_style(self.pattern_buttons[self.current_pattern], "inactive")
|
|
|
|
self.current_pattern = pattern
|
|
if pattern in self.pattern_buttons:
|
|
self.set_button_style(self.pattern_buttons[pattern], "active")
|
|
|
|
def set_active_distribution(self, distribution):
|
|
"""Set active distribution"""
|
|
if self.current_distribution in self.distribution_buttons:
|
|
self.set_button_style(self.distribution_buttons[self.current_distribution], "inactive")
|
|
|
|
self.current_distribution = distribution
|
|
if distribution in self.distribution_buttons:
|
|
self.set_button_style(self.distribution_buttons[distribution], "active")
|
|
self.update_distribution_description(distribution)
|
|
|
|
def arm_root_note(self, note_index):
|
|
"""Arm root note for pattern-end change"""
|
|
if self.armed_root_note_button:
|
|
self.set_button_style(self.armed_root_note_button, "inactive")
|
|
|
|
self.armed_root_note_button = self.root_note_buttons[note_index]
|
|
self.set_button_style(self.armed_root_note_button, "armed")
|
|
|
|
def arm_octave(self, octave):
|
|
"""Arm octave for pattern-end change"""
|
|
if self.armed_octave_button:
|
|
self.set_button_style(self.armed_octave_button, "inactive")
|
|
|
|
self.armed_octave_button = self.octave_buttons[octave]
|
|
self.set_button_style(self.armed_octave_button, "armed")
|
|
|
|
def arm_scale(self, scale):
|
|
"""Arm scale for pattern-end change"""
|
|
if self.armed_scale_button:
|
|
self.set_button_style(self.armed_scale_button, "inactive")
|
|
|
|
self.armed_scale_button = self.scale_buttons[scale]
|
|
self.set_button_style(self.armed_scale_button, "armed")
|
|
|
|
def arm_pattern(self, pattern):
|
|
"""Arm pattern for pattern-end change"""
|
|
if self.armed_pattern_button:
|
|
self.set_button_style(self.armed_pattern_button, "inactive")
|
|
|
|
self.armed_pattern_button = self.pattern_buttons[pattern]
|
|
self.set_button_style(self.armed_pattern_button, "armed")
|
|
|
|
def arm_distribution(self, distribution):
|
|
"""Arm distribution for pattern-end change"""
|
|
if self.armed_distribution_button:
|
|
self.set_button_style(self.armed_distribution_button, "inactive")
|
|
|
|
self.armed_distribution_button = self.distribution_buttons[distribution]
|
|
self.set_button_style(self.armed_distribution_button, "armed")
|
|
|
|
@pyqtSlot()
|
|
def update_armed_states(self):
|
|
"""Update when armed states are applied"""
|
|
if self.armed_root_note_button and self.arpeggiator.armed_root_note is None:
|
|
note_index = None
|
|
for n, btn in self.root_note_buttons.items():
|
|
if btn == self.armed_root_note_button:
|
|
note_index = n
|
|
break
|
|
if note_index is not None:
|
|
self.set_active_root_note(note_index)
|
|
|
|
if self.armed_octave_button and self.arpeggiator.armed_root_note is None:
|
|
octave = None
|
|
for o, btn in self.octave_buttons.items():
|
|
if btn == self.armed_octave_button:
|
|
octave = o
|
|
break
|
|
if octave is not None:
|
|
self.set_active_octave(octave)
|
|
|
|
if self.armed_scale_button and self.arpeggiator.armed_scale is None:
|
|
scale = None
|
|
for s, btn in self.scale_buttons.items():
|
|
if btn == self.armed_scale_button:
|
|
scale = s
|
|
break
|
|
if scale:
|
|
self.set_active_scale(scale)
|
|
|
|
if self.armed_pattern_button and self.arpeggiator.armed_pattern_type is None:
|
|
pattern = None
|
|
for p, btn in self.pattern_buttons.items():
|
|
if btn == self.armed_pattern_button:
|
|
pattern = p
|
|
break
|
|
if pattern:
|
|
self.set_active_pattern(pattern)
|
|
|
|
if self.armed_distribution_button and self.arpeggiator.armed_channel_distribution is None:
|
|
distribution = None
|
|
for d, btn in self.distribution_buttons.items():
|
|
if btn == self.armed_distribution_button:
|
|
distribution = d
|
|
break
|
|
if distribution:
|
|
self.set_active_distribution(distribution)
|
|
|
|
def update_distribution_description(self, distribution: str):
|
|
"""Update distribution description"""
|
|
descriptions = {
|
|
"up": "Channels: 1 → 2 → 3 → 4 → 5 → 6...",
|
|
"down": "Channels: 6 → 5 → 4 → 3 → 2 → 1...",
|
|
"up_down": "Channels: 1 → 2 → 3 → 4 → 5 → 6 → 5 → 4 → 3 → 2 → 1...",
|
|
"bounce": "Channels: 1 → 2 → 3 → 4 → 5 → 6 → 5 → 4 → 3 → 2 → 1...",
|
|
"random": "Channels: Random selection each note",
|
|
"cycle": "Channels: 1 → 2 → 3 → 4 → 5 → 6 → 1 → 2...",
|
|
"alternating": "Channels: 1 → 6 → 2 → 5 → 3 → 4...",
|
|
"single_channel": "Channels: All notes on channel 1"
|
|
}
|
|
self.distribution_description.setText(descriptions.get(distribution, "Unknown pattern"))
|
|
|
|
# Timing control handlers
|
|
@pyqtSlot(int)
|
|
def on_tempo_changed(self, tempo):
|
|
self.arpeggiator.set_tempo(float(tempo))
|
|
|
|
@pyqtSlot(str)
|
|
def on_speed_changed(self, speed):
|
|
self.arpeggiator.set_note_speed(speed)
|
|
|
|
@pyqtSlot(int)
|
|
def on_gate_changed(self, gate_percent):
|
|
self.arpeggiator.set_gate(gate_percent / 100.0)
|
|
self.gate_label.setText(f"{gate_percent}%")
|
|
|
|
@pyqtSlot(int)
|
|
def on_swing_changed(self, swing_percent):
|
|
self.arpeggiator.set_swing(swing_percent / 100.0)
|
|
self.swing_label.setText(f"{swing_percent}%")
|
|
|
|
@pyqtSlot(int)
|
|
def on_velocity_changed(self, velocity):
|
|
self.arpeggiator.set_velocity(velocity)
|
|
self.velocity_label.setText(str(velocity))
|
|
|
|
@pyqtSlot(int)
|
|
def on_octave_range_changed(self, index):
|
|
octaves = index + 1
|
|
self.arpeggiator.set_octave_range(octaves)
|
|
|
|
# Preset system
|
|
def save_current_preset(self):
|
|
"""Save current settings as preset"""
|
|
preset_name = f"Preset_{len(self.presets) + 1}"
|
|
preset = {
|
|
'root_note': self.current_root_note,
|
|
'octave': self.current_octave,
|
|
'scale': self.current_scale,
|
|
'pattern': self.current_pattern,
|
|
'distribution': self.current_distribution,
|
|
'octave_range': self.octave_range_combo.currentIndex(),
|
|
'tempo': self.tempo_spin.value(),
|
|
'speed': self.speed_combo.currentText(),
|
|
'gate': self.gate_slider.value(),
|
|
'swing': self.swing_slider.value(),
|
|
'velocity': self.velocity_slider.value()
|
|
}
|
|
self.presets[preset_name] = preset
|
|
print(f"Saved {preset_name}")
|
|
|
|
def load_preset_dialog(self):
|
|
"""Load next preset"""
|
|
if not self.presets:
|
|
print("No presets saved")
|
|
return
|
|
|
|
preset_names = list(self.presets.keys())
|
|
if self.current_preset in preset_names:
|
|
current_index = preset_names.index(self.current_preset)
|
|
next_index = (current_index + 1) % len(preset_names)
|
|
else:
|
|
next_index = 0
|
|
|
|
self.load_preset(preset_names[next_index])
|
|
|
|
def load_preset(self, preset_name: str):
|
|
"""Load specific preset"""
|
|
if preset_name not in self.presets:
|
|
return
|
|
|
|
preset = self.presets[preset_name]
|
|
self.current_preset = preset_name
|
|
|
|
# Apply settings
|
|
if self.arpeggiator.is_playing:
|
|
# Arm changes
|
|
midi_note = preset['octave'] * 12 + preset['root_note']
|
|
self.arpeggiator.arm_root_note(midi_note)
|
|
self.arpeggiator.arm_scale(preset['scale'])
|
|
self.arpeggiator.arm_pattern_type(preset['pattern'])
|
|
self.arpeggiator.arm_channel_distribution(preset['distribution'])
|
|
else:
|
|
# Apply immediately
|
|
self.set_active_root_note(preset['root_note'])
|
|
self.set_active_octave(preset['octave'])
|
|
self.set_active_scale(preset['scale'])
|
|
self.set_active_pattern(preset['pattern'])
|
|
self.set_active_distribution(preset['distribution'])
|
|
|
|
midi_note = preset['octave'] * 12 + preset['root_note']
|
|
self.arpeggiator.set_root_note(midi_note)
|
|
self.arpeggiator.set_scale(preset['scale'])
|
|
self.arpeggiator.set_pattern_type(preset['pattern'])
|
|
self.arpeggiator.set_channel_distribution(preset['distribution'])
|
|
|
|
# Apply other settings
|
|
self.octave_range_combo.setCurrentIndex(preset['octave_range'])
|
|
self.tempo_spin.setValue(preset['tempo'])
|
|
self.speed_combo.setCurrentText(preset['speed'])
|
|
self.gate_slider.setValue(preset['gate'])
|
|
self.swing_slider.setValue(preset['swing'])
|
|
self.velocity_slider.setValue(preset['velocity'])
|
|
|
|
print(f"Loaded {preset_name}")
|