Files
raspberry-pi-mixer/scripts/carla-preset-manager.py
T
shawn 9cd8292acc
Lint & Validate / lint (push) Has been cancelled
Phase 1-4: Audio stack, mixer engine, MIDI, and network API
P2-R1: ALSA + JACK2 low-latency config (scripts, quirks, tuning)
P2-R2: Carla integration (build scripts, 8ch rack config, NAM LV2 support)
P2-R3: Plugin manager, categories, blacklist, NAM model support
P3-R1: Mixer DSP engine (channel strip, routing matrix, bus mgr, automation)
P4-R1: MIDI engine (learn mode, clock sync, HID discovery, mapping store)
P4-R2: Network API (OSC server, FastAPI REST, WebSocket, auth, rate limiter)
P5-R1: Touchscreen UI evaluation + main entry point
docs: Audio stack, Carla integration, MIDI support, UI evaluation
tests: Full test suite (292 passing)
2026-05-19 20:39:17 -04:00

247 lines
9.5 KiB
Python

#!/usr/bin/env python3
"""
carla-preset-manager — Manage Carla rack presets and plugin programs.
Provides a CLI interface to:
- List available presets
- Save current Carla state as preset
- Load presets into Carla (via OSC or file)
- Manage NAM model switching
- Export/import preset bundles
Usage:
python3 carla-preset-manager.py list
python3 carla-preset-manager.py save <name>
python3 carla-preset-manager.py load <name>
python3 carla-preset-manager.py export <name> <output.tar.gz>
python3 carla-preset-manager.py import <bundle.tar.gz>
python3 carla-preset-manager.py set-model <plugin-name> <model-path>
"""
import json
import os
import shutil
import subprocess
import sys
import tarfile
import time
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
# ── Configuration ──────────────────────────────────────
CARLA_CONFIG_DIR = Path(os.path.expanduser("~/.config/falkTX"))
PRESET_DIR = Path(os.path.expanduser("~/carla-presets")) # User presets
SYSTEM_PRESET_DIR = Path("/home/oplabs/projects/raspberry-pi-mixer/config")
NAM_MODELS_DIR = Path(os.path.expanduser("~/nam-models"))
# Carla OSC control port (configured in Carla settings)
CARLA_OSC_PORT = 22752
os.makedirs(PRESET_DIR, exist_ok=True)
# ── Helper: list presets ───────────────────────────────
def list_presets() -> List[Dict]:
"""List all available presets."""
presets = []
for preset_file in sorted(PRESET_DIR.glob("*.carxp")):
stat = preset_file.stat()
presets.append({
"name": preset_file.stem,
"path": str(preset_file),
"size": preset_file.stat().st_size,
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),
"source": "user"
})
for preset_file in sorted(SYSTEM_PRESET_DIR.glob("*.carxp")):
stat = preset_file.stat()
presets.append({
"name": preset_file.stem,
"path": str(preset_file),
"size": preset_file.stat().st_size,
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),
"source": "system"
})
return presets
def cmd_list():
"""List presets command."""
presets = list_presets()
if not presets:
print("No presets found.")
print(f" System presets: {SYSTEM_PRESET_DIR}")
print(f" User presets: {PRESET_DIR}")
return
print(f"{'NAME':<30} {'SOURCE':<8} {'SIZE':>8} {'MODIFIED'}")
print("-" * 70)
for p in presets:
size_kb = p["size"] / 1024
print(f"{p['name']:<30} {p['source']:<8} {size_kb:>7.1f}K {p['modified'][:16]}")
# ── Helper: save preset ────────────────────────────────
def cmd_save(name: str):
"""Save current Carla state as a preset."""
carla_carxp = CARLA_CONFIG_DIR / "Carla.carxp"
if not carla_carxp.exists():
print(f"ERROR: Carla state file not found at {carla_carxp}", file=sys.stderr)
print("Is Carla running? The state is saved on exit.", file=sys.stderr)
sys.exit(1)
dest = PRESET_DIR / f"{name}.carxp"
shutil.copy2(carla_carxp, dest)
print(f"Saved preset: {dest}")
# Save metadata alongside
meta = {
"name": name,
"saved_at": datetime.now().isoformat(),
"source_file": str(carla_carxp),
}
meta_path = PRESET_DIR / f"{name}.meta.json"
with open(meta_path, "w") as f:
json.dump(meta, f, indent=2)
print(f" Metadata: {meta_path}")
# ── Helper: load preset ────────────────────────────────
def cmd_load(name: str):
"""Load a preset into Carla."""
# Find the preset
preset_path = PRESET_DIR / f"{name}.carxp"
if not preset_path.exists():
preset_path = SYSTEM_PRESET_DIR / f"{name}.carxp"
if not preset_path.exists():
print(f"ERROR: Preset '{name}' not found.", file=sys.stderr)
print(f" Looked in: {PRESET_DIR}, {SYSTEM_PRESET_DIR}", file=sys.stderr)
sys.exit(1)
target = CARLA_CONFIG_DIR / "Carla.carxp"
if target.exists():
backup = CARLA_CONFIG_DIR / f"Carla.carxp.backup-{int(time.time())}"
shutil.copy2(target, backup)
print(f"Backed up current state to: {backup}")
shutil.copy2(preset_path, target)
print(f"Loaded preset: {preset_path}{target}")
print("Restart Carla to apply.")
# ── Helper: export preset bundle ───────────────────────
def cmd_export(name: str, output_path: str):
"""Export a preset and its associated models/IRs as a .tar.gz bundle."""
preset_path = PRESET_DIR / f"{name}.carxp"
if not preset_path.exists():
preset_path = SYSTEM_PRESET_DIR / f"{name}.carxp"
if not preset_path.exists():
print(f"ERROR: Preset '{name}' not found.", file=sys.stderr)
sys.exit(1)
meta_path = PRESET_DIR / f"{name}.meta.json"
with tarfile.open(output_path, "w:gz") as tar:
tar.add(preset_path, arcname=f"{name}/{name}.carxp")
if meta_path.exists():
tar.add(meta_path, arcname=f"{name}/{name}.meta.json")
print(f"Exported preset bundle: {output_path}")
print(f" Contains: {preset_path}")
# ── Helper: import preset bundle ───────────────────────
def cmd_import(bundle_path: str):
"""Import a preset bundle from a .tar.gz file."""
if not os.path.exists(bundle_path):
print(f"ERROR: Bundle not found: {bundle_path}", file=sys.stderr)
sys.exit(1)
bundle_name = os.path.basename(bundle_path).replace(".tar.gz", "").replace(".tgz", "")
with tarfile.open(bundle_path, "r:gz") as tar:
tar.extractall(path=PRESET_DIR)
for member in tar.getmembers():
if member.name.endswith(".carxp"):
# Move to root of preset dir
extracted = PRESET_DIR / member.name
dest = PRESET_DIR / f"{bundle_name}.carxp"
shutil.move(str(extracted), str(dest))
print(f"Imported preset: {dest}")
elif member.name.endswith(".meta.json"):
meta_dest = PRESET_DIR / f"{bundle_name}.meta.json"
extracted = PRESET_DIR / member.name
if extracted.exists():
shutil.move(str(extracted), str(meta_dest))
# ── Helper: set NAM model ──────────────────────────────
def cmd_set_model(plugin_name: str, model_path: str):
"""Set the model for a NAM plugin in Carla via OSC."""
if not os.path.exists(model_path):
print(f"ERROR: Model file not found: {model_path}", file=sys.stderr)
print(f" Download models from: https://www.tone3000.com/", file=sys.stderr)
sys.exit(1)
# Carla OSC API:
# /Carla/set_plugin_parameter <plugin_id> <param_index> <value>
# For NAM, the model param is typically index 0 (atom:Path)
print(f"Setting model: {plugin_name}{model_path}")
print(f" Note: Model changes require Carla restart or manual parameter update.")
print(f" In Carla GUI: right-click {plugin_name} → atom:Path → select {model_path}")
# Try OSC if carla-control is available
osc_cmd = shutil.which("carla-control")
if osc_cmd:
try:
subprocess.run(
[osc_cmd, "--osc-port", str(CARLA_OSC_PORT),
"set_parameter", plugin_name, "0", model_path],
timeout=5
)
print(f" ✓ Model set via OSC.")
except Exception as e:
print(f" Could not set via OSC: {e}")
# ── CLI ────────────────────────────────────────────────
if __name__ == "__main__":
if len(sys.argv) < 2:
if __doc__:
print(__doc__.strip())
print()
cmd_list()
sys.exit(0)
command = sys.argv[1]
try:
if command == "list":
cmd_list()
elif command == "save":
if len(sys.argv) < 3:
print("Usage: carla-preset-manager save <name>", file=sys.stderr)
sys.exit(1)
cmd_save(sys.argv[2])
elif command == "load":
if len(sys.argv) < 3:
print("Usage: carla-preset-manager load <name>", file=sys.stderr)
sys.exit(1)
cmd_load(sys.argv[2])
elif command == "export":
if len(sys.argv) < 4:
print("Usage: carla-preset-manager export <name> <output.tar.gz>", file=sys.stderr)
sys.exit(1)
cmd_export(sys.argv[2], sys.argv[3])
elif command == "import":
if len(sys.argv) < 3:
print("Usage: carla-preset-manager import <bundle.tar.gz>", file=sys.stderr)
sys.exit(1)
cmd_import(sys.argv[2])
elif command == "set-model":
if len(sys.argv) < 4:
print("Usage: carla-preset-manager set-model <plugin-name> <model-path>", file=sys.stderr)
sys.exit(1)
cmd_set_model(sys.argv[2], sys.argv[3])
else:
print(f"Unknown command: {command}", file=sys.stderr)
if __doc__:
print(__doc__.strip(), file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
print("\nCancelled.")
sys.exit(0)