mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-01 20:42:50 +08:00
153 lines
4.8 KiB
TypeScript
153 lines
4.8 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { VoiceDuckingController } from "./voice-ducking.js";
|
|
|
|
function makeHarness(
|
|
enabled = true,
|
|
volumePercent = 30,
|
|
timing = { attackMs: 50, holdMs: 100, releaseMs: 200 },
|
|
) {
|
|
let now = 0;
|
|
const setDuckingGain = vi.fn<(gain: number, rampMs?: number) => void>();
|
|
const controller = new VoiceDuckingController(
|
|
{ setDuckingGain },
|
|
{ enabled, volumePercent },
|
|
{ timing, now: () => now },
|
|
);
|
|
|
|
const advance = (milliseconds: number) => {
|
|
now += milliseconds;
|
|
vi.advanceTimersByTime(milliseconds);
|
|
};
|
|
|
|
return { controller, setDuckingGain, advance };
|
|
}
|
|
|
|
describe("VoiceDuckingController", () => {
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("is inert while disabled", () => {
|
|
vi.useFakeTimers();
|
|
const { controller, setDuckingGain, advance } = makeHarness(false);
|
|
|
|
controller.handleVoiceActivity(12);
|
|
advance(1_000);
|
|
|
|
expect(setDuckingGain).not.toHaveBeenCalled();
|
|
expect(controller.isDucking()).toBe(false);
|
|
expect(controller.activeSpeakerCount()).toBe(0);
|
|
});
|
|
|
|
it("attacks once, refreshes the packet deadline, then releases", () => {
|
|
vi.useFakeTimers();
|
|
const { controller, setDuckingGain, advance } = makeHarness();
|
|
|
|
controller.handleVoiceActivity(12);
|
|
expect(setDuckingGain).toHaveBeenCalledWith(0.3, 50);
|
|
|
|
advance(60);
|
|
controller.handleVoiceActivity(12);
|
|
expect(setDuckingGain).toHaveBeenCalledTimes(1);
|
|
|
|
// The original t=100 sweep observes the refreshed t=160 deadline.
|
|
advance(40);
|
|
expect(controller.isDucking()).toBe(true);
|
|
expect(setDuckingGain).toHaveBeenCalledTimes(1);
|
|
|
|
advance(60);
|
|
expect(controller.isDucking()).toBe(false);
|
|
expect(setDuckingGain).toHaveBeenLastCalledWith(1, 200);
|
|
});
|
|
|
|
it("stays ducked until the last overlapping speaker expires", () => {
|
|
vi.useFakeTimers();
|
|
const { controller, setDuckingGain, advance } = makeHarness();
|
|
|
|
controller.handleVoiceActivity(1);
|
|
advance(50);
|
|
controller.handleVoiceActivity(2);
|
|
advance(50);
|
|
|
|
expect(controller.activeSpeakerCount()).toBe(1);
|
|
expect(controller.isDucking()).toBe(true);
|
|
expect(setDuckingGain).toHaveBeenCalledTimes(1);
|
|
|
|
advance(50);
|
|
expect(controller.activeSpeakerCount()).toBe(0);
|
|
expect(setDuckingGain).toHaveBeenLastCalledWith(1, 200);
|
|
});
|
|
|
|
it("removes a client immediately on leave without disturbing other speakers", () => {
|
|
vi.useFakeTimers();
|
|
const { controller, setDuckingGain } = makeHarness();
|
|
|
|
controller.handleVoiceActivity(1);
|
|
controller.handleVoiceActivity(2);
|
|
controller.removeSpeaker(1);
|
|
expect(controller.isDucking()).toBe(true);
|
|
expect(controller.activeSpeakerCount()).toBe(1);
|
|
|
|
controller.removeSpeaker(2);
|
|
expect(controller.isDucking()).toBe(false);
|
|
expect(setDuckingGain).toHaveBeenLastCalledWith(1, 200);
|
|
});
|
|
|
|
it("retargets a live duck and smoothly restores when disabled", () => {
|
|
vi.useFakeTimers();
|
|
const { controller, setDuckingGain } = makeHarness();
|
|
|
|
controller.handleVoiceActivity(7);
|
|
controller.updateSettings({ enabled: true, volumePercent: 45 });
|
|
expect(setDuckingGain).toHaveBeenLastCalledWith(0.45, 50);
|
|
|
|
controller.updateSettings({ enabled: false, volumePercent: 45 });
|
|
expect(controller.isDucking()).toBe(false);
|
|
expect(controller.activeSpeakerCount()).toBe(0);
|
|
expect(setDuckingGain).toHaveBeenLastCalledWith(1, 200);
|
|
});
|
|
|
|
it("attacks again when speech resumes during the release window", () => {
|
|
vi.useFakeTimers();
|
|
const { controller, setDuckingGain, advance } = makeHarness();
|
|
|
|
controller.handleVoiceActivity(7);
|
|
advance(100);
|
|
expect(setDuckingGain).toHaveBeenLastCalledWith(1, 200);
|
|
|
|
advance(50);
|
|
controller.handleVoiceActivity(7);
|
|
expect(controller.isDucking()).toBe(true);
|
|
expect(setDuckingGain).toHaveBeenLastCalledWith(0.3, 50);
|
|
});
|
|
|
|
it("invalidates an old expiry callback after reset", () => {
|
|
vi.useFakeTimers();
|
|
const { controller, setDuckingGain, advance } = makeHarness();
|
|
|
|
controller.handleVoiceActivity(8);
|
|
controller.reset(true);
|
|
const callsAfterReset = setDuckingGain.mock.calls.length;
|
|
advance(1_000);
|
|
|
|
expect(setDuckingGain).toHaveBeenCalledTimes(callsAfterReset);
|
|
expect(setDuckingGain).toHaveBeenLastCalledWith(1, 0);
|
|
});
|
|
|
|
it("rejects invalid client ids and supports an immediate lifecycle reset", () => {
|
|
vi.useFakeTimers();
|
|
const { controller, setDuckingGain } = makeHarness();
|
|
|
|
for (const id of [0, -1, 1.5, Number.NaN]) {
|
|
controller.handleVoiceActivity(id);
|
|
}
|
|
expect(setDuckingGain).not.toHaveBeenCalled();
|
|
|
|
controller.handleVoiceActivity(8);
|
|
controller.reset(true);
|
|
expect(controller.isDucking()).toBe(false);
|
|
expect(controller.activeSpeakerCount()).toBe(0);
|
|
expect(setDuckingGain).toHaveBeenLastCalledWith(1, 0);
|
|
});
|
|
});
|