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 ROOM_UNITS = 100; // spatial plane is 0..100 x 0..100
const SPATIAL_SCALE = 0.12; // room units -> audio world units 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 FALLBACK_ICE = { iceServers: [{ urls: ['stun:turn.k6n.net:3478', 'stun:stun.l.google.com:19302'] }] };
const LS_NAME = 'voicepoc.nick'; const LS_NAME = 'voicepoc.nick';
const LS_ICE = 'voicepoc.iceOverride'; const LS_ICE = 'voicepoc.iceOverride';
@@ -139,15 +144,39 @@ async function tuneAudioSender(sender) {
if (!sender?.getParameters || !sender?.setParameters) return; if (!sender?.getParameters || !sender?.setParameters) return;
const params = sender.getParameters(); const params = sender.getParameters();
if (!params.encodings?.length) params.encodings = [{}]; if (!params.encodings?.length) params.encodings = [{}];
// 128 kbps keeps Opus in its fullband LD/MD modes with headroom for FEC; // Hi-Fi rooms get 256 kbps stereo (see HIFI_ROOMS); elsewhere 128 kbps
// 64 kbps audibly compresses louder/denser speech. DTX stays off so the // keeps Opus in its fullband LD/MD modes with headroom for FEC — 64 kbps
// channel doesn't pump in and out of low-bitrate comfort noise. // audibly compresses louder/denser speech. DTX stays off so the channel
params.encodings[0].maxBitrate = 128000; // 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].dtx = false;
params.encodings[0].priority = 'high'; params.encodings[0].priority = 'high';
await sender.setParameters(params); 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) { function preferOpus(pc) {
const capabilities = globalThis.RTCRtpReceiver?.getCapabilities?.('audio'); const capabilities = globalThis.RTCRtpReceiver?.getCapabilities?.('audio');
const codecs = capabilities?.codecs || []; const codecs = capabilities?.codecs || [];
@@ -269,12 +298,12 @@ function effectiveIce() {
return S.iceServers || FALLBACK_ICE; 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 = { const audio = {
echoCancellation: true, echoCancellation: !hifi, // hi-fi rooms: raw capture — speakers may feed back, use headphones
noiseSuppression: false, noiseSuppression: false,
autoGainControl: false, autoGainControl: false,
channelCount: 1, channelCount: hifi ? 2 : 1, // ideal, not exact — mono-only mics downgrade gracefully
sampleRate: 48000, sampleRate: 48000,
sampleSize: 16, sampleSize: 16,
}; };
@@ -340,6 +369,9 @@ async function joinVoice(room) {
if (S.room) leaveVoice('switching channel'); if (S.room) leaveVoice('switching channel');
S.connecting = true; S.connecting = true;
setStatus(`connecting to ${room}…`, 'connecting'); 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 // Mic permission & AudioContext: request immediately on the user-click gesture
// so browsers don't block the microphone permission prompt or audio autoplay policy. // 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 }); window.addEventListener('keydown', resumeCtx, { passive: true });
try { 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); S.micStream = await S.audioManager.attachInput(S.rawMicStream);
populateAudioInputs(); populateAudioInputs();
} catch { } catch {
@@ -378,6 +410,7 @@ async function joinVoice(room) {
S.room = room; S.room = room;
S.connecting = false; S.connecting = false;
playRetroCue('join'); playRetroCue('join');
if (hifi) toast('Hi-Fi audio on: stereo, 256 kbps, echo cancellation off — headphones recommended', 'ok');
renderAll(); renderAll();
} }
@@ -679,7 +712,8 @@ function ensurePC(peer, initiator) {
pc.onnegotiationneeded = async () => { pc.onnegotiationneeded = async () => {
try { try {
peer.makingOffer = true; 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 }); send({ type: 'offer', to: peer.id, sdp: pc.localDescription });
} catch (err) { } catch (err) {
console.error('negotiation failed', err); console.error('negotiation failed', err);
@@ -840,7 +874,8 @@ async function onDescription(peer, desc) {
}); });
} }
if (desc.type === 'offer') { 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 }); send({ type: 'answer', to: peer.id, sdp: pc.localDescription });
} }
} }
+2 -2
View File
@@ -50,9 +50,9 @@
general <span class="hint">matrix</span></div> general <span class="hint">matrix</span></div>
<div class="channel-group-label">Voice channels</div> <div class="channel-group-label">Voice channels</div>
<div class="channel voice" data-room="general"><span class="material-symbols-outlined channel-icon">volume_up</span> General <span class="count" data-room-count="general"></span></div> <div class="channel voice" data-room="general"><span class="material-symbols-outlined channel-icon">volume_up</span> General <span class="count" data-room-count="general"></span></div>
<div class="channel voice" data-room="gaming"><span class="material-symbols-outlined channel-icon">volume_up</span> Gaming <span class="count" data-room-count="gaming"></span></div> <div class="channel voice" data-room="gaming"><span class="material-symbols-outlined channel-icon">volume_up</span> Gaming <span class="hifi-badge" title="Hi-Fi voice: stereo Opus, 256 kbps, echo cancellation off">HI-FI</span> <span class="count" data-room-count="gaming"></span></div>
<div class="channel voice" data-room="lounge"><span class="material-symbols-outlined channel-icon">volume_up</span> Lounge <span class="count" data-room-count="lounge"></span></div> <div class="channel voice" data-room="lounge"><span class="material-symbols-outlined channel-icon">volume_up</span> Lounge <span class="count" data-room-count="lounge"></span></div>
<div class="channel voice" data-room="code"><span class="material-symbols-outlined channel-icon">volume_up</span> Code <span class="count" data-room-count="code"></span></div> <div class="channel voice" data-room="code"><span class="material-symbols-outlined channel-icon">volume_up</span> Code <span class="hifi-badge" title="Hi-Fi voice: stereo Opus, 256 kbps, echo cancellation off">HI-FI</span> <span class="count" data-room-count="code"></span></div>
<div class="channel voice" data-room="work"><span class="material-symbols-outlined channel-icon">volume_up</span> Work <span class="count" data-room-count="work"></span></div> <div class="channel voice" data-room="work"><span class="material-symbols-outlined channel-icon">volume_up</span> Work <span class="count" data-room-count="work"></span></div>
<footer id="user-panel"> <footer id="user-panel">
+6
View File
@@ -116,6 +116,12 @@ body.sidebar-collapsed #btn-sidebar-expand { display: grid; }
.channel.text .hint { font-size: 10px; margin-left: 4px; } .channel.text .hint { font-size: 10px; margin-left: 4px; }
.channel.voice.active { background: rgba(88,101,242,.25); color: var(--text-0); } .channel.voice.active { background: rgba(88,101,242,.25); color: var(--text-0); }
.channel .count { margin-left: auto; color: var(--text-2); font-size: 12px; } .channel .count { margin-left: auto; color: var(--text-2); font-size: 12px; }
.hifi-badge {
font-size: 9px; font-weight: 700; letter-spacing: .5px; line-height: 1.5;
color: #23a55a; border: 1px solid rgba(35,165,90,.55); border-radius: 999px;
padding: 0 6px; flex-shrink: 0; user-select: none;
}
.channel.active .hifi-badge { color: #2dc770; border-color: rgba(45,199,112,.8); }
#user-panel { #user-panel {
margin-top: auto; background: var(--bg-0); padding: 8px; margin-top: auto; background: var(--bg-0); padding: 8px;