fix: harden adaptive audio resume and release
This commit is contained in:
@@ -141,12 +141,13 @@ export const createAdaptiveMixer: AdaptiveMixerFactory = (context, destination)
|
||||
liftPhase === 'off' ||
|
||||
liftPhase === 'exiting'
|
||||
) return;
|
||||
const entryWillComplete = liftPhase === 'entering';
|
||||
scheduledStart = nextBeatTime(
|
||||
now + LIFT_EXIT_DWELL_SECONDS,
|
||||
transportAnchor,
|
||||
);
|
||||
scheduledEnd = scheduledStart + MUSIC_BEAT_SECONDS;
|
||||
holdParameter(driveLiftGate.gain, now);
|
||||
if (!entryWillComplete) holdParameter(driveLiftGate.gain, now);
|
||||
driveLiftGate.gain.setValueAtTime(1, scheduledStart);
|
||||
driveLiftGate.gain.linearRampToValueAtTime(0, scheduledEnd);
|
||||
liftPhase = 'exiting';
|
||||
|
||||
@@ -499,8 +499,8 @@ export function createSoundtrackEngine(
|
||||
async resume() {
|
||||
if (closed) throw new DOMException('Audio graph is closed.', 'InvalidStateError');
|
||||
await context.resume();
|
||||
ensureDriveLiftLoad();
|
||||
if (transportStarted) return;
|
||||
ensureDriveLiftLoad();
|
||||
loadPromise ??= loadAndStart();
|
||||
await loadPromise;
|
||||
},
|
||||
|
||||
@@ -74,6 +74,32 @@ describe('adaptive mixer', () => {
|
||||
.toContainEqual(expect.objectContaining({ kind: 'linear', value: 0 }));
|
||||
});
|
||||
|
||||
it('finishes an audible lift entry smoothly before scheduling its exit', () => {
|
||||
const harness = createFakeAudioHarness();
|
||||
const mixer = createAdaptiveMixer(harness.context, harness.destination);
|
||||
const liftGate = harness.gainNodes[3]!.gain;
|
||||
const liftEvents = recordFor(harness, liftGate).events;
|
||||
mixer.setTransportAnchor(0);
|
||||
mixer.setDriveLiftAvailableAt(0);
|
||||
harness.setCurrentTime(1);
|
||||
mixer.setDriveEnergy({ energy: 0.7, source: 'physical' });
|
||||
const eventsBeforeDrop = liftEvents.length;
|
||||
|
||||
const entryMidpoint = 3.5 * MUSIC_BEAT_SECONDS;
|
||||
harness.setCurrentTime(entryMidpoint);
|
||||
mixer.setDriveEnergy({ energy: 0.2, source: 'physical' });
|
||||
|
||||
expect(liftEvents).toContainEqual({
|
||||
kind: 'linear',
|
||||
value: 1,
|
||||
at: 4 * MUSIC_BEAT_SECONDS,
|
||||
});
|
||||
expect(liftEvents.slice(eventsBeforeDrop)).toEqual([
|
||||
{ kind: 'set', value: 1, at: 5 * MUSIC_BEAT_SECONDS },
|
||||
{ kind: 'linear', value: 0, at: 6 * MUSIC_BEAT_SECONDS },
|
||||
]);
|
||||
});
|
||||
|
||||
it('replays cached strong drive only after a late loop source is ready', () => {
|
||||
const harness = createFakeAudioHarness();
|
||||
const mixer = createAdaptiveMixer(harness.context, harness.destination);
|
||||
|
||||
@@ -298,6 +298,20 @@ describe('soundtrack engine', () => {
|
||||
expect(test.mixer.setDriveLiftAvailableAt).toHaveBeenCalledWith(drive.starts[0]!.at);
|
||||
});
|
||||
|
||||
it('does not reload the attached drive lift after resuming established main playback', async () => {
|
||||
const test = createEngineHarness({
|
||||
chapter: resolveChapterIntent('experience'),
|
||||
drive: { energy: 0.8, source: 'physical' },
|
||||
});
|
||||
await test.graph.resume();
|
||||
await test.flushOptionalLoads();
|
||||
|
||||
await test.graph.suspend();
|
||||
await test.graph.resume();
|
||||
|
||||
expect(test.loadCalls.filter((url) => url === '/drive-lift.opus')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('routes drive commands immediately to texture and mixer', () => {
|
||||
const test = createEngineHarness();
|
||||
const sample = { energy: 0.72, source: 'physical' } as const;
|
||||
|
||||
Reference in New Issue
Block a user