Skip to content

feat(engine): render grouped audio through a summed, FX-processed bus - #3289

Open
vanceingalls wants to merge 1 commit into
wa-23f-carve-groupsfrom
wa-23d-group-render
Open

feat(engine): render grouped audio through a summed, FX-processed bus#3289
vanceingalls wants to merge 1 commit into
wa-23f-carve-groupsfrom
wa-23d-group-render

Conversation

@vanceingalls

Copy link
Copy Markdown
Collaborator

Summary

The risky one (per plans/audio-execution/B4-group-render.md, INDEX.md): renders in export what B3 (#3287) already routes in preview.

  • A group's members sub-mix into one PCM WAV at full composition length. Members are already adelay-positioned to their composition start inside the sub-mix, so the group WAV's t=0 IS composition time (design doc §1.3).
  • The group's own FX chain + automation run through the same applyAudioFxChain/envelope-bake path a member uses — no new bake path written.
  • The processed group WAV folds into the flat track list as one AudioTrack; mixAudioTracks (the final mix) never has to know groups exist.
  • A group carrying data-hidden drops every member before the sub-mix ever runs (RULES: mute-by-drop, never mute-by-volume-0) — parseAudioElements resolves groups once per parse now.

Gain law (the actual risk)

plans/spikes/amix-nesting-spike.sh measured that compensating a nested amix by 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 mixGroupMembers sub-mix prefers amix=...: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's amix rejects the normalize option.

Manual mutation check (doc's rule 4): temporarily broke the fallback compensation to memberTracks.length + 1 and 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.sh and packages/engine/src/services/audioMixer.grouping.test.ts existed only on the local plans/audio-mixer-groups docs branch, never merged to origin/main — every step branch in this stack descends from origin/main, so both are brought over here (unmodified, .gitignore included) since this is the step that actually needs them.

HfAudioGroup widened (packages/core/src/audioGroups.ts, from B1)

Added fxChain, automation, volume, hidden — read off the <hf-audio-group> element the same way resolveAudioGroups already reads data-label. audioGroups.test.ts updated for the wider shape (existing toEqual assertions gained the new fields) plus new coverage for the added reads. Extracted buildGroup/parseGroupVolume helpers to keep resolveAudioGroups under 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).
  • New: a group FX chain fully cutting its members leaves an ungrouped sibling track untouched (routing isolation, real headless-Chrome FX render).
  • New: a member's own volume envelope still applies inside a group (member-level processing survives the group path).

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 build clean
  • packages/engine: audioMixer.grouping.test.ts + audioMixer.test.ts + audioMixer.level.test.ts — 36/36 pass (gate per step doc)
  • packages/core full suite: 2366/2366
  • packages/engine full suite: 1532/1532
  • oxfmt/oxlint clean
  • Manual mutation check on the gain-compensation law (see above)

🤖 Generated with Claude Code

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>
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.

1 participant