diff --git a/src/web/server.py b/src/web/server.py index ed96818..110e9da 100644 --- a/src/web/server.py +++ b/src/web/server.py @@ -997,6 +997,39 @@ class WebServer: }) return {"ok": True, "channel_mode": mode} + # ── Per-channel output routing ──────────────────────────── + + @app.get("/api/channel-output") + async def get_channel_output(): + """Get output routing for each channel.""" + audio = self.deps.audio_system + if not audio or not audio.config: + return {"ch1": "both", "ch2": "both"} + cfg = audio.config.__dict__ if hasattr(audio.config, '__dict__') else {} + return { + "ch1": cfg.get("ch1_output", "both"), + "ch2": cfg.get("ch2_output", "both"), + } + + @app.post("/api/channel-output") + async def set_channel_output(data: dict): + """Set output routing for a channel. + + Body: { "ch": 1, "port": "playback_1"|"playback_2"|"both" } + """ + ch = int(data.get("ch", 1)) + port = data.get("port", "both") + if port not in ("playback_1", "playback_2", "both"): + raise HTTPException(status_code=400, detail=f"Invalid port: {port}") + audio = self.deps.audio_system + if audio and audio.config: + key = f"ch{ch}_output" + if hasattr(audio.config, 'input_device'): + setattr(audio.config, key, port) + # Reconnect JACK ports + audio.connect_fx_ports() + return {"ok": True, f"ch{ch}": port} + @app.get("/api/models") async def list_models(channel: str = "guitar"): """List available NAM models for the given channel.""" diff --git a/src/web/ui-v2-dist/index.html b/src/web/ui-v2-dist/index.html index fb40357..e16c3b7 100644 --- a/src/web/ui-v2-dist/index.html +++ b/src/web/ui-v2-dist/index.html @@ -300,7 +300,14 @@ body{display:flex;flex-direction:column;max-height:100vh;user-select:none;-webki -
Output: Main L/R (Scarlett 1+2)
+
+ Output: + +
@@ -317,7 +324,14 @@ body{display:flex;flex-direction:column;max-height:100vh;user-select:none;-webki
-
Output: Main L/R (Scarlett 1+2)
+
+ Output: + +
Dual Mono: each channel has independent presets. Same instrument = shared presets.
@@ -491,6 +505,19 @@ async function setAudioOutputDevice(id){ hideLoading(); toast('Output: '+id); } +async function setChOutput(ch,port){ + try{ + await API._fetch('/api/channel-output',{method:'POST',body:JSON.stringify({ch,port})}); + toast('CH'+ch+': '+port); + }catch(e){toast('Failed: '+e.message);} +} +async function loadChOutput(){ + try{ + const r=await API._fetch('/api/channel-output'); + if(r.ch1){const el=document.getElementById('ch1Output');if(el)el.value=r.ch1;} + if(r.ch2){const el=document.getElementById('ch2Output');if(el)el.value=r.ch2;} + }catch(e){} +} function setChInstrument(idx,val){ const arr=JSON.parse(localStorage.getItem('chInstruments')||'["guitar","bass"]'); arr[idx]=val; @@ -848,7 +875,7 @@ function init(){ btn.onclick=()=>{ const view=btn.dataset.view; if(view==='main'){document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));btn.classList.add('active');} - else if(view==='settings'){openOverlay('Settings');loadNetworkInfo();loadSystemInfo();loadAudioParams();loadAudioDevices();loadHotspotStatus();loadBtStatus();loadBtMidi();return;} + else if(view==='settings'){openOverlay('Settings');loadNetworkInfo();loadSystemInfo();loadAudioParams();loadAudioDevices();loadChOutput();loadHotspotStatus();loadBtStatus();loadBtMidi();return;} else if(view==='presets'){openOverlay('Presets');loadPresets();} else if(view==='snapshots'){openOverlay('Snapshots');loadSnapshots();} else if(view==='downloads')openOverlay('Downloads'); @@ -862,6 +889,7 @@ function init(){ loadSystemInfo(); loadAudioParams(); loadAudioDevices(); + loadChOutput(); loadHotspotStatus(); loadBtStatus(); loadBtMidi();