fix: make soundtrack loading terminal

This commit is contained in:
2026-09-04 16:25:29 +03:00
parent 5172b5bc4b
commit 1431e65768
2 changed files with 28 additions and 10 deletions

View File

@@ -387,7 +387,9 @@ export function createSoundtrackEngine(
mainSource = main; mainSource = main;
mainSchedule = schedule; mainSchedule = schedule;
attachMainEnded(main); attachMainEnded(main);
for (const source of introSourceNodes) { const committedIntroSources = introSourceNodes;
introSourceNodes = [];
for (const source of committedIntroSources) {
source.stopAt(transitionTime + EXIT_CROSSFADE_SECONDS); source.stopAt(transitionTime + EXIT_CROSSFADE_SECONDS);
} }
loadedIntroBuffer = null; loadedIntroBuffer = null;
@@ -456,22 +458,24 @@ export function createSoundtrackEngine(
void mainPromise.catch(() => undefined); void mainPromise.catch(() => undefined);
if (activatedFromStart) { 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(); if (closed || signal.aborted) throw abortError();
loadedIntroBuffer = introBuffer;
if (latestChapter.atStart) { if (latestChapter.atStart) {
transportStarted = true; transportStarted = true;
startIntro(loadedIntroBuffer, context.currentTime + SOURCE_LEAD_SECONDS); startIntro(loadedIntroBuffer, context.currentTime + SOURCE_LEAD_SECONDS);
return; return;
} }
transportStarted = true;
startDirectMain(mainBuffer);
return;
} }
const mainBuffer = await mainPromise; const mainBuffer = await mainPromise;
if (closed || signal.aborted) throw abortError(); if (closed || signal.aborted) throw abortError();
if (activatedFromStart && latestChapter.atStart && loadedIntroBuffer !== null) {
transportStarted = true;
startIntro(loadedIntroBuffer, context.currentTime + SOURCE_LEAD_SECONDS);
return;
}
transportStarted = true; transportStarted = true;
startDirectMain(mainBuffer); startDirectMain(mainBuffer);
}; };
@@ -514,6 +518,16 @@ export function createSoundtrackEngine(
mixer.dispose(); mixer.dispose();
for (const source of [...ownedSources]) source.cancel(); for (const source of [...ownedSources]) source.cancel();
ownedSources.clear(); 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); context.removeEventListener('statechange', onStateChange);
stateListeners.clear(); stateListeners.clear();
outputGain.disconnect(); outputGain.disconnect();

View File

@@ -306,14 +306,18 @@ describe('soundtrack engine', () => {
expect(test.mixer.setDriveEnergy).toHaveBeenLastCalledWith(sample); 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 }); const intro = createEngineHarness(initialState, { rejectIntro: true });
await expect(intro.graph.resume()).rejects.toThrow('/intro.opus unavailable'); 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'), chapter: resolveChapterIntent('profile'),
drive: { energy: 0, source: 'idle' }, drive: { energy: 0, source: 'idle' },
}, { rejectMain: true }); }, { 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 () => { it('stops and disconnects each owned source exactly once when closed twice', async () => {