Files
raspberry-pi-mixer/scripts/carla-jack-autoconnect
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

185 lines
7.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""
carla-jack-autoconnect — Auto-connect Carla plugins to JACK system ports.
Monitors JACK for new Carla plugin ports and auto-wires them according
to the default mixer routing (carxp preset). Runs as a daemon or one-shot.
Usage:
python3 carla-jack-autoconnect # One-shot: connect existing ports
python3 carla-jack-autoconnect --watch # Daemon: watch for new ports
python3 carla-jack-autoconnect --dry-run # Show what would be connected
python3 carla-jack-autoconnect --disconnect-all # Disconnect all Carla ports
"""
import subprocess
import sys
import time
import re
import json
import os
from typing import Dict, List, Tuple, Set
# ── Routing rules ──────────────────────────────────────
# Format: (source_regex, dest_regex, label)
# Order matters — first match wins for each dest port.
ROUTING_RULES: List[Tuple[str, str, str]] = [
# System inputs → Channel processing chain
(r"system:capture_1$", r".*Ch1.*Gate.*in[12]$", "Capture 1 → Ch1 Gate"),
(r"system:capture_2$", r".*Ch2.*Gate.*in[12]$", "Capture 2 → Ch2 Gate"),
(r"system:capture_3$", r".*Ch3.*NAM.*in1$", "Capture 3 → Guitar NAM"),
(r"system:capture_4$", r".*Ch4.*in[12]$", "Capture 4 → Ch4"),
# Chain outputs → chain inputs (plugin internal routing)
(r".*Ch1.*Gate.*out[12]$", r".*Ch1.*EQ.*in[12]$", "Ch1 Gate → Ch1 EQ"),
(r".*Ch1.*EQ.*out[12]$", r".*Ch1.*Comp.*in[12]$", "Ch1 EQ → Ch1 Comp"),
(r".*Ch2.*Gate.*out[12]$", r".*Ch2.*EQ.*in[12]$", "Ch2 Gate → Ch2 EQ"),
(r".*Ch2.*EQ.*out[12]$", r".*Ch2.*Comp.*in[12]$", "Ch2 EQ → Ch2 Comp"),
(r".*Ch3.*NAM.*out[12]$", r".*Ch3.*IR.*in[12]$", "Ch3 NAM → Ch3 IR Loader"),
# Master output → System playback
(r".*Master.*Limiter.*out1$", r"system:playback_1$", "Master L → Playback 1"),
(r".*Master.*Limiter.*out2$", r"system:playback_2$", "Master R → Playback 2"),
]
def jack_lsp() -> List[str]:
"""Get all JACK ports."""
try:
result = subprocess.run(
["jack_lsp", "-c"],
capture_output=True, text=True, timeout=5
)
return result.stdout.strip().split("\n") if result.stdout.strip() else []
except (FileNotFoundError, subprocess.TimeoutExpired):
print("[autoconnect] ERROR: jack_lsp not found. Is JACK running?", file=sys.stderr)
return []
def jack_connect(source: str, dest: str) -> bool:
"""Connect two JACK ports."""
try:
result = subprocess.run(
["jack_connect", source, dest],
capture_output=True, text=True, timeout=5
)
return result.returncode == 0
except Exception as e:
print(f"[autoconnect] Connect failed: {source}{dest}: {e}", file=sys.stderr)
return False
def jack_disconnect(source: str, dest: str) -> bool:
"""Disconnect two JACK ports."""
try:
result = subprocess.run(
["jack_disconnect", source, dest],
capture_output=True, text=True, timeout=5
)
return result.returncode == 0
except Exception:
return False
def get_current_connections() -> Set[Tuple[str, str]]:
"""Get all current JACK connections."""
try:
result = subprocess.run(
["jack_lsp", "-c"],
capture_output=True, text=True, timeout=5
)
connections = set()
for line in result.stdout.strip().split("\n"):
parts = line.strip().split()
if len(parts) >= 2:
# First token is the output port, subsequent are inputs
source = parts[0]
for dest in parts[1:]:
connections.add((source, dest))
return connections
except Exception:
return set()
def match_port(pattern: str, ports: List[str]) -> List[str]:
"""Return ports matching a regex pattern."""
matched = []
for port in ports:
if re.search(pattern, port):
matched.append(port)
return matched
def apply_routing(dry_run: bool = False, verbose: bool = True) -> Dict:
"""Apply routing rules to JACK ports. Returns stats."""
ports = jack_lsp()
if not ports:
return {"status": "no_jack", "connected": 0, "failed": 0, "skipped": 0}
stats = {"connected": 0, "failed": 0, "skipped": 0, "pairs": []}
for source_regex, dest_regex, label in ROUTING_RULES:
sources = match_port(source_regex, ports)
dests = match_port(dest_regex, ports)
for src in sources:
for dst in dests:
if dry_run:
print(f" [DRY-RUN] {src}{dst} ({label})")
stats["pairs"].append((src, dst, label))
else:
success = jack_connect(src, dst)
if success:
if verbose:
print(f"{src}{dst} ({label})")
stats["connected"] += 1
else:
print(f" ✗ FAILED: {src}{dst}", file=sys.stderr)
stats["failed"] += 1
return stats
def disconnect_all_carla():
"""Disconnect all Carla-related ports."""
ports = jack_lsp()
connections = get_current_connections()
count = 0
for src, dst in connections:
if "Carla" in src or "Carla" in dst:
if jack_disconnect(src, dst):
print(f"{src}{dst}")
count += 1
print(f"\n Disconnected {count} Carla connections.")
return count
def watch_loop(interval: float = 1.0):
"""Watch for new JACK ports and auto-connect them."""
print("[autoconnect] Watching for JACK port changes... (Ctrl+C to stop)")
seen_ports = set(jack_lsp())
while True:
try:
time.sleep(interval)
current_ports = set(jack_lsp())
new_ports = current_ports - seen_ports
if new_ports:
carla_new = {p for p in new_ports if "Carla" in p or "system" in p}
if carla_new:
print(f"\n[autoconnect] New ports detected: {', '.join(sorted(carla_new))}")
apply_routing(dry_run=False, verbose=True)
seen_ports = current_ports
except KeyboardInterrupt:
print("\n[autoconnect] Stopped.")
break
except Exception as e:
print(f"[autoconnect] Error: {e}", file=sys.stderr)
# ── CLI ────────────────────────────────────────────────
if __name__ == "__main__":
if "--watch" in sys.argv:
watch_loop()
elif "--dry-run" in sys.argv:
print("[autoconnect] DRY RUN — no connections made:\n")
apply_routing(dry_run=True)
elif "--disconnect-all" in sys.argv:
disconnect_all_carla()
else:
print("[autoconnect] One-shot port connection:\n")
stats = apply_routing(dry_run=False)
print(f"\n Connected: {stats['connected']} | Failed: {stats['failed']} | Skipped: {stats['skipped']}")