You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

2265 lines
104 KiB

#!/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()