Files
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

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)