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.
964 lines
38 KiB
964 lines
38 KiB
"""
|
|
Arpeggiator Controls GUI
|
|
|
|
Control panel for arpeggiator settings including patterns, scales, timing, etc.
|
|
"""
|
|
|
|
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
|
QGroupBox, QComboBox, QSlider, QSpinBox, QLabel,
|
|
QPushButton, QCheckBox, QFrame)
|
|
from PyQt5.QtCore import Qt, pyqtSlot
|
|
|
|
class ArpeggiatorControls(QWidget):
|
|
"""Control panel for arpeggiator parameters"""
|
|
|
|
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 # patterns
|
|
self.preset_rotation_timer = None
|
|
self.pattern_count_since_preset_change = 0
|
|
|
|
self.setup_ui()
|
|
self.connect_signals()
|
|
|
|
def setup_ui(self):
|
|
"""Set up the user interface with quadrant layout"""
|
|
layout = QGridLayout(self)
|
|
layout.setSpacing(5) # Reduced from 15 to 5
|
|
layout.setContentsMargins(5, 5, 5, 5) # Minimal margins
|
|
|
|
# Make columns equal width and rows equal height
|
|
layout.setColumnStretch(0, 1)
|
|
layout.setColumnStretch(1, 1)
|
|
layout.setRowStretch(0, 1)
|
|
layout.setRowStretch(1, 1)
|
|
|
|
# Top-left: Basic Settings
|
|
basic_group = self.create_basic_settings()
|
|
layout.addWidget(basic_group, 0, 0)
|
|
|
|
# Top-right: Channel Distribution
|
|
distribution_group = self.create_distribution_settings()
|
|
layout.addWidget(distribution_group, 0, 1)
|
|
|
|
# Bottom-left: Pattern Settings
|
|
pattern_group = self.create_pattern_settings()
|
|
layout.addWidget(pattern_group, 1, 0)
|
|
|
|
# Bottom-right: Timing Settings
|
|
timing_group = self.create_timing_settings()
|
|
layout.addWidget(timing_group, 1, 1)
|
|
|
|
# Add preset rotation controls
|
|
self.setup_preset_rotation()
|
|
|
|
def create_basic_settings(self) -> QGroupBox:
|
|
"""Create basic arpeggiator settings - no scrollbars, all visible"""
|
|
group = QGroupBox("Basic Settings")
|
|
layout = QVBoxLayout(group)
|
|
layout.setSpacing(2)
|
|
layout.setContentsMargins(5, 5, 5, 5)
|
|
|
|
# 12 Note buttons - all visible in a compact grid
|
|
note_frame = QFrame()
|
|
note_layout = QVBoxLayout(note_frame)
|
|
note_layout.setSpacing(3)
|
|
note_layout.addWidget(QLabel("Root Note:"))
|
|
note_widget = self.create_note_buttons()
|
|
note_layout.addWidget(note_widget)
|
|
layout.addWidget(note_frame)
|
|
|
|
# 12 Octave select buttons - all visible in a row
|
|
octave_frame = QFrame()
|
|
octave_layout = QVBoxLayout(octave_frame)
|
|
octave_layout.setSpacing(3)
|
|
octave_layout.addWidget(QLabel("Octave:"))
|
|
octave_widget = self.create_octave_buttons()
|
|
octave_layout.addWidget(octave_widget)
|
|
layout.addWidget(octave_frame)
|
|
|
|
# Scale buttons - compact grid, all visible
|
|
scale_frame = QFrame()
|
|
scale_layout = QVBoxLayout(scale_frame)
|
|
scale_layout.setSpacing(3)
|
|
scale_layout.addWidget(QLabel("Scale:"))
|
|
scale_widget = self.create_scale_buttons()
|
|
scale_layout.addWidget(scale_widget)
|
|
layout.addWidget(scale_frame)
|
|
|
|
# Octave Range dropdown
|
|
octave_frame = QFrame()
|
|
octave_layout = QVBoxLayout(octave_frame)
|
|
octave_layout.setSpacing(3)
|
|
octave_layout.addWidget(QLabel("Octave Range:"))
|
|
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) # 1 octave
|
|
octave_layout.addWidget(self.octave_range_combo)
|
|
layout.addWidget(octave_frame)
|
|
|
|
return group
|
|
|
|
def create_note_buttons(self) -> QWidget:
|
|
"""Create 12 note selection buttons in compact layout"""
|
|
widget = QWidget()
|
|
layout = QGridLayout(widget)
|
|
layout.setSpacing(1)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
# Note names for one octave
|
|
notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
|
|
self.root_note_buttons = {}
|
|
self.current_root_note = 0 # C (index in notes array)
|
|
self.armed_root_note_button = None
|
|
|
|
# Create 12 note buttons in 2 rows of 6
|
|
for i, note in enumerate(notes):
|
|
button = QPushButton(note)
|
|
button.setCheckable(True)
|
|
button.setFixedSize(40, 20)
|
|
button.clicked.connect(lambda checked, n=i: self.on_root_note_button_clicked(n))
|
|
|
|
# Set initial state
|
|
if i == 0: # C
|
|
button.setChecked(True)
|
|
self.update_root_note_button_style(button, "active")
|
|
else:
|
|
self.update_root_note_button_style(button, "inactive")
|
|
|
|
self.root_note_buttons[i] = button
|
|
layout.addWidget(button, i // 6, i % 6)
|
|
|
|
return widget
|
|
|
|
def create_octave_buttons(self) -> QWidget:
|
|
"""Create 12 octave select buttons (C0 to C11)"""
|
|
widget = QWidget()
|
|
layout = QGridLayout(widget)
|
|
layout.setSpacing(1)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
self.octave_buttons = {}
|
|
self.current_octave = 4 # C4
|
|
self.armed_octave_button = None
|
|
self.armed_root_note_button = None
|
|
|
|
# Create octave buttons C0 to C11 (12 octaves)
|
|
for octave in range(12):
|
|
button = QPushButton(f"C{octave}")
|
|
button.setCheckable(True)
|
|
button.setFixedSize(40, 20)
|
|
button.clicked.connect(lambda checked, o=octave: self.on_octave_button_clicked(o))
|
|
|
|
# Set initial state
|
|
if octave == 4: # C4
|
|
button.setChecked(True)
|
|
self.update_octave_button_style(button, "active")
|
|
else:
|
|
self.update_octave_button_style(button, "inactive")
|
|
|
|
self.octave_buttons[octave] = button
|
|
layout.addWidget(button, octave // 6, octave % 6)
|
|
|
|
return widget
|
|
|
|
def create_scale_buttons(self) -> QWidget:
|
|
"""Create scale selection buttons - compact layout"""
|
|
widget = QWidget()
|
|
layout = QGridLayout(widget)
|
|
layout.setSpacing(1)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
self.scale_buttons = {}
|
|
self.current_scale = "major"
|
|
self.armed_scale_button = None
|
|
|
|
scales = list(self.arpeggiator.SCALES.keys())
|
|
|
|
# Create scales in a compact grid
|
|
for i, scale in enumerate(scales):
|
|
display_name = scale.replace("_", " ").title()
|
|
if len(display_name) > 10:
|
|
display_name = display_name[:10] # Truncate long names
|
|
|
|
button = QPushButton(display_name)
|
|
button.setCheckable(True)
|
|
button.setFixedSize(70, 20)
|
|
button.clicked.connect(lambda checked, s=scale: self.on_scale_button_clicked(s))
|
|
|
|
# Set initial state
|
|
if scale == "major":
|
|
button.setChecked(True)
|
|
self.update_scale_button_style(button, "active")
|
|
else:
|
|
self.update_scale_button_style(button, "inactive")
|
|
|
|
self.scale_buttons[scale] = button
|
|
layout.addWidget(button, i // 4, i % 4) # 4 buttons per row
|
|
|
|
return widget
|
|
|
|
def update_root_note_button_style(self, button, state):
|
|
"""Update button styling based on state"""
|
|
if state == "active":
|
|
button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #2d5a2d;
|
|
color: white;
|
|
border: 2px solid #4a8a4a;
|
|
font-weight: bold;
|
|
font-size: 8px;
|
|
}
|
|
""")
|
|
elif state == "armed":
|
|
button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #5a4d2d;
|
|
color: white;
|
|
border: 2px solid #8a7a4a;
|
|
font-weight: bold;
|
|
font-size: 8px;
|
|
}
|
|
""")
|
|
else: # inactive
|
|
button.setStyleSheet("""
|
|
QPushButton {
|
|
font-size: 8px;
|
|
}
|
|
""")
|
|
|
|
def update_octave_button_style(self, button, state):
|
|
"""Update octave button styling based on state"""
|
|
if state == "active":
|
|
button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #2d5a2d;
|
|
color: white;
|
|
border: 2px solid #4a8a4a;
|
|
font-weight: bold;
|
|
font-size: 8px;
|
|
}
|
|
""")
|
|
elif state == "armed":
|
|
button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #5a4d2d;
|
|
color: white;
|
|
border: 2px solid #8a7a4a;
|
|
font-weight: bold;
|
|
font-size: 8px;
|
|
}
|
|
""")
|
|
else: # inactive
|
|
button.setStyleSheet("""
|
|
QPushButton {
|
|
font-size: 8px;
|
|
}
|
|
""")
|
|
|
|
def update_scale_button_style(self, button, state):
|
|
"""Update scale button styling based on state"""
|
|
if state == "active":
|
|
button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #2d5a2d;
|
|
color: white;
|
|
border: 2px solid #4a8a4a;
|
|
font-weight: bold;
|
|
font-size: 8px;
|
|
}
|
|
""")
|
|
elif state == "armed":
|
|
button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #5a4d2d;
|
|
color: white;
|
|
border: 2px solid #8a7a4a;
|
|
font-weight: bold;
|
|
font-size: 8px;
|
|
}
|
|
""")
|
|
else: # inactive
|
|
button.setStyleSheet("""
|
|
QPushButton {
|
|
font-size: 8px;
|
|
}
|
|
""")
|
|
|
|
def create_pattern_buttons(self) -> QWidget:
|
|
"""Create pattern selection buttons - compact grid"""
|
|
widget = QWidget()
|
|
layout = QGridLayout(widget)
|
|
layout.setSpacing(1)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
self.pattern_buttons = {}
|
|
self.current_pattern = "up"
|
|
self.armed_pattern_button = None
|
|
|
|
patterns = self.arpeggiator.PATTERN_TYPES
|
|
|
|
# Create patterns in a compact grid
|
|
for i, pattern in enumerate(patterns):
|
|
display_name = pattern.replace("_", " ").title()
|
|
if len(display_name) > 10:
|
|
display_name = display_name[:10] # Truncate long names
|
|
|
|
button = QPushButton(display_name)
|
|
button.setCheckable(True)
|
|
button.setFixedSize(70, 20)
|
|
button.clicked.connect(lambda checked, p=pattern: self.on_pattern_button_clicked(p))
|
|
|
|
# Set initial state
|
|
if pattern == "up":
|
|
button.setChecked(True)
|
|
self.update_pattern_button_style(button, "active")
|
|
else:
|
|
self.update_pattern_button_style(button, "inactive")
|
|
|
|
self.pattern_buttons[pattern] = button
|
|
layout.addWidget(button, i // 3, i % 3) # 3 buttons per row
|
|
|
|
return widget
|
|
|
|
def update_pattern_button_style(self, button, state):
|
|
"""Update pattern button styling based on state"""
|
|
if state == "active":
|
|
button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #2d5a2d;
|
|
color: white;
|
|
border: 2px solid #4a8a4a;
|
|
font-weight: bold;
|
|
font-size: 8px;
|
|
}
|
|
""")
|
|
elif state == "armed":
|
|
button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #5a4d2d;
|
|
color: white;
|
|
border: 2px solid #8a7a4a;
|
|
font-weight: bold;
|
|
font-size: 8px;
|
|
}
|
|
""")
|
|
else: # inactive
|
|
button.setStyleSheet("""
|
|
QPushButton {
|
|
font-size: 8px;
|
|
}
|
|
""")
|
|
|
|
def create_distribution_buttons(self) -> QWidget:
|
|
"""Create distribution selection buttons - compact grid"""
|
|
widget = QWidget()
|
|
layout = QGridLayout(widget)
|
|
layout.setSpacing(1)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
self.distribution_buttons = {}
|
|
self.current_distribution = "up"
|
|
self.armed_distribution_button = None
|
|
|
|
patterns = self.arpeggiator.CHANNEL_DISTRIBUTION_PATTERNS
|
|
|
|
# Create distributions in a compact grid
|
|
for i, pattern in enumerate(patterns):
|
|
display_name = pattern.replace("_", " ").title()
|
|
if len(display_name) > 12:
|
|
display_name = display_name[:12] # Truncate long names
|
|
|
|
button = QPushButton(display_name)
|
|
button.setCheckable(True)
|
|
button.setFixedSize(80, 20)
|
|
button.clicked.connect(lambda checked, p=pattern: self.on_distribution_button_clicked(p))
|
|
|
|
# Set initial state
|
|
if pattern == "up":
|
|
button.setChecked(True)
|
|
self.update_distribution_button_style(button, "active")
|
|
else:
|
|
self.update_distribution_button_style(button, "inactive")
|
|
|
|
self.distribution_buttons[pattern] = button
|
|
layout.addWidget(button, i // 3, i % 3) # 3 buttons per row
|
|
|
|
return widget
|
|
|
|
def update_distribution_button_style(self, button, state):
|
|
"""Update distribution button styling based on state"""
|
|
if state == "active":
|
|
button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #2d5a2d;
|
|
color: white;
|
|
border: 2px solid #4a8a4a;
|
|
font-weight: bold;
|
|
font-size: 8px;
|
|
}
|
|
""")
|
|
elif state == "armed":
|
|
button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #5a4d2d;
|
|
color: white;
|
|
border: 2px solid #8a7a4a;
|
|
font-weight: bold;
|
|
font-size: 8px;
|
|
}
|
|
""")
|
|
else: # inactive
|
|
button.setStyleSheet("""
|
|
QPushButton {
|
|
font-size: 8px;
|
|
}
|
|
""")
|
|
|
|
def create_pattern_settings(self) -> QGroupBox:
|
|
"""Create pattern settings controls - no scrollbars"""
|
|
group = QGroupBox("Pattern Settings")
|
|
layout = QVBoxLayout(group)
|
|
layout.setSpacing(2)
|
|
layout.setContentsMargins(5, 5, 5, 5)
|
|
|
|
# Pattern Type Buttons - all visible
|
|
pattern_frame = QFrame()
|
|
pattern_layout = QVBoxLayout(pattern_frame)
|
|
pattern_layout.addWidget(QLabel("Pattern:"))
|
|
pattern_widget = self.create_pattern_buttons()
|
|
pattern_layout.addWidget(pattern_widget)
|
|
layout.addWidget(pattern_frame)
|
|
|
|
return group
|
|
|
|
def create_distribution_settings(self) -> QGroupBox:
|
|
"""Create channel distribution settings - no scrollbars"""
|
|
group = QGroupBox("Channel Distribution")
|
|
layout = QVBoxLayout(group)
|
|
layout.setSpacing(2)
|
|
layout.setContentsMargins(5, 5, 5, 5)
|
|
|
|
# Channel Distribution Pattern Buttons - all visible
|
|
dist_frame = QFrame()
|
|
dist_layout = QVBoxLayout(dist_frame)
|
|
dist_layout.addWidget(QLabel("Distribution:"))
|
|
distribution_widget = self.create_distribution_buttons()
|
|
dist_layout.addWidget(distribution_widget)
|
|
layout.addWidget(dist_frame)
|
|
|
|
# Description label
|
|
self.distribution_description = QLabel("Channels: 1 → 2 → 3 → 4 → 5 → 6...")
|
|
self.distribution_description.setStyleSheet("color: #888888; font-style: italic;")
|
|
self.distribution_description.setWordWrap(True)
|
|
layout.addWidget(self.distribution_description)
|
|
|
|
return group
|
|
|
|
def create_timing_settings(self) -> QGroupBox:
|
|
"""Create timing controls"""
|
|
group = QGroupBox("Timing Settings")
|
|
layout = QGridLayout(group)
|
|
|
|
# Tempo
|
|
layout.addWidget(QLabel("Tempo:"), 0, 0)
|
|
self.tempo_spin = QSpinBox()
|
|
self.tempo_spin.setRange(40, 200)
|
|
self.tempo_spin.setValue(120)
|
|
self.tempo_spin.setSuffix(" BPM")
|
|
layout.addWidget(self.tempo_spin, 0, 1)
|
|
|
|
# Note Speed
|
|
layout.addWidget(QLabel("Note Speed:"), 1, 0)
|
|
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")
|
|
layout.addWidget(self.speed_combo, 1, 1)
|
|
|
|
# Gate (Note Length)
|
|
layout.addWidget(QLabel("Gate:"), 2, 0)
|
|
gate_layout = QHBoxLayout()
|
|
self.gate_slider = QSlider(Qt.Horizontal)
|
|
self.gate_slider.setRange(10, 200) # 10% to 200%
|
|
self.gate_slider.setValue(100)
|
|
self.gate_label = QLabel("100%")
|
|
self.gate_label.setFixedWidth(40)
|
|
gate_layout.addWidget(self.gate_slider)
|
|
gate_layout.addWidget(self.gate_label)
|
|
layout.addLayout(gate_layout, 2, 1)
|
|
|
|
# Swing
|
|
layout.addWidget(QLabel("Swing:"), 3, 0)
|
|
swing_layout = QHBoxLayout()
|
|
self.swing_slider = QSlider(Qt.Horizontal)
|
|
self.swing_slider.setRange(-100, 100)
|
|
self.swing_slider.setValue(0)
|
|
self.swing_label = QLabel("0%")
|
|
self.swing_label.setFixedWidth(40)
|
|
swing_layout.addWidget(self.swing_slider)
|
|
swing_layout.addWidget(self.swing_label)
|
|
layout.addLayout(swing_layout, 3, 1)
|
|
|
|
# Base Velocity
|
|
layout.addWidget(QLabel("Velocity:"), 4, 0)
|
|
velocity_layout = QHBoxLayout()
|
|
self.velocity_slider = QSlider(Qt.Horizontal)
|
|
self.velocity_slider.setRange(1, 127)
|
|
self.velocity_slider.setValue(80)
|
|
self.velocity_label = QLabel("80")
|
|
self.velocity_label.setFixedWidth(40)
|
|
velocity_layout.addWidget(self.velocity_slider)
|
|
velocity_layout.addWidget(self.velocity_label)
|
|
layout.addLayout(velocity_layout, 4, 1)
|
|
|
|
return group
|
|
|
|
def setup_preset_rotation(self):
|
|
"""Setup preset rotation controls"""
|
|
# Find the timing settings group that was just created
|
|
timing_groups = self.findChildren(QGroupBox)
|
|
timing_group = None
|
|
for group in timing_groups:
|
|
if group.title() == "Timing Settings":
|
|
timing_group = group
|
|
break
|
|
|
|
if timing_group and hasattr(timing_group, 'layout') and timing_group.layout():
|
|
layout = timing_group.layout()
|
|
|
|
# Preset rotation controls
|
|
layout.addWidget(QLabel("Preset Rotation:"), 6, 0)
|
|
|
|
preset_layout = QVBoxLayout()
|
|
|
|
# Enable checkbox
|
|
self.preset_rotation_checkbox = QPushButton("Enable Presets")
|
|
self.preset_rotation_checkbox.setCheckable(True)
|
|
self.preset_rotation_checkbox.setFixedSize(100, 25)
|
|
preset_layout.addWidget(self.preset_rotation_checkbox)
|
|
|
|
# Interval control
|
|
interval_layout = QHBoxLayout()
|
|
interval_layout.addWidget(QLabel("Every:"))
|
|
self.preset_interval_spin = QSpinBox()
|
|
self.preset_interval_spin.setRange(1, 16)
|
|
self.preset_interval_spin.setValue(4)
|
|
self.preset_interval_spin.setSuffix(" loops")
|
|
self.preset_interval_spin.setFixedSize(80, 25)
|
|
interval_layout.addWidget(self.preset_interval_spin)
|
|
preset_layout.addLayout(interval_layout)
|
|
|
|
# Preset buttons
|
|
preset_button_layout = QHBoxLayout()
|
|
self.save_preset_button = QPushButton("Save")
|
|
self.save_preset_button.setFixedSize(50, 25)
|
|
preset_button_layout.addWidget(self.save_preset_button)
|
|
|
|
self.load_preset_button = QPushButton("Load")
|
|
self.load_preset_button.setFixedSize(50, 25)
|
|
preset_button_layout.addWidget(self.load_preset_button)
|
|
preset_layout.addLayout(preset_button_layout)
|
|
|
|
layout.addLayout(preset_layout, 6, 1)
|
|
|
|
def connect_signals(self):
|
|
"""Connect GUI controls to arpeggiator"""
|
|
# Basic settings
|
|
self.octave_range_combo.currentIndexChanged.connect(self.on_octave_range_changed)
|
|
|
|
# Connect new button signals
|
|
for note_index, button in self.root_note_buttons.items():
|
|
button.clicked.connect(lambda checked, n=note_index: self.on_root_note_button_clicked(n))
|
|
|
|
for octave, button in self.octave_buttons.items():
|
|
button.clicked.connect(lambda checked, o=octave: self.on_octave_button_clicked(o))
|
|
|
|
# Timing settings
|
|
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)
|
|
|
|
# Arpeggiator state changes
|
|
self.arpeggiator.armed_state_changed.connect(self.update_armed_states)
|
|
|
|
|
|
def on_root_note_button_clicked(self, note_index):
|
|
"""Handle root note button click - uses note index (0-11) and octave"""
|
|
midi_note = self.current_octave * 12 + note_index
|
|
|
|
# If arpeggiator is playing, arm the change
|
|
if self.arpeggiator.is_playing:
|
|
# Clear previous armed state
|
|
if self.armed_root_note_button:
|
|
self.update_root_note_button_style(self.armed_root_note_button, "inactive")
|
|
|
|
# Set new armed state (but keep current active button green)
|
|
button = self.root_note_buttons[note_index]
|
|
self.armed_root_note_button = button
|
|
self.update_root_note_button_style(button, "armed")
|
|
self.arpeggiator.arm_root_note(midi_note)
|
|
else:
|
|
# Apply immediately
|
|
self.set_active_root_note(note_index)
|
|
self.arpeggiator.set_root_note(midi_note)
|
|
|
|
def on_scale_button_clicked(self, scale):
|
|
"""Handle scale button click - FIXED armed state logic"""
|
|
# If arpeggiator is playing, arm the change
|
|
if self.arpeggiator.is_playing:
|
|
# Clear previous armed state
|
|
if self.armed_scale_button:
|
|
self.update_scale_button_style(self.armed_scale_button, "inactive")
|
|
|
|
# Set new armed state (but keep current active button green)
|
|
button = self.scale_buttons[scale]
|
|
self.armed_scale_button = button
|
|
self.update_scale_button_style(button, "armed")
|
|
self.arpeggiator.arm_scale(scale)
|
|
else:
|
|
# Apply immediately
|
|
self.set_active_scale(scale)
|
|
self.arpeggiator.set_scale(scale)
|
|
|
|
def on_pattern_button_clicked(self, pattern):
|
|
"""Handle pattern button click - FIXED armed state logic"""
|
|
# If arpeggiator is playing, arm the change
|
|
if self.arpeggiator.is_playing:
|
|
# Clear previous armed state
|
|
if self.armed_pattern_button:
|
|
self.update_pattern_button_style(self.armed_pattern_button, "inactive")
|
|
|
|
# Set new armed state (but keep current active button green)
|
|
button = self.pattern_buttons[pattern]
|
|
self.armed_pattern_button = button
|
|
self.update_pattern_button_style(button, "armed")
|
|
self.arpeggiator.arm_pattern_type(pattern)
|
|
else:
|
|
# Apply immediately
|
|
self.set_active_pattern(pattern)
|
|
self.arpeggiator.set_pattern_type(pattern)
|
|
|
|
def on_distribution_button_clicked(self, distribution):
|
|
"""Handle distribution button click - FIXED armed state logic"""
|
|
# If arpeggiator is playing, arm the change
|
|
if self.arpeggiator.is_playing:
|
|
# Clear previous armed state
|
|
if self.armed_distribution_button:
|
|
self.update_distribution_button_style(self.armed_distribution_button, "inactive")
|
|
|
|
# Set new armed state (but keep current active button green)
|
|
button = self.distribution_buttons[distribution]
|
|
self.armed_distribution_button = button
|
|
self.update_distribution_button_style(button, "armed")
|
|
self.arpeggiator.arm_channel_distribution(distribution)
|
|
else:
|
|
# Apply immediately
|
|
self.set_active_distribution(distribution)
|
|
self.arpeggiator.set_channel_distribution(distribution)
|
|
|
|
def on_octave_button_clicked(self, octave):
|
|
"""Handle octave button click"""
|
|
# If arpeggiator is playing, arm the change
|
|
if self.arpeggiator.is_playing:
|
|
# Clear previous armed state
|
|
if self.armed_octave_button:
|
|
self.update_octave_button_style(self.armed_octave_button, "inactive")
|
|
|
|
# Set new armed state (but keep current active button green)
|
|
button = self.octave_buttons[octave]
|
|
self.armed_octave_button = button
|
|
self.update_octave_button_style(button, "armed")
|
|
# Arm the new MIDI note
|
|
midi_note = octave * 12 + self.current_root_note
|
|
self.arpeggiator.arm_root_note(midi_note)
|
|
else:
|
|
# Apply immediately
|
|
self.set_active_octave(octave)
|
|
midi_note = octave * 12 + self.current_root_note
|
|
self.arpeggiator.set_root_note(midi_note)
|
|
|
|
def set_active_root_note(self, note_index):
|
|
"""Set active root note button"""
|
|
# Clear current active state
|
|
if self.current_root_note in self.root_note_buttons:
|
|
self.update_root_note_button_style(self.root_note_buttons[self.current_root_note], "inactive")
|
|
|
|
# Clear armed state if it's the same note
|
|
if self.armed_root_note_button and self.armed_root_note_button == self.root_note_buttons.get(note_index):
|
|
self.armed_root_note_button = None
|
|
|
|
# Set new active state
|
|
self.current_root_note = note_index
|
|
if note_index in self.root_note_buttons:
|
|
self.update_root_note_button_style(self.root_note_buttons[note_index], "active")
|
|
|
|
def set_active_octave(self, octave):
|
|
"""Set active octave button"""
|
|
# Clear current active state
|
|
if self.current_octave in self.octave_buttons:
|
|
self.update_octave_button_style(self.octave_buttons[self.current_octave], "inactive")
|
|
|
|
# Clear armed state if it's the same octave
|
|
if self.armed_octave_button and self.armed_octave_button == self.octave_buttons.get(octave):
|
|
self.armed_octave_button = None
|
|
|
|
# Set new active state
|
|
self.current_octave = octave
|
|
if octave in self.octave_buttons:
|
|
self.update_octave_button_style(self.octave_buttons[octave], "active")
|
|
|
|
def set_active_scale(self, scale):
|
|
"""Set active scale button - FIXED to clear previous active"""
|
|
# Clear current active state
|
|
if self.current_scale in self.scale_buttons:
|
|
self.update_scale_button_style(self.scale_buttons[self.current_scale], "inactive")
|
|
|
|
# Clear armed state if it's the same scale
|
|
if self.armed_scale_button and self.armed_scale_button == self.scale_buttons.get(scale):
|
|
self.armed_scale_button = None
|
|
|
|
# Set new active state
|
|
self.current_scale = scale
|
|
if scale in self.scale_buttons:
|
|
self.update_scale_button_style(self.scale_buttons[scale], "active")
|
|
|
|
def set_active_pattern(self, pattern):
|
|
"""Set active pattern button - FIXED to clear previous active"""
|
|
# Clear current active state
|
|
if self.current_pattern in self.pattern_buttons:
|
|
self.update_pattern_button_style(self.pattern_buttons[self.current_pattern], "inactive")
|
|
|
|
# Clear armed state if it's the same pattern
|
|
if self.armed_pattern_button and self.armed_pattern_button == self.pattern_buttons.get(pattern):
|
|
self.armed_pattern_button = None
|
|
|
|
# Set new active state
|
|
self.current_pattern = pattern
|
|
if pattern in self.pattern_buttons:
|
|
self.update_pattern_button_style(self.pattern_buttons[pattern], "active")
|
|
|
|
def set_active_distribution(self, distribution):
|
|
"""Set active distribution button - FIXED to clear previous active"""
|
|
# Clear current active state
|
|
if self.current_distribution in self.distribution_buttons:
|
|
self.update_distribution_button_style(self.distribution_buttons[self.current_distribution], "inactive")
|
|
|
|
# Clear armed state if it's the same distribution
|
|
if self.armed_distribution_button and self.armed_distribution_button == self.distribution_buttons.get(distribution):
|
|
self.armed_distribution_button = None
|
|
|
|
# Set new active state
|
|
self.current_distribution = distribution
|
|
if distribution in self.distribution_buttons:
|
|
self.update_distribution_button_style(self.distribution_buttons[distribution], "active")
|
|
self.update_distribution_description(distribution)
|
|
|
|
@pyqtSlot()
|
|
def update_armed_states(self):
|
|
"""Update armed states when arpeggiator state changes - FIXED logic"""
|
|
# This is called when armed states are applied at pattern end
|
|
|
|
if self.armed_root_note_button and self.arpeggiator.armed_root_note is None:
|
|
# Armed root note was applied, move to active
|
|
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:
|
|
# Armed octave was applied, move to active
|
|
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:
|
|
# Armed scale was applied, move to active
|
|
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:
|
|
# Armed pattern was applied, move to active
|
|
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:
|
|
# Armed distribution was applied, move to active
|
|
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 pattern 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"
|
|
}
|
|
|
|
description = descriptions.get(distribution, "Unknown pattern")
|
|
self.distribution_description.setText(description)
|
|
|
|
@pyqtSlot(int)
|
|
def on_tempo_changed(self, tempo):
|
|
"""Handle tempo change"""
|
|
self.arpeggiator.set_tempo(float(tempo))
|
|
|
|
@pyqtSlot(str)
|
|
def on_speed_changed(self, speed):
|
|
"""Handle note speed change"""
|
|
self.arpeggiator.set_note_speed(speed)
|
|
|
|
@pyqtSlot(int)
|
|
def on_gate_changed(self, gate_percent):
|
|
"""Handle gate change"""
|
|
gate_value = gate_percent / 100.0
|
|
self.arpeggiator.set_gate(gate_value)
|
|
self.gate_label.setText(f"{gate_percent}%")
|
|
|
|
@pyqtSlot(int)
|
|
def on_swing_changed(self, swing_percent):
|
|
"""Handle swing change"""
|
|
swing_value = swing_percent / 100.0
|
|
self.arpeggiator.set_swing(swing_value)
|
|
self.swing_label.setText(f"{swing_percent}%")
|
|
|
|
@pyqtSlot(int)
|
|
def on_octave_range_changed(self, index):
|
|
"""Handle octave range change"""
|
|
octaves = index + 1 # Convert 0-based index to 1-4 range
|
|
self.arpeggiator.set_octave_range(octaves)
|
|
|
|
@pyqtSlot(int)
|
|
def on_velocity_changed(self, velocity):
|
|
"""Handle velocity change"""
|
|
self.arpeggiator.set_velocity(velocity)
|
|
self.velocity_label.setText(str(velocity))
|
|
|
|
@pyqtSlot(bool)
|
|
def on_preset_rotation_toggled(self, enabled):
|
|
"""Handle preset rotation enable/disable"""
|
|
self.preset_rotation_enabled = enabled
|
|
print(f"Preset rotation {'enabled' if enabled else 'disabled'}")
|
|
|
|
@pyqtSlot(int)
|
|
def on_preset_interval_changed(self, interval):
|
|
"""Handle preset rotation interval change"""
|
|
self.preset_rotation_interval = interval
|
|
print(f"Preset interval set to {interval} patterns")
|
|
|
|
def save_current_preset(self):
|
|
"""Save current settings as a 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() if hasattr(self, 'octave_range_combo') else 0,
|
|
'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):
|
|
"""Show preset selection dialog"""
|
|
if not self.presets:
|
|
print("No presets saved")
|
|
return
|
|
|
|
# For now, cycle through presets
|
|
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
|
|
|
|
next_preset = preset_names[next_index]
|
|
self.load_preset(next_preset)
|
|
|
|
def load_preset(self, preset_name: str):
|
|
"""Load a specific preset"""
|
|
if preset_name not in self.presets:
|
|
return
|
|
|
|
preset = self.presets[preset_name]
|
|
self.current_preset = preset_name
|
|
|
|
# Apply preset settings
|
|
if self.arpeggiator.is_playing:
|
|
# Arm changes for pattern-end application
|
|
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 immediately
|
|
if hasattr(self, 'octave_range_combo'):
|
|
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}")
|