backing tracks: add source selector + bluealsa in device list
- Audio devices endpoint now includes Bluetooth Audio (bluealsa) and other software PCM devices from aplay -L - Added GET/POST /api/backing-source endpoints with JACK port routing - Backing Track Source dropdown in Audio section (None, Bluetooth Audio, or any JACK capture port found) - Selecting a source routes it into fx_in:input_2 via jack_connect - Also supports bt_audio:capture_1/2 from existing JACK bridge
This commit is contained in:
@@ -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" | "<jack_source>" }
|
||||
"""
|
||||
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": {}}
|
||||
|
||||
@@ -281,6 +281,11 @@ body{display:flex;flex-direction:column;max-height:100vh;user-select:none;-webki
|
||||
<div class="setting-row"><span class="sr-label">🎸 Instrument</span><span class="sr-value" id="instrumentLabel">Guitar</span><button class="sr-action" id="instrumentCycleBtn" style="min-width:44px;padding:4px 10px">Cycle</button></div>
|
||||
<div class="setting-row"><span class="sr-label">🔄 Channel Mode</span><span class="sr-value" id="channelModeLabel">Dual Mono</span><button class="sr-action" id="channelModeToggle">Toggle</button></div>
|
||||
<div class="setting-row"><span class="sr-label">🔗 Routing</span><span class="sr-value" id="routingModeLabel">4CM</span><button class="sr-action" id="routingModeToggle">Toggle</button></div>
|
||||
<div class="setting-row"><span class="sr-label">🎵 Backing Track</span>
|
||||
<select id="backingSource" onchange="setBackingSource(this.value)" style="flex:1;max-width:160px;font-size:10px;padding:4px 6px;min-height:30px;border-radius:6px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-family:var(--font)">
|
||||
<option value="none">None</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-group" id="section-interface"><div class="setting-group-title">🎚 Audio Interface</div>
|
||||
<div class="setting-row"><span class="sr-label">🔌 Input Device</span><select id="audioInputDevice" onchange="setAudioInputDevice(this.value)" style="flex:1;min-width:0;font-size:10px;padding:4px 6px;min-height:30px"><option value="">Loading...</option></select></div>
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user