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

  1. Create a new StreamingPitchShifter for audio appended after finalize
  2. Reorder logic so all append calls happen before finalize
  3. Track finalized state in the caller and skip/reset instead of appending
  4. 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

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


AI-assisted analysis of remotion-dev/remotion@a6a7485a9a (2026-09-02). Data as JSON: /api/errors/047f2c3b10db4551. Report an issue: GitHub.