{"version":3,"file":"noise-floor.cjs","names":[],"sources":["../../src/audio/noise-floor.ts"],"sourcesContent":["import { windowRms } from \"./voice-chain-stages\";\n\n/** How long the measurement listens by default, in milliseconds. */\nconst DEFAULT_DURATION_MS = 3000;\n\n/** How often it samples the room, in milliseconds. */\nconst SAMPLE_MS = 50;\n\n/**\n * Headroom over the measured floor, so ordinary speech clears the gate.\n *\n * A gate set exactly at the noise level chatters, opening and closing on the\n * room's own variation. Ported from `tempest-mirror-screen`'s `voiceChain.ts`,\n * where it was arrived at by listening.\n */\nconst GATE_MARGIN = 2.4;\n\n/** The narrowest and widest thresholds worth suggesting. */\nconst GATE_MIN = 0.004;\nconst GATE_MAX = 0.12;\n\n/** Options for {@link measureNoiseFloor}. */\nexport interface NoiseFloorOptions {\n    /** Called with `0..1` so a dialog can show the wait. */\n    onProgress?: (fraction: number) => void;\n    /** How long to listen. Default `3000`. */\n    durationMs?: number;\n    /** Reuse an existing `AudioContext` rather than opening one. */\n    context?: AudioContext;\n}\n\n/** The `AudioContext` constructor this engine exposes, prefixed or not. */\nfunction audioContextConstructor(): typeof AudioContext | undefined {\n    if (typeof AudioContext !== \"undefined\") return AudioContext;\n    return (globalThis as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n}\n\n/**\n * Listen to a room at rest and report how loud its silence is.\n *\n * The number a person cannot guess. Setting a gate by ear means switching it on\n * and hearing what disappeared, which only works if you already know what should\n * have disappeared — so the usual outcome is leaving it at a default that is\n * wrong for that room. Measuring replaces the guess.\n *\n * The result is the **peak** RMS window observed, not the average: a threshold\n * under the loudest thing the quiet room did will be opened by that thing.\n *\n * @param stream - A live capture of the room while nobody speaks.\n * @param options - See {@link NoiseFloorOptions}.\n * @returns The peak RMS observed, `0..1`. `0` when this engine has no Web Audio\n *   or the stream carries no audio track — which {@link suggestGateThreshold}\n *   turns into its own floor rather than into a gate that never opens.\n *\n * @example\n * const floor = await measureNoiseFloor(stream, { onProgress: setProgress });\n * setThreshold(suggestGateThreshold(floor));\n */\nexport async function measureNoiseFloor(\n    stream: MediaStream,\n    {\n        onProgress,\n        durationMs = DEFAULT_DURATION_MS,\n        context: injectedContext,\n    }: NoiseFloorOptions = {},\n): Promise<number> {\n    const Ctor = audioContextConstructor();\n    const context = injectedContext ?? (Ctor ? new Ctor() : null);\n    const track = stream.getAudioTracks()[0];\n    if (!context || !track) return 0;\n\n    const source = context.createMediaStreamSource(stream);\n    const analyser = context.createAnalyser();\n    analyser.fftSize = 2048;\n    source.connect(analyser);\n\n    const buffer = new Float32Array(analyser.fftSize);\n    const started = performance.now();\n    let peak = 0;\n\n    await new Promise<void>((resolve) => {\n        const timer = setInterval(() => {\n            analyser.getFloatTimeDomainData(buffer);\n            peak = Math.max(peak, windowRms(buffer));\n\n            const elapsed = performance.now() - started;\n            onProgress?.(Math.min(1, elapsed / durationMs));\n            if (elapsed < durationMs) return;\n            clearInterval(timer);\n            resolve();\n        }, SAMPLE_MS);\n    });\n\n    source.disconnect();\n    analyser.disconnect();\n    return peak;\n}\n\n/**\n * Turn a measured noise floor into a threshold worth using.\n *\n * Sits above the floor by a margin rather than on it, and is clamped at both\n * ends: a room measured as perfectly silent (or not measured at all) would\n * otherwise get a threshold of zero, which is a gate that never closes, and a\n * room measured while somebody talked would get one nothing opens.\n *\n * @param noiseFloor - Peak RMS measured while nobody spoke.\n * @returns A threshold for {@link VoiceChainSettings.gateThreshold}, `0.004..0.12`.\n */\nexport function suggestGateThreshold(noiseFloor: number): number {\n    if (!Number.isFinite(noiseFloor)) return GATE_MIN;\n    return Math.min(GATE_MAX, Math.max(GATE_MIN, noiseFloor * GATE_MARGIN));\n}\n"],"mappings":"4CAGA,IAAM,EAAsB,IAGtB,EAAY,GASZ,EAAc,IAGd,EAAW,KACX,EAAW,IAajB,SAAS,GAA2D,CAEhE,OADI,OAAO,aAAiB,IAAoB,aACxC,WAA4D,kBACxE,CAuBA,eAAsB,EAClB,EACA,CACI,aACA,aAAa,EACb,QAAS,GACU,CAAC,EACT,CACf,IAAM,EAAO,EAAwB,EAC/B,EAAU,IAAoB,EAAO,IAAI,EAAS,MAClD,EAAQ,EAAO,eAAe,CAAC,CAAC,GACtC,GAAI,CAAC,GAAW,CAAC,EAAO,MAAO,GAE/B,IAAM,EAAS,EAAQ,wBAAwB,CAAM,EAC/C,EAAW,EAAQ,eAAe,EACxC,EAAS,QAAU,KACnB,EAAO,QAAQ,CAAQ,EAEvB,IAAM,EAAS,IAAI,aAAa,EAAS,OAAO,EAC1C,EAAU,YAAY,IAAI,EAC5B,EAAO,EAiBX,OAfA,MAAM,IAAI,QAAe,GAAY,CACjC,IAAM,EAAQ,gBAAkB,CAC5B,EAAS,uBAAuB,CAAM,EACtC,EAAO,KAAK,IAAI,EAAM,EAAA,UAAU,CAAM,CAAC,EAEvC,IAAM,EAAU,YAAY,IAAI,EAAI,EACpC,IAAa,KAAK,IAAI,EAAG,EAAU,CAAU,CAAC,EAC1C,IAAU,KACd,cAAc,CAAK,EACnB,EAAQ,EACZ,EAAG,CAAS,CAChB,CAAC,EAED,EAAO,WAAW,EAClB,EAAS,WAAW,EACb,CACX,CAaA,SAAgB,EAAqB,EAA4B,CAE7D,OADK,OAAO,SAAS,CAAU,EACxB,KAAK,IAAI,EAAU,KAAK,IAAI,EAAU,EAAa,CAAW,CAAC,EAD7B,CAE7C"}