File size: 2,996 Bytes
921d377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
 * Unit tests for the pure detector in bargeIn.ts. Covers the exact
 * § 6 invariants (single-spike rejection, sustain gate, refractory).
 * The hook form is intentionally not tested here — it's a thin
 * wrapper around the detector + rAF; coverage lives in the overlay
 * integration path.
 */
import { describe, it, expect } from 'vitest';
import { detectBargeIn } from './bargeIn';
const OPTS = {
    threshold: 0.08,
    minSustainMs: 80,
    minRefractoryMs: 400,
};
function genConstant(level, durMs, stepMs = 16) {
    const out = [];
    for (let t = 0; t <= durMs; t += stepMs) {
        out.push({ tMs: t, level });
    }
    return out;
}
describe('detectBargeIn', () => {
    it('fires once after sustained speech above threshold', () => {
        // Steady high level for 200 ms — comfortably over the 80 ms sustain.
        const events = detectBargeIn(genConstant(0.2, 200), OPTS);
        expect(events.length).toBe(1);
        expect(events[0]).toBeGreaterThanOrEqual(OPTS.minSustainMs);
    });
    it('does NOT fire on a single spike below sustain', () => {
        // One 16 ms spike followed by silence. Below 80 ms sustain → drops.
        const samples = [
            { tMs: 0, level: 0.0 },
            { tMs: 16, level: 0.2 },
            { tMs: 32, level: 0.0 },
            { tMs: 48, level: 0.0 },
            { tMs: 64, level: 0.0 },
        ];
        expect(detectBargeIn(samples, OPTS)).toEqual([]);
    });
    it('does NOT fire when level never exceeds threshold', () => {
        const events = detectBargeIn(genConstant(0.04, 400), OPTS);
        expect(events).toEqual([]);
    });
    it('honours the refractory window (no double-fire during sustained speech)', () => {
        // 1500 ms of steady speech. Without refractory we'd fire every
        // tick after 80 ms. With refractory we fire at most once per
        // minRefractoryMs (~400 ms).
        const events = detectBargeIn(genConstant(0.2, 1500), OPTS);
        expect(events.length).toBeGreaterThan(0);
        // At 16 ms steps and 400 ms refractory: expect ~3 events.
        expect(events.length).toBeLessThanOrEqual(4);
        // Consecutive events must be at least refractory-ms apart.
        for (let i = 1; i < events.length; i++) {
            expect(events[i] - events[i - 1]).toBeGreaterThanOrEqual(OPTS.minRefractoryMs);
        }
    });
    it('resets the sustain counter after a dip below threshold', () => {
        // 50 ms above, 30 ms below, 100 ms above. First run is too short;
        // second run also too short on its own because counter reset.
        const samples = [
            ...Array.from({ length: 4 }, (_, i) => ({
                tMs: i * 16, level: 0.2,
            })),
            { tMs: 64, level: 0.0 },
            { tMs: 80, level: 0.0 },
            ...Array.from({ length: 4 }, (_, i) => ({
                tMs: 96 + i * 16, level: 0.2,
            })),
        ];
        expect(detectBargeIn(samples, OPTS)).toEqual([]);
    });
});