Upload files to "/"
This commit is contained in:
commit
4f0392ac29
1 changed files with 152 additions and 0 deletions
152
VCSounds.js
Normal file
152
VCSounds.js
Normal file
|
|
@ -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();
|
||||
|
||||
})();
|
||||
Loading…
Add table
Reference in a new issue