diff --git a/packages/runtime/src/media/audio.ts b/packages/runtime/src/media/audio.ts index df05a8dd..b4a56112 100644 --- a/packages/runtime/src/media/audio.ts +++ b/packages/runtime/src/media/audio.ts @@ -228,17 +228,26 @@ export class AudioDecoder { } public reset() { + this.release(); + + for (const node of this.audioNodes) { + node.stop(); + } + this.audioNodes.clear(); + } + + /** + * Drops the decoding state but lets buffers that are already scheduled play out. + * `renderData` truncates every scheduled buffer at the clip's `trimEnd`, so the + * nodes stop on their own at the clip's out point and `onended` removes them. + */ + public release() { if (!this.iterator) return; this.iterator?.return(); this.iterator = null; this.firstBuffer = null; this.lastBuffer = null; - - for (const node of this.audioNodes) { - node.stop(); - } - this.audioNodes.clear(); } } diff --git a/packages/runtime/src/systems/playback.ts b/packages/runtime/src/systems/playback.ts index 8e963368..3cc0c0e3 100644 --- a/packages/runtime/src/systems/playback.ts +++ b/packages/runtime/src/systems/playback.ts @@ -196,6 +196,13 @@ function forwardAudioDecoder(world: World, scene: Entity, entity: Entity, audioS relativeDelay: (origin / fps) + audioDelay, }); framePromises(world)?.push(playPromise); + } else if (world.get(Mode)?.value !== 'realtime') { + // Offline rendering: the OfflineAudioContext runs behind the frame loop (the + // encoder lets it lag by up to a second of samples). Stopping the nodes here would + // cut the part of the clip the audio thread has not rendered yet, so every clip + // that ends before the scene does would lose its tail in the export. The scheduled + // buffers already end at the clip's out point, so let them finish. + decoder.release(); } else { decoder.reset(); }