9cd8292acc
Lint & Validate / lint (push) Has been cancelled
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)
215 lines
7.4 KiB
Python
215 lines
7.4 KiB
Python
#!/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)
|