Phase 1-4: Audio stack, mixer engine, MIDI, and network API
Lint & Validate / lint (push) Has been cancelled
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)
This commit is contained in:
Executable
+184
@@ -0,0 +1,184 @@
|
||||
#!/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']}")
|
||||
Reference in New Issue
Block a user