Skip to content

Export cuts the tail of every audio clip that ends before the scene #65

Description

@moneymaker-ux

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

  1. 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.
  2. On the first frame past a clip's end, forwardAudioDecoder sees visibility !== 1 and calls decoder.reset() (packages/runtime/src/systems/playback.ts:200).
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions