{"version":3,"file":"voice-activity.cjs","names":[],"sources":["../../src/audio/voice-activity.ts"],"sourcesContent":["import { createLevelMeter } from \"./level-meter\";\n\n/** RMS at which a window counts as speech. */\nconst DEFAULT_THRESHOLD = 0.045;\n\n/**\n * How long the indicator stays on after the level drops.\n *\n * RMS falls to nothing in the gaps between words, so a bare threshold strobes on\n * every one of them. Ported from `tempest-mirror-screen`'s `voiceActivity.ts`,\n * where it was arrived at by watching a real call.\n */\nconst DEFAULT_RELEASE_MS = 350;\n\n/** How often the level is read. */\nconst DEFAULT_POLL_MS = 100;\n\n/** Options for {@link monitorVoiceActivity}. */\nexport interface VoiceActivityOptions {\n    /** RMS at or above which a window counts as speech. Default `0.045`. */\n    threshold?: number;\n    /** How long the state stays `true` after the level drops. Default `350`. */\n    releaseMs?: number;\n    /** How often the level is sampled, in ms. Default `100`. */\n    pollMs?: number;\n    /**\n     * Reuse an existing `AudioContext` instead of creating one.\n     *\n     * A monitor per remote participant hits the browser's cap on live contexts\n     * (Chrome allows around six) in a five-person call. Open one and share it.\n     */\n    context?: AudioContext;\n}\n\n/** A running voice-activity monitor. */\nexport interface VoiceActivityMonitor {\n    /** Whether the track is currently carrying speech. */\n    readonly speaking: boolean;\n    /** Stop sampling and release the analysis graph. */\n    stop: () => void;\n}\n\n/**\n * Report when a track is carrying speech.\n *\n * Runs entirely on the **receiving** side, over a track the connection already\n * delivers, and that is the whole point: a speaking indicator changes several\n * times per second, so signalling it through a server floods the room with\n * messages carrying information every client can derive for free.\n *\n * Two details separate this from a threshold on a level meter:\n *\n * 1. **The release.** RMS drops to nothing in the gaps between words, so a bare\n *    comparison strobes on every syllable. The state only falls after\n *    `releaseMs` of quiet.\n * 2. **`onChange` fires on the flip, not on the sample.** A callback per poll\n *    would re-render a participant list ten times a second to say the same thing.\n *\n * The meter underneath runs with smoothing off: its eased decay plus this release\n * would hold the indicator open long after the speech stopped.\n *\n * @param stream - The stream whose first audio track is measured.\n * @param onChange - Called only when the speaking state actually flips.\n * @param options - See {@link VoiceActivityOptions}.\n * @returns A handle carrying the current state and the teardown.\n *\n * @example\n * const vad = monitorVoiceActivity(remoteStream, (speaking) => setSpeaking(speaking));\n * // later\n * vad.stop();\n */\nexport function monitorVoiceActivity(\n    stream: MediaStream,\n    onChange: (speaking: boolean) => void,\n    {\n        threshold = DEFAULT_THRESHOLD,\n        releaseMs = DEFAULT_RELEASE_MS,\n        pollMs = DEFAULT_POLL_MS,\n        context,\n    }: VoiceActivityOptions = {},\n): VoiceActivityMonitor {\n    const meter = createLevelMeter(stream, { decay: 0, ...(context ? { context } : {}) });\n    let speaking = false;\n    let lastLoudAt = 0;\n\n    const timer = setInterval(() => {\n        const now = performance.now();\n        if (meter.level() >= threshold) lastLoudAt = now;\n        const next = lastLoudAt > 0 && now - lastLoudAt < releaseMs;\n        if (next === speaking) return;\n        speaking = next;\n        onChange(speaking);\n    }, pollMs);\n\n    return {\n        get speaking(): boolean {\n            return speaking;\n        },\n        stop: (): void => {\n            clearInterval(timer);\n            meter.stop();\n        },\n    };\n}\n"],"mappings":"qCAGA,IAAM,EAAoB,KASpB,EAAqB,IAGrB,EAAkB,IAwDxB,SAAgB,EACZ,EACA,EACA,CACI,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,WACsB,CAAC,EACP,CACpB,IAAM,EAAQ,EAAA,iBAAiB,EAAQ,CAAE,MAAO,EAAG,GAAI,EAAU,CAAE,SAAQ,EAAI,CAAC,CAAG,CAAC,EAChF,EAAW,GACX,EAAa,EAEX,EAAQ,gBAAkB,CAC5B,IAAM,EAAM,YAAY,IAAI,EACxB,EAAM,MAAM,GAAK,IAAW,EAAa,GAC7C,IAAM,EAAO,EAAa,GAAK,EAAM,EAAa,EAC9C,IAAS,IACb,EAAW,EACX,EAAS,CAAQ,EACrB,EAAG,CAAM,EAET,MAAO,CACH,IAAI,UAAoB,CACpB,OAAO,CACX,EACA,SAAkB,CACd,cAAc,CAAK,EACnB,EAAM,KAAK,CACf,CACJ,CACJ"}