From 2888a54259414ea0a20db849a2740b530188f540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20T=C3=B6rnberg?= Date: Tue, 22 Sep 2026 17:24:34 +0200 Subject: [PATCH] fix mic meter and distribute participant positions --- public/app.js | 39 ++++++++++++++++----------------------- public/audio-manager.js | 9 +++++++-- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/public/app.js b/public/app.js index 98626ad..d3ff79e 100644 --- a/public/app.js +++ b/public/app.js @@ -589,6 +589,7 @@ function handleSignal(msg) { // deterministic initiator: lower id offers (no glare storm on join) ensurePC(S.peers.get(p.id), S.myId < p.id); } + S.me = chooseOpenParticipantPosition(); sendMyPos(true); renderAll(); setStatus(`voice connected — ${msg.peers.length + 1} in ${msg.room}`, 'online'); @@ -1090,6 +1091,21 @@ function stopScreenShare(stream, silent) { // --------------------------------------------------------------------------- // position sync // --------------------------------------------------------------------------- +function chooseOpenParticipantPosition() { + const occupied = [...S.peers.values()].map((peer) => ({ x: peer.x, y: peer.y })); + const candidates = []; + for (let row = 0; row < 5; row++) { + for (let col = 0; col < 7; col++) { + candidates.push({ x: 15 + col * 11.7, y: 15 + row * 17.5 }); + } + } + if (!occupied.length) return { x: 50, y: 50 }; + return candidates.sort((a, b) => { + const distance = (point) => Math.min(...occupied.map((other) => Math.hypot(point.x - other.x, point.y - other.y))); + return distance(b) - distance(a); + })[0]; +} + let lastPosSend = 0; function sendMyPos(force) { const now = performance.now(); @@ -2166,29 +2182,6 @@ setInterval(() => { } }, 5000); -// Spawn me slightly off-center so people don't land on top of each other. -// A single hashed angle on a *fixed* radius ring (the previous approach) -// puts anyone whose name hashes to a nearby angle almost exactly on top of -// each other too — on a small ring, "nearby angle" is only a few pixels of -// arc, and with just 360 angle buckets that's common (especially with the -// same recurring guest-xxxx test names). Hashing the radius independently -// from the angle means an angle collision no longer implies a full overlap. -// Both stay small enough that at refDistance=4/rolloffFactor=1.4 (see -// updatePanner) nobody starts out attenuated — this is purely about the -// dots being visually distinguishable, not audio range. -(() => { - const key = S.myId || S.name; - let hAngle = 0, hRadius = 0; - for (const c of key) { - hAngle = (hAngle * 33 + c.charCodeAt(0)) >>> 0; - hRadius = (hRadius * 17 + c.charCodeAt(0) + 7) >>> 0; - } - const a = (hAngle % 360) * (Math.PI / 180); - const r = 8 + (hRadius % 12); // 8..19 room units - S.me.x = 50 + r * Math.cos(a); - S.me.y = 50 + r * Math.sin(a); -})(); - window.addEventListener('keydown', (ev) => { if (ev.target.tagName === 'INPUT' || ev.target.tagName === 'TEXTAREA') return; if (ev.key === 'm') toggleMic(); diff --git a/public/audio-manager.js b/public/audio-manager.js index 66c349b..9538067 100644 --- a/public/audio-manager.js +++ b/public/audio-manager.js @@ -6,6 +6,7 @@ export class AudioManager { this.source = null; this.gainNode = null; this.analyser = null; + this.meterSink = null; this.meterData = new Float32Array(1024); this.gain = Number(localStorage.getItem('voicepoc.micGain') || 1); } @@ -25,20 +26,24 @@ export class AudioManager { this.gainNode.gain.value = this.gain; this.analyser = this.context.createAnalyser(); this.analyser.fftSize = 1024; + this.meterSink = this.context.createGain(); + this.meterSink.gain.value = 0; const destination = this.context.createMediaStreamDestination(); this.source.connect(this.gainNode).connect(this.analyser); + // Keep the analyser alive without sending the microphone to the speakers. + this.analyser.connect(this.meterSink).connect(this.context.destination); this.gainNode.connect(destination); this.outputStream = destination.stream; return this.outputStream; } detachInput() { - for (const node of [this.source, this.gainNode, this.analyser]) { + for (const node of [this.source, this.gainNode, this.analyser, this.meterSink]) { try { node?.disconnect(); } catch {} } this.outputStream?.getTracks().forEach((track) => track.stop()); this.rawStream?.getTracks().forEach((track) => track.stop()); - this.source = this.gainNode = this.analyser = null; + this.source = this.gainNode = this.analyser = this.meterSink = null; this.outputStream = this.rawStream = null; }