fix hifi mode
This commit is contained in:
1 parent
10481ad08c
commit
ce3d40aba8
3 files changed
+53
-12
No files matched your search
+45
-10
@@ -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
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user