feat: daybreak finale turns the contact arrival into a sunrise with a dawn chord
This commit is contained in:
88
tests/unit/dawn-resolve.test.ts
Normal file
88
tests/unit/dawn-resolve.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user