remotion-dev/remotion · error
Cannot append audio after the time stretcher was finalized.
Error message
Cannot append audio after the time stretcher was finalized.
What it means
StreamingPitchShifter is a streaming WSOLA-style time stretcher: you feed it audio chunks with append() and call finalize() once at the end to flush the remaining output. Once finalize() has run, the internal state is closed out (padding added, target length computed) and no more input can be accepted; appending afterwards throws this error. It guards against a misuse pattern where the stream lifecycle is violated.
Solutions
- Reorder the pipeline so finalize() is called only after the last append(), typically when the source signals end-of-stream.
- Construct a new StreamingPitchShifter instance for each new audio stream instead of reusing a finalized one.
- Guard the append site: track a `finalized` flag in the caller and skip/queue appends once finalize() has run.
- If extra audio genuinely arrives after finalize, buffer it up front and pass it before calling finalize().
Example fix
// before
for (const chunk of chunks) {
shifter.append(chunk);
}
shifter.finalize();
// ...late chunks keep arriving
shifter.append(lateChunk); // throws
// after
const pending: PlanarAudio[] = [];
for (const chunk of chunks) {
pending.push(chunk);
}
for (const chunk of pending) {
shifter.append(chunk);
}
shifter.finalize(); Defensive patterns
Strategy: validation
Validate before calling
// check before appending
if (shifter.isFinalized()) {
shifter = new StreamingPitchShifter({numberOfChannels, sampleRate, factor});
}
shifter.append(chunk); Try / catch
try {
shifter.append(chunk);
} catch (err) {
if (err instanceof Error && err.message.includes('finalized')) {
shifter = new StreamingPitchShifter({numberOfChannels, sampleRate, factor});
shifter.append(chunk);
} else {
throw err;
}
} Prevention
- Structure the pipeline as append... -> finalize() exactly once, driven by an explicit end-of-stream signal.
- Wrap the stream in a small class/closure that owns the finalized flag and no-ops or re-creates the shifter after finalize.
- Never cache StreamingPitchShifter instances across renders or audio streams.
- Keep finalize() out of shared cleanup paths that may run for multiple streams.
- In callbacks feeding chunks, check a done flag set immediately before finalize() to drop late chunks.
When it happens
Trigger: Calling pitchShifter.append(audio) after pitchShifter.finalize() has already been called on the same instance — e.g. feeding additional audio chunks after the finalization point, reusing a cached/finalized shifter for a new stream, or a loop that keeps appending after an end-of-stream branch already invoked finalize().
Common situations: Audio pipeline code that pushes chunks from a callback (e.g. an AudioWorklet or file reader) whose lifecycle doesn't align with the finalize call; retry/loop logic that re-enters append after the stream was finalized; holding the shifter in a long-lived object (per-composition mixer) and reusing it across renders instead of constructing a new one.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- The time stretcher has already been finalized.
- Cannot append audio after the time stretcher was finalized.
- Cannot change the behavior for pre-mounting audio tags…
- Changing the AudioContext sample rate dynamically is not…
- `_experimentalKeepAudioContextAlive` cannot be changed…
AI-assisted analysis of remotion-dev/remotion@46a3a6bf13 (2026-09-18).
Data as JSON: /api/errors/c0b0ed495e402d3b.
Report an issue: GitHub.
Appendix: source
Thrown at packages/web-renderer/src/streaming-pitch-shifter.ts:127
sampleRate: number;
factor: number;
}) {
this.numberOfChannels = numberOfChannels;
this.factor = factor;
this.hopSize = Math.max(
32,
Math.round((REFERENCE_HOP_SIZE * sampleRate) / REFERENCE_SAMPLE_RATE),
);
this.windowSize = this.hopSize * 2;
this.searchRadius = this.hopSize;
this.analysisHop = this.hopSize / factor;
this.input = makePlanarAudio(numberOfChannels, 65_536);
this.output = makePlanarAudio(numberOfChannels, 65_536);
}
public append(audio: PlanarAudio) {
if (this.finalized) {
throw new Error(
'Cannot append audio after the time stretcher was finalized.',
);
}
const {length} = audio[0];
this.input = ensurePlanarCapacity({
buffers: this.input,
requiredLength: this.inputLength + length,
});
for (let channel = 0; channel < this.numberOfChannels; channel++) {
this.input[channel].set(audio[channel], this.inputLength);
}
this.inputLength += length;
this.totalInputFrames += length;
this.process();
return this.drainFinalizedOutput();View on GitHub (pinned to 46a3a6bf13)