diff --git a/src/web/server.py b/src/web/server.py index 110e9da..ccc65a0 100644 --- a/src/web/server.py +++ b/src/web/server.py @@ -2110,10 +2110,79 @@ iface eth0 inet static if "USB" in line or "usb" in line.lower(): m = re.match(r"\s*(\d+)", line) if m: usb_id = int(m.group(1)) + # Add software PCM devices (bluealsa, etc.) as virtual cards + try: + r3 = subprocess.run(["aplay","-L"], capture_output=True, text=True, timeout=3) + for line in r3.stdout.splitlines(): + name = line.strip() + if name.startswith("bluealsa") or name.startswith("bluealsa:"): + if not any(c.get("name","") == "Bluetooth Audio" for c in cards): + cards.append({"card": -1, "name": "Bluetooth Audio", + "device": 0, "desc": "BlueALSA (BT playback)"}) + except: pass return {"devices": cards, "usb_card": usb_id, "suggested": f"hw:{usb_id or 0},0"} except Exception as e: return {"error": str(e), "devices": []} + # ── Backing track source ──────────────────────────────────── + + @app.get("/api/backing-source") + async def get_backing_source(): + """Get current backing track source and available sources.""" + import subprocess + audio = self.deps.audio_system + cfg = audio.config.__dict__ if audio and hasattr(audio.config, '__dict__') else {} + current = cfg.get("backing_source", "none") + # Check available JACK ports for possible sources + sources = [{"id": "none", "name": "None"}] + try: + r = subprocess.run(["jack_lsp"], capture_output=True, text=True, timeout=3) + for line in r.stdout.splitlines(): + line = line.strip() + if line.endswith(":capture_1") or line.endswith(":capture_2"): + name = line.split(":")[0] + if name != "system" and name != "pi-multifx" and name != "bt_audio": + sources.append({"id": name, "name": f"{name} (capture)"}) + # Always offer bluetooth if JACK has bt_audio ports + r2 = subprocess.run(["jack_lsp","bt_audio:capture_1"], capture_output=True, text=True, timeout=2) + if r2.returncode == 0: + sources.append({"id": "bt_audio", "name": "Bluetooth Audio"}) + except: pass + return {"current": current, "sources": sources} + + @app.post("/api/backing-source") + async def set_backing_source(data: dict): + """Set backing track source and route it into the FX chain. + + Body: { "source": "none" | "bt_audio" | "" } + """ + import subprocess + source = data.get("source", "none") + audio = self.deps.audio_system + # Save to config + if audio and hasattr(audio.config, '__dict__'): + audio.config.__dict__["backing_source"] = source + # Disconnect old backing connections + try: + r = subprocess.run(["jack_lsp","fx_in:input_2"], capture_output=True, text=True, timeout=2) + if r.returncode == 0: + subprocess.run(["jack_disconnect","bt_audio:capture_1","fx_in:input_2"], + capture_output=True, timeout=2) + subprocess.run(["jack_disconnect","bt_audio:capture_2","fx_in:input_2"], + capture_output=True, timeout=2) + except: pass + # Connect new source + if source != "none": + try: + if source == "bt_audio": + subprocess.run(["jack_connect","bt_audio:capture_1","fx_in:input_2"], + capture_output=True, timeout=3) + else: + subprocess.run(["jack_connect",f"{source}:capture_1","fx_in:input_2"], + capture_output=True, timeout=3) + except: pass + return {"ok": True, "source": source} + @app.get("/api/levels") async def get_signal_levels(): levels = {"channels": {}} diff --git a/src/web/ui-v2-dist/index.html b/src/web/ui-v2-dist/index.html index e16c3b7..9d3d2bb 100644 --- a/src/web/ui-v2-dist/index.html +++ b/src/web/ui-v2-dist/index.html @@ -281,6 +281,11 @@ body{display:flex;flex-direction:column;max-height:100vh;user-select:none;-webki
🎸 InstrumentGuitar
🔄 Channel ModeDual Mono
🔗 Routing4CM
+
🎵 Backing Track + +
🎚 Audio Interface
🔌 Input Device
@@ -518,6 +523,27 @@ async function loadChOutput(){ if(r.ch2){const el=document.getElementById('ch2Output');if(el)el.value=r.ch2;} }catch(e){} } +async function loadBackingSource(){ + try{ + const r=await API._fetch('/api/backing-source'); + const sel=document.getElementById('backingSource'); + if(!sel)return; + sel.innerHTML=''; + (r.sources||[]).forEach(s=>{ + const o=document.createElement('option'); + o.value=s.id;o.textContent=s.name; + if(s.id===r.current)o.selected=true; + sel.appendChild(o); + }); + }catch(e){} +} +async function setBackingSource(source){ + if(!source)return; + try{ + await API._fetch('/api/backing-source',{method:'POST',body:JSON.stringify({source})}); + toast('Backing: '+source); + }catch(e){toast('Failed: '+e.message);} +} function setChInstrument(idx,val){ const arr=JSON.parse(localStorage.getItem('chInstruments')||'["guitar","bass"]'); arr[idx]=val; @@ -875,7 +901,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();loadChOutput();loadHotspotStatus();loadBtStatus();loadBtMidi();return;} + else if(view==='settings'){openOverlay('Settings');loadNetworkInfo();loadSystemInfo();loadAudioParams();loadAudioDevices();loadChOutput();loadBackingSource();loadHotspotStatus();loadBtStatus();loadBtMidi();return;} else if(view==='presets'){openOverlay('Presets');loadPresets();} else if(view==='snapshots'){openOverlay('Snapshots');loadSnapshots();} else if(view==='downloads')openOverlay('Downloads'); @@ -890,6 +916,7 @@ function init(){ loadAudioParams(); loadAudioDevices(); loadChOutput(); + loadBackingSource(); loadHotspotStatus(); loadBtStatus(); loadBtMidi();