Summary
In an export, every <audio> clip that ends before the scene ends loses the end of its sound. Sync and total length are correct; the tail of each clip is silent:
- audio-only export (
.ogg, opus): 40–1000 ms lost per clip, different on every run;
- MP4 export (video + AAC): 10–60 ms lost per clip, about the same on every run.
Unaffected:
- a clip that runs to the end of the scene;
- preview playback.
It hits anything short: sound effects, a music cue that stops mid-scene, dialogue cut into clips.
Environment: macOS 26 (arm64). Reproduced with the released 0.204.2 app and with a local build of 4a652f5.
Reproduction
Ten back-to-back 1.5 s pink-noise clips inside a 16 s scene:
mkdir -p tail-repro/assets/clips && cd tail-repro
for i in 0 1 2 3 4 5 6 7 8 9; do
ffmpeg -f lavfi -i "anoisesrc=color=pink:amplitude=0.25:seed=$((i+1)):sample_rate=48000:duration=1.5" \
-af "pan=stereo|c0=c0|c1=c0" -c:a pcm_s16le assets/clips/clip-$i.wav
done
export default function Project() {
return (
<stage background="#161616" camera={[0.3, 0, 0, 0.3, 85, 150]}>
<scene id="main" name="tail-repro" width={1080} height={1920} fill="#000000" active>
<rect id="bg" start={0} end={16} width={1080} height={1920} fill="#20252b" />
<sequence id="clips" name="Clips">
<audio id="clip-0" src="clips/clip-0.wav" start={0} />
<audio id="clip-1" src="clips/clip-1.wav" start={1.5} />
{/* … clip-9 at 13.5 */}
</sequence>
</scene>
</stage>
);
}
Export it several times:
- audio only:
dapi export main out.ogg, with "diffusion": { "export": { "main": { "video": { "enabled": false }, "audio": { "codec": "opus" } } } } in package.json;
- MP4 with the default settings:
dapi export main out.mp4.
Compare each export with the ten clips concatenated, in 10 ms windows. A window where the source is louder than −45 dBFS and the export is quieter than −65 dBFS counts as lost. For MP4, shift by the 2112 samples of AAC encoder delay first.
| Build |
Audio-only, 3 runs |
MP4, 3 runs |
4a652f5 |
every clip loses 120–970 ms at its end, different per run |
every clip loses 20–50 ms at its end |
4a652f5 + fix |
nothing lost |
nothing lost |
Real projects show the same:
- a 36 s project with 11 WAV dialogue clips, on both the release app and a local build;
- a 90 s project with 23 dialogue clips, one of them trimmed and the rest moved up, on the release app.
With the fix, no export of these projects loses anything. On the 36 s project, each clip's level matches the source within 0.04 dB.
Moving the clips out of the <sequence> does not help, and neither does padding the WAV files past sourceOut.
Root cause
- The export renders audio with an
OfflineAudioContext that advances in step with the frame loop. The loop only waits when the audio budget is more than a second ahead (packages/encoder/src/encoder.ts:302), so the audio thread can run up to a second behind the current frame.
- On the first frame past a clip's end,
forwardAudioDecoder sees visibility !== 1 and calls decoder.reset() (packages/runtime/src/systems/playback.ts:200).
AudioDecoder.reset() calls node.stop() on every scheduled AudioBufferSourceNode (packages/runtime/src/media/audio.ts:239). With no argument, stop() takes effect at the context's current time. In an offline render that time is still behind the frame, so the part of the clip the audio thread has not rendered yet is cut.
This matches what we measured:
- audio-only exports run far faster than real time, so the lag and the loss are large and vary from run to run;
- MP4 exports are paced by video encoding, so the lag is small;
- a clip that ends with the scene is never reset while rendering.
Suggested fix
renderData already truncates every scheduled buffer at the clip's trimEnd, so the nodes stop at the clip's out point on their own. In offline modes, the decoder only needs to drop its decoding state when the clip ends, not stop the nodes it has scheduled. Realtime playback can keep stopping immediately, so seek and pause behave as before. PR to follow.
Summary
In an export, every
<audio>clip that ends before the scene ends loses the end of its sound. Sync and total length are correct; the tail of each clip is silent:.ogg, opus): 40–1000 ms lost per clip, different on every run;Unaffected:
It hits anything short: sound effects, a music cue that stops mid-scene, dialogue cut into clips.
Environment: macOS 26 (arm64). Reproduced with the released 0.204.2 app and with a local build of
4a652f5.Reproduction
Ten back-to-back 1.5 s pink-noise clips inside a 16 s scene:
Export it several times:
dapi export main out.ogg, with"diffusion": { "export": { "main": { "video": { "enabled": false }, "audio": { "codec": "opus" } } } }inpackage.json;dapi export main out.mp4.Compare each export with the ten clips concatenated, in 10 ms windows. A window where the source is louder than −45 dBFS and the export is quieter than −65 dBFS counts as lost. For MP4, shift by the 2112 samples of AAC encoder delay first.
4a652f54a652f5+ fixReal projects show the same:
With the fix, no export of these projects loses anything. On the 36 s project, each clip's level matches the source within 0.04 dB.
Moving the clips out of the
<sequence>does not help, and neither does padding the WAV files pastsourceOut.Root cause
OfflineAudioContextthat advances in step with the frame loop. The loop only waits when the audio budget is more than a second ahead (packages/encoder/src/encoder.ts:302), so the audio thread can run up to a second behind the current frame.forwardAudioDecoderseesvisibility !== 1and callsdecoder.reset()(packages/runtime/src/systems/playback.ts:200).AudioDecoder.reset()callsnode.stop()on every scheduledAudioBufferSourceNode(packages/runtime/src/media/audio.ts:239). With no argument,stop()takes effect at the context's current time. In an offline render that time is still behind the frame, so the part of the clip the audio thread has not rendered yet is cut.This matches what we measured:
Suggested fix
renderDataalready truncates every scheduled buffer at the clip'strimEnd, so the nodes stop at the clip's out point on their own. In offline modes, the decoder only needs to drop its decoding state when the clip ends, not stop the nodes it has scheduled. Realtime playback can keep stopping immediately, so seek and pause behave as before. PR to follow.