#!/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 python3 carla-preset-manager.py load python3 carla-preset-manager.py export python3 carla-preset-manager.py import python3 carla-preset-manager.py set-model """ 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 # 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 ", 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 ", 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 ", 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 ", 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 ", 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)