#!/usr/bin/env python3 """ carla-preset-gen — Generate Carla rack preset (.carxp) for the mixer setup. Creates a default 8-channel mixer rack with: - 2 USB input channels (capture_1/2 → EQ → Comp → Gate) - 1 NAM guitar channel (capture_3 → NAM → IR loader) - 2 stereo aux buses (Reverb, Delay) - Master output to playback_1/2 Usage: python3 scripts/carla-preset-gen.py > config/carla-default.carxp python3 scripts/carla-preset-gen.py --channels 4 > config/carla-4ch.carxp """ import sys import xml.etree.ElementTree as ET from datetime import datetime # JACK port mapping (matches audio-stack-config parent task) # hw:USB is configured with JACK at 48kHz, 128f/3p JACK_SAMPLE_RATE = 48000 JACK_BUFFER_SIZE = 128 def indent_xml(elem, level=0): """Pretty-print XML element tree.""" i = "\n" + level * " " if len(elem): if not elem.text or not elem.text.strip(): elem.text = i + " " if not elem.tail or not elem.tail.strip(): elem.tail = i for subelem in elem: indent_xml(subelem, level + 1) if not elem.tail or not elem.tail.strip(): elem.tail = i else: if level and (not elem.tail or not elem.tail.strip()): elem.tail = i def build_rack_xml(num_channels=8): """Construct a Carla rack preset XML with standard mixer layout.""" root = ET.Element("CARLA-PRESET", version="2.6.0") ET.SubElement(root, "Info", Name="RPi Mixer Default Rack", Author="hermes-kanban") # ── Engine settings ───────────────────────────────── engine = ET.SubElement(root, "Engine") ET.SubElement(engine, "AudioDriver").text = "JACK" ET.SubElement(engine, "ProcessMode").text = "MultipleClients" # each plugin = separate JACK client ET.SubElement(engine, "TransportMode").text = "Disabled" ET.SubElement(engine, "SampleRate").text = str(JACK_SAMPLE_RATE) ET.SubElement(engine, "BufferSize").text = str(JACK_BUFFER_SIZE) ET.SubElement(engine, "ForceStereo").text = "true" # ── Plugin rack ───────────────────────────────────── rack = ET.SubElement(root, "Rack") rack.set("enabled", "true") plugin_id = 0 def add_plugin(name, plugin_type, uri_or_binary, params=None, enabled="true", bypass="false"): nonlocal plugin_id pid = plugin_id plugin_id += 1 plugin = ET.SubElement(rack, "Plugin") plugin.set("id", str(pid)) plugin.set("name", name) plugin.set("type", plugin_type) plugin.set("uri", uri_or_binary) plugin.set("enabled", enabled) plugin.set("bypass", bypass) plugin.set("drywet", "1.0") plugin.set("volume", "0.0") # dB if params: for pname, pval in params.items(): param = ET.SubElement(plugin, "Parameter") param.set("name", pname) param.set("value", str(pval)) return pid # Channel 1 — Mic/Line input with EQ and Comp add_plugin("Ch1 Gate", "LV2", "http://calf.sourceforge.net/plugins/Gate", {"threshold": "-40", "ratio": "4.0", "attack": "10", "release": "100"}) add_plugin("Ch1 EQ", "LV2", "http://calf.sourceforge.net/plugins/Equalizer12Band", {}) add_plugin("Ch1 Comp", "LV2", "http://calf.sourceforge.net/plugins/Compressor", {"threshold": "-20", "ratio": "4.0", "attack": "5", "release": "50", "makeup": "3"}) # Channel 2 — Second input add_plugin("Ch2 Gate", "LV2", "http://calf.sourceforge.net/plugins/Gate", {"threshold": "-40", "ratio": "4.0"}) add_plugin("Ch2 EQ", "LV2", "http://calf.sourceforge.net/plugins/Equalizer8Band", {}) add_plugin("Ch2 Comp", "LV2", "http://calf.sourceforge.net/plugins/Compressor", {"threshold": "-20", "ratio": "4.0"}) # Channel 3 — Guitar / NAM channel add_plugin("Ch3 NAM", "LV2", "http://github.com/mikeoliphant/neural-amp-modeler-lv2", {"model": "/home/pi/nam-models/default.nam"}) add_plugin("Ch3 IR Loader", "LV2", "http://guitarix.org/plugins/gx_ampmodeller", {}) # AUX 1 — Reverb add_plugin("AUX1 Hall Reverb", "LV2", "http://calf.sourceforge.net/plugins/Reverb", {"decay_time": "2.5", "dry/wet": "0.3", "room_size": "0.7"}) # AUX 2 — Delay add_plugin("AUX2 Delay", "LV2", "http://calf.sourceforge.net/plugins/VintageDelay", {"time_l": "250", "time_r": "375", "feedback": "0.4", "dry/wet": "0.25"}) # Master chain — Limiter add_plugin("Master Limiter", "LV2", "http://calf.sourceforge.net/plugins/Limiter", {"limit": "-1.0", "threshold": "-3.0", "release": "20"}) return root def build_patchbay_xml(): """Construct patchbay connection rules for JACK auto-routing.""" root = ET.Element("CARLA-PATCHBAY", version="2.6.0") # Connection rules: source → destination with optional regex rules = [ # System captures → Channel inputs ("system:capture_1", "Carla:Ch1_Gate_in1", "Capture 1 → Ch1 Gate"), ("system:capture_2", "Carla:Ch2_Gate_in1", "Capture 2 → Ch2 Gate"), ("system:capture_3", "Carla:Ch3_NAM_in1", "Capture 3 → Guitar NAM"), # Master stereo output → system playback ("Carla:Master_Limiter_out1", "system:playback_1", "Master L → Playback 1"), ("Carla:Master_Limiter_out2", "system:playback_2", "Master R → Playback 2"), ] for idx, (source, dest, label) in enumerate(rules): conn = ET.SubElement(root, "Connection") conn.set("id", str(idx)) conn.set("source", source) conn.set("dest", dest) conn.set("label", label) conn.set("enabled", "true") return root # ── CLI interface ────────────────────────────────────── if __name__ == "__main__": num_channels = 8 mode = "rack" args = sys.argv[1:] i = 0 while i < len(args): if args[i] == "--channels" and i + 1 < len(args): num_channels = int(args[i + 1]) i += 2 elif args[i] == "--patchbay": mode = "patchbay" i += 1 else: print(f"Unknown arg: {args[i]}", file=sys.stderr) sys.exit(1) if mode == "patchbay": xml = build_patchbay_xml() else: xml = build_rack_xml(num_channels) indent_xml(xml) ET.dump(xml) # Also write to config/ directory import os script_dir = os.path.dirname(os.path.abspath(__file__)) config_dir = os.path.join(os.path.dirname(script_dir), "config") os.makedirs(config_dir, exist_ok=True) if mode == "patchbay": out_path = os.path.join(config_dir, "carla-patchbay.xml") else: out_path = os.path.join(config_dir, f"carla-{num_channels}ch-default.carxp") tree = ET.ElementTree(xml) indent_xml(xml) tree.write(out_path, encoding="utf-8", xml_declaration=True) print(f"\n[preset-gen] Written to {out_path}", file=sys.stderr)