#!/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, QInputDialog ) 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 get_time_signature_group_speeds(base_speed: str) -> List[str]: """Get note speeds grouped by time signature""" # Group 1: Powers of 2 (2/4/8/16 time signatures) powers_of_2 = ["1/2", "1/4", "1/8", "1/16"] # Group 2: Triplets (3/6/12 time signatures) triplets = ["1/2T", "1/4T", "1/8T"] # Determine which group the base speed belongs to if base_speed in powers_of_2: return powers_of_2 elif base_speed in triplets: return triplets else: # Default to powers of 2 if unknown return powers_of_2 @staticmethod def build_and_release(base_preset: Dict[str, Any], count: int, musical_logic: MusicalLogic, template: PresetTemplate, settings: Dict[str, Any] = None) -> List[Tuple[str, Dict[str, Any]]]: """Build intensity to peak point, then release back down with comprehensive parameter control""" print(f"DEBUG: build_and_release called with count={count}") print(f"DEBUG: settings keys: {list(settings.keys()) if settings else 'None'}") presets = [] if settings is None: settings = {} # Get generation settings build_percentage = settings.get('build_percentage', 70) / 100.0 intensity_factor = settings.get('intensity_factor', 1.0) randomization = settings.get('randomization', 0.1) print(f"DEBUG: Generation settings - build_percentage:{build_percentage}, intensity_factor:{intensity_factor}, randomization:{randomization}") print(f"DEBUG: Tempo settings - enabled:{settings.get('tempo_enabled', False)}, min:{settings.get('tempo_min', 90)}, max:{settings.get('tempo_max', 150)}") peak_point = int(count * build_percentage) # Prepare note speed progression if enabled available_note_speeds = [] if settings.get('note_speed_enabled', False) and settings.get('note_speeds', []): available_note_speeds = settings['note_speeds'] elif settings.get('time_signature_grouping', False): base_speed = base_preset["arpeggiator"].get("note_speed", "1/4") available_note_speeds = GenerationStrategies.get_time_signature_group_speeds(base_speed) 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 / max(1, peak_point) # 0.0 to 1.0 else: factor = 1.0 - ((i - peak_point) / max(1, count - peak_point)) # 1.0 back to 0.0 print(f"DEBUG: Preset {i+1}/{count}: raw_factor={factor:.3f}, peak_point={peak_point}, build_percentage={build_percentage}") # Apply intensity factor original_factor = factor factor *= intensity_factor print(f"DEBUG: After intensity_factor ({intensity_factor}): factor={factor:.3f}") # Add randomization if randomization > 0: import random rand_factor = 1.0 + (random.random() - 0.5) * randomization * 2 factor *= rand_factor factor = max(0.0, min(2.0, factor)) # Clamp to reasonable range print(f"DEBUG: After randomization: factor={factor:.3f}") # Apply parameter changes arp = preset["arpeggiator"] # Tempo progression if settings.get('tempo_enabled', False): min_tempo = settings.get('tempo_min', 90) max_tempo = settings.get('tempo_max', 150) # Interpolate between min and max based on factor target_tempo = min_tempo + (max_tempo - min_tempo) * factor final_tempo = max(min_tempo, min(max_tempo, target_tempo)) arp["tempo"] = final_tempo print(f"DEBUG: Tempo progression - min:{min_tempo}, max:{max_tempo}, factor:{factor:.3f}, target:{target_tempo:.1f}, final:{final_tempo:.1f}") else: print(f"DEBUG: Tempo progression DISABLED - tempo_enabled={settings.get('tempo_enabled', False)}") # Velocity progression if settings.get('velocity_enabled', False): min_vel = settings.get('velocity_min', 60) max_vel = settings.get('velocity_max', 127) # Interpolate between min and max based on factor target_velocity = min_vel + (max_vel - min_vel) * factor arp["velocity"] = int(max(min_vel, min(max_vel, target_velocity))) # Gate progression if settings.get('gate_enabled', False): min_gate = settings.get('gate_min', 0.5) max_gate = settings.get('gate_max', 1.0) # Interpolate between min and max based on factor target_gate = min_gate + (max_gate - min_gate) * factor arp["gate"] = max(min_gate, min(max_gate, target_gate)) # Swing progression if settings.get('swing_enabled', False): min_swing = settings.get('swing_min', 0.0) max_swing = settings.get('swing_max', 0.3) # Interpolate between min and max based on factor target_swing = min_swing + (max_swing - min_swing) * factor arp["swing"] = max(min_swing, min(max_swing, target_swing)) # Pattern length progression if settings.get('pattern_length_enabled', False): min_length = settings.get('pattern_length_min', 3) max_length = settings.get('pattern_length_max', 8) # Interpolate between min and max based on factor target_length = min_length + (max_length - min_length) * factor arp["user_pattern_length"] = int(max(min_length, min(max_length, target_length))) # Note speed progression if available_note_speeds: speed_index = int(factor * (len(available_note_speeds) - 1)) speed_index = max(0, min(len(available_note_speeds) - 1, speed_index)) arp["note_speed"] = available_note_speeds[speed_index] # Octave range progression if settings.get('octave_range_enabled', False): min_octave = settings.get('octave_range_min', 1) max_octave = settings.get('octave_range_max', 2) octave_range = (max_octave - min_octave) / 2 base_octave = base_preset["arpeggiator"]["octave_range"] target_octave = base_octave + int(octave_range * factor) arp["octave_range"] = max(min_octave, min(max_octave, target_octave)) # Musical parameter changes if settings.get('scale_note_start_enabled', False): min_start = settings.get('scale_note_start_min', 0) max_start = settings.get('scale_note_start_max', 3) base_start = base_preset["arpeggiator"]["scale_note_start"] # Handle case where base_start is "random" string if base_start == "random": # For random base, interpolate between min and max directly target_start = min_start + (max_start - min_start) * factor arp["scale_note_start"] = int(max(min_start, min(max_start, target_start))) else: # For numeric base, add progression from base start_range = (max_start - min_start) / 2 target_start = base_start + int(start_range * factor) arp["scale_note_start"] = max(min_start, min(max_start, target_start)) # Delay parameter changes if arp.get("delay_enabled", False): if settings.get('delay_length_enabled', False): min_del_len = settings.get('delay_length_min', 2) max_del_len = settings.get('delay_length_max', 4) del_len_range = (max_del_len - min_del_len) / 2 base_del_len = base_preset["arpeggiator"]["delay_length"] target_del_len = base_del_len + int(del_len_range * factor) arp["delay_length"] = max(min_del_len, min(max_del_len, target_del_len)) if settings.get('delay_fade_enabled', False): min_fade = settings.get('delay_fade_min', 0.2) max_fade = settings.get('delay_fade_max', 0.8) fade_range = (max_fade - min_fade) / 2 base_fade = base_preset["arpeggiator"]["delay_fade"] target_fade = base_fade + (fade_range * factor) arp["delay_fade"] = max(min_fade, min(max_fade, target_fade)) # Apply volume pattern settings volume_settings = preset.get("volume_patterns", {}) if settings.get('pattern_speed_enabled', False): min_speed = settings.get('pattern_speed_min', 0.5) max_speed = settings.get('pattern_speed_max', 2.0) speed_range = (max_speed - min_speed) / 2 base_speed = base_preset["volume_patterns"]["pattern_speed"] target_speed = base_speed + (speed_range * factor) volume_settings["pattern_speed"] = max(min_speed, min(max_speed, target_speed)) if settings.get('pattern_intensity_enabled', False): min_intensity = settings.get('pattern_intensity_min', 0.5) max_intensity = settings.get('pattern_intensity_max', 1.5) intensity_range = (max_intensity - min_intensity) / 2 base_intensity = base_preset["volume_patterns"]["pattern_intensity"] target_intensity = base_intensity + (intensity_range * factor) volume_settings["pattern_intensity"] = max(min_intensity, min(max_intensity, target_intensity)) preset["volume_patterns"] = volume_settings # Apply intensity progression parameters (overrides other parameter settings) if settings.get('gate_progression_enabled', False): gate_min = settings.get('gate_prog_min', 0.3) gate_max = settings.get('gate_prog_max', 1.0) arp["gate"] = gate_min + (gate_max - gate_min) * factor if settings.get('tempo_progression_enabled', False): tempo_min = settings.get('tempo_prog_min', 90) tempo_max = settings.get('tempo_prog_max', 140) arp["tempo"] = tempo_min + (tempo_max - tempo_min) * factor if settings.get('min_volume_progression_enabled', False): min_vol_start = settings.get('min_vol_prog_min', 0.0) min_vol_peak = settings.get('min_vol_prog_max', 0.5) current_min_vol = min_vol_start + (min_vol_peak - min_vol_start) * factor # Update volume settings if "global_volume_range" in volume_settings: volume_settings["global_volume_range"] = [current_min_vol, volume_settings["global_volume_range"][1]] else: volume_settings["global_volume_range"] = [current_min_vol, 1.0] if settings.get('max_volume_progression_enabled', False): max_vol_start = settings.get('max_vol_prog_min', 0.7) max_vol_peak = settings.get('max_vol_prog_max', 1.0) current_max_vol = max_vol_start + (max_vol_peak - max_vol_start) * factor # Update volume settings if "global_volume_range" in volume_settings: volume_settings["global_volume_range"] = [volume_settings["global_volume_range"][0], current_max_vol] else: volume_settings["global_volume_range"] = [0.0, current_max_vol] if settings.get('min_velocity_progression_enabled', False): min_vel_start = settings.get('min_vel_prog_min', 20) min_vel_peak = settings.get('min_vel_prog_max', 60) current_min_vel = min_vel_start + (min_vel_peak - min_vel_start) * factor # Update volume settings if "global_velocity_range" in volume_settings: volume_settings["global_velocity_range"] = [int(current_min_vel), volume_settings["global_velocity_range"][1]] else: volume_settings["global_velocity_range"] = [int(current_min_vel), 127] if settings.get('max_velocity_progression_enabled', False): max_vel_start = settings.get('max_vel_prog_min', 90) max_vel_peak = settings.get('max_vel_prog_max', 127) current_max_vel = max_vel_start + (max_vel_peak - max_vel_start) * factor # Update volume settings if "global_velocity_range" in volume_settings: volume_settings["global_velocity_range"] = [volume_settings["global_velocity_range"][0], int(current_max_vel)] else: volume_settings["global_velocity_range"] = [40, int(current_max_vel)] # Update volume settings with any intensity progression changes preset["volume_patterns"] = volume_settings # 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, settings: Dict[str, Any] = None) -> List[Tuple[str, Dict[str, Any]]]: """Progress through related musical modes with comprehensive parameter control""" presets = [] if settings is None: settings = {} # Get generation settings intensity_factor = settings.get('intensity_factor', 1.0) randomization = settings.get('randomization', 0.1) # Plan modal journey scale_sequence = ["major", "mixolydian", "dorian", "minor", "dorian", "mixolydian", "major"] # Prepare note speed progression if enabled available_note_speeds = [] if settings.get('note_speed_enabled', False) and settings.get('note_speeds', []): available_note_speeds = settings['note_speeds'] elif settings.get('time_signature_grouping', False): base_speed = base_preset["arpeggiator"].get("note_speed", "1/4") available_note_speeds = GenerationStrategies.get_time_signature_group_speeds(base_speed) for i in range(count): preset = copy.deepcopy(base_preset) arp = preset["arpeggiator"] # Calculate progression factor (0.0 to 1.0 across journey) progress_factor = i / max(1, count - 1) factor = progress_factor * intensity_factor # Add randomization if randomization > 0: import random rand_factor = 1.0 + (random.random() - 0.5) * randomization * 2 factor *= rand_factor factor = max(0.0, min(2.0, factor)) # Scale progression if settings.get('scale_enabled', True): # Default enabled for modal journey scale_idx = int(progress_factor * (len(scale_sequence) - 1)) arp["scale"] = scale_sequence[scale_idx] # Tempo changes - subtle sine wave drift or controlled progression if settings.get('tempo_enabled', False): min_tempo = settings.get('tempo_min', 90) max_tempo = settings.get('tempo_max', 150) tempo_range = (max_tempo - min_tempo) / 2 base_tempo = base_preset["arpeggiator"]["tempo"] # Sine wave drift combined with progression drift = math.sin(progress_factor * math.pi * 2) * 5 progression = tempo_range * factor * (1 if factor > 0.5 else -1) target_tempo = base_tempo + drift + progression arp["tempo"] = max(min_tempo, min(max_tempo, target_tempo)) else: # Default subtle drift base_tempo = base_preset["arpeggiator"]["tempo"] tempo_drift = math.sin(progress_factor * math.pi * 2) * 5 arp["tempo"] = base_tempo + tempo_drift # Apply all other parameter progressions similar to build_and_release if settings.get('velocity_enabled', False): min_vel = settings.get('velocity_min', 60) max_vel = settings.get('velocity_max', 127) base_velocity = base_preset["arpeggiator"]["velocity"] velocity_range = (max_vel - min_vel) / 2 target_velocity = base_velocity + (velocity_range * factor * (1 if factor > 0.5 else -1)) arp["velocity"] = int(max(min_vel, min(max_vel, target_velocity))) if settings.get('gate_enabled', False): min_gate = settings.get('gate_min', 0.5) max_gate = settings.get('gate_max', 1.0) gate_range = (max_gate - min_gate) / 2 base_gate = base_preset["arpeggiator"]["gate"] target_gate = base_gate + (gate_range * factor * (1 if factor > 0.5 else -1)) arp["gate"] = max(min_gate, min(max_gate, target_gate)) if settings.get('swing_enabled', False): min_swing = settings.get('swing_min', 0.0) max_swing = settings.get('swing_max', 0.3) swing_range = (max_swing - min_swing) / 2 base_swing = base_preset["arpeggiator"]["swing"] target_swing = base_swing + (swing_range * factor) arp["swing"] = max(min_swing, min(max_swing, target_swing)) if settings.get('pattern_length_enabled', False): min_length = settings.get('pattern_length_min', 3) max_length = settings.get('pattern_length_max', 8) length_range = (max_length - min_length) / 2 base_length = base_preset["arpeggiator"]["user_pattern_length"] target_length = base_length + int(length_range * factor) arp["user_pattern_length"] = max(min_length, min(max_length, target_length)) # Note speed progression if available_note_speeds: speed_index = int(progress_factor * (len(available_note_speeds) - 1)) speed_index = max(0, min(len(available_note_speeds) - 1, speed_index)) arp["note_speed"] = available_note_speeds[speed_index] # Other parameters follow same pattern as build_and_release... if settings.get('octave_range_enabled', False): min_octave = settings.get('octave_range_min', 1) max_octave = settings.get('octave_range_max', 2) octave_range = (max_octave - min_octave) / 2 base_octave = base_preset["arpeggiator"]["octave_range"] target_octave = base_octave + int(octave_range * factor) arp["octave_range"] = max(min_octave, min(max_octave, target_octave)) # Determine current scale name for preset naming current_scale = arp.get("scale", scale_sequence[0]) preset_name = f"modal_{current_scale}_{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, settings: Dict[str, Any] = None) -> Dict[str, Any]: """Generate a complete master preset file""" # Apply base preset modifications modified_base_preset = copy.deepcopy(self.base_preset) if settings is None: settings = {} print(f"DEBUG: Applying base preset modifications:") print(f"DEBUG: base_scale = {settings.get('base_scale', 'Use Original')}") print(f"DEBUG: base_root_note = {settings.get('base_root_note', 'Use Original')}") print(f"DEBUG: base_scale_note_start = {settings.get('base_scale_note_start', 'Use Original')}") print(f"DEBUG: base_pattern_type = {settings.get('base_pattern_type', 'Use Original')}") # Apply base scale override if settings.get('base_scale', 'Use Original') != 'Use Original': modified_base_preset["arpeggiator"]["scale"] = settings['base_scale'] print(f"DEBUG: Applied base scale: {settings['base_scale']}") # Apply base root note override if settings.get('base_root_note', 'Use Original') != 'Use Original': root_note_str = settings['base_root_note'] # Extract MIDI note number from string like "C (60)" if '(' in root_note_str and ')' in root_note_str: midi_note = int(root_note_str.split('(')[1].split(')')[0]) modified_base_preset["arpeggiator"]["root_note"] = midi_note print(f"DEBUG: Applied base root note: {midi_note}") # Apply base scale note start override if settings.get('base_scale_note_start', 'Use Original') != 'Use Original': scale_start_str = settings['base_scale_note_start'] print(f"DEBUG: Processing scale_start_str = '{scale_start_str}'") if scale_start_str == "Random": modified_base_preset["arpeggiator"]["scale_note_start"] = "random" print(f"DEBUG: Applied base scale note start: random") elif 'Scale Note' in scale_start_str: scale_note_num = int(scale_start_str.split('Scale Note ')[1]) - 1 # Convert to 0-based modified_base_preset["arpeggiator"]["scale_note_start"] = scale_note_num print(f"DEBUG: Applied base scale note start: {scale_note_num}") else: print(f"DEBUG: Unknown scale_start_str format: '{scale_start_str}'") # Apply base pattern type override if settings.get('base_pattern_type', 'Use Original') != 'Use Original': pattern_type_str = settings['base_pattern_type'] modified_base_preset["arpeggiator"]["pattern_type"] = pattern_type_str print(f"DEBUG: Applied base pattern type: {pattern_type_str}") # Generate presets using selected strategy print(f"DEBUG: About to generate presets using strategy: {strategy}") print(f"DEBUG: Settings being passed: {settings}") try: if strategy == "build_and_release": print(f"DEBUG: Calling GenerationStrategies.build_and_release") preset_list = GenerationStrategies.build_and_release( modified_base_preset, count, self.musical_logic, self.template, settings ) elif strategy == "modal_journey": print(f"DEBUG: Calling GenerationStrategies.modal_journey") preset_list = GenerationStrategies.modal_journey( modified_base_preset, count, self.musical_logic, self.template, settings ) else: raise ValueError(f"Unknown strategy: {strategy}") print(f"DEBUG: Generation completed, got {len(preset_list)} presets") except Exception as e: print(f"DEBUG: Exception during preset generation: {e}") import traceback traceback.print_exc() raise # 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.advanced_settings ) 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): """Comprehensive parameter control panel""" def __init__(self): super().__init__() self.parameter_controls = {} self.init_ui() def init_ui(self): # Create main layout main_layout = QVBoxLayout(self) # Create tab widget for organized sections self.tab_widget = QTabWidget() main_layout.addWidget(self.tab_widget) # Create individual tabs self.create_arpeggiator_tab() self.create_pattern_tab() self.create_musical_tab() self.create_delay_tab() self.create_volume_tab() self.create_channel_tab() self.create_intensity_progression_tab() self.create_generation_tab() def create_arpeggiator_tab(self): """Create main arpeggiator parameter tab""" tab = QWidget() layout = QVBoxLayout(tab) group = QGroupBox("Arpeggiator Parameters") form_layout = QFormLayout(group) # Tempo controls tempo_layout = QHBoxLayout() self.parameter_controls['tempo_enabled'] = QCheckBox("Vary") self.parameter_controls['tempo_min'] = QSpinBox() self.parameter_controls['tempo_min'].setRange(60, 200) self.parameter_controls['tempo_min'].setValue(90) self.parameter_controls['tempo_min'].setSuffix(" BPM") self.parameter_controls['tempo_max'] = QSpinBox() self.parameter_controls['tempo_max'].setRange(60, 200) self.parameter_controls['tempo_max'].setValue(150) self.parameter_controls['tempo_max'].setSuffix(" BPM") tempo_layout.addWidget(self.parameter_controls['tempo_enabled']) tempo_layout.addWidget(QLabel("Min:")) tempo_layout.addWidget(self.parameter_controls['tempo_min']) tempo_layout.addWidget(QLabel("Max:")) tempo_layout.addWidget(self.parameter_controls['tempo_max']) form_layout.addRow("Tempo Range:", tempo_layout) # Velocity controls velocity_layout = QHBoxLayout() self.parameter_controls['velocity_enabled'] = QCheckBox("Vary") self.parameter_controls['velocity_min'] = QSpinBox() self.parameter_controls['velocity_min'].setRange(1, 127) self.parameter_controls['velocity_min'].setValue(60) self.parameter_controls['velocity_max'] = QSpinBox() self.parameter_controls['velocity_max'].setRange(1, 127) self.parameter_controls['velocity_max'].setValue(127) velocity_layout.addWidget(self.parameter_controls['velocity_enabled']) velocity_layout.addWidget(QLabel("Min:")) velocity_layout.addWidget(self.parameter_controls['velocity_min']) velocity_layout.addWidget(QLabel("Max:")) velocity_layout.addWidget(self.parameter_controls['velocity_max']) form_layout.addRow("Velocity Range:", velocity_layout) # Gate controls gate_layout = QHBoxLayout() self.parameter_controls['gate_enabled'] = QCheckBox("Vary") self.parameter_controls['gate_min'] = QDoubleSpinBox() self.parameter_controls['gate_min'].setRange(0.1, 1.0) self.parameter_controls['gate_min'].setValue(0.5) self.parameter_controls['gate_min'].setSingleStep(0.1) self.parameter_controls['gate_max'] = QDoubleSpinBox() self.parameter_controls['gate_max'].setRange(0.1, 1.0) self.parameter_controls['gate_max'].setValue(1.0) self.parameter_controls['gate_max'].setSingleStep(0.1) gate_layout.addWidget(self.parameter_controls['gate_enabled']) gate_layout.addWidget(QLabel("Min:")) gate_layout.addWidget(self.parameter_controls['gate_min']) gate_layout.addWidget(QLabel("Max:")) gate_layout.addWidget(self.parameter_controls['gate_max']) form_layout.addRow("Gate Range:", gate_layout) # Swing controls swing_layout = QHBoxLayout() self.parameter_controls['swing_enabled'] = QCheckBox("Vary") self.parameter_controls['swing_min'] = QDoubleSpinBox() self.parameter_controls['swing_min'].setRange(0.0, 0.5) self.parameter_controls['swing_min'].setValue(0.0) self.parameter_controls['swing_min'].setSingleStep(0.05) self.parameter_controls['swing_max'] = QDoubleSpinBox() self.parameter_controls['swing_max'].setRange(0.0, 0.5) self.parameter_controls['swing_max'].setValue(0.3) self.parameter_controls['swing_max'].setSingleStep(0.05) swing_layout.addWidget(self.parameter_controls['swing_enabled']) swing_layout.addWidget(QLabel("Min:")) swing_layout.addWidget(self.parameter_controls['swing_min']) swing_layout.addWidget(QLabel("Max:")) swing_layout.addWidget(self.parameter_controls['swing_max']) form_layout.addRow("Swing Range:", swing_layout) layout.addWidget(group) layout.addStretch() self.tab_widget.addTab(tab, "Arpeggiator") def create_pattern_tab(self): """Create pattern and note speed tab""" tab = QWidget() layout = QVBoxLayout(tab) group = QGroupBox("Pattern & Note Speed") form_layout = QFormLayout(group) # Pattern length controls length_layout = QHBoxLayout() self.parameter_controls['pattern_length_enabled'] = QCheckBox("Vary") self.parameter_controls['pattern_length_min'] = QSpinBox() self.parameter_controls['pattern_length_min'].setRange(2, 16) self.parameter_controls['pattern_length_min'].setValue(3) self.parameter_controls['pattern_length_max'] = QSpinBox() self.parameter_controls['pattern_length_max'].setRange(2, 16) self.parameter_controls['pattern_length_max'].setValue(8) length_layout.addWidget(self.parameter_controls['pattern_length_enabled']) length_layout.addWidget(QLabel("Min:")) length_layout.addWidget(self.parameter_controls['pattern_length_min']) length_layout.addWidget(QLabel("Max:")) length_layout.addWidget(self.parameter_controls['pattern_length_max']) form_layout.addRow("Pattern Length:", length_layout) # Pattern type progression self.parameter_controls['pattern_type_enabled'] = QCheckBox("Change pattern types") self.parameter_controls['pattern_types'] = QComboBox() self.parameter_controls['pattern_types'].addItems(["up", "down", "up_down", "down_up", "random"]) form_layout.addRow("Pattern Types:", self.parameter_controls['pattern_type_enabled']) form_layout.addRow("Available Types:", self.parameter_controls['pattern_types']) # Note speed progression self.parameter_controls['note_speed_enabled'] = QCheckBox("Progress through note speeds") note_speed_layout = QVBoxLayout() self.parameter_controls['note_speeds'] = [] speeds = ["1/1", "1/2", "1/4", "1/8", "1/16", "1/32", "1/2T", "1/4T", "1/8T", "1/16T"] for speed in speeds: cb = QCheckBox(speed) self.parameter_controls['note_speeds'].append(cb) note_speed_layout.addWidget(cb) form_layout.addRow("Note Speed Progression:", self.parameter_controls['note_speed_enabled']) form_layout.addRow("Available Speeds:", note_speed_layout) # Octave range octave_layout = QHBoxLayout() self.parameter_controls['octave_range_enabled'] = QCheckBox("Vary") self.parameter_controls['octave_range_min'] = QSpinBox() self.parameter_controls['octave_range_min'].setRange(1, 4) self.parameter_controls['octave_range_min'].setValue(1) self.parameter_controls['octave_range_max'] = QSpinBox() self.parameter_controls['octave_range_max'].setRange(1, 4) self.parameter_controls['octave_range_max'].setValue(2) octave_layout.addWidget(self.parameter_controls['octave_range_enabled']) octave_layout.addWidget(QLabel("Min:")) octave_layout.addWidget(self.parameter_controls['octave_range_min']) octave_layout.addWidget(QLabel("Max:")) octave_layout.addWidget(self.parameter_controls['octave_range_max']) form_layout.addRow("Octave Range:", octave_layout) layout.addWidget(group) layout.addStretch() self.tab_widget.addTab(tab, "Pattern & Speed") def create_musical_tab(self): """Create musical scale and key tab""" tab = QWidget() layout = QVBoxLayout(tab) group = QGroupBox("Musical Parameters") form_layout = QFormLayout(group) # Root note progression self.parameter_controls['root_note_enabled'] = QCheckBox("Change root notes") self.parameter_controls['root_note_progression'] = QComboBox() self.parameter_controls['root_note_progression'].addItems([ "Stay Same", "Circle of Fifths", "Chromatic Up", "Chromatic Down", "Random" ]) form_layout.addRow("Root Note Changes:", self.parameter_controls['root_note_enabled']) form_layout.addRow("Progression Type:", self.parameter_controls['root_note_progression']) # Scale progression self.parameter_controls['scale_enabled'] = QCheckBox("Change scales") self.parameter_controls['scale_progression'] = QComboBox() self.parameter_controls['scale_progression'].addItems([ "Stay Same", "Modal Journey", "Major/Minor Only", "All Modes", "Random" ]) form_layout.addRow("Scale Changes:", self.parameter_controls['scale_enabled']) form_layout.addRow("Scale Progression:", self.parameter_controls['scale_progression']) # Scale note start scale_start_layout = QHBoxLayout() self.parameter_controls['scale_note_start_enabled'] = QCheckBox("Vary") self.parameter_controls['scale_note_start_min'] = QSpinBox() self.parameter_controls['scale_note_start_min'].setRange(0, 6) self.parameter_controls['scale_note_start_min'].setValue(0) self.parameter_controls['scale_note_start_max'] = QSpinBox() self.parameter_controls['scale_note_start_max'].setRange(0, 6) self.parameter_controls['scale_note_start_max'].setValue(3) scale_start_layout.addWidget(self.parameter_controls['scale_note_start_enabled']) scale_start_layout.addWidget(QLabel("Min:")) scale_start_layout.addWidget(self.parameter_controls['scale_note_start_min']) scale_start_layout.addWidget(QLabel("Max:")) scale_start_layout.addWidget(self.parameter_controls['scale_note_start_max']) form_layout.addRow("Scale Note Start:", scale_start_layout) layout.addWidget(group) layout.addStretch() self.tab_widget.addTab(tab, "Musical") def create_delay_tab(self): """Create delay and effects tab""" tab = QWidget() layout = QVBoxLayout(tab) group = QGroupBox("Delay & Effects") form_layout = QFormLayout(group) # Delay enabled progression self.parameter_controls['delay_enabled_changes'] = QCheckBox("Toggle delay on/off") form_layout.addRow("Delay Toggling:", self.parameter_controls['delay_enabled_changes']) # Delay length delay_length_layout = QHBoxLayout() self.parameter_controls['delay_length_enabled'] = QCheckBox("Vary") self.parameter_controls['delay_length_min'] = QSpinBox() self.parameter_controls['delay_length_min'].setRange(1, 8) self.parameter_controls['delay_length_min'].setValue(2) self.parameter_controls['delay_length_max'] = QSpinBox() self.parameter_controls['delay_length_max'].setRange(1, 8) self.parameter_controls['delay_length_max'].setValue(4) delay_length_layout.addWidget(self.parameter_controls['delay_length_enabled']) delay_length_layout.addWidget(QLabel("Min:")) delay_length_layout.addWidget(self.parameter_controls['delay_length_min']) delay_length_layout.addWidget(QLabel("Max:")) delay_length_layout.addWidget(self.parameter_controls['delay_length_max']) form_layout.addRow("Delay Length:", delay_length_layout) # Delay timing self.parameter_controls['delay_timing_enabled'] = QCheckBox("Change delay timing") delay_timings_layout = QVBoxLayout() self.parameter_controls['delay_timings'] = [] timings = ["1/4", "1/8", "1/16", "1/4T", "1/8T", "1/16T", "2/1", "1/1", "1/2"] for timing in timings: cb = QCheckBox(timing) self.parameter_controls['delay_timings'].append(cb) delay_timings_layout.addWidget(cb) form_layout.addRow("Delay Timing Changes:", self.parameter_controls['delay_timing_enabled']) form_layout.addRow("Available Timings:", delay_timings_layout) # Delay fade delay_fade_layout = QHBoxLayout() self.parameter_controls['delay_fade_enabled'] = QCheckBox("Vary") self.parameter_controls['delay_fade_min'] = QDoubleSpinBox() self.parameter_controls['delay_fade_min'].setRange(0.0, 1.0) self.parameter_controls['delay_fade_min'].setValue(0.2) self.parameter_controls['delay_fade_min'].setSingleStep(0.05) self.parameter_controls['delay_fade_max'] = QDoubleSpinBox() self.parameter_controls['delay_fade_max'].setRange(0.0, 1.0) self.parameter_controls['delay_fade_max'].setValue(0.8) self.parameter_controls['delay_fade_max'].setSingleStep(0.05) delay_fade_layout.addWidget(self.parameter_controls['delay_fade_enabled']) delay_fade_layout.addWidget(QLabel("Min:")) delay_fade_layout.addWidget(self.parameter_controls['delay_fade_min']) delay_fade_layout.addWidget(QLabel("Max:")) delay_fade_layout.addWidget(self.parameter_controls['delay_fade_max']) form_layout.addRow("Delay Fade:", delay_fade_layout) layout.addWidget(group) layout.addStretch() self.tab_widget.addTab(tab, "Delay & Effects") def create_volume_tab(self): """Create volume and lighting tab""" tab = QWidget() layout = QVBoxLayout(tab) group = QGroupBox("Volume & Lighting") form_layout = QFormLayout(group) # Volume pattern changes self.parameter_controls['volume_pattern_enabled'] = QCheckBox("Change volume patterns") self.parameter_controls['volume_patterns'] = QComboBox() self.parameter_controls['volume_patterns'].addItems([ "static", "1_bar_swell", "2_bar_swell", "4_bar_swell", "8_bar_swell", "accent_2", "accent_4", "random", "cascade_up", "cascade_down" ]) form_layout.addRow("Volume Patterns:", self.parameter_controls['volume_pattern_enabled']) form_layout.addRow("Available Patterns:", self.parameter_controls['volume_patterns']) # Pattern speed pattern_speed_layout = QHBoxLayout() self.parameter_controls['pattern_speed_enabled'] = QCheckBox("Vary") self.parameter_controls['pattern_speed_min'] = QDoubleSpinBox() self.parameter_controls['pattern_speed_min'].setRange(0.1, 4.0) self.parameter_controls['pattern_speed_min'].setValue(0.5) self.parameter_controls['pattern_speed_min'].setSingleStep(0.1) self.parameter_controls['pattern_speed_max'] = QDoubleSpinBox() self.parameter_controls['pattern_speed_max'].setRange(0.1, 4.0) self.parameter_controls['pattern_speed_max'].setValue(2.0) self.parameter_controls['pattern_speed_max'].setSingleStep(0.1) pattern_speed_layout.addWidget(self.parameter_controls['pattern_speed_enabled']) pattern_speed_layout.addWidget(QLabel("Min:")) pattern_speed_layout.addWidget(self.parameter_controls['pattern_speed_min']) pattern_speed_layout.addWidget(QLabel("Max:")) pattern_speed_layout.addWidget(self.parameter_controls['pattern_speed_max']) form_layout.addRow("Pattern Speed:", pattern_speed_layout) # Pattern intensity pattern_intensity_layout = QHBoxLayout() self.parameter_controls['pattern_intensity_enabled'] = QCheckBox("Vary") self.parameter_controls['pattern_intensity_min'] = QDoubleSpinBox() self.parameter_controls['pattern_intensity_min'].setRange(0.1, 2.0) self.parameter_controls['pattern_intensity_min'].setValue(0.5) self.parameter_controls['pattern_intensity_min'].setSingleStep(0.1) self.parameter_controls['pattern_intensity_max'] = QDoubleSpinBox() self.parameter_controls['pattern_intensity_max'].setRange(0.1, 2.0) self.parameter_controls['pattern_intensity_max'].setValue(1.5) self.parameter_controls['pattern_intensity_max'].setSingleStep(0.1) pattern_intensity_layout.addWidget(self.parameter_controls['pattern_intensity_enabled']) pattern_intensity_layout.addWidget(QLabel("Min:")) pattern_intensity_layout.addWidget(self.parameter_controls['pattern_intensity_min']) pattern_intensity_layout.addWidget(QLabel("Max:")) pattern_intensity_layout.addWidget(self.parameter_controls['pattern_intensity_max']) form_layout.addRow("Pattern Intensity:", pattern_intensity_layout) layout.addWidget(group) layout.addStretch() self.tab_widget.addTab(tab, "Volume & Lighting") def create_channel_tab(self): """Create channel and distribution tab""" tab = QWidget() layout = QVBoxLayout(tab) group = QGroupBox("Channel Settings") form_layout = QFormLayout(group) # Channel distribution self.parameter_controls['channel_distribution_enabled'] = QCheckBox("Change distribution") self.parameter_controls['channel_distributions'] = QComboBox() self.parameter_controls['channel_distributions'].addItems(["up", "down", "random"]) form_layout.addRow("Channel Distribution:", self.parameter_controls['channel_distribution_enabled']) form_layout.addRow("Available Types:", self.parameter_controls['channel_distributions']) # Active synth count (locked in live mode) synth_count_layout = QHBoxLayout() self.parameter_controls['synth_count_enabled'] = QCheckBox("Vary (Not recommended for live)") self.parameter_controls['synth_count_enabled'].setEnabled(False) # Disabled by default for live performance self.parameter_controls['synth_count_min'] = QSpinBox() self.parameter_controls['synth_count_min'].setRange(1, 16) self.parameter_controls['synth_count_min'].setValue(3) self.parameter_controls['synth_count_max'] = QSpinBox() self.parameter_controls['synth_count_max'].setRange(1, 16) self.parameter_controls['synth_count_max'].setValue(8) synth_count_layout.addWidget(self.parameter_controls['synth_count_enabled']) synth_count_layout.addWidget(QLabel("Min:")) synth_count_layout.addWidget(self.parameter_controls['synth_count_min']) synth_count_layout.addWidget(QLabel("Max:")) synth_count_layout.addWidget(self.parameter_controls['synth_count_max']) form_layout.addRow("Active Synth Count:", synth_count_layout) layout.addWidget(group) layout.addStretch() self.tab_widget.addTab(tab, "Channel Settings") def create_intensity_progression_tab(self): """Create intensity progression tab for specific parameters""" tab = QWidget() layout = QVBoxLayout(tab) # Description desc_label = QLabel("Configure how specific parameters change over time based on intensity curve.") desc_label.setWordWrap(True) desc_label.setStyleSheet("color: #cccccc; font-style: italic; margin-bottom: 10px;") layout.addWidget(desc_label) # Gate progression gate_group = QGroupBox("Gate Progression") gate_layout = QFormLayout(gate_group) gate_enable_layout = QHBoxLayout() self.parameter_controls['gate_progression_enabled'] = QCheckBox("Enable Gate Progression") self.parameter_controls['gate_progression_enabled'].setToolTip("Gate will progress from min to max and back based on intensity curve") gate_enable_layout.addWidget(self.parameter_controls['gate_progression_enabled']) gate_layout.addRow(gate_enable_layout) gate_range_layout = QHBoxLayout() self.parameter_controls['gate_prog_min'] = QDoubleSpinBox() self.parameter_controls['gate_prog_min'].setRange(0.1, 1.0) self.parameter_controls['gate_prog_min'].setValue(0.3) self.parameter_controls['gate_prog_min'].setSingleStep(0.1) self.parameter_controls['gate_prog_max'] = QDoubleSpinBox() self.parameter_controls['gate_prog_max'].setRange(0.1, 1.0) self.parameter_controls['gate_prog_max'].setValue(1.0) self.parameter_controls['gate_prog_max'].setSingleStep(0.1) gate_range_layout.addWidget(QLabel("Min:")) gate_range_layout.addWidget(self.parameter_controls['gate_prog_min']) gate_range_layout.addWidget(QLabel("Max:")) gate_range_layout.addWidget(self.parameter_controls['gate_prog_max']) gate_layout.addRow("Gate Range:", gate_range_layout) # Tempo progression tempo_group = QGroupBox("Tempo Progression") tempo_layout = QFormLayout(tempo_group) tempo_enable_layout = QHBoxLayout() self.parameter_controls['tempo_progression_enabled'] = QCheckBox("Enable Tempo Progression") self.parameter_controls['tempo_progression_enabled'].setToolTip("Tempo will progress from min to max and back based on intensity curve") tempo_enable_layout.addWidget(self.parameter_controls['tempo_progression_enabled']) tempo_layout.addRow(tempo_enable_layout) tempo_range_layout = QHBoxLayout() self.parameter_controls['tempo_prog_min'] = QSpinBox() self.parameter_controls['tempo_prog_min'].setRange(60, 200) self.parameter_controls['tempo_prog_min'].setValue(90) self.parameter_controls['tempo_prog_min'].setSuffix(" BPM") self.parameter_controls['tempo_prog_max'] = QSpinBox() self.parameter_controls['tempo_prog_max'].setRange(60, 200) self.parameter_controls['tempo_prog_max'].setValue(140) self.parameter_controls['tempo_prog_max'].setSuffix(" BPM") tempo_range_layout.addWidget(QLabel("Min:")) tempo_range_layout.addWidget(self.parameter_controls['tempo_prog_min']) tempo_range_layout.addWidget(QLabel("Max:")) tempo_range_layout.addWidget(self.parameter_controls['tempo_prog_max']) tempo_layout.addRow("Tempo Range:", tempo_range_layout) # Volume progression volume_group = QGroupBox("Volume Progression") volume_layout = QFormLayout(volume_group) # Min Volume progression min_vol_enable_layout = QHBoxLayout() self.parameter_controls['min_volume_progression_enabled'] = QCheckBox("Enable Min Volume Progression") self.parameter_controls['min_volume_progression_enabled'].setToolTip("Minimum volume will progress based on intensity curve") min_vol_enable_layout.addWidget(self.parameter_controls['min_volume_progression_enabled']) volume_layout.addRow(min_vol_enable_layout) min_vol_range_layout = QHBoxLayout() self.parameter_controls['min_vol_prog_min'] = QDoubleSpinBox() self.parameter_controls['min_vol_prog_min'].setRange(0.0, 1.0) self.parameter_controls['min_vol_prog_min'].setValue(0.0) self.parameter_controls['min_vol_prog_min'].setSingleStep(0.05) self.parameter_controls['min_vol_prog_max'] = QDoubleSpinBox() self.parameter_controls['min_vol_prog_max'].setRange(0.0, 1.0) self.parameter_controls['min_vol_prog_max'].setValue(0.5) self.parameter_controls['min_vol_prog_max'].setSingleStep(0.05) min_vol_range_layout.addWidget(QLabel("Start:")) min_vol_range_layout.addWidget(self.parameter_controls['min_vol_prog_min']) min_vol_range_layout.addWidget(QLabel("Peak:")) min_vol_range_layout.addWidget(self.parameter_controls['min_vol_prog_max']) volume_layout.addRow("Min Volume Range:", min_vol_range_layout) # Max Volume progression max_vol_enable_layout = QHBoxLayout() self.parameter_controls['max_volume_progression_enabled'] = QCheckBox("Enable Max Volume Progression") self.parameter_controls['max_volume_progression_enabled'].setToolTip("Maximum volume will progress based on intensity curve") max_vol_enable_layout.addWidget(self.parameter_controls['max_volume_progression_enabled']) volume_layout.addRow(max_vol_enable_layout) max_vol_range_layout = QHBoxLayout() self.parameter_controls['max_vol_prog_min'] = QDoubleSpinBox() self.parameter_controls['max_vol_prog_min'].setRange(0.0, 1.0) self.parameter_controls['max_vol_prog_min'].setValue(0.7) self.parameter_controls['max_vol_prog_min'].setSingleStep(0.05) self.parameter_controls['max_vol_prog_max'] = QDoubleSpinBox() self.parameter_controls['max_vol_prog_max'].setRange(0.0, 1.0) self.parameter_controls['max_vol_prog_max'].setValue(1.0) self.parameter_controls['max_vol_prog_max'].setSingleStep(0.05) max_vol_range_layout.addWidget(QLabel("Start:")) max_vol_range_layout.addWidget(self.parameter_controls['max_vol_prog_min']) max_vol_range_layout.addWidget(QLabel("Peak:")) max_vol_range_layout.addWidget(self.parameter_controls['max_vol_prog_max']) volume_layout.addRow("Max Volume Range:", max_vol_range_layout) # Velocity progression velocity_group = QGroupBox("Velocity Progression") velocity_layout = QFormLayout(velocity_group) # Min Velocity progression min_vel_enable_layout = QHBoxLayout() self.parameter_controls['min_velocity_progression_enabled'] = QCheckBox("Enable Min Velocity Progression") self.parameter_controls['min_velocity_progression_enabled'].setToolTip("Minimum velocity will progress based on intensity curve") min_vel_enable_layout.addWidget(self.parameter_controls['min_velocity_progression_enabled']) velocity_layout.addRow(min_vel_enable_layout) min_vel_range_layout = QHBoxLayout() self.parameter_controls['min_vel_prog_min'] = QSpinBox() self.parameter_controls['min_vel_prog_min'].setRange(1, 127) self.parameter_controls['min_vel_prog_min'].setValue(20) self.parameter_controls['min_vel_prog_max'] = QSpinBox() self.parameter_controls['min_vel_prog_max'].setRange(1, 127) self.parameter_controls['min_vel_prog_max'].setValue(60) min_vel_range_layout.addWidget(QLabel("Start:")) min_vel_range_layout.addWidget(self.parameter_controls['min_vel_prog_min']) min_vel_range_layout.addWidget(QLabel("Peak:")) min_vel_range_layout.addWidget(self.parameter_controls['min_vel_prog_max']) velocity_layout.addRow("Min Velocity Range:", min_vel_range_layout) # Max Velocity progression max_vel_enable_layout = QHBoxLayout() self.parameter_controls['max_velocity_progression_enabled'] = QCheckBox("Enable Max Velocity Progression") self.parameter_controls['max_velocity_progression_enabled'].setToolTip("Maximum velocity will progress based on intensity curve") max_vel_enable_layout.addWidget(self.parameter_controls['max_velocity_progression_enabled']) velocity_layout.addRow(max_vel_enable_layout) max_vel_range_layout = QHBoxLayout() self.parameter_controls['max_vel_prog_min'] = QSpinBox() self.parameter_controls['max_vel_prog_min'].setRange(1, 127) self.parameter_controls['max_vel_prog_min'].setValue(90) self.parameter_controls['max_vel_prog_max'] = QSpinBox() self.parameter_controls['max_vel_prog_max'].setRange(1, 127) self.parameter_controls['max_vel_prog_max'].setValue(127) max_vel_range_layout.addWidget(QLabel("Start:")) max_vel_range_layout.addWidget(self.parameter_controls['max_vel_prog_min']) max_vel_range_layout.addWidget(QLabel("Peak:")) max_vel_range_layout.addWidget(self.parameter_controls['max_vel_prog_max']) velocity_layout.addRow("Max Velocity Range:", max_vel_range_layout) # Add all groups to layout layout.addWidget(gate_group) layout.addWidget(tempo_group) layout.addWidget(volume_group) layout.addWidget(velocity_group) layout.addStretch() self.tab_widget.addTab(tab, "Intensity Progression") def create_generation_tab(self): """Create generation strategy tab""" tab = QWidget() layout = QVBoxLayout(tab) group = QGroupBox("Generation Strategy") form_layout = QFormLayout(group) # Build percentage (where peak occurs) self.parameter_controls['build_percentage'] = QSpinBox() self.parameter_controls['build_percentage'].setRange(50, 90) self.parameter_controls['build_percentage'].setValue(70) self.parameter_controls['build_percentage'].setSuffix("% point") form_layout.addRow("Peak Intensity At:", self.parameter_controls['build_percentage']) # Overall intensity factor self.parameter_controls['intensity_factor'] = QDoubleSpinBox() self.parameter_controls['intensity_factor'].setRange(0.1, 3.0) self.parameter_controls['intensity_factor'].setValue(1.0) self.parameter_controls['intensity_factor'].setSingleStep(0.1) self.parameter_controls['intensity_factor'].setToolTip("Overall intensity multiplier for all parameter changes") form_layout.addRow("Intensity Factor:", self.parameter_controls['intensity_factor']) # Randomization factor self.parameter_controls['randomization'] = QDoubleSpinBox() self.parameter_controls['randomization'].setRange(0.0, 1.0) self.parameter_controls['randomization'].setValue(0.1) self.parameter_controls['randomization'].setSingleStep(0.05) self.parameter_controls['randomization'].setToolTip("Amount of random variation to add (0.0 = none, 1.0 = maximum)") form_layout.addRow("Randomization:", self.parameter_controls['randomization']) layout.addWidget(group) layout.addStretch() self.tab_widget.addTab(tab, "Generation Strategy") def get_settings(self) -> Dict[str, Any]: """Get comprehensive parameter control settings""" settings = {} # Extract all control values for param, control in self.parameter_controls.items(): if isinstance(control, QCheckBox): settings[param] = control.isChecked() elif isinstance(control, (QSpinBox, QDoubleSpinBox)): settings[param] = control.value() elif isinstance(control, QComboBox): settings[param] = control.currentText() elif isinstance(control, list): # For note speeds and delay timings if param == 'note_speeds': settings[param] = [cb.text() for cb in control if cb.isChecked()] elif param == 'delay_timings': settings[param] = [cb.text() for cb in control if cb.isChecked()] return settings 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") # Settings Management section settings_section = self.create_settings_management_section() main_layout.addWidget(settings_section) # 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) # Base Preset Modifications base_preset_group = QGroupBox("Base Preset Modifications") base_preset_layout = QFormLayout(base_preset_group) # Scale selection self.base_scale = QComboBox() self.base_scale.addItems([ "Use Original", "major", "minor", "dorian", "mixolydian", "lydian", "phrygian", "locrian", "harmonic_minor", "melodic_minor", "pentatonic", "blues" ]) self.base_scale.setToolTip("Override the base preset's scale") # Root note selection self.base_root_note = QComboBox() root_notes = ["Use Original"] for i in range(12): note_name = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"][i] root_notes.append(f"{note_name} ({60 + i})") self.base_root_note.addItems(root_notes) self.base_root_note.setToolTip("Override the base preset's root note") # Scale note start selection self.base_scale_note_start = QComboBox() scale_starts = ["Use Original"] for i in range(7): scale_starts.append(f"Scale Note {i + 1}") scale_starts.append("Random") self.base_scale_note_start.addItems(scale_starts) self.base_scale_note_start.setToolTip("Override which note in the scale to start the arpeggio from") # Pattern type selection self.base_pattern_type = QComboBox() pattern_types = ["Use Original", "up", "down", "up_down", "down_up", "random", "note_order", "chord", "random_chord"] self.base_pattern_type.addItems(pattern_types) self.base_pattern_type.setToolTip("Override the base preset's arpeggio pattern type") base_preset_layout.addRow("Base Scale:", self.base_scale) base_preset_layout.addRow("Base Root Note:", self.base_root_note) base_preset_layout.addRow("Scale Note Start:", self.base_scale_note_start) base_preset_layout.addRow("Pattern Type:", self.base_pattern_type) # 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)") self.time_signature_grouping = QCheckBox("Time Signature Grouping") self.time_signature_grouping.setChecked(False) self.time_signature_grouping.setToolTip("Group note speeds by time signature (2/4/8/16 or 3/6/12)") quick_layout.addRow(self.subtle_changes) quick_layout.addRow(self.preserve_feel) quick_layout.addRow(self.live_performance) quick_layout.addRow(self.time_signature_grouping) # Add all groups layout.addWidget(master_group) layout.addWidget(base_preset_group) layout.addWidget(strategy_group) layout.addWidget(quick_group) layout.addStretch() return widget def create_settings_management_section(self): """Create settings management section""" group = QGroupBox("Generator Settings") layout = QHBoxLayout(group) # Settings preset management settings_layout = QHBoxLayout() # Settings preset dropdown self.settings_preset_combo = QComboBox() self.settings_preset_combo.setMinimumWidth(200) self.settings_preset_combo.setEditable(False) self.refresh_settings_presets() # Settings management buttons save_settings_button = QPushButton("Save Settings") save_settings_button.setToolTip("Save current generator settings as a preset") save_settings_button.clicked.connect(self.save_generator_settings) load_settings_button = QPushButton("Load Settings") load_settings_button.setToolTip("Load a saved generator settings preset") load_settings_button.clicked.connect(self.load_generator_settings) delete_settings_button = QPushButton("Delete") delete_settings_button.setToolTip("Delete the selected settings preset") delete_settings_button.clicked.connect(self.delete_generator_settings) # Style the buttons save_settings_button.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #00aa44, stop:1 #007733); border: 1px solid #00cc55; color: white; font-weight: bold; padding: 8px 16px; border-radius: 6px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #00cc55, stop:1 #009944); border: 1px solid #00ff66; } """) load_settings_button.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #0099ff, stop:1 #0066cc); border: 1px solid #00aaff; color: white; font-weight: bold; padding: 8px 16px; border-radius: 6px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #00aaff, stop:1 #0077dd); border: 1px solid #00ccff; } """) delete_settings_button.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #ff4444, stop:1 #cc1111); border: 1px solid #ff5555; color: white; font-weight: bold; padding: 8px 16px; border-radius: 6px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #ff5555, stop:1 #dd2222); border: 1px solid #ff7777; } """) settings_layout.addWidget(QLabel("Saved Settings:")) settings_layout.addWidget(self.settings_preset_combo) settings_layout.addWidget(save_settings_button) settings_layout.addWidget(load_settings_button) settings_layout.addWidget(delete_settings_button) settings_layout.addStretch() layout.addLayout(settings_layout) return group 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() # Add basic settings to advanced settings advanced_settings.update({ "time_signature_grouping": self.time_signature_grouping.isChecked(), "subtle_changes": self.subtle_changes.isChecked(), "preserve_feel": self.preserve_feel.isChecked(), "live_performance": self.live_performance.isChecked(), "base_scale": self.base_scale.currentText(), "base_root_note": self.base_root_note.currentText(), "base_scale_note_start": self.base_scale_note_start.currentText(), "base_pattern_type": self.base_pattern_type.currentText() }) # 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; } /* Form Controls Styling for Better Readability */ QLabel { color: #ffffff; font-size: 11px; font-weight: normal; padding: 2px; background: transparent; } QCheckBox { color: #ffffff; font-size: 11px; background: transparent; spacing: 5px; } QCheckBox::indicator { width: 16px; height: 16px; border: 2px solid #555555; border-radius: 3px; background: #2a2a2e; } QCheckBox::indicator:checked { background: #00aaff; border: 2px solid #00aaff; } QSpinBox, QDoubleSpinBox { background: #3a3a3e; border: 1px solid #555555; border-radius: 4px; color: #ffffff; font-size: 11px; padding: 4px; min-width: 60px; } QSpinBox:focus, QDoubleSpinBox:focus { border: 2px solid #00aaff; background: #4a4a4e; } QComboBox { background: #3a3a3e; border: 1px solid #555555; border-radius: 4px; color: #ffffff; font-size: 11px; padding: 4px; min-width: 100px; } QComboBox:focus { border: 2px solid #00aaff; background: #4a4a4e; } QComboBox::drop-down { border: none; width: 20px; } QComboBox::down-arrow { image: none; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid #ffffff; } QLineEdit { background: #3a3a3e; border: 1px solid #555555; border-radius: 4px; color: #ffffff; font-size: 11px; padding: 4px; } QLineEdit:focus { border: 2px solid #00aaff; background: #4a4a4e; } QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #505054, stop:1 #404044); border: 1px solid #666666; border-radius: 6px; color: white; font-size: 11px; font-weight: bold; padding: 6px 12px; min-width: 80px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #606064, stop:1 #505054); border: 1px solid #00aaff; } QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #404044, stop:1 #303034); } QScrollArea { background: transparent; border: none; } QScrollBar:vertical { background: #2a2a2e; width: 12px; border-radius: 6px; } QScrollBar::handle:vertical { background: #555555; border-radius: 6px; min-height: 20px; } QScrollBar::handle:vertical:hover { background: #00aaff; } """) def refresh_settings_presets(self): """Refresh the settings preset dropdown""" self.settings_preset_combo.clear() self.settings_preset_combo.addItem("-- Select Settings Preset --") settings_dir = "generator_settings" if os.path.exists(settings_dir): for filename in os.listdir(settings_dir): if filename.endswith(".json"): preset_name = filename[:-5] # Remove .json extension self.settings_preset_combo.addItem(preset_name) def collect_current_settings(self): """Collect all current generator settings""" # Get basic settings settings = { "basic": { "master_name": self.master_name.text(), "preset_count": self.preset_count.value(), "loop_count": self.loop_count.value(), "base_scale": self.base_scale.currentText(), "base_root_note": self.base_root_note.currentText(), "base_scale_note_start": self.base_scale_note_start.currentText(), "base_pattern_type": self.base_pattern_type.currentText(), "strategy": self.strategy_combo.currentText(), "subtle_changes": self.subtle_changes.isChecked(), "preserve_feel": self.preserve_feel.isChecked(), "live_performance": self.live_performance.isChecked(), "time_signature_grouping": self.time_signature_grouping.isChecked() }, "advanced": self.advanced_widget.get_settings() } return settings def apply_settings(self, settings): """Apply saved settings to all controls""" try: # Apply basic settings basic = settings.get("basic", {}) self.master_name.setText(basic.get("master_name", "generated_master")) self.preset_count.setValue(basic.get("preset_count", 16)) self.loop_count.setValue(basic.get("loop_count", 4)) # Set combo box selections base_scale = basic.get("base_scale", "Use Original") base_scale_index = self.base_scale.findText(base_scale) if base_scale_index >= 0: self.base_scale.setCurrentIndex(base_scale_index) base_root_note = basic.get("base_root_note", "Use Original") root_note_index = self.base_root_note.findText(base_root_note) if root_note_index >= 0: self.base_root_note.setCurrentIndex(root_note_index) base_scale_note_start = basic.get("base_scale_note_start", "Use Original") scale_start_index = self.base_scale_note_start.findText(base_scale_note_start) if scale_start_index >= 0: self.base_scale_note_start.setCurrentIndex(scale_start_index) base_pattern_type = basic.get("base_pattern_type", "Use Original") pattern_type_index = self.base_pattern_type.findText(base_pattern_type) if pattern_type_index >= 0: self.base_pattern_type.setCurrentIndex(pattern_type_index) strategy = basic.get("strategy", "build_and_release") strategy_index = self.strategy_combo.findText(strategy) if strategy_index >= 0: self.strategy_combo.setCurrentIndex(strategy_index) # Set checkboxes self.subtle_changes.setChecked(basic.get("subtle_changes", True)) self.preserve_feel.setChecked(basic.get("preserve_feel", True)) self.live_performance.setChecked(basic.get("live_performance", True)) self.time_signature_grouping.setChecked(basic.get("time_signature_grouping", False)) # Apply advanced settings advanced = settings.get("advanced", {}) for param, value in advanced.items(): if param in self.advanced_widget.parameter_controls: control = self.advanced_widget.parameter_controls[param] if isinstance(control, QCheckBox): control.setChecked(value) elif isinstance(control, (QSpinBox, QDoubleSpinBox)): control.setValue(value) elif isinstance(control, QComboBox): index = control.findText(value) if index >= 0: control.setCurrentIndex(index) elif isinstance(control, list): # For note speeds/delay timings for cb in control: cb.setChecked(cb.text() in value) except Exception as e: QMessageBox.warning(self, "Settings Error", f"Error applying settings: {str(e)}") def save_generator_settings(self): """Save current generator settings as a preset""" name, ok = QInputDialog.getText(self, "Save Generator Settings", "Enter a name for this settings preset:") if ok and name.strip(): try: settings = self.collect_current_settings() settings["timestamp"] = datetime.now().isoformat() settings["version"] = "1.0" # Create settings directory if it doesn't exist settings_dir = "generator_settings" os.makedirs(settings_dir, exist_ok=True) # Save settings filename = os.path.join(settings_dir, f"{name.strip()}.json") with open(filename, 'w') as f: json.dump(settings, f, indent=2) # Refresh dropdown self.refresh_settings_presets() # Select the newly saved preset index = self.settings_preset_combo.findText(name.strip()) if index >= 0: self.settings_preset_combo.setCurrentIndex(index) self.statusBar().showMessage(f"Generator settings saved as '{name.strip()}'") except Exception as e: QMessageBox.critical(self, "Save Error", f"Error saving settings: {str(e)}") def load_generator_settings(self): """Load selected generator settings preset""" preset_name = self.settings_preset_combo.currentText() if preset_name == "-- Select Settings Preset --": QMessageBox.information(self, "No Selection", "Please select a settings preset to load.") return try: settings_dir = "generator_settings" filename = os.path.join(settings_dir, f"{preset_name}.json") if not os.path.exists(filename): QMessageBox.warning(self, "File Not Found", f"Settings file '{preset_name}.json' not found.") self.refresh_settings_presets() return with open(filename, 'r') as f: settings = json.load(f) self.apply_settings(settings) self.statusBar().showMessage(f"Loaded generator settings '{preset_name}'") except Exception as e: QMessageBox.critical(self, "Load Error", f"Error loading settings: {str(e)}") def delete_generator_settings(self): """Delete selected generator settings preset""" preset_name = self.settings_preset_combo.currentText() if preset_name == "-- Select Settings Preset --": QMessageBox.information(self, "No Selection", "Please select a settings preset to delete.") return reply = QMessageBox.question(self, "Confirm Delete", f"Are you sure you want to delete the settings preset '{preset_name}'?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: try: settings_dir = "generator_settings" filename = os.path.join(settings_dir, f"{preset_name}.json") if os.path.exists(filename): os.remove(filename) self.refresh_settings_presets() self.statusBar().showMessage(f"Deleted generator settings '{preset_name}'") else: QMessageBox.warning(self, "File Not Found", f"Settings file '{preset_name}.json' not found.") self.refresh_settings_presets() except Exception as e: QMessageBox.critical(self, "Delete Error", f"Error deleting settings: {str(e)}") 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()