Skip to content

fix(audio): preserve causes and use portable padding - #2769

Merged
jrusso1020 merged 2 commits into
mainfrom
fix/audio-failure-causes-portable-pad
Jul 25, 2026
Merged

fix(audio): preserve causes and use portable padding#2769
jrusso1020 merged 2 commits into
mainfrom
fix/audio-failure-causes-portable-pad

Conversation

@jrusso1020

@jrusso1020 jrusso1020 commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

What

  • replace apad=whole_dur=... with the portable, finite apad,atrim=0:<compositionDuration> filter chain
  • preserve bounded typed audio failure causes through engine, in-process producer, and distributed planning
  • emit structured reason/stage/owner/retryability telemetry without logging raw stderr
  • keep warning snapshots isolated by deep-cloning mutable warning fields

Why

Production audio_processing_failed events currently discard the underlying FFmpeg/ffprobe cause. A reproduced Windows failure on the bundled FFmpeg rejects apad=whole_dur with Option 'whole_dur' not found, even though the same composition can render video and be muxed locally.

The missing causes also make deterministic input failures indistinguishable from transient infrastructure failures. This change fixes the known portability bug and makes future failures actionable without weakening the existing fail-closed audio correctness policy.

How

  • pad each delayed audio input indefinitely and trim it to the composition duration before amix, preserving authored clip/delay timing while avoiding the unsupported whole_dur option
  • classify source, download, probe, extract, prepare, mix, timeout, binary, cancellation, and internal failures
  • treat invalid authored media as user-owned only in extract/prepare; producer-generated mix/silence failures stay system-owned
  • redact URLs and absolute paths (including paths with spaces), cap every detail and aggregate error at 2,000 characters, and preserve unknown legacy retryability as unknown
  • aggregate retryability is true only when every typed cause is retryable

This is additive/backward-compatible telemetry and does not change retry or fallback policy. It is safe to canary independently; any later policy change can key off the new bounded taxonomy.

Test plan

  • Unit tests added/updated
  • Focused engine/producer/distributed suites: 76 tests passed
  • Engine and producer typechecks
  • Lint, formatting, tracked-artifact, and fallow gates
  • Manual production canary after merge
  • Documentation updated (not applicable)

Independent review: approved after timing, privacy, propagation, retry, and snapshot-isolation findings were addressed.

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE

Portability fix + typed failure taxonomy — well-executed, well-tested, additive. The apad,atrim=0:X shape is semantically equivalent to apad=whole_dur=X for the pipeline's needs (delay + trim to composition duration), and the taxonomy propagation preserves diagnostic content without introducing new metric tags or unbounded telemetry. Windows CI (Render on windows-latest 8m18s, Tests on windows-latest 11m5s — both pass) is direct end-to-end validation of the very platform this fix targets.

Empirical premises verified against main @ base 3b3d4f5:

  • RunFfmpegResult.terminationReasonpackages/engine/src/utils/runFfmpeg.ts:28, exercised across streaming/chunk encoder / videoFrameExtractor already. ✓
  • formatFfmpegErrorpackages/engine/src/utils/runFfmpeg.ts:71, re-exported from engine index. ✓
  • formatFilterNumberpackages/engine/src/services/audioMixer.ts:27, existing helper. ✓
  • applyDistributedAudioWarningPolicy runtime callers: only packages/producer/src/services/distributed/plan.ts:946, which the PR updates. No positional-log breakage risk. ✓
  • Legacy inline warning-clone sites that the PR replaces (frameCapture.ts:3776, renderEventPublisher.ts:8, captureHdrStage.ts:131, renderOrchestrator.ts:618) all only cloned sources; the new shared cloneCaptureWarning also deep-clones the new failureReasons/failureStages fields, closing the aliasing hole that would otherwise ship with the taxonomy. ✓
  • No other user of apad=whole_dur in the live code path (see P2 below for the dead-code sibling). ✓

End-to-end validation

  • User-facing exercise path: plan() (distributed) and executeRenderPipeline (in-process) → runAudioStageprocessCompositionAudio → filter emission at packages/engine/src/services/audioMixer.ts (post-PR L522). Downstream: audioResult.audioFailuresapplyDistributedAudioWarningPolicy / orchestrator's inline aggregation → applyRenderWarningPolicy → structured log.warn with warningReasons/warningStages/warningOwners/warningRetryable.
  • Filter-string test-lock: packages/engine/src/services/audioMixer.test.ts L79-83 asserts apad,atrim=0:2 present and whole_dur absent — the exact shape swap. Multi-track filter test at L603-609 pins atrim= count to trackCount * 2 and apad, count to trackCount, catching any silent revert to the single-atrim whole_dur shape. Golden churn on the multi-track test is bounded and semantic — checking that the new shape emits both trims and one apad per track, not merely a text-match.
  • Taxonomy propagation coverage: plan.test.ts — typed-details, mixed-cause retryability rule (every retryable only), legacy-untyped preservation (does not invent ownership). renderEventPublisher.test.ts — deep-clone-across-sink-boundary (mutating job.warnings[0].details.failureReasons after publish() does not affect the delivered snapshot; sink mutation does not bleed back), plus explicit taxonomy log-shape assertions.
  • Windows-native run: Render on windows-latest (8m18s pass) and Tests on windows-latest (11m5s pass) both green on head 4b024ed. That is the direct field of failure this PR targets.
  • Producer-lane check: the one non-green matrix job (Producer: ${{ matrix.lane }} tests, job 89645115967) is cancelled, not failed. Not blocking; not a real signal.

P2 (address before merge or acknowledge)

packages/producer/src/services/audioExtractor.ts:210 — same portability bug, dead code.
The filter builder there emits the identical broken shape:

[${i}:a]atrim=0:${trimDuration},volume=${track.volume},adelay=${delayMs}|${delayMs},apad=whole_dur=${totalDuration}[a${i}]

Zero runtime callers today (rg processAudio\|from.*audioExtractor outside the file itself returns nothing) — the module is orphaned. So it does not block this PR's correctness on Windows. But because the whole thesis is "the portable filter shape is apad,atrim, apad=whole_dur is not supported by the bundled Windows FFmpeg", leaving the identical broken shape in a sibling is a re-materialization landmine the moment anyone revives or copies from that module. Suggest either:

  • align the shape here (mirror the change, no other refactor), or
  • delete audioExtractor.ts in a follow-up (there is a scoped ticket-worthy cleanup here regardless of this PR).

