feat: Add support for inbound "calls" from triggers (#2500)

This commit is contained in:
Dermot Duffy
2026-06-30 17:45:13 -07:00
committed by dermotduffy
parent f4c686c298
commit 15e335a647
39 changed files with 2915 additions and 133 deletions
@@ -0,0 +1,78 @@
// @vitest-environment jsdom
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ChimeTone } from '../../../../src/card-controller/call/tones/chime';
import { useAudioMocks } from './test-utils';
const audio = useAudioMocks();
// Each strike emits a 3-layer bell stack in the order: sparkle (one octave
// above the fundamental), fundamental, hum (one octave below). The frequencies
// here are the per-layer values derived from each strike's fundamental.
describe('ChimeTone', () => {
it('should play DING (Eb5) then DOOOOONG (B4) 0.5s later', () => {
new ChimeTone(0).start();
// 2 strikes × 3 layers = 6 oscillators.
expect(audio.oscillators).toHaveLength(6);
// DING -- Eb5 (622.25Hz) at t=0.
expect(audio.oscillators[0].frequency.value).toBe(622.25 * 2);
expect(audio.oscillators[1].frequency.value).toBe(622.25);
expect(audio.oscillators[2].frequency.value).toBe(622.25 / 2);
expect(audio.oscillators[0].start).toBeCalledWith(0);
expect(audio.oscillators[1].start).toBeCalledWith(0);
expect(audio.oscillators[2].start).toBeCalledWith(0);
// DOOOOONG -- B4 (493.88Hz) at t=0.5.
expect(audio.oscillators[3].frequency.value).toBe(493.88 * 2);
expect(audio.oscillators[4].frequency.value).toBe(493.88);
expect(audio.oscillators[5].frequency.value).toBe(493.88 / 2);
expect(audio.oscillators[3].start).toBeCalledWith(0.5);
expect(audio.oscillators[4].start).toBeCalledWith(0.5);
expect(audio.oscillators[5].start).toBeCalledWith(0.5);
});
it('should give DING a brighter, shorter bell envelope', () => {
new ChimeTone(0).start();
// Sparkle / fundamental / hum peaks for DING.
expect(audio.gainParams[0].linearRampToValueAtTime).toBeCalledWith(0.1, 0.005);
expect(audio.gainParams[1].linearRampToValueAtTime).toBeCalledWith(0.22, 0.005);
expect(audio.gainParams[2].linearRampToValueAtTime).toBeCalledWith(0.08, 0.005);
// Decay constants (sparkle fades fastest, hum lingers).
expect(audio.gainParams[0].setTargetAtTime).toBeCalledWith(0, 0.005, 0.3);
expect(audio.gainParams[1].setTargetAtTime).toBeCalledWith(0, 0.005, 0.8);
expect(audio.gainParams[2].setTargetAtTime).toBeCalledWith(0, 0.005, 1.2);
});
it('should give DOOOOONG a fuller, longer bell envelope', () => {
new ChimeTone(0).start();
expect(audio.gainParams[3].linearRampToValueAtTime).toBeCalledWith(0.11, 0.505);
expect(audio.gainParams[4].linearRampToValueAtTime).toBeCalledWith(0.28, 0.505);
expect(audio.gainParams[5].linearRampToValueAtTime).toBeCalledWith(0.1, 0.505);
expect(audio.gainParams[3].setTargetAtTime).toBeCalledWith(0, 0.505, 0.5);
expect(audio.gainParams[4].setTargetAtTime).toBeCalledWith(0, 0.505, 1.3);
expect(audio.gainParams[5].setTargetAtTime).toBeCalledWith(0, 0.505, 1.8);
});
describe('with fake timers', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should schedule the next iteration 5 seconds after a strike pair', () => {
new ChimeTone(0).start();
expect(audio.oscillators).toHaveLength(6);
vi.advanceTimersByTime(5_000);
// A second iteration ran, producing another 6 oscillators.
expect(audio.oscillators).toHaveLength(12);
});
});
});