zebra-audio: music mode — disable voice isolation for playing audio

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.
This commit is contained in:
Russell Ballestrini 2026-05-28 17:38:41 -04:00
parent d106bd224e
commit 60615a2e73
No known key found for this signature in database

View file

@ -88,6 +88,9 @@
<div class="dot" id="dot-path"></div>
<span id="path-status" class="status-line">path: —</span>
</div>
<div class="row">
<label class="note"><input type="checkbox" id="music-mode"> music mode — raw mic, no echo/noise cancellation (for playing audio through it)</label>
</div>
<p class="note">
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.');
})();
</script>