import { describe, expect, it, vi } from 'vitest'; import { createSoundtrackEngine } from '@/audio/soundtrack-engine'; import { resolveChapterIntent } from '@/audio/journey-music-state'; import { AFTERGLOW_LOOP_END_SECONDS, AFTERGLOW_LOOP_START_SECONDS, DRIVE_LIFT_LOOP_SECONDS, INTRO_RUNTIME_END_SECONDS, MAIN_DURATION_SECONDS, mainPhraseOffsetSeconds, phaseAlignedLoopOffset, } from '@/audio/soundtrack-transport'; import type { AdaptiveMixer } from '@/audio/adaptive-mixer'; import type { DriveTexture } from '@/audio/drive-texture'; import type { AudioInitialState, SoundtrackSources, } from '@/audio/types'; import { createFakeAudioHarness, type FakeAudioHarness, type FakeSourceRecord, } from '../helpers/fake-audio-context'; const sources: SoundtrackSources = { intro: [{ url: '/intro.opus', mediaType: 'audio/ogg' }], main: [{ url: '/main.opus', mediaType: 'audio/ogg' }], afterglow: [{ url: '/afterglow.opus', mediaType: 'audio/ogg' }], driveLift: [{ url: '/drive-lift.opus', mediaType: 'audio/ogg' }], }; const initialState: AudioInitialState = { chapter: resolveChapterIntent('start'), drive: { energy: 0, source: 'idle' }, }; interface EngineHarnessOptions { readonly rejectIntro?: boolean; readonly rejectMain?: boolean; readonly rejectAfterglow?: boolean; readonly rejectDriveLift?: boolean; readonly rejectDriveTexture?: boolean; readonly deferDriveLift?: boolean; } interface Deferred { readonly promise: Promise; resolve(value: T): void; } function deferred(): Deferred { let resolve!: (value: T) => void; const promise = new Promise((settle) => { resolve = settle; }); return { promise, resolve }; } function createEngineHarness( state: AudioInitialState = initialState, options: EngineHarnessOptions = {}, ) { const harness = createFakeAudioHarness(); const buffers = new Map([ ['/intro.opus', harness.decodedBuffer(INTRO_RUNTIME_END_SECONDS)], ['/main.opus', harness.decodedBuffer(MAIN_DURATION_SECONDS)], ['/afterglow.opus', harness.decodedBuffer(AFTERGLOW_LOOP_END_SECONDS)], ['/drive-lift.opus', harness.decodedBuffer(DRIVE_LIFT_LOOP_SECONDS)], ]); const pendingDriveLift = deferred(); const loadCalls: string[] = []; const loadPromises: Promise[] = []; const bedInput = harness.context.createGain(); const driveLiftInput = harness.context.createGain(); const driveTextureInput = harness.context.createGain(); const mixer = { bedInput, driveLiftInput, driveTextureInput, setChapterIntent: vi.fn(), setDriveEnergy: vi.fn(), setTransportAnchor: vi.fn(), setDriveLiftAvailableAt: vi.fn(), dispose: vi.fn(), } satisfies AdaptiveMixer; const texture = { setEnergy: vi.fn(), dispose: vi.fn(), } satisfies DriveTexture; const createMixer = vi.fn(() => mixer); const createDriveTexture = vi.fn(() => { if (options.rejectDriveTexture) throw new Error('drive texture unavailable'); return texture; }); const loadBuffer = vi.fn(( _context: BaseAudioContext, variants: SoundtrackSources['intro'], ): Promise => { const url = variants[0]?.url; if (url === undefined) return Promise.reject(new Error('missing source')); loadCalls.push(url); const reject = ( (url === '/intro.opus' && options.rejectIntro) || (url === '/main.opus' && options.rejectMain) || (url === '/afterglow.opus' && options.rejectAfterglow) || (url === '/drive-lift.opus' && options.rejectDriveLift) ); const buffer = buffers.get(url); let promise: Promise; if (reject || buffer === undefined) promise = Promise.reject(new Error(`${url} unavailable`)); else if (url === '/drive-lift.opus' && options.deferDriveLift) promise = pendingDriveLift.promise; else promise = Promise.resolve(buffer); loadPromises.push(promise); return promise; }); const graph = createSoundtrackEngine(sources, state, { createContext: () => harness.context, loadBuffer, createMixer, createDriveTexture, }); const sourcesFor = (url: string): FakeSourceRecord[] => { const buffer = buffers.get(url); return harness.sources.filter((record) => ( (record.node as AudioBufferSourceNode).buffer === buffer )); }; const sourceFor = (url: string): FakeSourceRecord => { const record = sourcesFor(url).at(-1); if (record === undefined) throw new Error(`Missing source for ${url}.`); return record; }; const markers = (): FakeSourceRecord[] => harness.sources.filter((record) => { const buffer = (record.node as AudioBufferSourceNode).buffer; return record.kind === 'buffer' && buffer !== null && ![...buffers.values()].includes(buffer); }); const flushOptionalLoads = async (): Promise => { await Promise.allSettled([...loadPromises]); await Promise.resolve(); await Promise.resolve(); }; return { graph, harness, mixer, texture, loadCalls, sourcesFor, sourceFor, markers, flushOptionalLoads, resolveDriveLift() { const buffer = buffers.get('/drive-lift.opus'); if (buffer === undefined) throw new Error('Missing drive lift buffer.'); pendingDriveLift.resolve(buffer); }, }; } describe('soundtrack engine', () => { it('cancels a Start departure before its audible bar boundary', async () => { const test = createEngineHarness(); await test.graph.resume(); test.graph.setChapterIntent(resolveChapterIntent('profile', 1)); const pendingMain = test.sourcesFor('/main.opus').at(-1)!; const pendingOutro = test.sourcesFor('/intro.opus').at(-1)!; expect(pendingMain.starts).toHaveLength(1); expect(pendingOutro.starts).toHaveLength(1); test.graph.setChapterIntent(resolveChapterIntent('start', 2)); expect(pendingMain.stops).toHaveLength(1); expect(pendingOutro.stops).toHaveLength(1); expect(test.mixer.setTransportAnchor).not.toHaveBeenCalledWith(expect.any(Number)); }); it('reuses one pending Start departure across rapid non-Start moods', async () => { const test = createEngineHarness(); await test.graph.resume(); test.graph.setChapterIntent(resolveChapterIntent('profile', 1)); const mainCount = test.sourcesFor('/main.opus').length; const introCount = test.sourcesFor('/intro.opus').length; test.graph.setChapterIntent(resolveChapterIntent('experience', 2)); test.graph.setChapterIntent(resolveChapterIntent('toolkit', 3)); expect(test.sourcesFor('/main.opus')).toHaveLength(mainCount); expect(test.sourcesFor('/intro.opus')).toHaveLength(introCount); expect(test.mixer.setChapterIntent).toHaveBeenLastCalledWith( resolveChapterIntent('toolkit', 3), ); }); it('keeps the main anchor unchanged across every mood command', async () => { const test = createEngineHarness({ chapter: resolveChapterIntent('experience'), drive: { energy: 0, source: 'idle' }, }); await test.graph.resume(); const main = test.sourceFor('/main.opus'); const firstAnchor = test.mixer.setTransportAnchor.mock.calls.find( ([anchor]) => typeof anchor === 'number', )?.[0]; test.graph.setChapterIntent(resolveChapterIntent('contact', 1)); test.graph.setChapterIntent(resolveChapterIntent('start', 2)); expect(test.mixer.setTransportAnchor.mock.calls.filter( ([anchor]) => typeof anchor === 'number', ).map(([anchor]) => anchor)).toEqual([firstAnchor]); expect(test.sourcesFor('/main.opus')).toEqual([main]); expect(test.harness.params.find( ({ param }) => param === (main.node as AudioBufferSourceNode).playbackRate, )?.events).toEqual([]); }); it('loads afterglow only once the intro path main source has begun', async () => { const test = createEngineHarness(); await test.graph.resume(); expect(test.loadCalls).toEqual(expect.arrayContaining(['/intro.opus', '/main.opus', '/drive-lift.opus'])); expect(test.loadCalls).not.toContain('/afterglow.opus'); test.graph.setChapterIntent(resolveChapterIntent('profile', 1)); const [commitMarker, mainStartMarker] = test.markers(); expect(commitMarker).toBeDefined(); expect(mainStartMarker).toBeDefined(); test.harness.setCurrentTime(commitMarker!.starts[0]!.at); commitMarker!.emitEnded(); expect(test.loadCalls).not.toContain('/afterglow.opus'); const committedMain = test.sourceFor('/main.opus'); const committedOutro = test.sourcesFor('/intro.opus').at(-1)!; test.graph.setChapterIntent(resolveChapterIntent('start', 2)); expect(committedMain.stops).toHaveLength(0); expect(committedOutro.stops).toHaveLength(0); test.harness.setCurrentTime(mainStartMarker!.starts[0]!.at); mainStartMarker!.emitEnded(); expect(test.loadCalls.filter((url) => url === '/afterglow.opus')).toHaveLength(1); }); it('starts one afterglow transition and loops only its eight-bar idle region', async () => { const test = createEngineHarness({ chapter: resolveChapterIntent('toolkit'), drive: { energy: 0, source: 'idle' }, }); await test.graph.resume(); await test.flushOptionalLoads(); const afterglow = test.sourceFor('/afterglow.opus'); expect(afterglow.starts).toHaveLength(1); expect((afterglow.node as AudioBufferSourceNode).loop).toBe(true); expect((afterglow.node as AudioBufferSourceNode).loopStart) .toBe(AFTERGLOW_LOOP_START_SECONDS); expect((afterglow.node as AudioBufferSourceNode).loopEnd) .toBe(AFTERGLOW_LOOP_END_SECONDS); }); it('contains optional failures and still resolves dominant playback', async () => { const intro = createEngineHarness(initialState, { rejectDriveLift: true, rejectDriveTexture: true, }); await expect(intro.graph.resume()).resolves.toBeUndefined(); await intro.flushOptionalLoads(); expect(intro.sourceFor('/intro.opus').starts).toHaveLength(1); const direct = createEngineHarness({ chapter: resolveChapterIntent('profile'), drive: { energy: 0, source: 'idle' }, }, { rejectAfterglow: true }); await expect(direct.graph.resume()).resolves.toBeUndefined(); await direct.flushOptionalLoads(); expect(direct.sourceFor('/main.opus').starts).toHaveLength(1); expect(direct.sourcesFor('/afterglow.opus')).toHaveLength(0); }); it('starts direct activation at the mapped phrase-seven source offset', async () => { const test = createEngineHarness({ chapter: resolveChapterIntent('contact'), drive: { energy: 0, source: 'idle' }, }); await test.graph.resume(); expect(test.sourceFor('/main.opus').starts[0]?.offset) .toBe(mainPhraseOffsetSeconds(7)); }); it('phase-aligns a drive source that becomes ready after main starts', async () => { const test = createEngineHarness({ chapter: resolveChapterIntent('experience'), drive: { energy: 0.8, source: 'physical' }, }, { deferDriveLift: true }); await test.graph.resume(); const mainStart = test.sourceFor('/main.opus').starts[0]!; const anchor = mainStart.at - mainStart.offset; test.harness.setCurrentTime(13); test.resolveDriveLift(); await test.flushOptionalLoads(); const drive = test.sourceFor('/drive-lift.opus'); expect(drive.starts).toHaveLength(1); expect(drive.starts[0]!.offset).toBeCloseTo( phaseAlignedLoopOffset(drive.starts[0]!.at, anchor, DRIVE_LIFT_LOOP_SECONDS), 8, ); expect(test.mixer.setDriveLiftAvailableAt).toHaveBeenCalledWith(drive.starts[0]!.at); }); it('routes drive commands immediately to texture and mixer', () => { const test = createEngineHarness(); const sample = { energy: 0.72, source: 'physical' } as const; test.graph.setDriveEnergy(sample); expect(test.texture.setEnergy).toHaveBeenLastCalledWith(sample); expect(test.mixer.setDriveEnergy).toHaveBeenLastCalledWith(sample); }); it('keeps dominant intro and direct-main load failures terminal', async () => { const intro = createEngineHarness(initialState, { rejectIntro: true }); await expect(intro.graph.resume()).rejects.toThrow('/intro.opus unavailable'); const main = createEngineHarness({ chapter: resolveChapterIntent('profile'), drive: { energy: 0, source: 'idle' }, }, { rejectMain: true }); await expect(main.graph.resume()).rejects.toThrow('/main.opus unavailable'); }); it('stops and disconnects each owned source exactly once when closed twice', async () => { const test = createEngineHarness(); await test.graph.resume(); test.graph.setChapterIntent(resolveChapterIntent('profile', 1)); expect(test.markers()).toHaveLength(2); await test.graph.close(); await test.graph.close(); expect(test.harness.sources.length).toBeGreaterThan(0); for (const source of test.harness.sources) { expect(source.stops).toHaveLength(1); expect(source.disconnectCount).toBe(1); } expect(test.texture.dispose).toHaveBeenCalledOnce(); expect(test.mixer.dispose).toHaveBeenCalledOnce(); expect(test.harness.closeCount).toBe(1); }); });