Not blocking — flag with a one-line commit or a TODO(#…).

P3 nits

  • packages/producer/src/services/render/audioPadTrim.ts:249 — comment references apad=whole_dur as the shape used in the codebase (paraphrased: "vs a working manual apad=whole_dur command"). After merge, the module the comment contrasts against no longer emits whole_dur. Consider updating the comment or noting the migration.
  • packages/engine/src/services/audioMixer.ts probeFailure, L292 — the timedOut regex is /(?:timed?\s*out|timeout|deadline|inactivity|aborted)/i. aborted in retryable: !invalidMedia && (unavailable || timedOut) yields retryable: true, but user-initiated aborts should be retryable: false. Low-frequency because extractAudioMetadata is called guarded by a signal check, but the semantics are wrong. Consider splitting aborted off — either into its own branch (with owner: "user", retryable: false, reason: "cancelled") or dropping it from the timedOut set.
  • Duplicate ownership/retryability aggregation — the same 8-line failureOwner/retryable derivation appears in both packages/producer/src/services/distributed/plan.ts L273-279 and packages/producer/src/services/renderOrchestrator.ts L2196-2203. Not a bug (they must stay identical for cross-mode parity), but it's ripe for extraction to a helper (e.g. deriveAudioFailureAggregate(audioFailures)). Skip if you'd rather keep the PR tight.
  • boundedDetail regex fallback — the POSIX path regex at audioMixer.ts L307-311 lazy-matches \/.+?(?=:\s[A-Z]|\s(?:ENOENT|EACCES|EPERM)\b|\r?$). On an error string like /var/log/render.log then something else happened (path with no terminator marker), the lazy match will extend to end-of-line and swallow the trailing prose. That is over-redaction, not a leak, so it's safe; just noting so you're not surprised by aggressive redaction in unusual FFmpeg outputs.

Nice-to-have (non-nit)

  • The cloneCaptureWarning helper is now the canonical clone. Consider adding a tiny unit test at packages/engine/src/services/captureWarning.test.ts that mutates each of sources, failureReasons, failureStages on the clone and asserts the source array is untouched. Currently the invariant is only exercised transitively via the renderEventPublisher sink-boundary test.

— Via

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent exact-head review on 4b024ed0e1a50e6871ccd5e40d255123c200e978: no additional blocking findings beyond Via’s documented non-blocking follow-ups.

Fresh validation:

  • 76/76 focused tests passed in their native lanes (33 Vitest + 43 Bun).
  • Full audio-mux-parity render passed end to end: compile, 300-frame Chrome capture, FFmpeg audio processing/mux, 100/100 visual checkpoints, stream drift 0.005s, audio correlation 0.99997 with zero lag.
  • All required GitHub checks are green on this head, including Windows render/tests.

Approved.

@jrusso1020
jrusso1020 force-pushed the fix/audio-failure-causes-portable-pad branch from 4b024ed to b550865 Compare July 25, 2026 19:15

Copy link
Copy Markdown
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@jrusso1020

Copy link
Copy Markdown
Collaborator Author

Addressed the actionable review follow-ups in b55086513:

  • aligned the orphaned audioExtractor.ts filter with portable apad,atrim
  • updated the stale audioPadTrim comment
  • classified probe aborts as user-owned, non-retryable cancelled failures and timeouts as retryable system ffmpeg_timeout failures, with direct tests
  • added a direct deep-clone test for every mutable CaptureWarning.details array

I intentionally left the duplicate distributed/in-process audio aggregation in place to keep this correctness PR narrow; the two copies remain parity-tested. The bounded-detail over-redaction note is safe and unchanged.

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R2 exact-head re-review on .\n\nThe rebase is clean: shows the previously approved implementation commit is patch-equivalent ( → ), with one additive review-fix commit.\n\nVerified the addressed concerns:\n- the orphaned filter now uses portable ;\n- the stale comment is corrected;\n- probe abort/cancellation is user-owned and non-retryable, while timeout/deadline/inactivity is system-owned and retryable;\n- direct tests pin both classifications;\n- now has direct mutation-isolation coverage for every mutable detail array.\n\nFresh exact-head validation: 21/21 focused Vitest cases pass. All CI lanes are green, including all 8 regression shards, Windows render/tests, CodeQL, CLI smoke, and global install.\n\nNo new blocking findings. Approved.

@jrusso1020
jrusso1020 merged commit 37b8868 into main Jul 25, 2026
51 checks passed
@jrusso1020
jrusso1020 deleted the fix/audio-failure-causes-portable-pad branch July 25, 2026 21:12

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R2 verification

Head b550865 vs R1 4b024ed: PR moved forward (branch also rebased through base drift). Every R1 finding checked at the new head — the actionable ones landed, the "skip if you'd rather keep it tight" nit was intentionally deferred. Verdict: APPROVE.

Prior-finding status

Finding Status Evidence at head b550865
P2 (a) sibling apad=whole_dur landmine in audioExtractor.ts Addressed packages/producer/src/services/audioExtractor.ts:210 — now atrim=0:${trimDuration},volume=…,adelay=…,apad,atrim=0:${totalDuration} (portable shape, matches the fix).
P3 (a) stale apad=whole_dur reference in audioPadTrim.ts comment Addressed packages/producer/src/services/render/audioPadTrim.ts:246 — comment now reads "…the module's arg shape vs a working manual pad/trim command"; no whole_dur mention.
P3 (b) probeFailure misclassifies aborted as retryable Addressed packages/engine/src/services/audioMixer.ts:220-244 — new cancelled = /(?:aborted|AbortError|cancelled|canceled)/i boolean; timedOut no longer contains aborted; retryable: !cancelled && !invalidMedia && (unavailable || timedOut); owner: cancelled || invalidMedia ? "user" : "system"; reason: cancelled ? "cancelled" : …. Sibling ffmpegFailure L266-269 mirrors via result.terminationReason === "abort"reason: "cancelled", owner: "user", retryable stays false. Test lock in audioMixer.test.ts:69-84 (parametrized it.each — cancelled vs timeout with expected owner/retryable pins).
P3 (c) duplicate owner/retryable aggregation Not addressed (acceptable) Still duplicated at packages/producer/src/services/distributed/plan.ts:270-278 and packages/producer/src/services/renderOrchestrator.ts:2196-2205. R1 explicitly offered "Skip if you'd rather keep the PR tight" — deferring the extract is a legitimate choice, both sites are identical and behave the same, cross-mode parity is preserved. Ledger-only.
Nice-to-have cloneCaptureWarning unit test Added packages/engine/src/services/captureWarning.test.ts — mutates sources / failureReasons / failureStages on the clone and asserts source is untouched. Exact invariant I sketched.
P3 boundedDetail POSIX path over-redaction Softened audioMixer.ts:212-215 — regex now gated by (^|[\s"'(]) prefix, matching only paths at start-of-line or after whitespace/quote/paren. Doesn't fully close the lazy-match issue but narrows the false-positive surface; still an over-redaction (never a leak).

Fresh light-touch adversarial pass on delta code

  • ffmpegFailure termination-reason switch (audioMixer.ts:262-289): "abort" / "deadline" / "inactivity" / "spawn_error" values match the ManagedProcessTerminationReason union at packages/engine/src/utils/managedChildProcess.ts:3-9. No dangling case — the tail regex fallback (unrecognized option / invalid data found) handles stderr-only errors when terminationReason === "exit". No new escape hatch that skips ownership/retryable initialization.
  • cloneCaptureWarning new shared helper: called from every legacy inline warning-clone site (renderEventPublisher.ts, frameCapture.ts, captureHdrStage.ts, renderOrchestrator.ts). Confirmed all four now delegate to the shared clone rather than re-implementing — closes the aliasing hole for failureReasons / failureStages.
  • No net-new call sites of apad=whole_dur: rg 'apad=whole_dur' at head returns zero live-code hits.

Verdict

APPROVE — every actionable R1 finding is addressed with test coverage, the intentionally-deferred duplicate-aggregation nit is a legitimate scope call, and the fresh delta doesn't introduce new correctness or coverage regressions.

— Via

dahans-msft2 pushed a commit to dahans-msft2/hyperframes that referenced this pull request Aug 6, 2026
* fix(audio): preserve causes and use portable padding

* fix(audio): address failure taxonomy review
jrusso1020 pushed a commit that referenced this pull request Aug 21, 2026
…hes (#3380)

On FFmpeg 5.x through 8.0.x the samples `apad` appends carry timestamps
the following `atrim` misreads. A delayed branch then sounds at t=0
instead of its offset and, once four or more branches are mixed, the last
one disappears from the output entirely. No error is raised; the render
succeeds with wrong audio.

Reverting to `apad=whole_dur=` is not an option: #2769 moved off that
form because some builds reject the option outright ("Error applying
option 'whole_dur': Option not found"). Inserting `asetpts=N/SR/TB`
between the pad and the trim rebuilds the timestamps from the sample
count using only filters every build ships, so it fixes the misplacement
without giving up the portability that change bought.

Verified on FFmpeg 4.2.7, 7.0.2, an 8.x nightly and 8.1.1: the current
form is wrong on the middle two, the new form is correct on all four.

audioPadTrim.ts also pads with apad+atrim but has no adelay and is
correct on every version tested, so it is left alone.

Closes #3344
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.

3 participants