fix hifi mode
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 26s

This commit is contained in:
developer committed 2026-09-23 14:34:49 +00:00
1 parent 10481ad08c
commit ce3d40aba8
3 files changed
+53 -12

No files matched your search

+45 -10
View File
@@ -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 });
}
}