diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 342a06f..b84f5e8 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -3,9 +3,22 @@ "allow": [ "Bash(python:*)", "Bash(copy guiarpeggiator_controls.py guiarpeggiator_controls_backup.py)", - "Bash(copy guiarpeggiator_controls_new.py guiarpeggiator_controls.py)" + "Bash(copy guiarpeggiator_controls_new.py guiarpeggiator_controls.py)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git push:*)", + "Bash(git reset:*)", + "Bash(grep:*)", + "Bash(dir)", + "Bash(run_in_venv.bat)", + "Bash(./run_in_venv.bat)", + "Bash(git rm:*)", + "Bash(del test_group_cycling.py)" ], "deny": [], - "ask": [] + "ask": [], + "additionalDirectories": [ + "C:\\c\\git" + ] } } \ No newline at end of file diff --git a/__pycache__/main.cpython-310.pyc b/__pycache__/main.cpython-310.pyc index a5651f5..8cce9ea 100644 Binary files a/__pycache__/main.cpython-310.pyc and b/__pycache__/main.cpython-310.pyc differ diff --git a/core/__pycache__/arpeggiator_engine.cpython-310.pyc b/core/__pycache__/arpeggiator_engine.cpython-310.pyc index 8d587d4..6b12e36 100644 Binary files a/core/__pycache__/arpeggiator_engine.cpython-310.pyc and b/core/__pycache__/arpeggiator_engine.cpython-310.pyc differ diff --git a/core/__pycache__/output_manager.cpython-310.pyc b/core/__pycache__/output_manager.cpython-310.pyc index 22091ad..5ef29bf 100644 Binary files a/core/__pycache__/output_manager.cpython-310.pyc and b/core/__pycache__/output_manager.cpython-310.pyc differ diff --git a/core/__pycache__/volume_pattern_engine.cpython-310.pyc b/core/__pycache__/volume_pattern_engine.cpython-310.pyc index a3ccb08..75bc4a2 100644 Binary files a/core/__pycache__/volume_pattern_engine.cpython-310.pyc and b/core/__pycache__/volume_pattern_engine.cpython-310.pyc differ diff --git a/gui/__pycache__/arpeggiator_controls.cpython-310.pyc b/gui/__pycache__/arpeggiator_controls.cpython-310.pyc index 5c07be7..db87535 100644 Binary files a/gui/__pycache__/arpeggiator_controls.cpython-310.pyc and b/gui/__pycache__/arpeggiator_controls.cpython-310.pyc differ diff --git a/gui/__pycache__/channel_controls.cpython-310.pyc b/gui/__pycache__/channel_controls.cpython-310.pyc index 40c7e88..81baf0b 100644 Binary files a/gui/__pycache__/channel_controls.cpython-310.pyc and b/gui/__pycache__/channel_controls.cpython-310.pyc differ diff --git a/gui/__pycache__/main_window.cpython-310.pyc b/gui/__pycache__/main_window.cpython-310.pyc index 86b0083..bfb78bc 100644 Binary files a/gui/__pycache__/main_window.cpython-310.pyc and b/gui/__pycache__/main_window.cpython-310.pyc differ diff --git a/gui/__pycache__/output_controls.cpython-310.pyc b/gui/__pycache__/output_controls.cpython-310.pyc index 893444d..dbff333 100644 Binary files a/gui/__pycache__/output_controls.cpython-310.pyc and b/gui/__pycache__/output_controls.cpython-310.pyc differ diff --git a/gui/__pycache__/preset_controls.cpython-310.pyc b/gui/__pycache__/preset_controls.cpython-310.pyc index 1497be3..a5450d3 100644 Binary files a/gui/__pycache__/preset_controls.cpython-310.pyc and b/gui/__pycache__/preset_controls.cpython-310.pyc differ diff --git a/gui/__pycache__/simulator_display.cpython-310.pyc b/gui/__pycache__/simulator_display.cpython-310.pyc index 61859df..9d4a945 100644 Binary files a/gui/__pycache__/simulator_display.cpython-310.pyc and b/gui/__pycache__/simulator_display.cpython-310.pyc differ diff --git a/gui/__pycache__/volume_controls.cpython-310.pyc b/gui/__pycache__/volume_controls.cpython-310.pyc index 324d208..08ee6b8 100644 Binary files a/gui/__pycache__/volume_controls.cpython-310.pyc and b/gui/__pycache__/volume_controls.cpython-310.pyc differ diff --git a/gui/arpeggiator_controls.py b/gui/arpeggiator_controls.py index 4a874cc..d2ba99d 100644 --- a/gui/arpeggiator_controls.py +++ b/gui/arpeggiator_controls.py @@ -4,7 +4,7 @@ Arpeggiator Controls - READABLE BUTTONS WITH PROPER SIZING from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QGroupBox, QComboBox, QSlider, QSpinBox, QLabel, - QPushButton, QFrame) + QPushButton, QFrame, QSizePolicy) from PyQt5.QtCore import Qt, pyqtSlot class ArpeggiatorControls(QWidget): @@ -28,6 +28,10 @@ class ArpeggiatorControls(QWidget): self.speed_buttons = {} self.pattern_length_buttons = {} + # Scaling support + self.scale_factor = 1.0 + self.all_buttons = [] # Track all buttons for scaling updates + self.current_root_note = 0 self.current_octave = 4 self.current_scale = "major" @@ -49,19 +53,143 @@ class ArpeggiatorControls(QWidget): self.setup_ui() self.connect_signals() + def create_scalable_button(self, text, base_width=40, base_height=22, base_font_size=12, + checkable=False, style_type="normal"): + """Create a button with scalable sizing and styling""" + button = QPushButton(text) + button.setCheckable(checkable) + + # Set size policy for expansion + button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + + # Set minimum size based on scaled values + min_width = max(30, int(base_width * self.scale_factor)) + min_height = max(18, int(base_height * self.scale_factor)) + button.setMinimumSize(min_width, min_height) + + # Apply initial styling + self.apply_button_style(button, base_font_size, style_type) + + # Track button for scaling updates + self.all_buttons.append(button) + + return button + + def apply_button_style(self, button, base_font_size=12, style_type="normal"): + """Apply scalable styling to a button""" + font_size = max(8, int(base_font_size * self.scale_factor)) + padding = max(1, int(3 * self.scale_factor)) + + if style_type == "active": + button.setStyleSheet(f""" + QPushButton {{ + background: #00aa44; + color: white; + font-size: {font_size}px; + font-weight: bold; + padding: {padding}px; + border: 1px solid #00cc55; + }} + QPushButton:hover {{ + background: #00cc66; + border: 1px solid #00ee77; + }} + """) + elif style_type == "orange": + button.setStyleSheet(f""" + QPushButton {{ + background: #cc6600; + color: white; + font-size: {font_size}px; + font-weight: bold; + padding: {padding}px; + border: 1px solid #ee8800; + }} + QPushButton:hover {{ + background: #ee7700; + border: 1px solid #ffaa00; + }} + """) + elif style_type == "blue": + button.setStyleSheet(f""" + QPushButton {{ + background: #0066cc; + color: white; + font-size: {font_size}px; + font-weight: bold; + padding: {padding}px; + border: 1px solid #0088ee; + }} + QPushButton:hover {{ + background: #0088ee; + border: 1px solid #00aaff; + }} + """) + else: # normal + button.setStyleSheet(f""" + QPushButton {{ + background: #3a3a3a; + color: #ffffff; + font-size: {font_size}px; + font-weight: bold; + padding: {padding}px; + border: 1px solid #555555; + }} + QPushButton:hover {{ + background: #505050; + border: 1px solid #777777; + }} + """) + + def apply_scaling(self, scale_factor): + """Apply new scaling factor to all buttons""" + self.scale_factor = scale_factor + + # Update all tracked buttons + for button in self.all_buttons: + # Update minimum size + current_size = button.minimumSize() + new_width = max(30, int(40 * scale_factor)) # Base width 40 + new_height = max(18, int(22 * scale_factor)) # Base height 22 + button.setMinimumSize(new_width, new_height) + + # Re-apply styling with new scale + # Determine button type by checking current style + current_style = button.styleSheet() + if "#00aa44" in current_style: + self.apply_button_style(button, 12, "active") + elif "#cc6600" in current_style: + self.apply_button_style(button, 12, "orange") + elif "#0066cc" in current_style: + self.apply_button_style(button, 12, "blue") + else: + self.apply_button_style(button, 12, "normal") + def setup_ui(self): """Clean quadrant layout with readable buttons""" - # Main grid + # Main grid with better spacing and expansion main = QGridLayout(self) - main.setSpacing(8) - main.setContentsMargins(8, 8, 8, 8) + main.setSpacing(12) + main.setContentsMargins(12, 12, 12, 12) + + # Equal quadrants with size policies + basic_quad = self.basic_quadrant() + basic_quad.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + main.addWidget(basic_quad, 0, 0) - # Equal quadrants - main.addWidget(self.basic_quadrant(), 0, 0) - main.addWidget(self.distribution_quadrant(), 0, 1) - main.addWidget(self.pattern_quadrant(), 1, 0) - main.addWidget(self.timing_quadrant(), 1, 1) + dist_quad = self.distribution_quadrant() + dist_quad.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + main.addWidget(dist_quad, 0, 1) + pattern_quad = self.pattern_quadrant() + pattern_quad.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + main.addWidget(pattern_quad, 1, 0) + + timing_quad = self.timing_quadrant() + timing_quad.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + main.addWidget(timing_quad, 1, 1) + + # Equal stretch for all quadrants main.setRowStretch(0, 1) main.setRowStretch(1, 1) main.setColumnStretch(0, 1) @@ -83,15 +211,12 @@ class ArpeggiatorControls(QWidget): notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] for i, note in enumerate(notes): - btn = QPushButton(note) - btn.setFixedSize(40, 22) # Taller buttons for better readability - btn.setCheckable(True) - btn.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;") + btn = self.create_scalable_button(note, 40, 22, 12, checkable=True, + style_type="active" if i == 0 else "normal") btn.clicked.connect(lambda checked, n=i: self.on_root_note_clicked(n)) if i == 0: btn.setChecked(True) - btn.setStyleSheet("background: #00aa44; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #00cc55;") self.root_note_buttons[i] = btn notes_layout.addWidget(btn) @@ -106,15 +231,12 @@ class ArpeggiatorControls(QWidget): octave_layout.setContentsMargins(0, 0, 0, 0) for octave in range(3, 9): # C3 to C8 - btn = QPushButton(f"C{octave}") - btn.setFixedSize(50, 22) # Taller buttons for better readability - btn.setCheckable(True) - btn.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;") + btn = self.create_scalable_button(f"C{octave}", 50, 22, 12, checkable=True, + style_type="active" if octave == 4 else "normal") btn.clicked.connect(lambda checked, o=octave: self.on_octave_clicked(o)) if octave == 4: btn.setChecked(True) - btn.setStyleSheet("background: #00aa44; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #00cc55;") self.octave_buttons[octave] = btn octave_layout.addWidget(btn) @@ -135,15 +257,12 @@ class ArpeggiatorControls(QWidget): if len(display_name) > 10: display_name = display_name[:10] - btn = QPushButton(display_name) - btn.setFixedSize(120, 22) # Taller buttons for better readability - btn.setCheckable(True) - btn.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;") + btn = self.create_scalable_button(display_name, 120, 22, 12, checkable=True, + style_type="active" if scale == "major" else "normal") btn.clicked.connect(lambda checked, s=scale: self.on_scale_clicked(s)) if scale == "major": btn.setChecked(True) - btn.setStyleSheet("background: #00aa44; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #00cc55;") self.scale_buttons[scale] = btn scales_layout.addWidget(btn, i // 4, i % 4) diff --git a/gui/main_window.py b/gui/main_window.py index 7b1f2a9..149e5cf 100644 --- a/gui/main_window.py +++ b/gui/main_window.py @@ -7,8 +7,9 @@ Integrates all GUI components into a cohesive interface. from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QPushButton, QLabel, QSlider, QComboBox, - QSpinBox, QGroupBox, QTabWidget, QSplitter, QFrame) -from PyQt5.QtCore import Qt, QTimer, pyqtSlot + QSpinBox, QGroupBox, QTabWidget, QSplitter, QFrame, + QSizePolicy) +from PyQt5.QtCore import Qt, QTimer, pyqtSlot, QSize from PyQt5.QtGui import QFont, QPalette, QColor, QKeySequence from .arpeggiator_controls import ArpeggiatorControls @@ -18,6 +19,65 @@ from .simulator_display import SimulatorDisplay from .output_controls import OutputControls from .preset_controls import PresetControls +class ScalingUtils: + """Utility class for handling dynamic GUI scaling""" + + @staticmethod + def get_scale_factor(widget): + """Calculate scale factor based on widget size""" + # Base size is 1200x800 + base_width = 1200 + base_height = 800 + + # Get actual widget size + actual_size = widget.size() + width_factor = actual_size.width() / base_width + height_factor = actual_size.height() / base_height + + # Use the smaller factor to maintain proportions + scale_factor = min(width_factor, height_factor) + + # Clamp between 0.8 and 3.0 for reasonable bounds + return max(0.8, min(3.0, scale_factor)) + + @staticmethod + def scale_font_size(base_size, scale_factor): + """Scale font size with factor""" + return max(8, int(base_size * scale_factor)) + + @staticmethod + def scale_button_size(base_width, base_height, scale_factor): + """Scale button dimensions with factor""" + return ( + max(30, int(base_width * scale_factor)), + max(20, int(base_height * scale_factor)) + ) + + @staticmethod + def apply_scalable_button_style(button, base_font_size=12, scale_factor=1.0): + """Apply scalable styling to a button""" + font_size = ScalingUtils.scale_font_size(base_font_size, scale_factor) + padding = max(2, int(5 * scale_factor)) + + button.setStyleSheet(f""" + QPushButton {{ + background: #3a3a3a; + color: #ffffff; + font-size: {font_size}px; + font-weight: bold; + border: 1px solid #555555; + padding: {padding}px; + min-height: {max(18, int(22 * scale_factor))}px; + }} + QPushButton:hover {{ + background: #505050; + border: 1px solid #777777; + }} + """) + + # Set size policy to allow expansion + button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + class MainWindow(QMainWindow): """ Main application window containing all GUI components. @@ -37,6 +97,10 @@ class MainWindow(QMainWindow): self.setWindowTitle("MIDI Arpeggiator - Lighting Controller") self.setMinimumSize(1200, 800) + # Scaling support + self.scale_factor = 1.0 + self.scaling_enabled = True + # Keyboard note mapping self.keyboard_notes = { Qt.Key_A: 60, # C @@ -66,6 +130,8 @@ class MainWindow(QMainWindow): # Create main layout with full-window tabs main_layout = QVBoxLayout(central_widget) + main_layout.setContentsMargins(8, 8, 8, 8) + main_layout.setSpacing(8) # Transport controls at top transport_frame = self.create_transport_controls() @@ -73,7 +139,8 @@ class MainWindow(QMainWindow): # Create tabbed interface that fills the window tab_widget = QTabWidget() - main_layout.addWidget(tab_widget) + tab_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + main_layout.addWidget(tab_widget, 1) # Give tab widget all extra space # Arpeggiator tab with quadrant layout self.arp_controls = ArpeggiatorControls(self.arpeggiator, self.channel_manager, self.simulator) @@ -622,6 +689,29 @@ class MainWindow(QMainWindow): super().keyReleaseEvent(event) + def resizeEvent(self, event): + """Handle window resize for dynamic scaling""" + super().resizeEvent(event) + + if self.scaling_enabled and hasattr(self, 'arp_controls'): + # Calculate new scale factor + new_scale_factor = ScalingUtils.get_scale_factor(self) + + # Only update if scale factor changed significantly + if abs(new_scale_factor - self.scale_factor) > 0.1: + self.scale_factor = new_scale_factor + self.update_scaling() + + def update_scaling(self): + """Update all GUI elements with new scaling""" + # Update controls with new scaling + if hasattr(self.arp_controls, 'apply_scaling'): + self.arp_controls.apply_scaling(self.scale_factor) + if hasattr(self.volume_controls, 'apply_scaling'): + self.volume_controls.apply_scaling(self.scale_factor) + if hasattr(self.preset_controls, 'apply_scaling'): + self.preset_controls.apply_scaling(self.scale_factor) + def closeEvent(self, event): """Handle window close event""" # Clean up resources diff --git a/gui/preset_controls.py b/gui/preset_controls.py index f930463..3c7ca31 100644 --- a/gui/preset_controls.py +++ b/gui/preset_controls.py @@ -7,10 +7,12 @@ Interface for saving, loading, and managing presets. from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QGroupBox, QListWidget, QPushButton, QLineEdit, QLabel, QFileDialog, QMessageBox, QListWidgetItem, - QInputDialog, QFrame) -from PyQt5.QtCore import Qt, pyqtSlot + QInputDialog, QFrame, QSpinBox, QComboBox, QCheckBox, + QSplitter) +from PyQt5.QtCore import Qt, pyqtSlot, QTimer import json import os +import random class PresetControls(QWidget): """Control panel for preset management""" @@ -26,6 +28,18 @@ class PresetControls(QWidget): self.current_preset = None self.presets_directory = "presets" + # Preset group functionality + self.preset_group = [] # List of preset names in the group + self.group_enabled = False + self.group_current_index = 0 + self.group_loop_count = 1 # How many times to play each preset + self.group_current_loops = 0 # Current loop count for active preset + self.group_order = "in_order" # "in_order" or "random" + self.group_pattern_note_count = 0 # Count notes played in current pattern loop + self.group_timer = QTimer() + self.group_timer.setSingleShot(True) + self.group_timer.timeout.connect(self.advance_group_preset) + # Ensure presets directory exists os.makedirs(self.presets_directory, exist_ok=True) @@ -35,22 +49,51 @@ class PresetControls(QWidget): # Connect to armed state changes self.arpeggiator.armed_state_changed.connect(self.on_armed_state_changed) + + # Connect to playing state changes for group cycling + self.arpeggiator.playing_state_changed.connect(self.on_playing_state_changed) + self.arpeggiator.pattern_step.connect(self.on_pattern_step) + + def apply_scaling(self, scale_factor): + """Apply scaling to preset controls (placeholder for future implementation)""" + # For now, preset controls don't need special scaling + # Individual buttons already use expanding size policies from their styling + pass def setup_ui(self): """Set up the user interface""" layout = QVBoxLayout(self) - # Preset list + # Create splitter to divide preset management and preset groups + splitter = QSplitter(Qt.Horizontal) + layout.addWidget(splitter) + + # Left side: Original preset management + preset_widget = QWidget() + preset_layout = QVBoxLayout(preset_widget) + preset_group = self.create_preset_list() - layout.addWidget(preset_group) + preset_layout.addWidget(preset_group) - # Preset operations operations_group = self.create_operations() - layout.addWidget(operations_group) + preset_layout.addWidget(operations_group) - # File operations file_group = self.create_file_operations() - layout.addWidget(file_group) + preset_layout.addWidget(file_group) + + splitter.addWidget(preset_widget) + + # Right side: Preset group functionality + group_widget = QWidget() + group_layout = QVBoxLayout(group_widget) + + preset_group_section = self.create_preset_group_section() + group_layout.addWidget(preset_group_section) + + splitter.addWidget(group_widget) + + # Set equal sizes for both sides + splitter.setSizes([400, 400]) def create_preset_list(self) -> QGroupBox: """Create preset list display""" @@ -153,6 +196,124 @@ class PresetControls(QWidget): return group + def create_preset_group_section(self) -> QGroupBox: + """Create preset group functionality section""" + group = QGroupBox("Preset Groups") + layout = QVBoxLayout(group) + + # Enable/Disable group cycling + self.group_enable_checkbox = QCheckBox("Enable Group Cycling") + self.group_enable_checkbox.stateChanged.connect(self.on_group_enable_changed) + layout.addWidget(self.group_enable_checkbox) + + # Current group status + status_layout = QHBoxLayout() + status_layout.addWidget(QLabel("Status:")) + self.group_status_label = QLabel("Inactive") + self.group_status_label.setStyleSheet("color: #888888;") + status_layout.addWidget(self.group_status_label) + status_layout.addStretch() + layout.addLayout(status_layout) + + # Group preset list + layout.addWidget(QLabel("Presets in Group:")) + self.group_preset_list = QListWidget() + self.group_preset_list.setMaximumHeight(150) + self.group_preset_list.setDragDropMode(QListWidget.InternalMove) # Allow reordering + layout.addWidget(self.group_preset_list) + + # Add/Remove buttons + group_buttons_layout = QHBoxLayout() + + self.add_to_group_button = QPushButton("Add Selected →") + self.add_to_group_button.setEnabled(False) + self.add_to_group_button.clicked.connect(self.add_preset_to_group) + self.add_to_group_button.setStyleSheet("background: #3a3a3a; color: #ffffff; font-weight: bold; font-size: 12px; border: 1px solid #555555; padding: 5px 10px;") + group_buttons_layout.addWidget(self.add_to_group_button) + + self.remove_from_group_button = QPushButton("← Remove") + self.remove_from_group_button.setEnabled(False) + self.remove_from_group_button.clicked.connect(self.remove_preset_from_group) + self.remove_from_group_button.setStyleSheet("background: #5a2d2d; color: #ff9999; font-weight: bold; font-size: 12px; border: 1px solid #aa5555; padding: 5px 10px;") + group_buttons_layout.addWidget(self.remove_from_group_button) + + layout.addLayout(group_buttons_layout) + + # Clear group button + self.clear_group_button = QPushButton("Clear Group") + self.clear_group_button.clicked.connect(self.clear_preset_group) + self.clear_group_button.setStyleSheet("background: #5a2d2d; color: #ff9999; font-weight: bold; font-size: 12px; border: 1px solid #aa5555; padding: 5px 10px;") + layout.addWidget(self.clear_group_button) + + # Group settings + settings_frame = QFrame() + settings_frame.setFrameStyle(QFrame.Box) + settings_layout = QGridLayout(settings_frame) + + # Loop count + settings_layout.addWidget(QLabel("Loop Count:"), 0, 0) + self.loop_count_spinbox = QSpinBox() + self.loop_count_spinbox.setRange(1, 99) + self.loop_count_spinbox.setValue(1) + self.loop_count_spinbox.valueChanged.connect(self.on_loop_count_changed) + settings_layout.addWidget(self.loop_count_spinbox, 0, 1) + + # Preset order + settings_layout.addWidget(QLabel("Order:"), 1, 0) + self.order_combo = QComboBox() + self.order_combo.addItems(["In Order", "Random"]) + self.order_combo.currentTextChanged.connect(self.on_order_changed) + settings_layout.addWidget(self.order_combo, 1, 1) + + layout.addWidget(settings_frame) + + # Manual controls + manual_layout = QHBoxLayout() + + self.prev_preset_button = QPushButton("◀ Previous") + self.prev_preset_button.setEnabled(False) + self.prev_preset_button.clicked.connect(self.goto_previous_preset) + self.prev_preset_button.setStyleSheet("background: #3a3a3a; color: #ffffff; font-weight: bold; font-size: 12px; border: 1px solid #555555; padding: 5px 10px;") + manual_layout.addWidget(self.prev_preset_button) + + self.next_preset_button = QPushButton("Next ▶") + self.next_preset_button.setEnabled(False) + self.next_preset_button.clicked.connect(self.goto_next_preset) + self.next_preset_button.setStyleSheet("background: #3a3a3a; color: #ffffff; font-weight: bold; font-size: 12px; border: 1px solid #555555; padding: 5px 10px;") + manual_layout.addWidget(self.next_preset_button) + + # TEMPORARY DEBUG BUTTON + self.debug_advance_button = QPushButton("DEBUG: Force Advance") + self.debug_advance_button.clicked.connect(self.advance_group_preset) + self.debug_advance_button.setStyleSheet("background: #5a2d5a; color: #ffaaff; font-weight: bold; font-size: 12px; border: 1px solid #8a4a8a; padding: 5px 10px;") + manual_layout.addWidget(self.debug_advance_button) + + layout.addLayout(manual_layout) + + # Master file controls + master_frame = QFrame() + master_frame.setFrameStyle(QFrame.Box) + master_layout = QGridLayout(master_frame) + + master_layout.addWidget(QLabel("Master Files:"), 0, 0, 1, 2) + + self.save_master_button = QPushButton("Save Master...") + self.save_master_button.clicked.connect(self.save_master_file) + self.save_master_button.setStyleSheet("background: #2d5a2d; color: #ffffff; font-weight: bold; font-size: 12px; border: 1px solid #4a8a4a; padding: 5px 10px;") + master_layout.addWidget(self.save_master_button, 1, 0) + + self.load_master_button = QPushButton("Load Master...") + self.load_master_button.clicked.connect(self.load_master_file) + self.load_master_button.setStyleSheet("background: #3a3a3a; color: #ffffff; font-weight: bold; font-size: 12px; border: 1px solid #555555; padding: 5px 10px;") + master_layout.addWidget(self.load_master_button, 1, 1) + + layout.addWidget(master_frame) + + # Connect group list selection + self.group_preset_list.itemSelectionChanged.connect(self.on_group_selection_changed) + + return group + def capture_current_settings(self) -> dict: """Capture current settings into a preset dictionary""" preset = { @@ -163,6 +324,7 @@ class PresetControls(QWidget): "arpeggiator": { "root_note": self.arpeggiator.root_note, "scale": self.arpeggiator.scale, + "scale_note_start": self.arpeggiator.scale_note_start, "pattern_type": self.arpeggiator.pattern_type, "octave_range": self.arpeggiator.octave_range, "note_speed": self.arpeggiator.note_speed, @@ -170,7 +332,7 @@ class PresetControls(QWidget): "swing": self.arpeggiator.swing, "velocity": self.arpeggiator.velocity, "tempo": self.arpeggiator.tempo, - "pattern_length": getattr(self.arpeggiator, 'pattern_length', 8), + "user_pattern_length": getattr(self.arpeggiator, 'user_pattern_length', 8), "channel_distribution": self.arpeggiator.channel_distribution, "delay_enabled": self.arpeggiator.delay_enabled, "delay_length": self.arpeggiator.delay_length, @@ -211,6 +373,7 @@ class PresetControls(QWidget): arp_settings = preset.get("arpeggiator", {}) self.arpeggiator.set_root_note(arp_settings.get("root_note", 60)) self.arpeggiator.set_scale(arp_settings.get("scale", "major")) + self.arpeggiator.set_scale_note_start(arp_settings.get("scale_note_start", 0)) self.arpeggiator.set_pattern_type(arp_settings.get("pattern_type", "up")) self.arpeggiator.set_octave_range(arp_settings.get("octave_range", 1)) self.arpeggiator.set_note_speed(arp_settings.get("note_speed", "1/8")) @@ -219,10 +382,12 @@ class PresetControls(QWidget): self.arpeggiator.set_velocity(arp_settings.get("velocity", 80)) self.arpeggiator.set_tempo(arp_settings.get("tempo", 120.0)) - # Apply pattern length if available - if "pattern_length" in arp_settings: - if hasattr(self.arpeggiator, 'set_pattern_length'): - self.arpeggiator.set_pattern_length(arp_settings["pattern_length"]) + # Apply user pattern length (check both old and new names for compatibility) + pattern_length = arp_settings.get("user_pattern_length") or arp_settings.get("pattern_length", 8) + if hasattr(self.arpeggiator, 'set_user_pattern_length'): + self.arpeggiator.set_user_pattern_length(pattern_length) + elif hasattr(self.arpeggiator, 'set_pattern_length'): + self.arpeggiator.set_pattern_length(pattern_length) # Apply channel distribution self.arpeggiator.set_channel_distribution(arp_settings.get("channel_distribution", "up")) @@ -299,6 +464,9 @@ class PresetControls(QWidget): self.rename_button.setEnabled(has_selection) self.duplicate_button.setEnabled(has_selection) self.export_button.setEnabled(has_selection) + + # Update group UI state (for add button enablement) + self.update_group_ui_state() except RuntimeError: # Item was deleted, disable all buttons self.load_button.setEnabled(False) @@ -629,4 +797,435 @@ class PresetControls(QWidget): def on_armed_state_changed(self): """Handle armed state changes""" # Update UI colors when armed state changes - self.update_preset_list_colors() \ No newline at end of file + self.update_preset_list_colors() + + # ======= PRESET GROUP FUNCTIONALITY ======= + + def on_group_enable_changed(self, state): + """Handle group cycling enable/disable""" + self.group_enabled = state == Qt.Checked + print(f"DEBUG: Group cycling enabled changed to: {self.group_enabled} (state={state})") + + if self.group_enabled and len(self.preset_group) > 0: + print("DEBUG: Calling start_group_cycling") + self.start_group_cycling() + else: + print("DEBUG: Calling stop_group_cycling") + self.stop_group_cycling() + + self.update_group_ui_state() + + def on_group_selection_changed(self): + """Handle selection change in group preset list""" + self.remove_from_group_button.setEnabled(len(self.group_preset_list.selectedItems()) > 0) + + def on_loop_count_changed(self, value): + """Handle loop count change""" + print(f"DEBUG: Loop count changed from {self.group_loop_count} to {value}") + self.group_loop_count = value + # Only reset current loops if we're not actively cycling + if not self.group_enabled or not self.group_timer.isActive(): + print("DEBUG: Resetting current loops (not actively cycling)") + self.group_current_loops = 0 + else: + print("DEBUG: NOT resetting current loops (actively cycling)") + + def on_order_changed(self, text): + """Handle order change""" + self.group_order = "random" if text == "Random" else "in_order" + + # If random, shuffle the current group + if self.group_order == "random" and len(self.preset_group) > 1: + # Create a new random order without changing the original list + pass # We'll handle randomization in advance_group_preset + + def add_preset_to_group(self): + """Add selected preset to the group""" + current_item = self.preset_list.currentItem() + if current_item: + preset_name = current_item.text() + if preset_name not in self.preset_group: + self.preset_group.append(preset_name) + self.update_group_preset_list() + self.update_group_ui_state() + + def remove_preset_from_group(self): + """Remove selected preset from the group""" + current_item = self.group_preset_list.currentItem() + if current_item: + preset_name = current_item.text() + if preset_name in self.preset_group: + self.preset_group.remove(preset_name) + self.update_group_preset_list() + self.update_group_ui_state() + + def clear_preset_group(self): + """Clear all presets from the group""" + self.preset_group.clear() + self.stop_group_cycling() + self.update_group_preset_list() + self.update_group_ui_state() + + def update_group_preset_list(self): + """Update the group preset list display""" + self.group_preset_list.clear() + for preset_name in self.preset_group: + item = QListWidgetItem(preset_name) + # Highlight current preset in group + if self.group_enabled and preset_name == self.get_current_group_preset(): + item.setBackground(Qt.darkBlue) + self.group_preset_list.addItem(item) + + def update_group_ui_state(self): + """Update group UI elements based on current state""" + has_presets = len(self.preset_group) > 0 + is_active = self.group_enabled and has_presets + + # Update status + if is_active: + current_preset = self.get_current_group_preset() + # Show note progress instead of loop progress + pattern_length = self.arpeggiator.user_pattern_length if hasattr(self.arpeggiator, 'user_pattern_length') else 0 + total_notes_needed = pattern_length * self.group_loop_count + progress_text = f" (Note {self.group_pattern_note_count}/{total_notes_needed})" + self.group_status_label.setText(f"Active: {current_preset}{progress_text}") + self.group_status_label.setStyleSheet("color: #00aa00; font-weight: bold;") + elif self.group_enabled: + self.group_status_label.setText("Enabled - No Presets") + self.group_status_label.setStyleSheet("color: #aaaa00;") + else: + self.group_status_label.setText("Inactive") + self.group_status_label.setStyleSheet("color: #888888;") + + # Update button states + self.prev_preset_button.setEnabled(is_active) + self.next_preset_button.setEnabled(is_active) + + # Update add button based on selection + current_item = self.preset_list.currentItem() + can_add = (current_item is not None and + current_item.text() not in self.preset_group) + self.add_to_group_button.setEnabled(can_add) + + def start_group_cycling(self): + """Start automatic group cycling""" + print(f"DEBUG: start_group_cycling called with {len(self.preset_group)} presets in group") + + if len(self.preset_group) > 0: + # Only reset position if we're not already cycling + if not self.group_timer.isActive(): + print("DEBUG: Resetting group position (first time start)") + self.group_current_index = 0 + self.group_current_loops = 0 + + # Load first preset + first_preset = self.preset_group[0] + print(f"DEBUG: Loading first preset: '{first_preset}'") + + if first_preset in self.presets: + self.apply_preset_settings(self.presets[first_preset]) + self.current_preset = first_preset + print(f"DEBUG: Successfully loaded first preset: '{first_preset}'") + else: + print(f"DEBUG: ERROR - First preset '{first_preset}' not found in presets!") + else: + print("DEBUG: Group cycling already active, not resetting position") + + # Initialize pattern note counter + self.group_pattern_note_count = 0 + + self.update_group_preset_list() + print("DEBUG: Group cycling started - waiting for arpeggiator to start playing") + + def stop_group_cycling(self): + """Stop automatic group cycling""" + self.group_timer.stop() + + # Disconnect from arpeggiator if connected + if hasattr(self.arpeggiator, 'pattern_completed'): + try: + self.arpeggiator.pattern_completed.disconnect(self.on_pattern_completed) + except TypeError: + pass # Already disconnected + + def get_current_group_preset(self): + """Get the currently active preset in the group""" + if 0 <= self.group_current_index < len(self.preset_group): + return self.preset_group[self.group_current_index] + return None + + def advance_group_preset(self): + """Advance to the next preset in the group""" + print(f"DEBUG: advance_group_preset called - enabled: {self.group_enabled}, group_size: {len(self.preset_group)}") + + if not self.group_enabled or len(self.preset_group) == 0: + print("DEBUG: advance_group_preset - early return (not enabled or no presets)") + return + + old_index = self.group_current_index + + # Move to next preset + if self.group_order == "random": + print("DEBUG: Using random order") + # Pick a random preset that's different from current (if possible) + if len(self.preset_group) > 1: + available_indices = [i for i in range(len(self.preset_group)) + if i != self.group_current_index] + self.group_current_index = random.choice(available_indices) + # If only one preset, stay on it + else: # in_order + print("DEBUG: Using in_order") + self.group_current_index = (self.group_current_index + 1) % len(self.preset_group) + + print(f"DEBUG: Index changed from {old_index} to {self.group_current_index}") + + # Load the next preset + next_preset = self.preset_group[self.group_current_index] + print(f"DEBUG: Loading next preset: '{next_preset}'") + + if next_preset in self.presets: + self.apply_preset_settings(self.presets[next_preset]) + self.current_preset = next_preset + print(f"Group cycling: Advanced to preset '{next_preset}' (index {self.group_current_index})") + else: + print(f"DEBUG: ERROR - preset '{next_preset}' not found in presets dict!") + + self.update_group_ui_state() + self.update_group_preset_list() + + def goto_previous_preset(self): + """Manually go to previous preset in group""" + if not self.group_enabled or len(self.preset_group) <= 1: + return + + self.group_current_index = (self.group_current_index - 1) % len(self.preset_group) + self.group_current_loops = 0 + + prev_preset = self.preset_group[self.group_current_index] + if prev_preset in self.presets: + self.apply_preset_settings(self.presets[prev_preset]) + self.current_preset = prev_preset + + self.update_group_ui_state() + self.update_group_preset_list() + + def goto_next_preset(self): + """Manually go to next preset in group""" + if not self.group_enabled or len(self.preset_group) <= 1: + return + + if self.group_order == "random": + # Pick a random preset that's different from current (if possible) + if len(self.preset_group) > 1: + available_indices = [i for i in range(len(self.preset_group)) + if i != self.group_current_index] + self.group_current_index = random.choice(available_indices) + else: # in_order + self.group_current_index = (self.group_current_index + 1) % len(self.preset_group) + + self.group_current_loops = 0 + + next_preset = self.preset_group[self.group_current_index] + if next_preset in self.presets: + self.apply_preset_settings(self.presets[next_preset]) + self.current_preset = next_preset + + self.update_group_ui_state() + self.update_group_preset_list() + + # Note: Timer-based cycling replaced with note-counting approach + + def on_pattern_completed(self): + """Handle arpeggiator pattern completion for timing""" + # This method exists for future integration with arpeggiator pattern signals + # For now, we use the playing state change to initiate cycling + pass + + def on_playing_state_changed(self, is_playing): + """Handle arpeggiator play/stop state changes""" + print(f"DEBUG: on_playing_state_changed called - is_playing: {is_playing}, group_enabled: {self.group_enabled}, group_size: {len(self.preset_group)}") + + if is_playing and self.group_enabled and len(self.preset_group) > 0: + print("DEBUG: Arpeggiator started - resetting note counter") + # Reset note counter when arpeggiator starts playing + self.group_pattern_note_count = 0 + elif not is_playing: + print("DEBUG: Arpeggiator stopped") + # Stop any pending preset changes when arpeggiator stops + self.group_timer.stop() + + def on_pattern_step(self, current_step): + """Handle each pattern step (note) played by the arpeggiator""" + if not self.group_enabled or len(self.preset_group) == 0: + return + + # Increment our note counter + self.group_pattern_note_count += 1 + + # Calculate how many notes should be played for current preset + pattern_length = self.arpeggiator.user_pattern_length + total_notes_needed = pattern_length * self.group_loop_count + + print(f"DEBUG: Pattern step {current_step}, note count: {self.group_pattern_note_count}/{total_notes_needed}") + + # Check if we've played enough notes to advance to next preset + if self.group_pattern_note_count >= total_notes_needed: + print("DEBUG: Note count reached, advancing to next preset") + self.group_pattern_note_count = 0 # Reset counter + self.advance_group_preset() + + # ======= MASTER FILE FUNCTIONALITY ======= + + def save_master_file(self): + """Save current presets and group configuration as a master file""" + try: + # Create master files directory if it doesn't exist + master_dir = "master_files" + os.makedirs(master_dir, exist_ok=True) + + # Open file dialog + filename, _ = QFileDialog.getSaveFileName( + self, + "Save Master File", + os.path.join(master_dir, "master.json"), + "Master Files (*.json);;All Files (*)" + ) + + if not filename: + return + + # Capture master file data + master_data = { + "version": "1.0", + "timestamp": os.path.basename(filename).replace('.json', ''), + "type": "master_file", + + # All individual presets + "presets": self.presets.copy(), + + # Preset group configuration + "preset_group": { + "enabled": self.group_enabled, + "presets": self.preset_group.copy(), + "loop_count": self.group_loop_count, + "order": self.group_order, + "current_index": self.group_current_index, + "current_loops": self.group_current_loops + } + } + + # Add timestamp + from datetime import datetime + master_data["timestamp"] = datetime.now().isoformat() + + # Write to file + with open(filename, 'w') as f: + json.dump(master_data, f, indent=2) + + QMessageBox.information(self, "Master File Saved", + f"Master file saved successfully:\n{filename}") + + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to save master file:\n{str(e)}") + + def load_master_file(self): + """Load presets and group configuration from a master file""" + try: + # Open file dialog + master_dir = "master_files" + os.makedirs(master_dir, exist_ok=True) + + filename, _ = QFileDialog.getOpenFileName( + self, + "Load Master File", + master_dir, + "Master Files (*.json);;All Files (*)" + ) + + if not filename: + return + + # Confirm loading (this will replace current presets) + reply = QMessageBox.question( + self, + "Load Master File", + "This will replace all current presets and group configuration.\n" + "Are you sure you want to continue?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply != QMessageBox.Yes: + return + + # Load master file + with open(filename, 'r') as f: + master_data = json.load(f) + + # Validate master file + if master_data.get("type") != "master_file": + QMessageBox.warning(self, "Invalid File", + "This doesn't appear to be a valid master file.") + return + + # Stop any active group cycling + self.stop_group_cycling() + + # Load presets + loaded_presets = master_data.get("presets", {}) + self.presets = loaded_presets.copy() + + # Update preset list display + self.update_preset_list() + + # Load group configuration + group_config = master_data.get("preset_group", {}) + self.preset_group = group_config.get("presets", []) + self.group_loop_count = group_config.get("loop_count", 1) + self.group_order = group_config.get("order", "in_order") + self.group_current_index = 0 # Reset to start + self.group_current_loops = 0 # Reset loops + + # Update UI controls + self.loop_count_spinbox.setValue(self.group_loop_count) + order_text = "Random" if self.group_order == "random" else "In Order" + self.order_combo.setCurrentText(order_text) + + # Update group list display + self.update_group_preset_list() + + # Don't auto-enable group cycling - let user decide + self.group_enabled = False + self.group_enable_checkbox.setChecked(False) + + # Update all UI states + self.update_group_ui_state() + self.update_preset_list_colors() + + loaded_count = len(loaded_presets) + group_count = len(self.preset_group) + + QMessageBox.information( + self, + "Master File Loaded", + f"Successfully loaded:\n" + f"• {loaded_count} presets\n" + f"• Preset group with {group_count} presets\n\n" + f"From: {os.path.basename(filename)}" + ) + + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to load master file:\n{str(e)}") + + def update_preset_list(self): + """Update the main preset list display""" + self.preset_list.clear() + for preset_name in sorted(self.presets.keys()): + item = QListWidgetItem(preset_name) + self.preset_list.addItem(item) + + # Update current preset display + if self.current_preset and self.current_preset in self.presets: + self.current_preset_label.setText(self.current_preset) + else: + self.current_preset_label.setText("None") \ No newline at end of file diff --git a/gui/volume_controls.py b/gui/volume_controls.py index aad743d..c11fc19 100644 --- a/gui/volume_controls.py +++ b/gui/volume_controls.py @@ -6,7 +6,7 @@ Interface for tempo-linked volume and brightness pattern controls. from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QGroupBox, QComboBox, QSlider, QSpinBox, QLabel, - QPushButton, QFrame, QScrollArea) + QPushButton, QFrame, QScrollArea, QSizePolicy) from PyQt5.QtCore import Qt, pyqtSlot class VolumeControls(QWidget): @@ -38,9 +38,89 @@ class VolumeControls(QWidget): self.current_pattern = "static" self.armed_pattern_button = None self.pattern_buttons = {} + + # Scaling support + self.scale_factor = 1.0 + self.setup_ui() self.connect_signals() + def apply_scaling(self, scale_factor): + """Apply new scaling factor to all buttons""" + self.scale_factor = scale_factor + + # Update all pattern buttons with new scaling + for button in self.pattern_buttons.values(): + self.update_pattern_button_style_with_scale(button, self.get_button_state(button)) + + def get_button_state(self, button): + """Determine button state from current styling""" + style = button.styleSheet() + if "#2d5a2d" in style or "#00aa44" in style: + return "active" + elif "#ff8800" in style: + return "armed" + else: + return "inactive" + + def update_pattern_button_style_with_scale(self, button, state): + """Update pattern button styling with current scale factor""" + font_size = max(8, int(12 * self.scale_factor)) + padding = max(2, int(5 * self.scale_factor)) + min_height = max(20, int(30 * self.scale_factor)) + + # Set size policy for expansion + button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + + if state == "active": + button.setStyleSheet(f""" + QPushButton {{ + background: #2d5a2d; + color: white; + border: 1px solid #4a8a4a; + font-weight: bold; + font-size: {font_size}px; + min-height: {min_height}px; + padding: {padding}px; + }} + QPushButton:hover {{ + background: #3d6a3d; + border: 1px solid #5aaa5a; + }} + """) + elif state == "armed": + button.setStyleSheet(f""" + QPushButton {{ + background: #ff8800; + color: white; + border: 1px solid #ffaa00; + font-weight: bold; + font-size: {font_size}px; + min-height: {min_height}px; + padding: {padding}px; + }} + QPushButton:hover {{ + background: #ffaa00; + border: 1px solid #ffcc33; + }} + """) + else: # inactive + button.setStyleSheet(f""" + QPushButton {{ + background: #3a3a3a; + color: #ffffff; + border: 1px solid #555555; + font-weight: bold; + font-size: {font_size}px; + min-height: {min_height}px; + padding: {padding}px; + }} + QPushButton:hover {{ + background: #505050; + border: 1px solid #777777; + }} + """) + def setup_ui(self): """Set up the user interface""" layout = QVBoxLayout(self) @@ -105,42 +185,7 @@ class VolumeControls(QWidget): def update_pattern_button_style(self, button, state): """Update pattern button styling based on state""" - if state == "active": - button.setStyleSheet(""" - QPushButton { - background: #2d5a2d; - color: white; - border: 1px solid #4a8a4a; - font-weight: bold; - font-size: 12px; - min-height: 30px; - padding: 5px 10px; - } - """) - elif state == "armed": - button.setStyleSheet(""" - QPushButton { - background: #ff8800; - color: white; - border: 1px solid #ffaa00; - font-weight: bold; - font-size: 12px; - min-height: 30px; - padding: 5px 10px; - } - """) - else: # inactive - button.setStyleSheet(""" - QPushButton { - background: #3a3a3a; - color: #ffffff; - border: 1px solid #555555; - font-weight: bold; - font-size: 12px; - min-height: 30px; - padding: 5px 10px; - } - """) + self.update_pattern_button_style_with_scale(button, state) def create_global_settings(self) -> QGroupBox: """Create global volume/velocity range settings""" diff --git a/master_files/example_master.json b/master_files/example_master.json new file mode 100644 index 0000000..a1c47c2 --- /dev/null +++ b/master_files/example_master.json @@ -0,0 +1,129 @@ +{ + "version": "1.0", + "timestamp": "2025-09-09T18:57:00.000000", + "type": "master_file", + "presets": { + "Slow Ambient": { + "version": "1.0", + "timestamp": "2025-09-09T18:57:00.000000", + "arpeggiator": { + "root_note": 60, + "scale": "minor", + "scale_note_start": 0, + "pattern_type": "up", + "octave_range": 2, + "note_speed": "1/2", + "gate": 0.8, + "swing": 0.0, + "velocity": 60, + "tempo": 70.0, + "user_pattern_length": 8, + "channel_distribution": "up", + "delay_enabled": true, + "delay_length": 3, + "delay_timing": "1/4", + "delay_fade": 0.5 + }, + "channels": { + "active_synth_count": 4, + "channel_instruments": { + "1": 0, "2": 0, "3": 0, "4": 0, "5": 0, "6": 0, "7": 0, "8": 0, + "9": 0, "10": 0, "11": 0, "12": 0, "13": 0, "14": 0, "15": 0, "16": 0 + } + }, + "volume_patterns": { + "current_pattern": "swell", + "pattern_speed": 0.5, + "pattern_intensity": 0.8, + "global_volume_range": [0.2, 0.7], + "global_velocity_range": [40, 80], + "channel_volume_ranges": {}, + "velocity_ranges": {} + } + }, + "Fast Dance": { + "version": "1.0", + "timestamp": "2025-09-09T18:57:00.000000", + "arpeggiator": { + "root_note": 64, + "scale": "major", + "scale_note_start": 0, + "pattern_type": "up_down", + "octave_range": 1, + "note_speed": "1/16", + "gate": 0.5, + "swing": 0.2, + "velocity": 120, + "tempo": 140.0, + "user_pattern_length": 4, + "channel_distribution": "bounce", + "delay_enabled": false, + "delay_length": 0, + "delay_timing": "1/8", + "delay_fade": 0.3 + }, + "channels": { + "active_synth_count": 8, + "channel_instruments": { + "1": 0, "2": 0, "3": 0, "4": 0, "5": 0, "6": 0, "7": 0, "8": 0, + "9": 0, "10": 0, "11": 0, "12": 0, "13": 0, "14": 0, "15": 0, "16": 0 + } + }, + "volume_patterns": { + "current_pattern": "random_sparkle", + "pattern_speed": 2.0, + "pattern_intensity": 1.0, + "global_volume_range": [0.6, 1.0], + "global_velocity_range": [100, 127], + "channel_volume_ranges": {}, + "velocity_ranges": {} + } + }, + "Med Groove": { + "version": "1.0", + "timestamp": "2025-09-09T18:57:00.000000", + "arpeggiator": { + "root_note": 67, + "scale": "dorian", + "scale_note_start": 2, + "pattern_type": "down_up", + "octave_range": 2, + "note_speed": "1/8", + "gate": 0.7, + "swing": 0.1, + "velocity": 90, + "tempo": 110.0, + "user_pattern_length": 6, + "channel_distribution": "single_channel", + "delay_enabled": true, + "delay_length": 2, + "delay_timing": "1/8T", + "delay_fade": 0.4 + }, + "channels": { + "active_synth_count": 3, + "channel_instruments": { + "1": 0, "2": 0, "3": 0, "4": 0, "5": 0, "6": 0, "7": 0, "8": 0, + "9": 0, "10": 0, "11": 0, "12": 0, "13": 0, "14": 0, "15": 0, "16": 0 + } + }, + "volume_patterns": { + "current_pattern": "accent_4", + "pattern_speed": 1.0, + "pattern_intensity": 0.9, + "global_volume_range": [0.3, 0.9], + "global_velocity_range": [70, 110], + "channel_volume_ranges": {}, + "velocity_ranges": {} + } + } + }, + "preset_group": { + "enabled": false, + "presets": ["Slow Ambient", "Med Groove", "Fast Dance"], + "loop_count": 2, + "order": "in_order", + "current_index": 0, + "current_loops": 0 + } +} \ No newline at end of file diff --git a/master_files/test master.json b/master_files/test master.json new file mode 100644 index 0000000..31628e7 --- /dev/null +++ b/master_files/test master.json @@ -0,0 +1,193 @@ +{ + "version": "1.0", + "timestamp": "2025-09-09T13:59:32.284079", + "type": "master_file", + "presets": { + "two Copy Copy": { + "version": "1.0", + "timestamp": "2025-09-09T13:51:05.409379", + "arpeggiator": { + "root_note": 60, + "scale": "mixolydian", + "scale_note_start": 3, + "pattern_type": "down", + "octave_range": 1, + "note_speed": "1/2", + "gate": 0.71, + "swing": 0.0, + "velocity": 127, + "tempo": 120.0, + "user_pattern_length": 3, + "channel_distribution": "up", + "delay_enabled": true, + "delay_length": 2, + "delay_timing": "2/1T", + "delay_fade": 0.44 + }, + "channels": { + "active_synth_count": 3, + "channel_instruments": { + "1": 0, + "2": 0, + "3": 0, + "4": 0, + "5": 0, + "6": 0, + "7": 0, + "8": 0, + "9": 0, + "10": 0, + "11": 0, + "12": 0, + "13": 0, + "14": 0, + "15": 0, + "16": 0 + } + }, + "volume_patterns": { + "current_pattern": "static", + "pattern_speed": 2.0, + "pattern_intensity": 1.0, + "global_volume_range": [ + 0.0, + 1.0 + ], + "global_velocity_range": [ + 40, + 127 + ], + "channel_volume_ranges": {}, + "velocity_ranges": {} + } + }, + "two Copy": { + "version": "1.0", + "timestamp": "2025-09-09T13:50:13.070241", + "arpeggiator": { + "root_note": 60, + "scale": "mixolydian", + "scale_note_start": 0, + "pattern_type": "down", + "octave_range": 1, + "note_speed": "1/2", + "gate": 0.71, + "swing": 0.0, + "velocity": 127, + "tempo": 120.0, + "user_pattern_length": 3, + "channel_distribution": "up", + "delay_enabled": true, + "delay_length": 2, + "delay_timing": "2/1T", + "delay_fade": 0.44 + }, + "channels": { + "active_synth_count": 3, + "channel_instruments": { + "1": 0, + "2": 0, + "3": 0, + "4": 0, + "5": 0, + "6": 0, + "7": 0, + "8": 0, + "9": 0, + "10": 0, + "11": 0, + "12": 0, + "13": 0, + "14": 0, + "15": 0, + "16": 0 + } + }, + "volume_patterns": { + "current_pattern": "static", + "pattern_speed": 2.0, + "pattern_intensity": 1.0, + "global_volume_range": [ + 0.0, + 1.0 + ], + "global_velocity_range": [ + 40, + 127 + ], + "channel_volume_ranges": {}, + "velocity_ranges": {} + } + }, + "two": { + "version": "1.0", + "timestamp": "2025-09-09T13:49:16.446087", + "arpeggiator": { + "root_note": 60, + "scale": "mixolydian", + "scale_note_start": 1, + "pattern_type": "down", + "octave_range": 1, + "note_speed": "1/2", + "gate": 0.71, + "swing": 0.0, + "velocity": 127, + "tempo": 120.0, + "user_pattern_length": 3, + "channel_distribution": "up", + "delay_enabled": true, + "delay_length": 2, + "delay_timing": "2/1T", + "delay_fade": 0.44 + }, + "channels": { + "active_synth_count": 3, + "channel_instruments": { + "1": 0, + "2": 0, + "3": 0, + "4": 0, + "5": 0, + "6": 0, + "7": 0, + "8": 0, + "9": 0, + "10": 0, + "11": 0, + "12": 0, + "13": 0, + "14": 0, + "15": 0, + "16": 0 + } + }, + "volume_patterns": { + "current_pattern": "static", + "pattern_speed": 2.0, + "pattern_intensity": 1.0, + "global_volume_range": [ + 0.0, + 1.0 + ], + "global_velocity_range": [ + 40, + 127 + ], + "channel_volume_ranges": {}, + "velocity_ranges": {} + } + } + }, + "preset_group": { + "enabled": true, + "presets": [ + "two", + "two Copy", + "two Copy Copy" + ], + "loop_count": 1, + "order": "in_order", + "current_index": 0, + "current_loops": 0 + } +} \ No newline at end of file diff --git a/presets/butt 2.json b/presets/two Copy Copy.json similarity index 72% rename from presets/butt 2.json rename to presets/two Copy Copy.json index 61b8c57..d873174 100644 --- a/presets/butt 2.json +++ b/presets/two Copy Copy.json @@ -1,22 +1,23 @@ { "version": "1.0", - "timestamp": "2025-09-09T08:50:29.583440", + "timestamp": "2025-09-09T13:51:05.409379", "arpeggiator": { - "root_note": 62, - "scale": "major", + "root_note": 60, + "scale": "mixolydian", + "scale_note_start": 3, "pattern_type": "down", "octave_range": 1, - "note_speed": "1/4", + "note_speed": "1/2", "gate": 0.71, "swing": 0.0, - "velocity": 47, + "velocity": 127, "tempo": 120.0, - "pattern_length": 3, + "user_pattern_length": 3, "channel_distribution": "up", - "delay_enabled": false, - "delay_length": 3, + "delay_enabled": true, + "delay_length": 2, "delay_timing": "2/1T", - "delay_fade": 0.9 + "delay_fade": 0.44 }, "channels": { "active_synth_count": 3, @@ -40,7 +41,7 @@ } }, "volume_patterns": { - "current_pattern": "accent_4", + "current_pattern": "static", "pattern_speed": 2.0, "pattern_intensity": 1.0, "global_volume_range": [ diff --git a/presets/two Copy.json b/presets/two Copy.json new file mode 100644 index 0000000..fa0bb1b --- /dev/null +++ b/presets/two Copy.json @@ -0,0 +1,58 @@ +{ + "version": "1.0", + "timestamp": "2025-09-09T13:50:13.070241", + "arpeggiator": { + "root_note": 60, + "scale": "mixolydian", + "scale_note_start": 0, + "pattern_type": "down", + "octave_range": 1, + "note_speed": "1/2", + "gate": 0.71, + "swing": 0.0, + "velocity": 127, + "tempo": 120.0, + "user_pattern_length": 3, + "channel_distribution": "up", + "delay_enabled": true, + "delay_length": 2, + "delay_timing": "2/1T", + "delay_fade": 0.44 + }, + "channels": { + "active_synth_count": 3, + "channel_instruments": { + "1": 0, + "2": 0, + "3": 0, + "4": 0, + "5": 0, + "6": 0, + "7": 0, + "8": 0, + "9": 0, + "10": 0, + "11": 0, + "12": 0, + "13": 0, + "14": 0, + "15": 0, + "16": 0 + } + }, + "volume_patterns": { + "current_pattern": "static", + "pattern_speed": 2.0, + "pattern_intensity": 1.0, + "global_volume_range": [ + 0.0, + 1.0 + ], + "global_velocity_range": [ + 40, + 127 + ], + "channel_volume_ranges": {}, + "velocity_ranges": {} + } +} \ No newline at end of file diff --git a/presets/two.json b/presets/two.json new file mode 100644 index 0000000..9aebdad --- /dev/null +++ b/presets/two.json @@ -0,0 +1,58 @@ +{ + "version": "1.0", + "timestamp": "2025-09-09T13:49:16.446087", + "arpeggiator": { + "root_note": 60, + "scale": "mixolydian", + "scale_note_start": 1, + "pattern_type": "down", + "octave_range": 1, + "note_speed": "1/2", + "gate": 0.71, + "swing": 0.0, + "velocity": 127, + "tempo": 120.0, + "user_pattern_length": 3, + "channel_distribution": "up", + "delay_enabled": true, + "delay_length": 2, + "delay_timing": "2/1T", + "delay_fade": 0.44 + }, + "channels": { + "active_synth_count": 3, + "channel_instruments": { + "1": 0, + "2": 0, + "3": 0, + "4": 0, + "5": 0, + "6": 0, + "7": 0, + "8": 0, + "9": 0, + "10": 0, + "11": 0, + "12": 0, + "13": 0, + "14": 0, + "15": 0, + "16": 0 + } + }, + "volume_patterns": { + "current_pattern": "static", + "pattern_speed": 2.0, + "pattern_intensity": 1.0, + "global_volume_range": [ + 0.0, + 1.0 + ], + "global_velocity_range": [ + 40, + 127 + ], + "channel_volume_ranges": {}, + "velocity_ranges": {} + } +} \ No newline at end of file