From ce3d40aba89dea3cff370ef15379d4b6ed57ef6b Mon Sep 17 00:00:00 2001 From: developer Date: Wed, 23 Sep 2026 14:34:49 +0000 Subject: [PATCH] fix hifi mode --- public/app.js | 55 ++++++++++++++++++++++++++++++++++++++--------- public/index.html | 4 ++-- public/style.css | 6 ++++++ 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/public/app.js b/public/app.js index b4dfa8c..703cdb9 100644 --- a/public/app.js +++ b/public/app.js @@ -33,6 +33,11 @@ window.addEventListener('unhandledrejection', (event) => { // --------------------------------------------------------------------------- const ROOM_UNITS = 100; // spatial plane is 0..100 x 0..100 const SPATIAL_SCALE = 0.12; // room units -> audio world units +// Hi-Fi rooms: stereo Opus at 256 kbps with echo cancellation disabled — +// for music/listening channels where fidelity beats echo-proofing. Marked +// with a HI-FI badge in the channel list. +const HIFI_ROOMS = new Set(['code', 'gaming']); +function roomIsHifi(room = S.room) { return HIFI_ROOMS.has(room); } const FALLBACK_ICE = { iceServers: [{ urls: ['stun:turn.k6n.net:3478', 'stun:stun.l.google.com:19302'] }] }; const LS_NAME = 'voicepoc.nick'; const LS_ICE = 'voicepoc.iceOverride'; @@ -139,15 +144,39 @@ async function tuneAudioSender(sender) { if (!sender?.getParameters || !sender?.setParameters) return; const params = sender.getParameters(); if (!params.encodings?.length) params.encodings = [{}]; - // 128 kbps keeps Opus in its fullband LD/MD modes with headroom for FEC; - // 64 kbps audibly compresses louder/denser speech. DTX stays off so the - // channel doesn't pump in and out of low-bitrate comfort noise. - params.encodings[0].maxBitrate = 128000; + // Hi-Fi rooms get 256 kbps stereo (see HIFI_ROOMS); elsewhere 128 kbps + // keeps Opus in its fullband LD/MD modes with headroom for FEC — 64 kbps + // audibly compresses louder/denser speech. DTX stays off so the channel + // doesn't pump in and out of low-bitrate comfort noise. + params.encodings[0].maxBitrate = roomIsHifi() ? 256000 : 128000; params.encodings[0].dtx = false; params.encodings[0].priority = 'high'; await sender.setParameters(params); } +// Hi-Fi rooms force Opus stereo and lift the in-band bitrate cap. The sender +// encodings' maxBitrate alone is not enough: Chrome clamps Opus to the SDP's +// maxaveragebandwidth, and stereo only survives the wire when both sides +// declare stereo=1 in fmtp. Applied to our local offers AND answers. +function mungeOpusHifi(sdp) { + const pts = [...sdp.matchAll(/a=rtpmap:(\d+) opus\/48000\/2\r?\n/g)].map((m) => m[1]); + if (!pts.length) return sdp; + const extra = 'stereo=1;sprop-stereo=1;maxaveragebandwidth=256000;maxplaybackrate=48000'; + for (const pt of pts) { + if (new RegExp(`a=fmtp:${pt} `).test(sdp)) { + sdp = sdp.replace(new RegExp(`a=fmtp:${pt} `), `a=fmtp:${pt} ${extra};`); + } else { + sdp = sdp.replace(new RegExp(`(a=rtpmap:${pt} opus\\/48000\\/2\\r?\\n)`), `$1a=fmtp:${pt} ${extra}\r\n`); + } + } + return sdp; +} + +function hifiDescription(desc) { + if (!desc || !roomIsHifi()) return desc; + return { type: desc.type, sdp: mungeOpusHifi(desc.sdp) }; +} + function preferOpus(pc) { const capabilities = globalThis.RTCRtpReceiver?.getCapabilities?.('audio'); const codecs = capabilities?.codecs || []; @@ -269,12 +298,12 @@ function effectiveIce() { return S.iceServers || FALLBACK_ICE; } -function microphoneConstraints(deviceId = localStorage.getItem(LS_AUDIO_INPUT)) { +function microphoneConstraints(deviceId = localStorage.getItem(LS_AUDIO_INPUT), hifi = roomIsHifi()) { const audio = { - echoCancellation: true, + echoCancellation: !hifi, // hi-fi rooms: raw capture — speakers may feed back, use headphones noiseSuppression: false, autoGainControl: false, - channelCount: 1, + channelCount: hifi ? 2 : 1, // ideal, not exact — mono-only mics downgrade gracefully sampleRate: 48000, sampleSize: 16, }; @@ -340,6 +369,9 @@ async function joinVoice(room) { if (S.room) leaveVoice('switching channel'); S.connecting = true; setStatus(`connecting to ${room}…`, 'connecting'); + // Hi-Fi profile must be resolved before capture: S.room still names the + // previous channel here, so the target room is passed explicitly. + const hifi = HIFI_ROOMS.has(room); // Mic permission & AudioContext: request immediately on the user-click gesture // so browsers don't block the microphone permission prompt or audio autoplay policy. @@ -357,7 +389,7 @@ async function joinVoice(room) { window.addEventListener('keydown', resumeCtx, { passive: true }); try { - S.rawMicStream = await navigator.mediaDevices.getUserMedia(microphoneConstraints()); + S.rawMicStream = await navigator.mediaDevices.getUserMedia(microphoneConstraints(undefined, hifi)); S.micStream = await S.audioManager.attachInput(S.rawMicStream); populateAudioInputs(); } catch { @@ -378,6 +410,7 @@ async function joinVoice(room) { S.room = room; S.connecting = false; playRetroCue('join'); + if (hifi) toast('Hi-Fi audio on: stereo, 256 kbps, echo cancellation off — headphones recommended', 'ok'); renderAll(); } @@ -679,7 +712,8 @@ function ensurePC(peer, initiator) { pc.onnegotiationneeded = async () => { try { peer.makingOffer = true; - await pc.setLocalDescription(); + const offer = await pc.createOffer(); + await pc.setLocalDescription(hifiDescription(offer)); send({ type: 'offer', to: peer.id, sdp: pc.localDescription }); } catch (err) { console.error('negotiation failed', err); @@ -840,7 +874,8 @@ async function onDescription(peer, desc) { }); } if (desc.type === 'offer') { - await pc.setLocalDescription(); + const answer = await pc.createAnswer(); + await pc.setLocalDescription(hifiDescription(answer)); send({ type: 'answer', to: peer.id, sdp: pc.localDescription }); } } diff --git a/public/index.html b/public/index.html index 1796bfe..fa0fdf3 100644 --- a/public/index.html +++ b/public/index.html @@ -50,9 +50,9 @@ general matrix
Voice channels
volume_up General
-
volume_up Gaming
+
volume_up Gaming HI-FI
volume_up Lounge
-
volume_up Code
+
volume_up Code HI-FI
volume_up Work