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 buffers input audio and only releases it on finalize(). Once finalize() has run, the internal state is drained and the instance refuses further input by throwing this error from append().
Source
Thrown at packages/media/src/audio/pitch-shift.ts:129
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 a6a7485a9a)
Solutions
- Create a new StreamingPitchShifter for audio appended after finalize
- Reorder logic so all append calls happen before finalize
- Track finalized state in the caller and skip/reset instead of appending
- For a new segment, instantiate a fresh shifter (as pitchShiftAudioIterator does on segment boundaries)
Example fix
// before
const out1 = shifter.append(a); shifter.finalize(); const out2 = shifter.append(b);
// after
const out1 = shifter.append(a); shifter.finalize();
const shifter2 = new StreamingPitchShifter({numberOfChannels, sampleRate, toneFrequency});
const out2 = shifter2.append(b); Defensive patterns
Strategy: validation
Validate before calling
if (shifter.finalized) {
shifter = new StreamingPitchShifter({numberOfChannels, sampleRate, toneFrequency});
} Type guard
const canAppend = (s: StreamingPitchShifter): boolean => !s.finalized;
Try / catch
try {
shifter.append(audio);
} catch (e) {
if (e instanceof Error && e.message.includes('finalized')) {
shifter = new StreamingPitchShifter({numberOfChannels, sampleRate, toneFrequency});
shifter.append(audio);
} else throw e;
} Prevention
- Treat a shifter as single-use: append-then-finalize, then discard
- Guard cleanup/finally paths from double finalization
- Track finalized state explicitly in streaming loops
- Create a new shifter per segment instead of reusing instances
When it happens
Trigger: Calling shifter.append(planarAudio) after shifter.finalize() has already been called on the same StreamingPitchShifter instance.
Common situations: Reusing a cached shifter across segments or files without creating a new one; calling finalize on end-of-stream and then appending late buffers; a generator loop that finalizes and continues feeding the same instance.
Related errors
- The time stretcher has already been finalized.
- Pitch shifter was not initialized.
- No audio stream found in '${input_path}'. Ensure the video c
- Could not determine duration of ${src}, but "loop" was set.
- No audio track found
AI-assisted analysis of remotion-dev/remotion@a6a7485a9a (2026-09-02).
Data as JSON: /api/errors/047f2c3b10db4551.
Report an issue: GitHub.