(function () { if (window.__VC_SOUNDS__) return; window.__VC_SOUNDS__ = true; const ctx = new (window.AudioContext || window.webkitAudioContext)(); document.addEventListener("click", () => { if (ctx.state === "suspended") ctx.resume(); }, { once: true }); // ─── Synth ──────────────────────────────────────────────────────────────────── // Warm, low, soft — layered sine + triangle with a slow attack and long decay. // No harsh transients. Frequencies kept in the 200-500hz range. function playNote(freq, startTime, duration, volume) { // Main body: sine (pure, smooth) const osc1 = ctx.createOscillator(); const gain1 = ctx.createGain(); osc1.type = "sine"; osc1.frequency.value = freq; osc1.connect(gain1); gain1.connect(ctx.destination); // Warmth layer: triangle an octave down (adds body without brightness) const osc2 = ctx.createOscillator(); const gain2 = ctx.createGain(); osc2.type = "triangle"; osc2.frequency.value = freq / 2; osc2.connect(gain2); gain2.connect(ctx.destination); // Soft attack (~30ms), smooth exponential decay gain1.gain.setValueAtTime(0, startTime); gain1.gain.linearRampToValueAtTime(volume, startTime + 0.03); gain1.gain.exponentialRampToValueAtTime(0.001, startTime + duration); gain2.gain.setValueAtTime(0, startTime); gain2.gain.linearRampToValueAtTime(volume * 0.4, startTime + 0.03); gain2.gain.exponentialRampToValueAtTime(0.001, startTime + duration); osc1.start(startTime); osc1.stop(startTime + duration + 0.05); osc2.start(startTime); osc2.stop(startTime + duration + 0.05); } // Join: two soft ascending notes, low register, gentle interval function playJoin() { if (ctx.state === "suspended") ctx.resume(); const t = ctx.currentTime + 0.01; playNote(294, t, 0.35, 0.14); // D4 playNote(370, t + 0.28, 0.45, 0.11); // F#4 } // Leave: same notes descending function playLeave() { if (ctx.state === "suspended") ctx.resume(); const t = ctx.currentTime + 0.01; playNote(370, t, 0.35, 0.14); // F#4 playNote(294, t + 0.28, 0.45, 0.11); // D4 } // ─── State ──────────────────────────────────────────────────────────────────── let inVoice = false; let initialising = false; let initTimer = null; let globalObserver = null; function onSelfJoined() { if (inVoice) return; inVoice = true; initialising = true; playJoin(); console.debug("[VCSounds] self joined"); clearTimeout(initTimer); initTimer = setTimeout(() => { initialising = false; }, 1500); } function onSelfLeft() { if (!inVoice) return; inVoice = false; initialising = false; clearTimeout(initTimer); playLeave(); console.debug("[VCSounds] self left"); } // ─── Participant entry detection ────────────────────────────────────────────── function isParticipantEntry(el) { if (el.nodeType !== 1) return false; const c = el.className || ""; return ( c.includes("p_var(--gap-sm)") && c.includes("pos_relative") && c.includes("d_flex") && c.includes("ai_center") ); } // ─── Global mutation observer ───────────────────────────────────────────────── function startObserver() { if (globalObserver) return; globalObserver = new MutationObserver((mutations) => { if (!inVoice || initialising) return; for (const m of mutations) { for (const node of m.addedNodes) { if (isParticipantEntry(node)) { console.debug("[VCSounds] participant joined"); playJoin(); } } for (const node of m.removedNodes) { if (isParticipantEntry(node)) { console.debug("[VCSounds] participant left"); playLeave(); } } } }); globalObserver.observe(document.body, { childList: true, subtree: true }); } // ─── Fetch hook ─────────────────────────────────────────────────────────────── const originalFetch = window.fetch; window.fetch = async function (...args) { const url = typeof args[0] === "string" ? args[0] : args[0]?.url ?? ""; const response = await originalFetch.apply(this, args); if (url.includes("/join_call") && response.ok) { setTimeout(onSelfJoined, 300); } return response; }; // ─── WebSocket hook ─────────────────────────────────────────────────────────── const OriginalWebSocket = window.WebSocket; window.WebSocket = function (url, protocols) { const ws = protocols ? new OriginalWebSocket(url, protocols) : new OriginalWebSocket(url); if (typeof url === "string" && url.includes("/livekit/rtc")) { ws.addEventListener("close", () => onSelfLeft()); } return ws; }; Object.assign(window.WebSocket, OriginalWebSocket); window.WebSocket.prototype = OriginalWebSocket.prototype; startObserver(); })();