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
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()
|