From 60615a2e73d084e6297d8127045f63e52b314791 Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Thu, 28 May 2026 17:38:41 -0400 Subject: [PATCH] =?UTF-8?q?zebra-audio:=20music=20mode=20=E2=80=94=20disab?= =?UTF-8?q?le=20voice=20isolation=20for=20playing=20audio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a "music mode" toggle that re-acquires the mic with echo cancellation, noise suppression, and auto-gain OFF (so music/audio passes through instead of being treated as noise and pumped), tags the track contentHint='music' so the Opus encoder drops speech optimizations (DTX etc.), and raises the send bitrate. Switchable mid-call via replaceTrack — hot-swaps the track with no renegotiation, preserving mute state. Verified mid-call switch stays connected both directions. --- web/zebra-audio.html | 49 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/web/zebra-audio.html b/web/zebra-audio.html index 5d3e2d7..d1c0a33 100644 --- a/web/zebra-audio.html +++ b/web/zebra-audio.html @@ -88,6 +88,9 @@
path: — +
+ +

both partners type the same code and hit call. audio is end-to-end encrypted (DTLS-SRTP), peer-to-peer when the network allows, relayed through @@ -173,14 +176,44 @@ async function refreshTurnCred(){ } /* ---- audio ---- */ -let micStream = null, audioCtx = null; +let micStream = null, audioCtx = null, musicMode = false; +/* voice mode = echo-cancel + noise-suppress + auto-gain (clean speech). + * music mode = all of that OFF so music/audio passes through un-mangled. */ +function micConstraints(){ + return musicMode + ? { echoCancellation:false, noiseSuppression:false, autoGainControl:false } + : { echoCancellation:true, noiseSuppression:true, autoGainControl:true }; +} +function tagTrack(t){ if (t) t.contentHint = musicMode ? 'music' : 'speech'; } +async function setSenderBitrate(sender){ + if (!sender) return; + try { + const p = sender.getParameters(); + if (!p.encodings || !p.encodings.length) p.encodings = [{}]; + p.encodings[0].maxBitrate = musicMode ? 160000 : 40000; /* bits/s */ + await sender.setParameters(p); + } catch(_){} +} async function getMic(){ if (micStream) return micStream; - micStream = await navigator.mediaDevices.getUserMedia({ - audio: { echoCancellation:true, noiseSuppression:true, autoGainControl:true }, video:false - }); + micStream = await navigator.mediaDevices.getUserMedia({ audio: micConstraints(), video:false }); + tagTrack(micStream.getAudioTracks()[0]); return micStream; } +/* switch voice<->music mid-call: re-acquire the mic and hot-swap the track on + * the live connection (no renegotiation), preserving mute state */ +async function applyMicMode(){ + const ns = await navigator.mediaDevices.getUserMedia({ audio: micConstraints(), video:false }); + const nt = ns.getAudioTracks()[0]; + tagTrack(nt); nt.enabled = !muted; + if (pc){ + const sender = pc.getSenders().find(s=>s.track && s.track.kind==='audio') || pc.getSenders()[0]; + if (sender){ try { await sender.replaceTrack(nt); } catch(_){} setSenderBitrate(sender); } + } + if (micStream) micStream.getTracks().forEach(t=>t.stop()); + micStream = ns; + meterFor(micStream, 'meter-mic'); +} function meterFor(stream, fillId){ if (!audioCtx) audioCtx = new (window.AudioContext||window.webkitAudioContext)(); const src = audioCtx.createMediaStreamSource(stream); @@ -214,7 +247,8 @@ function setPath(msg, cls){ const e=$('path-status'); e.textContent=msg; e.class async function ensurePC(){ if (pc) return pc; pc = new RTCPeerConnection(rtcConfig); - for (const tr of micStream.getTracks()) pc.addTrack(tr, micStream); + for (const tr of micStream.getTracks()) { tagTrack(tr); pc.addTrack(tr, micStream); } + setSenderBitrate(pc.getSenders().find(s=>s.track && s.track.kind==='audio')); pc.ontrack = (ev) => { const stream = ev.streams[0] || new MediaStream([ev.track]); $('remote-audio').srcObject = stream; @@ -353,6 +387,11 @@ $('btn-call').addEventListener('click', startCall); $('btn-hangup').addEventListener('click', hangup); $('btn-mute').addEventListener('click', toggleMute); $('rdv-code').addEventListener('keydown', e=>{ if(e.key==='Enter'){ e.preventDefault(); startCall(); } }); +$('music-mode').addEventListener('change', async (e)=>{ + musicMode = e.target.checked; + logLine('', 'mic mode: '+(musicMode?'MUSIC — raw, no voice isolation':'VOICE — echo/noise cancel')); + if (micStream){ try { await applyMicMode(); } catch(err){ logLine('err','mic mode switch failed: '+err.message); } } +}); logLine('', 'ready — type a rendezvous code and call. mic stays muted to the room until connected.'); })();