{"record":{"id":"d7e35b3b392a4a30","repo":"remotion-dev/remotion","slug":"the-time-stretcher-has-already-been-finalized","errorCode":null,"errorMessage":"The time stretcher has already been finalized.","messagePattern":"The time stretcher has already been finalized\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/media/src/audio/pitch-shift.ts","lineNumber":323,"sourceCode":"\t\tthis.outputLength -= finalizedLength;\n\t\tthis.synthesisPosition -= finalizedLength;\n\n\t\tconst inputFramesToDiscard = Math.max(\n\t\t\t0,\n\t\t\tMath.floor(this.analysisPosition) - this.searchRadius,\n\t\t);\n\t\tfor (let channel = 0; channel < this.numberOfChannels; channel++) {\n\t\t\tthis.input[channel].copyWithin(0, inputFramesToDiscard, this.inputLength);\n\t\t}\n\n\t\tthis.inputLength -= inputFramesToDiscard;\n\t\tthis.analysisPosition -= inputFramesToDiscard;\n\t\treturn result;\n\t}\n\n\tpublic finalize() {\n\t\tif (this.finalized) {\n\t\t\tthrow new Error('The time stretcher has already been finalized.');\n\t\t}\n\n\t\tthis.finalized = true;\n\t\tconst targetLength = Math.round(this.totalInputFrames * this.factor);\n\t\tconst padding = makePlanarAudio(\n\t\t\tthis.numberOfChannels,\n\t\t\tthis.windowSize + this.searchRadius * 2,\n\t\t);\n\t\tthis.input = ensurePlanarCapacity({\n\t\t\tbuffers: this.input,\n\t\t\trequiredLength: this.inputLength + padding[0].length,\n\t\t});\n\t\tfor (let channel = 0; channel < this.numberOfChannels; channel++) {\n\t\t\tthis.input[channel].set(padding[channel], this.inputLength);\n\t\t}\n\n\t\tthis.inputLength += padding[0].length;\n\t\tthis.process();","sourceCodeStart":305,"sourceCodeEnd":341,"githubUrl":"https://github.com/remotion-dev/remotion/blob/a6a7485a9aeb04219510fbe4874f8941486424df/packages/media/src/audio/pitch-shift.ts#L305-L341","documentation":"finalize() on StreamingPitchShifter flushes remaining audio and marks the instance finished. Calling finalize twice would produce duplicated tail padding/invalid state, so the second call throws this error.","triggerScenarios":"Calling shifter.finalize() more than once on the same instance — e.g. flush() invoked on both segment end and stream end, or finalize in both an error-cleanup path and the normal completion path.","commonSituations":"Double flush in streaming pipelines (error handler + finally block); reusing an iterator's internal shifter after segment boundary flush; retry logic calling finalize again after a partial failure.","solutions":["Call finalize exactly once per StreamingPitchShifter instance; guard with a boolean flag in caller code","Remove duplicate flush calls (e.g. from both catch/finally and normal completion)","On retry, create a new shifter instead of re-finalizing the old one"],"exampleFix":"// before\nshifter.finalize(); cleanup(); // cleanup also calls shifter.finalize()\n// after\nshifter.finalize(); cleanup(); // cleanup: if (!finalized) shifter.finalize();","handlingStrategy":"validation","validationCode":"let finalized = false;\nfunction finalizeOnce(s: StreamingPitchShifter) {\n  if (!finalized) { s.finalize(); finalized = true; }\n}","typeGuard":"const canFinalize = (s: StreamingPitchShifter): boolean => !s.finalized;","tryCatchPattern":"try {\n  shifter.finalize();\n} catch (e) {\n  if (e instanceof Error && e.message.includes('already been finalized')) {\n    // already flushed — safe to ignore\n  } else throw e;\n}","preventionTips":["Finalize exactly once per instance; centralize the call in one code path","Avoid finalize in both catch and finally blocks","Use a boolean flag or idempotent wrapper around finalize","For retries, build a fresh shifter"],"tags":["audio","pitch-shift","state-machine","media"],"backgroundTag":"double-finalize","analyzedSha":"a6a7485a9aeb04219510fbe4874f8941486424df","analyzedAt":"2026-09-02T16:25:19.997Z","contentChangedAt":"2026-09-02T16:25:19.997Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}