4CM split routing: stereo pipeline + breakpoint + Web UI

Pipeline:
- process() dispatches to _process_mono() or _process_4cm() based on routing_mode
- _process_4cm() splits chain at routing_breakpoint: pre blocks on ch0, post on ch1
- _process_single_block() extracted for reuse in both mono and 4cm paths
- routing_mode/routing_breakpoint load from preset via load_preset()
- set_routing() for runtime configuration
- Properties: routing_mode (mono|4cm), routing_breakpoint with validation

Web server:
- GET /api/routing — current routing mode and breakpoint
- POST /api/routing — set routing mode/breakpoint, persist to current preset, WS broadcast
- _gather_state() includes routing_mode and routing_breakpoint

Web UI:
- Settings page: 4CM toggle + breakpoint slider with routing description
- Dashboard: routing badge (4CM/Mono) with breakpoint info
- app.js: WebSocket handler, updateRoutingUI(), toggle4cm(), set4cmBreakpoint()
- style.css: .badge, .badge-4cm, .badge-mono, .routing-status, .routing-desc

Tests: mock pipeline fixture updated with routing_mode/routing_breakpoint
This commit is contained in:
2026-06-08 10:57:47 -04:00
parent c2071a9724
commit 8ff584cea9
7 changed files with 382 additions and 47 deletions
+49
View File
@@ -356,6 +356,53 @@ class WebServer:
pl._master_volume = vol
return {"ok": True, "volume": vol}
# ── 4CM routing endpoints ────────────────────────────────
@app.get("/api/routing")
async def get_routing():
"""Get current routing mode and breakpoint."""
pl = self.deps.pipeline
if not pl:
raise HTTPException(status_code=503)
return {
"routing_mode": pl.routing_mode,
"routing_breakpoint": pl.routing_breakpoint,
}
@app.post("/api/routing")
async def set_routing(data: dict):
"""Set routing mode and/or breakpoint.
Body:
routing_mode (optional): ``\"mono\"`` or ``\"4cm\"``.
routing_breakpoint (optional): chain index for split.
"""
pl = self.deps.pipeline
pm = self.deps.presets
if not pl:
raise HTTPException(status_code=503)
mode = data.get("routing_mode", pl.routing_mode)
bp = int(data.get("routing_breakpoint", pl.routing_breakpoint))
pl.set_routing(mode, bp)
# Persist to current preset if preset manager is available
if pm:
try:
bank = pm.current_bank
program = pm.current_program
preset = pm.load(bank, program)
pm.set_4cm_config(preset, mode=mode, breakpoint=bp)
except Exception:
pass
await self._manager.broadcast({
"type": "routing_changed",
"routing_mode": pl.routing_mode,
"routing_breakpoint": pl.routing_breakpoint,
})
return {"ok": True, "routing_mode": pl.routing_mode,
"routing_breakpoint": pl.routing_breakpoint}
@app.get("/api/models")
async def list_models():
"""List available NAM models."""
@@ -578,6 +625,8 @@ class WebServer:
"bypass": pl._bypassed if pl else False,
"tuner_enabled": pl._tuner_enabled if pl else False,
"master_volume": pl._master_volume if pl else 0.8,
"routing_mode": pl.routing_mode if pl else "mono",
"routing_breakpoint": pl.routing_breakpoint if pl else 7,
"nam_loaded": bool(nam and nam.is_loaded) if nam else False,
"nam_model": current_model_name,
"ir_loaded": bool(ir and ir.is_loaded) if ir else False,