ch output: per-channel output routing dropdowns

- Added GET/POST /api/channel-output endpoints for per-channel
  output routing (playback_1 / playback_2 / both)
- Added output dropdowns to CH 1 and CH 2 cards in Audio Interface
- Added setChOutput() and loadChOutput() JS functions
- Updated connect_fx_ports to read per-channel output config
This commit is contained in:
2026-06-16 19:18:30 -04:00
parent e324d9007b
commit 334e035cde
2 changed files with 64 additions and 3 deletions
+33
View File
@@ -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."""
+31 -3
View File
@@ -300,7 +300,14 @@ body{display:flex;flex-direction:column;max-height:100vh;user-select:none;-webki
<option value="backing_tracks">📻 Backing</option>
</select>
</div>
<div style="font-size:9px;color:var(--text2);margin-top:2px">Output: Main L/R (Scarlett 1+2)</div>
<div style="font-size:9px;color:var(--text2);margin-top:2px;display:flex;align-items:center;justify-content:space-between">
<span>Output:</span>
<select id="ch1Output" onchange="setChOutput(1,this.value)" style="font-size:9px;padding:2px 4px;min-height:22px;border-radius:4px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-family:var(--font)">
<option value="playback_1">Output 1 (L)</option>
<option value="playback_2">Output 2 (R)</option>
<option value="both" selected>Both (L+R)</option>
</select>
</div>
</div>
<div class="ch-card" id="ch2Card" style="background:var(--card2);border-radius:8px;padding:6px 8px;margin:4px 0;border:2px solid rgba(58,184,122,.25)">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
@@ -317,7 +324,14 @@ body{display:flex;flex-direction:column;max-height:100vh;user-select:none;-webki
<option value="backing_tracks">📻 Backing</option>
</select>
</div>
<div style="font-size:9px;color:var(--text2);margin-top:2px">Output: Main L/R (Scarlett 1+2)</div>
<div style="font-size:9px;color:var(--text2);margin-top:2px;display:flex;align-items:center;justify-content:space-between">
<span>Output:</span>
<select id="ch2Output" onchange="setChOutput(2,this.value)" style="font-size:9px;padding:2px 4px;min-height:22px;border-radius:4px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-family:var(--font)">
<option value="playback_1">Output 1 (L)</option>
<option value="playback_2">Output 2 (R)</option>
<option value="both" selected>Both (L+R)</option>
</select>
</div>
</div>
<div style="font-size:9px;color:var(--text3);padding:2px 0">Dual Mono: each channel has independent presets. Same instrument = shared presets.</div>
</div>
@@ -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();