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)
185 lines
7.1 KiB
Python
Executable File
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']}")
|