diff --git a/changelog.d/147.fixed.md b/changelog.d/147.fixed.md new file mode 100644 index 0000000..711e43c --- /dev/null +++ b/changelog.d/147.fixed.md @@ -0,0 +1 @@ +Preserve composition audio beds in loop and scrub head-only composition modes. diff --git a/src/runtime/scene-loader.ts b/src/runtime/scene-loader.ts index 2f278a9..292f838 100644 --- a/src/runtime/scene-loader.ts +++ b/src/runtime/scene-loader.ts @@ -1584,6 +1584,9 @@ export function createSceneLoader(options: SceneLoaderOptions): SceneLoader { id: resolved.composition.id, manifestSlice: Object.freeze([headEntry]), sceneSlice: Object.freeze([headScene]), + ...(resolved.composition.audioBed === undefined + ? {} + : { audioBed: resolved.composition.audioBed }), // PUL-F029 / ADR-028: preserve the absolute composition start // index even when the slice is truncated to its head, so a // failure diagnostic names the right manifest entry. diff --git a/src/runtime/scene-navigation.ts b/src/runtime/scene-navigation.ts index b108668..37ab12f 100644 --- a/src/runtime/scene-navigation.ts +++ b/src/runtime/scene-navigation.ts @@ -504,6 +504,9 @@ function truncateToHead(target: SceneNavigationTarget, headOnly: boolean): Scene id: target.composition.id, manifestSlice: Object.freeze([headEntry]), sceneSlice: Object.freeze([headScene]), + ...(target.composition.audioBed === undefined + ? {} + : { audioBed: target.composition.audioBed }), // PUL-F029 / ADR-028: preserve the absolute composition start // index so a failure diagnostic still points operators at the // right manifest entry even after the slice was truncated to diff --git a/tests/runtime/scene-loader-audio.test.ts b/tests/runtime/scene-loader-audio.test.ts index 7ba78a9..7fef2b9 100644 --- a/tests/runtime/scene-loader-audio.test.ts +++ b/tests/runtime/scene-loader-audio.test.ts @@ -357,6 +357,7 @@ describe('scene loader — audio service wiring (PUL-F024 / ADR-004)', () => { const head = buildScene({ id: 'head', assets: ['/audio/head.mp3'], + audio: ['/audio/head.mp3'], create: (ctx) => { // Would register a sound — but prompter never mounts the scene. (ctx as { audio: AudioService }).audio.load('head-bed', { src: '/audio/head.mp3' }); @@ -1233,6 +1234,35 @@ describe('scene loader — composition audio bed (PUL-F014 / ADR-004)', () => { expect(audio.calls.some((c) => c.method === 'loop')).toBe(true); }); + it.each(['loop', 'scrub'] as const)( + 'preserves and starts the composition audio bed under mode=%s head slices', + async (mode) => { + const audio = recordingAudioEngine(); + const { adapter } = buildRecordingUnlockAdapter('resolve'); + const loader = createSceneLoader({ + scenes: createSceneRegistry([buildScene({ id: 'head' })]), + compositions: bedComposition(), + stage: null, + buildCtx: stubCtx, + createPreloader: () => () => Promise.resolve(), + timeline: noopTimeline, + audioEngine: audio.engine, + audioUnlockAdapter: adapter, + }); + + await loader.handle({ + locator: { kind: 'composition', composition: 'deck' }, + mode, + }); + await loader.idle(); + + expect(audio.created).toHaveLength(1); + expect(audio.created[0]?.src).toEqual([BED_SRC]); + expect(audio.calls).toContainEqual({ sound: 0, method: 'play' }); + expect(audio.calls).toContainEqual({ sound: 0, method: 'loop' }); + }, + ); + it('suppresses the composition audio bed under mode=standalone', async () => { const audio = recordingAudioEngine(); const loader = createSceneLoader({