Files
Jerrai e1925d4317 feat(audio): composition master bus (data-fx-chain on the root) (#5420)
* feat(audio): composition master bus (data-fx-chain on the root)

A clip can belong to only one audio group, so there was no way to put one
effect, such as a limiter, across the whole mix. The data-fx-chain on the
top-level composition root now runs once over the sum of every clip and every
group, after each group's own chain and fader. No new attribute: a sub-composition
root's data-fx-chain was never applied to anything and still is not.

Render: when a master chain exists, mixAudioTracks writes a float WAV instead
of AAC, applyAudioFxChain runs the chain over it, and ffmpeg encodes AAC last,
so the chain reads unclipped samples. An unreadable chain is a mix failure, as
for a group. Preview: the transport splices the chain between the clips' sum and
the monitor, re-anchored per play generation and watched for edits.

Only the top-level root is read. No master automation: the root's data-automation
is not read as the bus's. No lint rule: data-fx-chain is valid on clips, groups
and roots, and the one place it does nothing (a sub-composition root) has always
been silent.

— Jerrai

* fix(core): preview routes every audible video through the master bus

A root data-fx-chain is attached only inside the Web Audio graph, but a plainly
audible <video> joined that graph only for its own chain, group, automation or a
volume over 1. So preview limited the music and left the talking head dry, and a
composition whose only audio is video got no master chain at all, while export
sums every audible element into the master. A master chain on the top-level
composition root is now a reason to join, the same as a video's own chain.
Elements under a root with no chain keep native output. Sub-composition roots
are still ignored.

— Jerrai

* fix(audio): master bus review round 2 (temp dir, tests, docs, Studio meter)

- audioMixer: a fatal FX error in the master pass or a group bus left the
  work directory (the full float sum, about 1.4 GB an hour) on disk. The
  final mix now runs in try/finally and the group rethrow removes it first.
- Tests: the "once over the sum" case now uses a limiter then a gain, so a
  per-clip chain (-10.4) and a doubled one (-18) both fail it; a new case
  proves the sum stays float until the chain reads it; the fader case uses
  a group volume of 0.5 so the fader's position is exercised; the work-dir
  removal is covered for both paths.
- Docs: "Three rules exist" (the master-bus lint rule is gone), a bad master
  chain stops the render with audio_processing_failed, a sub-composition
  file opened on its own has its root chain as the master bus, and a seek
  clears a group or master limiter.
- Studio: TimelineElement.masterChain carries the root's data-fx-chain, so
  the program meter shows for a video-only composition whose only audio
  routing is the master chain, as preview now plays it.
- Comment blocks over four lines cut; webAudioTransport.setRate split so
  its CRAP score is under the Fallow threshold.

— Jerrai

* fix(audio): master chain reaches tracks the render adds; honest plain-limiter copy

- Master chain: it was carried on each parsed AudioElement, so a track the
  producer adds itself (appendAutoDetectedVideoAudio, the browser-discovered
  audio in probeStage) never had it, and a composition whose only audio was
  such a track exported with no master chain, no ceiling and no warning. It is
  now a composition property: parseMasterFxChain(html) at compile, carried on
  CompiledComposition and CompositionMetadata, passed to processCompositionAudio
  by runAudioStage (in-process and distributed plan). AudioElement.masterFxChain
  is removed so there is one source. Each forward (compile stage, local
  orchestrator, distributed plan) has a test that fails when it is dropped.
- Plain limiter: it follows the level with no lookahead and Level after is
  applied past the ceiling, so it cannot promise "nothing gets past" or "never
  clips". Studio copy (description, panel text, knob label, summary) and the
  hyperframes-audio skill now say a fast or inter-sample peak can pass it and
  point to the true-peak limiter for a delivery limit. A copy test guards it.

— Jerrai

* fix(producer): keep master bus comments within the ratchet; run the plan test with the ffmpeg tests

Drop three one-line field comments that raised their files' comment share,
and list plan.masterBus.test.ts with the integration tests because plan()
reads the ffmpeg version.

— Jerrai

* chore(ci): repin the producer test-classification source in the reachability map

The reachability map pins the hash of test-classification.mjs, which now
lists one more integration test. The map's producer lane command already
covers every test under src, in both the unit and integration matrix lanes.

— Jerrai
2026-10-11 19:07:00 +00:00
..