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.
 

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