Browse Source
Add Master Preset Generator with GUI and command-line versions
Add Master Preset Generator with GUI and command-line versions
- Create comprehensive preset generation system for live performances - GUI version with PyQt5 interface matching main app styling - Command-line version for scripting and automation - Protects synth count and hardware settings (never changes during generation) - Only generates performance-friendly speeds (1/1, 1/2, 1/4 - no faster) - Multiple generation strategies: build_and_release, modal_journey - Advanced settings: tempo ranges, velocity ranges, scale progressions - Real-time preview with parameter progression analysis - Background generation with progress updates - Comprehensive validation for live performance suitability Features: - Locked parameters for live stability (synth count, channel instruments) - Subtle parameter changes suitable for live performance - Musical logic using circle of fifths and scale relationships - Dark theme GUI matching main arpeggiator interface - Export to master_files directory for immediate use 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>master
2 changed files with 1444 additions and 0 deletions
@ -0,0 +1,439 @@ |
|||
#!/usr/bin/env python3 |
|||
""" |
|||
Master Preset Generator for MIDI Arpeggiator |
|||
|
|||
Creates master preset files with subtle, musical progressions based on a template preset. |
|||
Designed for live performance use - maintains consistent synth count and hardware setup. |
|||
""" |
|||
|
|||
import json |
|||
import copy |
|||
import argparse |
|||
import os |
|||
from datetime import datetime |
|||
from typing import Dict, List, Any, Tuple |
|||
import math |
|||
|
|||
|
|||
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 |
|||
|
|||
def get_compatible_scale(self, current_scale: str) -> str: |
|||
"""Get a scale that works harmonically with current scale""" |
|||
if current_scale in self.scale_relationships: |
|||
compatible = self.scale_relationships[current_scale] |
|||
# Prefer staying in same scale family, but allow gentle changes |
|||
return compatible[0] if len(compatible) > 1 else current_scale |
|||
return current_scale |
|||
|
|||
|
|||
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}, |
|||
} |
|||
|
|||
# Note speeds - ONLY slow to medium speeds for live performance |
|||
self.note_speeds = ["1/1", "1/2", "1/4"] |
|||
|
|||
# Pattern types for variety |
|||
self.pattern_types = ["up", "down", "up_down", "down_up", "chord"] |
|||
|
|||
# Scales for modal journeys |
|||
self.scales = ["major", "minor", "mixolydian", "dorian", "lydian", "phrygian", "pentatonic"] |
|||
|
|||
# Delay timings |
|||
self.delay_timings = ["1/4", "1/2", "1/1", "2/1", "1/4T", "1/2T", "2/1T"] |
|||
|
|||
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 |
|||
|
|||
def apply_subtle_change(self, preset: Dict[str, Any], param_path: str, direction: int = 1, magnitude: float = 1.0) -> None: |
|||
"""Apply a subtle change to a parameter""" |
|||
path_parts = param_path.split('.') |
|||
|
|||
# Navigate to the parameter |
|||
current = preset |
|||
for part in path_parts[:-1]: |
|||
if part in current: |
|||
current = current[part] |
|||
else: |
|||
return |
|||
|
|||
param_name = path_parts[-1] |
|||
if param_name not in current: |
|||
return |
|||
|
|||
current_value = current[param_name] |
|||
|
|||
# Apply change based on parameter type |
|||
if param_name in self.parameter_ranges: |
|||
range_info = self.parameter_ranges[param_name] |
|||
step = range_info["step"] * direction * magnitude |
|||
new_value = current_value + step |
|||
current[param_name] = self.clamp_parameter(param_name, new_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[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 = 25 # +/- 25 BPM |
|||
arp["tempo"] = base_tempo + (tempo_range * factor * 0.6) # Subtle tempo increase |
|||
|
|||
# Velocity progression |
|||
base_velocity = base_preset["arpeggiator"]["velocity"] |
|||
velocity_range = 20 |
|||
arp["velocity"] = int(base_velocity + (velocity_range * factor * 0.8)) |
|||
|
|||
# Gate progression (tighter at peak) |
|||
base_gate = base_preset["arpeggiator"]["gate"] |
|||
gate_change = 0.15 * factor |
|||
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.15 * factor |
|||
arp["swing"] = template.clamp_parameter("swing", base_swing + swing_change) |
|||
|
|||
# Pattern length progression |
|||
base_length = base_preset["arpeggiator"]["user_pattern_length"] |
|||
length_change = int(3 * factor) # Add up to 3 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.2 * factor |
|||
arp["delay_fade"] = template.clamp_parameter("delay_fade", base_fade + fade_change) |
|||
|
|||
# Add some variety every few presets |
|||
if i > 0 and i % 4 == 0: |
|||
# Subtle scale note shift |
|||
current_start = arp.get("scale_note_start", 0) |
|||
new_start = (current_start + 1) % 7 |
|||
arp["scale_note_start"] = new_start |
|||
|
|||
if i > 0 and i % 6 == 0: |
|||
# Gentle pattern type change |
|||
pattern_types = ["up", "down", "up_down"] |
|||
current_pattern = arp.get("pattern_type", "up") |
|||
if current_pattern in pattern_types: |
|||
current_idx = pattern_types.index(current_pattern) |
|||
new_idx = (current_idx + 1) % len(pattern_types) |
|||
arp["pattern_type"] = pattern_types[new_idx] |
|||
|
|||
# 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[Dict[str, Any]]: |
|||
"""Progress through related musical modes""" |
|||
presets = [] |
|||
current_scale = base_preset["arpeggiator"].get("scale", "major") |
|||
|
|||
# Plan the modal journey |
|||
scale_sequence = ["major", "mixolydian", "dorian", "minor", "phrygian", "minor", "dorian", "major"] |
|||
|
|||
for i in range(count): |
|||
preset = copy.deepcopy(base_preset) |
|||
arp = preset["arpeggiator"] |
|||
|
|||
# Progress through scales |
|||
scale_idx = int((i / count) * len(scale_sequence)) |
|||
scale_idx = min(scale_idx, len(scale_sequence) - 1) |
|||
arp["scale"] = scale_sequence[scale_idx] |
|||
|
|||
# Subtle root note movement every few presets |
|||
if i > 0 and i % 3 == 0: |
|||
current_root = arp.get("root_note", 60) |
|||
new_root = musical_logic.get_next_root_note(current_root, "circle_of_fifths") |
|||
# Keep in reasonable range |
|||
if 48 <= new_root <= 72: |
|||
arp["root_note"] = new_root |
|||
|
|||
# Other subtle progressions |
|||
factor = i / (count - 1) if count > 1 else 0 |
|||
|
|||
# Gentle tempo drift |
|||
base_tempo = base_preset["arpeggiator"]["tempo"] |
|||
tempo_drift = math.sin(factor * math.pi * 2) * 8 # +/- 8 BPM sine wave |
|||
arp["tempo"] = base_tempo + tempo_drift |
|||
|
|||
# Pattern length variation |
|||
base_length = base_preset["arpeggiator"]["user_pattern_length"] |
|||
if i % 5 == 0 and i > 0: |
|||
length_options = [base_length, base_length + 1, base_length - 1] |
|||
arp["user_pattern_length"] = template.clamp_parameter("user_pattern_length", |
|||
length_options[i % len(length_options)]) |
|||
|
|||
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 < 80 or tempo > 140: |
|||
errors.append(f"Tempo {tempo} outside live performance range (80-140)") |
|||
|
|||
velocity = arp.get("velocity", 100) |
|||
if velocity < 1 or velocity > 127: |
|||
errors.append(f"Velocity {velocity} outside MIDI range (1-127)") |
|||
|
|||
gate = arp.get("gate", 1.0) |
|||
if gate < 0.1 or gate > 2.0: |
|||
errors.append(f"Gate {gate} outside reasonable range (0.1-2.0)") |
|||
|
|||
# Check note speed is performance-friendly |
|||
note_speed = arp.get("note_speed", "1/4") |
|||
if note_speed not in ["1/1", "1/2", "1/4"]: |
|||
errors.append(f"Note speed {note_speed} too fast for live performance") |
|||
|
|||
return len(errors) == 0, errors |
|||
|
|||
def check_progression_smoothness(self, preset_list: List[Tuple[str, Dict[str, Any]]]) -> List[str]: |
|||
"""Check for jarring jumps between adjacent presets""" |
|||
warnings = [] |
|||
|
|||
for i in range(1, len(preset_list)): |
|||
prev_name, prev_preset = preset_list[i-1] |
|||
curr_name, curr_preset = preset_list[i] |
|||
|
|||
prev_arp = prev_preset["arpeggiator"] |
|||
curr_arp = curr_preset["arpeggiator"] |
|||
|
|||
# Check tempo jumps |
|||
tempo_diff = abs(curr_arp["tempo"] - prev_arp["tempo"]) |
|||
if tempo_diff > 15: |
|||
warnings.append(f"Large tempo jump ({tempo_diff:.1f} BPM) between {prev_name} and {curr_name}") |
|||
|
|||
# Check velocity jumps |
|||
vel_diff = abs(curr_arp["velocity"] - prev_arp["velocity"]) |
|||
if vel_diff > 25: |
|||
warnings.append(f"Large velocity jump ({vel_diff}) between {prev_name} and {curr_name}") |
|||
|
|||
return warnings |
|||
|
|||
|
|||
class MasterPresetGenerator: |
|||
"""Main generator class""" |
|||
|
|||
def __init__(self, base_preset_path: str): |
|||
with open(base_preset_path, 'r') as f: |
|||
self.base_preset = json.load(f) |
|||
|
|||
self.musical_logic = MusicalLogic() |
|||
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""" |
|||
|
|||
print(f"Generating master preset '{name}' with {count} presets using '{strategy}' strategy...") |
|||
|
|||
# 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}") |
|||
|
|||
# Validate all presets |
|||
print("Validating generated presets...") |
|||
all_valid = True |
|||
for preset_name, preset in preset_list: |
|||
valid, errors = self.validator.validate_preset(preset) |
|||
if not valid: |
|||
print(f" WARNING: {preset_name} has validation errors: {errors}") |
|||
all_valid = False |
|||
|
|||
# Check progression smoothness |
|||
warnings = self.validator.check_progression_smoothness(preset_list) |
|||
if warnings: |
|||
print(" Progression warnings:") |
|||
for warning in warnings: |
|||
print(f" - {warning}") |
|||
|
|||
# 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() |
|||
master_preset["presets"][preset_name] = preset |
|||
master_preset["preset_group"]["presets"].append(preset_name) |
|||
|
|||
print(f"✓ Generated {len(preset_list)} presets successfully") |
|||
print(f"✓ Synth count locked at {self.base_preset['channels']['active_synth_count']} channels") |
|||
|
|||
return master_preset |
|||
|
|||
def save_master_preset(self, master_preset: Dict[str, Any], output_path: str): |
|||
"""Save master preset to file""" |
|||
os.makedirs(os.path.dirname(output_path), exist_ok=True) |
|||
|
|||
with open(output_path, 'w') as f: |
|||
json.dump(master_preset, f, indent=2) |
|||
|
|||
print(f"✓ Saved master preset to: {output_path}") |
|||
|
|||
|
|||
def main(): |
|||
parser = argparse.ArgumentParser(description="Generate master preset files for MIDI Arpeggiator") |
|||
parser.add_argument("--base-preset", "-b", required=True, help="Path to base preset JSON file") |
|||
parser.add_argument("--name", "-n", required=True, help="Name for the master preset") |
|||
parser.add_argument("--count", "-c", type=int, default=16, help="Number of presets to generate") |
|||
parser.add_argument("--strategy", "-s", choices=["build_and_release", "modal_journey"], |
|||
default="build_and_release", help="Generation strategy") |
|||
parser.add_argument("--loop-count", "-l", type=int, default=4, help="Loop count for preset group") |
|||
parser.add_argument("--output", "-o", help="Output directory (default: master_files/)") |
|||
|
|||
args = parser.parse_args() |
|||
|
|||
# Set default output path |
|||
if not args.output: |
|||
args.output = "master_files/" |
|||
|
|||
output_path = os.path.join(args.output, f"{args.name}.json") |
|||
|
|||
try: |
|||
# Generate master preset |
|||
generator = MasterPresetGenerator(args.base_preset) |
|||
master_preset = generator.generate_master_preset( |
|||
args.name, args.count, args.strategy, args.loop_count |
|||
) |
|||
|
|||
# Save to file |
|||
generator.save_master_preset(master_preset, output_path) |
|||
|
|||
print(f"\n🎵 Master preset '{args.name}' generated successfully!") |
|||
print(f" Base: {args.base_preset}") |
|||
print(f" Strategy: {args.strategy}") |
|||
print(f" Presets: {args.count}") |
|||
print(f" Output: {output_path}") |
|||
|
|||
except Exception as e: |
|||
print(f"❌ Error generating master preset: {e}") |
|||
return 1 |
|||
|
|||
return 0 |
|||
|
|||
|
|||
if __name__ == "__main__": |
|||
exit(main()) |
|||
1005
master_preset_generator_gui.py
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save
Reference in new issue