commit 4f0392ac29d821ab532922443dff7df0d7a4f1ea Author: ad3laid3 Date: Mon May 4 20:48:57 2026 -0400 Upload files to "/" diff --git a/VCSounds.js b/VCSounds.js new file mode 100644 index 0000000..1785589 --- /dev/null +++ b/VCSounds.js @@ -0,0 +1,152 @@ +(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(); + +})();