{"record":{"id":"c0b0ed495e402d3b","repo":"remotion-dev/remotion","slug":"cannot-append-audio-after-the-time-stretcher-was-finalized-c0b0ed","errorCode":null,"errorMessage":"Cannot append audio after the time stretcher was finalized.","messagePattern":"Cannot append audio after the time stretcher was finalized\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/web-renderer/src/streaming-pitch-shifter.ts","lineNumber":127,"sourceCode":"\t\tsampleRate: number;\n\t\tfactor: number;\n\t}) {\n\t\tthis.numberOfChannels = numberOfChannels;\n\t\tthis.factor = factor;\n\t\tthis.hopSize = Math.max(\n\t\t\t32,\n\t\t\tMath.round((REFERENCE_HOP_SIZE * sampleRate) / REFERENCE_SAMPLE_RATE),\n\t\t);\n\t\tthis.windowSize = this.hopSize * 2;\n\t\tthis.searchRadius = this.hopSize;\n\t\tthis.analysisHop = this.hopSize / factor;\n\t\tthis.input = makePlanarAudio(numberOfChannels, 65_536);\n\t\tthis.output = makePlanarAudio(numberOfChannels, 65_536);\n\t}\n\n\tpublic append(audio: PlanarAudio) {\n\t\tif (this.finalized) {\n\t\t\tthrow new Error(\n\t\t\t\t'Cannot append audio after the time stretcher was finalized.',\n\t\t\t);\n\t\t}\n\n\t\tconst {length} = audio[0];\n\t\tthis.input = ensurePlanarCapacity({\n\t\t\tbuffers: this.input,\n\t\t\trequiredLength: this.inputLength + length,\n\t\t});\n\n\t\tfor (let channel = 0; channel < this.numberOfChannels; channel++) {\n\t\t\tthis.input[channel].set(audio[channel], this.inputLength);\n\t\t}\n\n\t\tthis.inputLength += length;\n\t\tthis.totalInputFrames += length;\n\t\tthis.process();\n\t\treturn this.drainFinalizedOutput();","sourceCodeStart":109,"sourceCodeEnd":145,"githubUrl":"https://github.com/remotion-dev/remotion/blob/46a3a6bf13ca367376ff18bbb06dae2e87e0d068/packages/web-renderer/src/streaming-pitch-shifter.ts#L109-L145","documentation":"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.","triggerScenarios":"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().","commonSituations":"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.","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()."],"exampleFix":"// before\nfor (const chunk of chunks) {\n  shifter.append(chunk);\n}\nshifter.finalize();\n// ...late chunks keep arriving\nshifter.append(lateChunk); // throws\n\n// after\nconst pending: PlanarAudio[] = [];\nfor (const chunk of chunks) {\n  pending.push(chunk);\n}\nfor (const chunk of pending) {\n  shifter.append(chunk);\n}\nshifter.finalize();","handlingStrategy":"validation","validationCode":"// check before appending\nif (shifter.isFinalized()) {\n  shifter = new StreamingPitchShifter({numberOfChannels, sampleRate, factor});\n}\nshifter.append(chunk);","typeGuard":null,"tryCatchPattern":"try {\n  shifter.append(chunk);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('finalized')) {\n    shifter = new StreamingPitchShifter({numberOfChannels, sampleRate, factor});\n    shifter.append(chunk);\n  } else {\n    throw err;\n  }\n}","preventionTips":["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."],"tags":["audio","streaming","lifecycle","pitch-shifter"],"backgroundTag":"invalid-state-transition","analyzedSha":"46a3a6bf13ca367376ff18bbb06dae2e87e0d068","analyzedAt":"2026-09-18T00:12:20.256Z","contentChangedAt":"2026-09-18T00:12:20.256Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}