diff --git a/src/audio/soundtrack-engine.ts b/src/audio/soundtrack-engine.ts index d5f5383..f708f5c 100644 --- a/src/audio/soundtrack-engine.ts +++ b/src/audio/soundtrack-engine.ts @@ -387,7 +387,9 @@ export function createSoundtrackEngine( mainSource = main; mainSchedule = schedule; attachMainEnded(main); - for (const source of introSourceNodes) { + const committedIntroSources = introSourceNodes; + introSourceNodes = []; + for (const source of committedIntroSources) { source.stopAt(transitionTime + EXIT_CROSSFADE_SECONDS); } loadedIntroBuffer = null; @@ -456,22 +458,24 @@ export function createSoundtrackEngine( void mainPromise.catch(() => undefined); if (activatedFromStart) { - loadedIntroBuffer = await dependencies.loadBuffer(context, sources.intro, signal); + const [introBuffer, mainBuffer] = await Promise.all([ + dependencies.loadBuffer(context, sources.intro, signal), + mainPromise, + ]); if (closed || signal.aborted) throw abortError(); + loadedIntroBuffer = introBuffer; if (latestChapter.atStart) { transportStarted = true; startIntro(loadedIntroBuffer, context.currentTime + SOURCE_LEAD_SECONDS); return; } + transportStarted = true; + startDirectMain(mainBuffer); + return; } const mainBuffer = await mainPromise; if (closed || signal.aborted) throw abortError(); - if (activatedFromStart && latestChapter.atStart && loadedIntroBuffer !== null) { - transportStarted = true; - startIntro(loadedIntroBuffer, context.currentTime + SOURCE_LEAD_SECONDS); - return; - } transportStarted = true; startDirectMain(mainBuffer); }; @@ -514,6 +518,16 @@ export function createSoundtrackEngine( mixer.dispose(); for (const source of [...ownedSources]) source.cancel(); ownedSources.clear(); + pendingIntroExit = null; + introSourceNodes = []; + introProgramGain = null; + mainSource = null; + driveLiftSource = null; + afterglowSource = null; + mainSchedule = null; + mainBufferPromise = null; + loadPromise = null; + exitLoadRequest = null; context.removeEventListener('statechange', onStateChange); stateListeners.clear(); outputGain.disconnect(); diff --git a/tests/unit/soundtrack-engine.test.ts b/tests/unit/soundtrack-engine.test.ts index b76a400..40426b6 100644 --- a/tests/unit/soundtrack-engine.test.ts +++ b/tests/unit/soundtrack-engine.test.ts @@ -306,14 +306,18 @@ describe('soundtrack engine', () => { expect(test.mixer.setDriveEnergy).toHaveBeenLastCalledWith(sample); }); - it('keeps dominant intro and direct-main load failures terminal', async () => { + it('keeps dominant intro and main load failures terminal on every activation path', async () => { const intro = createEngineHarness(initialState, { rejectIntro: true }); await expect(intro.graph.resume()).rejects.toThrow('/intro.opus unavailable'); - const main = createEngineHarness({ + const startMain = createEngineHarness(initialState, { rejectMain: true }); + await expect(startMain.graph.resume()).rejects.toThrow('/main.opus unavailable'); + expect(startMain.sourcesFor('/intro.opus')).toHaveLength(0); + + const directMain = createEngineHarness({ chapter: resolveChapterIntent('profile'), drive: { energy: 0, source: 'idle' }, }, { rejectMain: true }); - await expect(main.graph.resume()).rejects.toThrow('/main.opus unavailable'); + await expect(directMain.graph.resume()).rejects.toThrow('/main.opus unavailable'); }); it('stops and disconnects each owned source exactly once when closed twice', async () => {