79 lines
3.2 KiB
TypeScript
79 lines
3.2 KiB
TypeScript
import { readFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
import { createPulseTap, readSwitchboard, setMusicPulseAudible } from '@/audio/music-pulse';
|
|
import { TRAFFIC_PASS_GAP_SECONDS, TRAFFIC_PASS_PAN, playTrafficPass } from '@/audio/traffic-pass';
|
|
|
|
/** A minimal audio graph that records what the swell builds. */
|
|
function fakeContext() {
|
|
const pans: number[] = [];
|
|
const param = () => ({ setValueAtTime: (v: number) => pans.push(v), exponentialRampToValueAtTime: () => undefined });
|
|
const node = (extra: object = {}) => ({ connect: () => undefined, disconnect: () => undefined, ...extra });
|
|
const context = {
|
|
state: 'running',
|
|
currentTime: 10,
|
|
sampleRate: 8000,
|
|
started: 0,
|
|
createGain: () => node({ context, gain: { setValueAtTime: () => undefined, exponentialRampToValueAtTime: () => undefined } }),
|
|
createAnalyser: () => {
|
|
throw new Error('no analyser');
|
|
},
|
|
createBuffer: (_channels: number, length: number) => ({ sampleRate: 8000, getChannelData: () => new Float32Array(length) }),
|
|
createBufferSource: () =>
|
|
node({
|
|
buffer: null,
|
|
onended: null,
|
|
start: () => {
|
|
context.started += 1;
|
|
},
|
|
stop: () => undefined,
|
|
}),
|
|
createBiquadFilter: () =>
|
|
node({
|
|
type: '',
|
|
Q: { setValueAtTime: () => undefined },
|
|
frequency: { setValueAtTime: () => undefined, exponentialRampToValueAtTime: () => undefined },
|
|
}),
|
|
createStereoPanner: () => node({ pan: param() }),
|
|
};
|
|
return { context, pans };
|
|
}
|
|
|
|
describe('traffic pass swell', () => {
|
|
afterEach(() => setMusicPulseAudible(false));
|
|
|
|
it('is silent with no audible soundtrack', () => {
|
|
expect(readSwitchboard()).toBeNull();
|
|
expect(playTrafficPass(1)).toBe(false);
|
|
});
|
|
|
|
it('plays once per gap, panned to the passing side', () => {
|
|
const { context, pans } = fakeContext();
|
|
createPulseTap(context as unknown as BaseAudioContext, { connect: () => undefined } as unknown as AudioNode);
|
|
setMusicPulseAudible(true);
|
|
expect(playTrafficPass(-1)).toBe(true);
|
|
expect(pans).toContain(-TRAFFIC_PASS_PAN);
|
|
expect(playTrafficPass(1)).toBe(false);
|
|
context.currentTime += TRAFFIC_PASS_GAP_SECONDS + 0.01;
|
|
expect(playTrafficPass(1)).toBe(true);
|
|
expect(pans).toContain(TRAFFIC_PASS_PAN);
|
|
expect(context.started).toBe(2);
|
|
});
|
|
|
|
it('never plays on a suspended context', () => {
|
|
const { context } = fakeContext();
|
|
context.state = 'suspended';
|
|
createPulseTap(context as unknown as BaseAudioContext, { connect: () => undefined } as unknown as AudioNode);
|
|
setMusicPulseAudible(true);
|
|
expect(playTrafficPass(1)).toBe(false);
|
|
});
|
|
|
|
it('stays out of the initial bundle: only the scene chunk imports it, lazily, never on lite', () => {
|
|
const environment = readFileSync(resolve(process.cwd(), 'src/scene/world/environment.ts'), 'utf8');
|
|
expect(environment).toContain("import('@/audio/traffic-pass')");
|
|
expect(environment).not.toMatch(/from '@\/audio\/traffic-pass'/u);
|
|
const budgets = readFileSync(resolve(process.cwd(), 'scripts/lib/bundle-budgets.mjs'), 'utf8');
|
|
expect(budgets).toContain('traffic-pass-');
|
|
});
|
|
});
|