feat(engine): render grouped audio through a summed, FX-processed bus - #3289
Open
vanceingalls wants to merge 1 commit into
Open
feat(engine): render grouped audio through a summed, FX-processed bus#3289vanceingalls wants to merge 1 commit into
vanceingalls wants to merge 1 commit into
Conversation
Renders what B3 already routes in preview: a group's members sub-mix into
one PCM WAV at full composition length (adelay already places each member
at its composition position, so the group WAV's t=0 IS composition time),
run through the group's own FX chain and automation via the same
applyAudioFxChain/envelope-bake path a member uses, then fold into the flat
track list as one processed AudioTrack — the final mixAudioTracks call
never has to know groups exist.
Gain law verified against plans/spikes/amix-nesting-spike.sh (brought over
from the plans branch, along with audioMixer.grouping.test.ts, since both
were committed there and never merged to origin/main — every step branch in
this stack descends from origin/main): the sub-mix's own amix prefers
normalize=0 (nulls exactly against a flat mix), falling back to per-node
compensation by the group's OWN member count only when this ffmpeg build's
amix rejects the option. Carrying any other count into a nested amix node
is the exact +2.499 dB silent failure the spike measured — confirmed by a
manual mutation check (wrong-count compensation landed 3.5 dB hot, exactly
20*log10(3/2) for a 2-member group compensated as 3; reverted after
confirming the level test catches it).
A group element carrying data-hidden drops every member before the sub-mix
ever runs (RULES: mute-by-drop, never mute-by-volume-0) — parseAudioElements
now resolves groups once per parse and skips hidden-group members the same
way it already skips data-hidden ancestors.
HfAudioGroup (packages/core/src/audioGroups.ts, from B1) gains fxChain,
automation, volume and hidden, read off the group element the same way
resolveAudioGroups already reads data-label — audioGroups.test.ts updated
for the wider shape plus new coverage for the added reads.
it.todo("mixes a grouped composition at the same level as the ungrouped
one") is now a real, passing test; two more added per the step doc (FX
routing isolation, member-level envelope survives grouping).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The risky one (per
plans/audio-execution/B4-group-render.md, INDEX.md): renders in export what B3 (#3287) already routes in preview.adelay-positioned to their composition start inside the sub-mix, so the group WAV'st=0IS composition time (design doc §1.3).applyAudioFxChain/envelope-bake path a member uses — no new bake path written.AudioTrack;mixAudioTracks(the final mix) never has to know groups exist.data-hiddendrops every member before the sub-mix ever runs (RULES: mute-by-drop, never mute-by-volume-0) —parseAudioElementsresolves groups once per parse now.Gain law (the actual risk)
plans/spikes/amix-nesting-spike.shmeasured that compensating a nestedamixby anything other than its own input count is a silent +2.499 dB (20·log10(4/3) case) — audible, and invisible to every existing test because it only shows up in export, never preview.The new
mixGroupMemberssub-mix prefersamix=...:normalize=0(nulls exactly against a flat mix, per the spike), falling back to manual per-node compensation by the group's own member count only — never the render's global track count — when an ffmpeg build'samixrejects thenormalizeoption.Manual mutation check (doc's rule 4): temporarily broke the fallback compensation to
memberTracks.length + 1and forced that path. The new level test failed at 3.5 dB error — exactly 20·log10(3/2) for a 2-member group compensated as 3. Reverted after confirming the test catches it.Process note
plans/spikes/amix-nesting-spike.shandpackages/engine/src/services/audioMixer.grouping.test.tsexisted only on the localplans/audio-mixer-groupsdocs branch, never merged toorigin/main— every step branch in this stack descends fromorigin/main, so both are brought over here (unmodified,.gitignoreincluded) since this is the step that actually needs them.HfAudioGroupwidened (packages/core/src/audioGroups.ts, from B1)Added
fxChain,automation,volume,hidden— read off the<hf-audio-group>element the same wayresolveAudioGroupsalready readsdata-label.audioGroups.test.tsupdated for the wider shape (existingtoEqualassertions gained the new fields) plus new coverage for the added reads. ExtractedbuildGroup/parseGroupVolumehelpers to keepresolveAudioGroupsunder fallow's complexity threshold.Tests
it.todo("mixes a grouped composition at the same level as the ungrouped one")is now a real, passing test (< 0.3 dB from flat).Depends on
Stacked on, in order: #3288 (B6) → #3287 (B3) → #3286 (B2) → #3278 (B1) → #3277 (P2) → #3276 (P1) → #3275 (A2) → #3274 (A1). Should merge after all of them.
Test plan
bun run buildcleanpackages/engine:audioMixer.grouping.test.ts+audioMixer.test.ts+audioMixer.level.test.ts— 36/36 pass (gate per step doc)packages/corefull suite: 2366/2366packages/enginefull suite: 1532/1532🤖 Generated with Claude Code