Skip to content

fix(engine): write the audio mix filter graph to a file, not the command line#1890

Draft
miguel-heygen wants to merge 1 commit into
mainfrom
fix/audiomixer-filter-complex-script
Draft

fix(engine): write the audio mix filter graph to a file, not the command line#1890
miguel-heygen wants to merge 1 commit into
mainfrom
fix/audiomixer-filter-complex-script

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

Summary

mixAudioTracks built the ffmpeg -filter_complex argument as one inline string that scales linearly with track count. Reported in the wild at 146 timed audio clips: the resulting command line exceeded the OS argument-length limit and spawn failed with ENAMETOOLONG, dropping audio entirely until the user manually consolidated clips to reduce the count.

Fix

FFmpeg supports -filter_complex_script <file> specifically for this — the same filter graph read from a file instead of inlined as a command-line argument. -i <path> pairs for each track still scale with count, but each is short and fixed-size; the one component that actually grew unbounded (the filter graph string) is off the command line entirely.

The temp script file is written right before the runFfmpeg call and cleaned up immediately after it resolves (success or failure, via .finally()), matching the existing sibling temp-file convention already used in audioVolumeEnvelope.ts.

Test plan

  • bunx vitest run packages/engine/src/services/audioMixer.test.ts — 7 tests pass (6 existing, updated to read the filter graph from the mock's captured file content instead of the now-removed inline -filter_complex string, + 1 new regression test)
  • New test: 150 tracks (reproducing the reported 146-clip shape) keeps the ffmpeg args array's total character length under 20K, uses -filter_complex_script (not -filter_complex), and the captured filter script's content still contains a correct amix=inputs=150 and one atrim= segment per track
  • bunx vitest run packages/engine/ — full package, 841 tests pass, no regressions
  • Verified end-to-end against a real ffmpeg binary (not just mocked): generated two real sine-wave WAV files, ran processCompositionAudio unmocked, confirmed correct output audio (right duration, right codec via ffprobe) and no leftover temp files after completion

…and line

mixAudioTracks built the ffmpeg -filter_complex argument as one inline
string scaling linearly with track count. Reported in the wild at 146
timed audio clips: the resulting command line exceeded the OS length
limit and spawn failed with ENAMETOOLONG, dropping audio entirely until
the user manually consolidated clips to reduce the count.

FFmpeg supports -filter_complex_script specifically for this - the same
filter graph read from a file instead of inlined as an argument. The -i
pairs for each track still scale with count but stay short and fixed-size
each, so the one component that actually grew unbounded (the filter
string) no longer sits on the command line at all. The temp file is
cleaned up immediately after ffmpeg exits, matching the existing sibling
temp-file convention in audioVolumeEnvelope.ts.

Verified end-to-end against a real ffmpeg binary (not just mocked): a
two-track mix produced correct output audio with no leftover temp files.
const inputs: string[] = [];
tracks.forEach((track) => inputs.push("-i", track.srcPath));
const scriptPath = join(outputDir, `.filter-complex-${randomBytes(6).toString("hex")}.txt`);
writeFileSync(scriptPath, buildFilterComplex(ignoreAutomation));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants