#!/usr/bin/env python3 """ Master Preset Generator GUI for MIDI Arpeggiator Interactive GUI for creating master preset files with customizable parameters. Maintains consistent synth count for live performance use. """ import json import copy import os import sys from datetime import datetime from typing import Dict, List, Any, Tuple import math from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QPushButton, QLabel, QSpinBox, QDoubleSpinBox, QComboBox, QSlider, QGroupBox, QFileDialog, QLineEdit, QTextEdit, QProgressBar, QCheckBox, QMessageBox, QTabWidget, QFormLayout, QFrame, QScrollArea ) from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QThread, pyqtSlot from PyQt5.QtGui import QFont, QPalette, QColor, QPixmap # Embedded generator classes class MusicalLogic: """Handles musical theory and harmonic progressions""" def __init__(self): # Circle of fifths progression (musically smooth root note changes) self.circle_of_fifths = [0, 7, 2, 9, 4, 11, 6, 1, 8, 3, 10, 5] # C,G,D,A,E,B,F#,C#,G#,D#,A#,F # Scale relationships - which scales work well together self.scale_relationships = { "major": ["lydian", "mixolydian", "major"], "minor": ["dorian", "phrygian", "minor"], "mixolydian": ["major", "dorian", "mixolydian"], "dorian": ["minor", "mixolydian", "dorian"], "phrygian": ["minor", "dorian", "phrygian"], "lydian": ["major", "mixolydian", "lydian"], "pentatonic": ["major", "minor", "pentatonic"] } # Note names for display self.note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] def get_next_root_note(self, current_root: int, progression_type: str = "circle_of_fifths") -> int: """Get next root note following musical logic""" if progression_type == "circle_of_fifths": current_note = current_root % 12 try: current_index = self.circle_of_fifths.index(current_note) next_index = (current_index + 1) % len(self.circle_of_fifths) return (current_root // 12) * 12 + self.circle_of_fifths[next_index] except ValueError: # If current note not in circle, go to nearest one return current_root + 7 # Perfect fifth up elif progression_type == "chromatic_up": return current_root + 1 elif progression_type == "chromatic_down": return current_root - 1 return current_root class PresetTemplate: """Manages preset template and parameter ranges""" def __init__(self, base_preset: Dict[str, Any]): self.template = copy.deepcopy(base_preset) self.locked_parameters = { # CRITICAL: These never change for live performance "active_synth_count", "channel_instruments", } # Define safe parameter ranges self.parameter_ranges = { "tempo": {"min": 80, "max": 140, "step": 2}, "velocity": {"min": 60, "max": 127, "step": 3}, "gate": {"min": 0.4, "max": 1.2, "step": 0.03}, "swing": {"min": -0.25, "max": 0.3, "step": 0.03}, "delay_fade": {"min": 0.2, "max": 0.8, "step": 0.04}, "delay_length": {"min": 1, "max": 6, "step": 1}, "user_pattern_length": {"min": 3, "max": 12, "step": 1}, "scale_note_start": {"min": 0, "max": 6, "step": 1}, "octave_range": {"min": 1, "max": 3, "step": 1}, } def clamp_parameter(self, param_name: str, value: Any) -> Any: """Ensure parameter stays within safe bounds""" if param_name in self.parameter_ranges: range_info = self.parameter_ranges[param_name] return max(range_info["min"], min(range_info["max"], value)) return value class GenerationStrategies: """Different strategies for generating preset progressions""" @staticmethod def build_and_release(base_preset: Dict[str, Any], count: int, musical_logic: MusicalLogic, template: PresetTemplate) -> List[Tuple[str, Dict[str, Any]]]: """Build intensity to 70% point, then release back down""" presets = [] peak_point = int(count * 0.7) for i in range(count): preset = copy.deepcopy(base_preset) # Calculate progression factor (0.0 to 1.0 and back down) if i <= peak_point: factor = i / peak_point # 0.0 to 1.0 else: factor = 1.0 - ((i - peak_point) / (count - peak_point)) # 1.0 back to 0.0 # Apply gradual changes arp = preset["arpeggiator"] # Tempo progression base_tempo = base_preset["arpeggiator"]["tempo"] tempo_range = 15 # +/- 15 BPM (more subtle) arp["tempo"] = base_tempo + (tempo_range * factor * 0.8) # Velocity progression base_velocity = base_preset["arpeggiator"]["velocity"] velocity_range = 15 # More subtle arp["velocity"] = int(base_velocity + (velocity_range * factor * 0.6)) # Gate progression (tighter at peak) base_gate = base_preset["arpeggiator"]["gate"] gate_change = 0.1 * factor # More subtle arp["gate"] = template.clamp_parameter("gate", base_gate + gate_change) # Swing progression (add swing as it builds) base_swing = base_preset["arpeggiator"]["swing"] swing_change = 0.1 * factor # More subtle arp["swing"] = template.clamp_parameter("swing", base_swing + swing_change) # Pattern length progression (more conservative) base_length = base_preset["arpeggiator"]["user_pattern_length"] length_change = int(2 * factor) # Add up to 2 notes at peak arp["user_pattern_length"] = template.clamp_parameter("user_pattern_length", base_length + length_change) # Delay progression if arp.get("delay_enabled", False): base_fade = base_preset["arpeggiator"]["delay_fade"] fade_change = 0.15 * factor arp["delay_fade"] = template.clamp_parameter("delay_fade", base_fade + fade_change) # Add some variety every few presets (very subtle) if i > 0 and i % 5 == 0: # Subtle scale note shift current_start = arp.get("scale_note_start", 0) new_start = (current_start + 1) % 4 # Only first 4 scale notes arp["scale_note_start"] = new_start # Generate preset name preset_name = f"preset_{i+1:02d}_{int(factor*100):02d}pct" presets.append((preset_name, preset)) return presets @staticmethod def modal_journey(base_preset: Dict[str, Any], count: int, musical_logic: MusicalLogic, template: PresetTemplate) -> List[Tuple[str, Dict[str, Any]]]: """Progress through related musical modes""" presets = [] # Plan a gentle modal journey scale_sequence = ["major", "mixolydian", "dorian", "minor", "dorian", "mixolydian", "major"] for i in range(count): preset = copy.deepcopy(base_preset) arp = preset["arpeggiator"] # Progress through scales gently scale_idx = int((i / max(1, count - 1)) * (len(scale_sequence) - 1)) arp["scale"] = scale_sequence[scale_idx] # Very subtle tempo drift base_tempo = base_preset["arpeggiator"]["tempo"] tempo_drift = math.sin(i / count * math.pi * 2) * 5 # +/- 5 BPM sine wave arp["tempo"] = base_tempo + tempo_drift preset_name = f"modal_{scale_sequence[scale_idx]}_{i+1:02d}" presets.append((preset_name, preset)) return presets class PresetValidator: """Validates generated presets for quality and live performance suitability""" def validate_preset(self, preset: Dict[str, Any]) -> Tuple[bool, List[str]]: """Validate a single preset""" errors = [] arp = preset.get("arpeggiator", {}) # Check critical parameters are within performance ranges tempo = arp.get("tempo", 120) if tempo < 70 or tempo > 150: errors.append(f"Tempo {tempo} outside live performance range (70-150)") velocity = arp.get("velocity", 100) if velocity < 1 or velocity > 127: errors.append(f"Velocity {velocity} outside MIDI range (1-127)") return len(errors) == 0, errors class MasterPresetGenerator: """Main generator class""" def __init__(self, base_preset_path: str = None): if base_preset_path: with open(base_preset_path, 'r') as f: self.base_preset = json.load(f) else: self.base_preset = None self.musical_logic = MusicalLogic() if self.base_preset: self.template = PresetTemplate(self.base_preset) self.validator = PresetValidator() def generate_master_preset(self, name: str, count: int, strategy: str = "build_and_release", loop_count: int = 4) -> Dict[str, Any]: """Generate a complete master preset file""" # Generate presets using selected strategy if strategy == "build_and_release": preset_list = GenerationStrategies.build_and_release( self.base_preset, count, self.musical_logic, self.template ) elif strategy == "modal_journey": preset_list = GenerationStrategies.modal_journey( self.base_preset, count, self.musical_logic, self.template ) else: raise ValueError(f"Unknown strategy: {strategy}") # Build master preset structure master_preset = { "version": "1.0", "timestamp": datetime.now().isoformat(), "type": "master_file", "presets": {}, "preset_group": { "enabled": True, "presets": [], "loop_count": loop_count, "order": "in_order", "current_index": 0, "current_loops": 0 } } # Add all generated presets for preset_name, preset in preset_list: # Add timestamp to each preset preset["timestamp"] = datetime.now().isoformat() preset["version"] = "1.0" master_preset["presets"][preset_name] = preset master_preset["preset_group"]["presets"].append(preset_name) return master_preset class PresetGeneratorThread(QThread): """Background thread for generating presets""" progress_update = pyqtSignal(int, str) # progress, message generation_complete = pyqtSignal(dict) # master_preset error_occurred = pyqtSignal(str) # error_message def __init__(self, generator, name, count, strategy, loop_count, advanced_settings): super().__init__() self.generator = generator self.name = name self.count = count self.strategy = strategy self.loop_count = loop_count self.advanced_settings = advanced_settings def run(self): try: self.progress_update.emit(10, "Initializing generator...") # Apply advanced settings to generator self.apply_advanced_settings() self.progress_update.emit(30, f"Generating {self.count} presets...") # Generate the master preset master_preset = self.generator.generate_master_preset( self.name, self.count, self.strategy, self.loop_count ) self.progress_update.emit(90, "Validating presets...") # Final validation preset_count = len(master_preset["presets"]) self.progress_update.emit(100, f"Complete! Generated {preset_count} presets") self.generation_complete.emit(master_preset) except Exception as e: self.error_occurred.emit(str(e)) def apply_advanced_settings(self): """Apply advanced settings to the generator""" # Modify parameter ranges based on advanced settings template = self.generator.template if "tempo_range" in self.advanced_settings: min_tempo, max_tempo = self.advanced_settings["tempo_range"] template.parameter_ranges["tempo"]["min"] = min_tempo template.parameter_ranges["tempo"]["max"] = max_tempo if "velocity_range" in self.advanced_settings: min_vel, max_vel = self.advanced_settings["velocity_range"] template.parameter_ranges["velocity"]["min"] = min_vel template.parameter_ranges["velocity"]["max"] = max_vel # Add more advanced settings as needed class AdvancedSettingsWidget(QWidget): """Advanced settings panel""" def __init__(self): super().__init__() self.init_ui() def init_ui(self): layout = QVBoxLayout(self) # Tempo Range Settings tempo_group = QGroupBox("Tempo Range") tempo_layout = QFormLayout(tempo_group) self.min_tempo_spin = QSpinBox() self.min_tempo_spin.setRange(60, 180) self.min_tempo_spin.setValue(90) self.min_tempo_spin.setSuffix(" BPM") self.max_tempo_spin = QSpinBox() self.max_tempo_spin.setRange(60, 180) self.max_tempo_spin.setValue(140) self.max_tempo_spin.setSuffix(" BPM") tempo_layout.addRow("Minimum Tempo:", self.min_tempo_spin) tempo_layout.addRow("Maximum Tempo:", self.max_tempo_spin) # Velocity Range Settings velocity_group = QGroupBox("Velocity Range") velocity_layout = QFormLayout(velocity_group) self.min_velocity_spin = QSpinBox() self.min_velocity_spin.setRange(1, 127) self.min_velocity_spin.setValue(60) self.max_velocity_spin = QSpinBox() self.max_velocity_spin.setRange(1, 127) self.max_velocity_spin.setValue(127) velocity_layout.addRow("Minimum Velocity:", self.min_velocity_spin) velocity_layout.addRow("Maximum Velocity:", self.max_velocity_spin) # Pattern Settings pattern_group = QGroupBox("Pattern Constraints") pattern_layout = QFormLayout(pattern_group) self.min_pattern_length_spin = QSpinBox() self.min_pattern_length_spin.setRange(2, 16) self.min_pattern_length_spin.setValue(3) self.max_pattern_length_spin = QSpinBox() self.max_pattern_length_spin.setRange(2, 16) self.max_pattern_length_spin.setValue(8) pattern_layout.addRow("Min Pattern Length:", self.min_pattern_length_spin) pattern_layout.addRow("Max Pattern Length:", self.max_pattern_length_spin) # Scale Progression Settings scale_group = QGroupBox("Scale Progression") scale_layout = QFormLayout(scale_group) self.root_note_progression = QComboBox() self.root_note_progression.addItems([ "Stay Same", "Circle of Fifths", "Chromatic Up", "Chromatic Down" ]) self.root_note_progression.setCurrentText("Circle of Fifths") self.scale_progression = QComboBox() self.scale_progression.addItems([ "Stay Same", "Modal Journey", "Major/Minor Only", "All Modes" ]) self.scale_progression.setCurrentText("Modal Journey") scale_layout.addRow("Root Note Movement:", self.root_note_progression) scale_layout.addRow("Scale Changes:", self.scale_progression) # Intensity Settings intensity_group = QGroupBox("Intensity Curve") intensity_layout = QFormLayout(intensity_group) self.build_percentage_spin = QSpinBox() self.build_percentage_spin.setRange(50, 90) self.build_percentage_spin.setValue(70) self.build_percentage_spin.setSuffix("%") self.intensity_factor_spin = QDoubleSpinBox() self.intensity_factor_spin.setRange(0.5, 2.0) self.intensity_factor_spin.setValue(1.0) self.intensity_factor_spin.setSingleStep(0.1) intensity_layout.addRow("Build to Peak at:", self.build_percentage_spin) intensity_layout.addRow("Intensity Factor:", self.intensity_factor_spin) # Add all groups layout.addWidget(tempo_group) layout.addWidget(velocity_group) layout.addWidget(pattern_group) layout.addWidget(scale_group) layout.addWidget(intensity_group) layout.addStretch() def get_settings(self) -> Dict[str, Any]: """Get current advanced settings as dictionary""" return { "tempo_range": (self.min_tempo_spin.value(), self.max_tempo_spin.value()), "velocity_range": (self.min_velocity_spin.value(), self.max_velocity_spin.value()), "pattern_length_range": (self.min_pattern_length_spin.value(), self.max_pattern_length_spin.value()), "root_note_progression": self.root_note_progression.currentText(), "scale_progression": self.scale_progression.currentText(), "build_percentage": self.build_percentage_spin.value() / 100.0, "intensity_factor": self.intensity_factor_spin.value(), } class PresetPreviewWidget(QWidget): """Widget for previewing generated presets""" def __init__(self): super().__init__() self.master_preset = None self.init_ui() def init_ui(self): layout = QVBoxLayout(self) # Preview info info_layout = QHBoxLayout() self.preset_count_label = QLabel("Presets: 0") self.total_duration_label = QLabel("Est. Duration: 0:00") info_layout.addWidget(self.preset_count_label) info_layout.addWidget(self.total_duration_label) info_layout.addStretch() layout.addLayout(info_layout) # Preset list self.preset_list = QTextEdit() self.preset_list.setMaximumHeight(200) self.preset_list.setReadOnly(True) layout.addWidget(QLabel("Preset Overview:")) layout.addWidget(self.preset_list) # Parameter progression charts (simplified text display) self.progression_display = QTextEdit() self.progression_display.setReadOnly(True) layout.addWidget(QLabel("Parameter Progression:")) layout.addWidget(self.progression_display) def update_preview(self, master_preset: Dict[str, Any]): """Update preview with generated master preset""" self.master_preset = master_preset presets = master_preset.get("presets", {}) preset_group = master_preset.get("preset_group", {}) # Update counts preset_count = len(presets) loop_count = preset_group.get("loop_count", 1) # Estimate duration (rough calculation) avg_tempo = 120 # Default assumption if presets: first_preset = list(presets.values())[0] avg_tempo = first_preset.get("arpeggiator", {}).get("tempo", 120) # Very rough duration estimate estimated_minutes = (preset_count * loop_count * 4) / (avg_tempo / 60) / 4 # 4 bars per preset estimate duration_str = f"{int(estimated_minutes)}:{int((estimated_minutes % 1) * 60):02d}" self.preset_count_label.setText(f"Presets: {preset_count}") self.total_duration_label.setText(f"Est. Duration: {duration_str}") # Update preset list preset_list_text = "" for i, (name, preset_data) in enumerate(presets.items()): arp = preset_data.get("arpeggiator", {}) tempo = arp.get("tempo", 120) scale = arp.get("scale", "major") pattern = arp.get("pattern_type", "up") root_note = arp.get("root_note", 60) note_name = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"][root_note % 12] preset_list_text += f"{i+1:2d}. {name}: {note_name} {scale} {pattern} @ {tempo:.0f}BPM\n" self.preset_list.setPlainText(preset_list_text) # Update progression display self.update_progression_display() def update_progression_display(self): """Show parameter progression analysis""" if not self.master_preset: return presets = self.master_preset.get("presets", {}) if not presets: return progression_text = "" # Analyze tempo progression tempos = [] velocities = [] gates = [] for preset_data in presets.values(): arp = preset_data.get("arpeggiator", {}) tempos.append(arp.get("tempo", 120)) velocities.append(arp.get("velocity", 100)) gates.append(arp.get("gate", 1.0)) if tempos: progression_text += f"Tempo Range: {min(tempos):.0f} - {max(tempos):.0f} BPM\n" progression_text += f"Velocity Range: {min(velocities)} - {max(velocities)}\n" progression_text += f"Gate Range: {min(gates):.2f} - {max(gates):.2f}\n\n" # Show progression pattern progression_text += "Tempo Progression:\n" for i, tempo in enumerate(tempos[::max(1, len(tempos)//10)]): # Show every 10th or so progression_text += f" {i*max(1, len(tempos)//10)+1:2d}: {tempo:.0f} BPM\n" self.progression_display.setPlainText(progression_text) class MasterPresetGeneratorGUI(QMainWindow): """Main GUI window for master preset generation""" def __init__(self): super().__init__() self.base_preset = None self.generator = None self.generated_master_preset = None self.init_ui() self.apply_dark_theme() # Set default paths self.load_base_preset("presets/two.json") # Try to load default def init_ui(self): self.setWindowTitle("Master Preset Generator - MIDI Arpeggiator") self.setMinimumSize(1000, 700) # Create central widget with tabs central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) # File selection section file_section = self.create_file_section() main_layout.addWidget(file_section) # Tab widget for main content tab_widget = QTabWidget() main_layout.addWidget(tab_widget) # Basic Settings Tab basic_tab = self.create_basic_settings_tab() tab_widget.addTab(basic_tab, "Basic Settings") # Advanced Settings Tab self.advanced_widget = AdvancedSettingsWidget() tab_widget.addTab(self.advanced_widget, "Advanced Settings") # Preview Tab self.preview_widget = PresetPreviewWidget() tab_widget.addTab(self.preview_widget, "Preview") # Generation controls generation_section = self.create_generation_section() main_layout.addWidget(generation_section) # Status bar self.statusBar().showMessage("Ready - Load a base preset to begin") def create_file_section(self): """Create file selection section""" group = QGroupBox("Base Preset") layout = QHBoxLayout(group) self.base_preset_path = QLineEdit() self.base_preset_path.setReadOnly(True) self.base_preset_path.setPlaceholderText("Select base preset file...") browse_button = QPushButton("Browse...") browse_button.clicked.connect(self.browse_base_preset) # Base preset info display self.base_preset_info = QLabel("No preset loaded") self.base_preset_info.setStyleSheet("color: #888888;") layout.addWidget(QLabel("Base Preset:")) layout.addWidget(self.base_preset_path) layout.addWidget(browse_button) layout.addWidget(self.base_preset_info) layout.addStretch() return group def create_basic_settings_tab(self): """Create basic settings tab""" widget = QWidget() layout = QVBoxLayout(widget) # Master preset settings master_group = QGroupBox("Master Preset Settings") master_layout = QFormLayout(master_group) self.master_name = QLineEdit("generated_master") self.preset_count = QSpinBox() self.preset_count.setRange(5, 50) self.preset_count.setValue(16) self.loop_count = QSpinBox() self.loop_count.setRange(1, 20) self.loop_count.setValue(4) self.loop_count.setToolTip("How many times each preset loops before advancing") master_layout.addRow("Master Preset Name:", self.master_name) master_layout.addRow("Number of Presets:", self.preset_count) master_layout.addRow("Loop Count per Preset:", self.loop_count) # Generation strategy strategy_group = QGroupBox("Generation Strategy") strategy_layout = QFormLayout(strategy_group) self.strategy_combo = QComboBox() self.strategy_combo.addItems(["build_and_release", "modal_journey"]) self.strategy_combo.currentTextChanged.connect(self.on_strategy_changed) self.strategy_description = QLabel() self.strategy_description.setWordWrap(True) self.strategy_description.setStyleSheet("color: #cccccc; font-style: italic;") strategy_layout.addRow("Strategy:", self.strategy_combo) strategy_layout.addRow("Description:", self.strategy_description) # Update description for default strategy self.on_strategy_changed("build_and_release") # Quick settings quick_group = QGroupBox("Quick Settings") quick_layout = QFormLayout(quick_group) self.subtle_changes = QCheckBox("Subtle Changes Only") self.subtle_changes.setChecked(True) self.subtle_changes.setToolTip("Keep all parameter changes small and gradual") self.preserve_feel = QCheckBox("Preserve Musical Feel") self.preserve_feel.setChecked(True) self.preserve_feel.setToolTip("Maintain the character of the base preset") self.live_performance = QCheckBox("Live Performance Mode") self.live_performance.setChecked(True) self.live_performance.setToolTip("Optimize for live performance (no fast notes, stable synth count)") quick_layout.addRow(self.subtle_changes) quick_layout.addRow(self.preserve_feel) quick_layout.addRow(self.live_performance) # Add all groups layout.addWidget(master_group) layout.addWidget(strategy_group) layout.addWidget(quick_group) layout.addStretch() return widget def create_generation_section(self): """Create generation controls section""" group = QGroupBox("Generation") layout = QVBoxLayout(group) # Progress bar self.progress_bar = QProgressBar() self.progress_bar.setVisible(False) self.progress_label = QLabel("") self.progress_label.setVisible(False) # Buttons button_layout = QHBoxLayout() self.generate_button = QPushButton("Generate Master Preset") self.generate_button.setStyleSheet(""" QPushButton { background: #2d5a2d; color: white; font-size: 14px; font-weight: bold; padding: 12px 24px; border-radius: 6px; } QPushButton:hover { background: #3d6a3d; } QPushButton:disabled { background: #555555; color: #999999; } """) self.generate_button.clicked.connect(self.generate_master_preset) self.save_button = QPushButton("Save Master Preset") self.save_button.setEnabled(False) self.save_button.setStyleSheet(""" QPushButton { background: #5a2d5a; color: white; font-size: 14px; font-weight: bold; padding: 12px 24px; border-radius: 6px; } QPushButton:hover { background: #6a3d6a; } QPushButton:disabled { background: #555555; color: #999999; } """) self.save_button.clicked.connect(self.save_master_preset) button_layout.addWidget(self.generate_button) button_layout.addWidget(self.save_button) button_layout.addStretch() layout.addWidget(self.progress_bar) layout.addWidget(self.progress_label) layout.addLayout(button_layout) return group def browse_base_preset(self): """Browse for base preset file""" file_path, _ = QFileDialog.getOpenFileName( self, "Select Base Preset", "presets/", "JSON Files (*.json)" ) if file_path: self.load_base_preset(file_path) def load_base_preset(self, file_path: str): """Load base preset from file""" try: if not os.path.exists(file_path): return with open(file_path, 'r') as f: self.base_preset = json.load(f) self.base_preset_path.setText(file_path) # Update info display arp = self.base_preset.get("arpeggiator", {}) channels = self.base_preset.get("channels", {}) info_text = f"Scale: {arp.get('scale', 'unknown')} | " info_text += f"Tempo: {arp.get('tempo', 120)} BPM | " info_text += f"Synths: {channels.get('active_synth_count', '?')}" self.base_preset_info.setText(info_text) self.base_preset_info.setStyleSheet("color: #00aa00;") # Create generator self.generator = MasterPresetGenerator.__new__(MasterPresetGenerator) self.generator.base_preset = self.base_preset self.generator.musical_logic = MusicalLogic() self.generator.template = PresetTemplate(self.base_preset) self.generator.validator = PresetValidator() self.statusBar().showMessage(f"Loaded base preset: {os.path.basename(file_path)}") self.generate_button.setEnabled(True) except Exception as e: QMessageBox.critical(self, "Error Loading Preset", f"Could not load preset file:\n{str(e)}") self.statusBar().showMessage("Error loading preset") def on_strategy_changed(self, strategy: str): """Update strategy description""" descriptions = { "build_and_release": "Gradually builds intensity to a peak (70% through), then releases back down. Perfect for sets that need a musical arc with climax and resolution.", "modal_journey": "Progresses through related musical modes and scales, with gentle root note changes. Great for ambient and experimental performances." } self.strategy_description.setText(descriptions.get(strategy, "Unknown strategy")) def generate_master_preset(self): """Generate master preset in background thread""" if not self.base_preset or not self.generator: QMessageBox.warning(self, "No Base Preset", "Please load a base preset first.") return # Get settings name = self.master_name.text().strip() if not name: QMessageBox.warning(self, "Invalid Name", "Please enter a name for the master preset.") return count = self.preset_count.value() strategy = self.strategy_combo.currentText() loop_count = self.loop_count.value() advanced_settings = self.advanced_widget.get_settings() # Show progress self.progress_bar.setVisible(True) self.progress_label.setVisible(True) self.progress_bar.setValue(0) self.generate_button.setEnabled(False) # Start generation thread self.generation_thread = PresetGeneratorThread( self.generator, name, count, strategy, loop_count, advanced_settings ) self.generation_thread.progress_update.connect(self.on_progress_update) self.generation_thread.generation_complete.connect(self.on_generation_complete) self.generation_thread.error_occurred.connect(self.on_generation_error) self.generation_thread.start() @pyqtSlot(int, str) def on_progress_update(self, progress: int, message: str): """Handle progress updates from generation thread""" self.progress_bar.setValue(progress) self.progress_label.setText(message) @pyqtSlot(dict) def on_generation_complete(self, master_preset: Dict[str, Any]): """Handle successful generation completion""" self.generated_master_preset = master_preset # Hide progress self.progress_bar.setVisible(False) self.progress_label.setVisible(False) self.generate_button.setEnabled(True) self.save_button.setEnabled(True) # Update preview self.preview_widget.update_preview(master_preset) # Show success message preset_count = len(master_preset.get("presets", {})) QMessageBox.information(self, "Generation Complete", f"Successfully generated {preset_count} presets!") self.statusBar().showMessage(f"Generated {preset_count} presets - Ready to save") @pyqtSlot(str) def on_generation_error(self, error_message: str): """Handle generation errors""" self.progress_bar.setVisible(False) self.progress_label.setVisible(False) self.generate_button.setEnabled(True) QMessageBox.critical(self, "Generation Error", f"Error generating presets:\n{error_message}") self.statusBar().showMessage("Generation failed") def save_master_preset(self): """Save generated master preset to file""" if not self.generated_master_preset: return name = self.master_name.text().strip() default_filename = f"{name}.json" file_path, _ = QFileDialog.getSaveFileName( self, "Save Master Preset", f"master_files/{default_filename}", "JSON Files (*.json)" ) if file_path: try: # Ensure directory exists os.makedirs(os.path.dirname(file_path), exist_ok=True) with open(file_path, 'w') as f: json.dump(self.generated_master_preset, f, indent=2) QMessageBox.information(self, "Saved Successfully", f"Master preset saved to:\n{file_path}") self.statusBar().showMessage(f"Saved: {os.path.basename(file_path)}") except Exception as e: QMessageBox.critical(self, "Save Error", f"Could not save file:\n{str(e)}") def apply_dark_theme(self): """Apply dark theme matching the main arpeggiator interface""" dark_palette = QPalette() # Modern dark colors matching main app dark_palette.setColor(QPalette.Window, QColor(32, 32, 36)) dark_palette.setColor(QPalette.WindowText, QColor(255, 255, 255)) dark_palette.setColor(QPalette.Base, QColor(18, 18, 20)) dark_palette.setColor(QPalette.AlternateBase, QColor(42, 42, 46)) dark_palette.setColor(QPalette.ToolTipBase, QColor(0, 0, 0)) dark_palette.setColor(QPalette.ToolTipText, QColor(255, 255, 255)) dark_palette.setColor(QPalette.Text, QColor(255, 255, 255)) dark_palette.setColor(QPalette.Button, QColor(48, 48, 52)) dark_palette.setColor(QPalette.ButtonText, QColor(255, 255, 255)) dark_palette.setColor(QPalette.BrightText, QColor(255, 100, 100)) dark_palette.setColor(QPalette.Link, QColor(100, 200, 255)) dark_palette.setColor(QPalette.Highlight, QColor(0, 150, 255)) dark_palette.setColor(QPalette.HighlightedText, QColor(255, 255, 255)) self.setPalette(dark_palette) # Matching stylesheet self.setStyleSheet(""" QMainWindow { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #202024, stop:1 #181820); } QGroupBox { font-weight: bold; font-size: 13px; color: #ffffff; border: 2px solid #00aaff; border-radius: 10px; margin-top: 20px; padding-top: 15px; background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #2a2a2e, stop:1 #242428); } QGroupBox::title { subcontrol-origin: margin; left: 15px; padding: 0 10px 0 10px; color: #00aaff; background-color: #2a2a2e; border-radius: 5px; } QTabWidget::pane { border: 2px solid #00aaff; border-radius: 8px; background-color: #2a2a2e; } QTabBar::tab { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #404044, stop:1 #353538); color: white; padding: 12px 20px; margin: 2px; border-top-left-radius: 8px; border-top-right-radius: 8px; min-width: 120px; font-size: 12px; font-weight: bold; } QTabBar::tab:selected { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #0099ff, stop:1 #0077cc); color: white; border: 2px solid #00aaff; } """) def main(): app = QApplication(sys.argv) # Set application info app.setApplicationName("Master Preset Generator") app.setApplicationVersion("1.0") app.setOrganizationName("MIDI Arpeggiator") # Create and show main window window = MasterPresetGeneratorGUI() window.show() sys.exit(app.exec_()) if __name__ == "__main__": main()