import { describe, expect, it } from 'vitest'; import { createDawnResolve, DAWN_CHORD_HZ, DAWN_DETUNE_CENTS, DAWN_OPEN_SECONDS, DAWN_PAD_GAIN, DAWN_RELEASE_SECONDS, DAWN_SWELL_SECONDS, } from '@/audio/dawn-resolve'; import { createFakeAudioHarness, type FakeAudioHarness } from '../helpers/fake-audio-context'; const events = (harness: FakeAudioHarness) => harness.params.flatMap((record) => record.events); describe('dawn resolve', () => { it('preallocates a silent four-note pad, each note a +/-4 cent pair, plus one breath source', () => { const harness = createFakeAudioHarness(); createDawnResolve(harness.context, harness.destination); const oscillators = harness.sources.filter((record) => record.kind === 'oscillator'); const buffers = harness.sources.filter((record) => record.kind === 'buffer'); expect(oscillators).toHaveLength(DAWN_CHORD_HZ.length * 2); expect(buffers).toHaveLength(1); const notes = oscillators.map((record) => { const node = record.node as OscillatorNode; return [node.frequency.value, node.detune.value]; }); for (const frequency of [110, 164.8, 220, 329.6]) { expect(notes).toContainEqual([frequency, -DAWN_DETUNE_CENTS]); expect(notes).toContainEqual([frequency, DAWN_DETUNE_CENTS]); } for (const source of harness.sources) expect(source.starts).toHaveLength(1); // Silent until arrival: no ramp is scheduled at construction. expect(events(harness).filter((event) => event.kind === 'linear' || event.kind === 'exponential')) .toEqual([]); }); it('swells in over 5 s while the low-pass opens 900 -> 2600 Hz over 6 s, without new nodes', () => { const harness = createFakeAudioHarness(); const dawn = createDawnResolve(harness.context, harness.destination); const sourceCount = harness.sources.length; const gainCount = harness.gainNodes.length; harness.setCurrentTime(10); dawn.setDawn(true); const scheduled = events(harness); expect(scheduled).toContainEqual({ kind: 'linear', value: DAWN_PAD_GAIN, at: 10 + DAWN_SWELL_SECONDS }); expect(scheduled).toContainEqual({ kind: 'exponential', value: 2_600, at: 10 + DAWN_OPEN_SECONDS }); expect(DAWN_PAD_GAIN).toBe(0.09); expect(harness.sources).toHaveLength(sourceCount); expect(harness.gainNodes).toHaveLength(gainCount); }); it('releases over 1.6 s when driving away and ignores repeated states', () => { const harness = createFakeAudioHarness(); const dawn = createDawnResolve(harness.context, harness.destination); dawn.setDawn(false); expect(events(harness).filter((event) => event.kind === 'linear')).toEqual([]); harness.setCurrentTime(1); dawn.setDawn(true); const afterOn = events(harness).length; dawn.setDawn(true); expect(events(harness)).toHaveLength(afterOn); harness.setCurrentTime(4); dawn.setDawn(false); // Events are grouped per parameter, so pick the release out by its start time. const release = events(harness).filter((event) => event.at >= 4); expect(release).toContainEqual({ kind: 'linear', value: 0, at: 4 + DAWN_RELEASE_SECONDS }); expect(release).toContainEqual({ kind: 'exponential', value: 900, at: 4 + DAWN_RELEASE_SECONDS }); // Every ramp holds its parameter first, so a release mid-swell never jumps. expect(release.filter((event) => event.kind === 'hold' && event.at === 4)).toHaveLength(4); }); it('stops and disconnects every source exactly once and goes quiet after dispose', () => { const harness = createFakeAudioHarness(); const dawn = createDawnResolve(harness.context, harness.destination); dawn.dispose(); dawn.dispose(); for (const source of harness.sources) { expect(source.stops).toHaveLength(1); expect(source.disconnectCount).toBe(1); } for (const node of harness.gainNodes) { expect((node as GainNode & { disconnectCount: number }).disconnectCount).toBe(1); } const before = events(harness).length; dawn.setDawn(true); expect(events(harness)).toHaveLength(before); }); });