While the mic is muted LiveKit keeps the capture alive and merely disables the published MediaStreamTrack, so VAD says nothing. lotusMutedSpeech taps a CLONE of the published track (post-denoise when the in-source processor is active), samples RMS at 10 Hz through an AnalyserNode and runs a hysteresis gate (300 ms of voice on, 800 ms of quiet off, threshold ≈ −36 dBFS). The tap only exists while a muted mic publication exists; the flag is set on the local entry of io.lotus.call_state only and is never sent to other participants. Gate is unit-tested; verified end-to-end: muted with a tone mic → true within 2.5 s, silence → false, unmute → false. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
28 lines
896 B
TypeScript
28 lines
896 B
TypeScript
/*
|
|
Copyright 2026 Lotus Guild
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
import { expect, test } from "vitest";
|
|
|
|
import { MutedSpeechGate } from "./lotusMutedSpeech";
|
|
|
|
test("needs 300 ms of voice to switch on and 800 ms of quiet to switch off", () => {
|
|
const g = new MutedSpeechGate(0.015);
|
|
expect(g.push(0.1)).toBe(false);
|
|
expect(g.push(0.1)).toBe(false);
|
|
expect(g.push(0.1)).toBe(true);
|
|
// a short dip does not drop it
|
|
for (let i = 0; i < 7; i++) expect(g.push(0.0)).toBe(true);
|
|
expect(g.push(0.0)).toBe(false);
|
|
// one loud sample after quiet does not re-trigger
|
|
expect(g.push(0.2)).toBe(false);
|
|
});
|
|
|
|
test("keyboard-level noise below the threshold never triggers", () => {
|
|
const g = new MutedSpeechGate(0.015);
|
|
for (let i = 0; i < 50; i++) expect(g.push(0.01)).toBe(false);
|
|
});
|