Skip to content

feat(studio,core): mute groups, and hear-only-this that cannot reach the export - #3291

Open
vanceingalls wants to merge 1 commit into
wa-23g-bus-stripfrom
wa-23e-group-mute-solo
Open

feat(studio,core): mute groups, and hear-only-this that cannot reach the export#3291
vanceingalls wants to merge 1 commit into
wa-23g-bus-stripfrom
wa-23e-group-mute-solo

Conversation

@vanceingalls

Copy link
Copy Markdown
Collaborator

B5 — mute and solo, on groups and tracks

Stacks on and depends on:

Group mute

Persisted as data-hidden on the <hf-audio-group> element itself (never written onto members — design doc §2.1's state-restoration warning). Studio action reuses B7's generic setAudioGroupAttribute (setQuiet/setLive) rather than duplicating toggleTimelineTrackHidden's shape. Render: B4 already drops every member of a data-hidden group — confirmed by a new audioMixer.test.ts case, no production change needed. Preview: a dedicated muteGain node (groupInput -> [fx] -> muteGain -> output -> master) so a mute toggle never fights scheduleVolumeLane's ramps on the same param. Mid-playback toggles sync via a new syncAudioGroupMute pass in init.ts (a group carries no data-start, so it's invisible to the existing visibility-node query). Members of a muted group get the strikethrough label treatment, display only.

Solo — "Hear only this"

New session-only store slice (audioSoloSlice, soloed: ReadonlySet<string> of clip/group ids, never track numbers, never serialized). Shared predicate isAudibleUnderSolo (packages/core/src/audioGroups.ts): audible only if the element or its own group is soloed. "Siblings, never ancestors" lives in the graph — solo gain is a per-element stage only, group buses are never attenuated by solo. Preview: dedicated per-element soloGain in webAudioTransport.ts, pushed via window.__hf.setAudioSolo (a direct call, not an attribute write). media.ts's HTMLMedia fallback folds the same predicate into its per-tick volume computation. Half-lit group indicator for "not soloed itself, but a member is." Exclusive-by-default toggle, ⌘/Ctrl-click to add/remove. Transport-bar banner ("Hearing only <label> — your export is not affected", Clear button).

Export-safety

The most important property: toggling/adding/clearing solo never calls setAttribute/removeAttribute on any element and never invokes the project save path — both asserted directly via spies in audioSoloSlice.test.ts. Solo cannot reach an export by construction.

Incidental fixes

  • Extracted useHydrateActiveCompPathFromUrl out of App.tsx (pre-existing, unrelated effect) to stay under the 600-line filesize cap after wiring the solo bridge in.
  • Fixed a circular dependency the solo-banner wiring introduced: useAudioSoloBridge.ts now imports usePlayerStore from its concrete module instead of the player/ barrel (which re-exports PlayerControls.tsx — that's what closed the cycle).

Gates

  • bun run build clean
  • packages/core full suite: 2379/2379
  • packages/studio full suite: 4276/4294 (18 pre-existing todo)
  • packages/engine audioMixer.grouping.test.ts: 5/5
  • oxfmt/oxlint clean on all 23 touched files
  • fallow clean (0 new circular deps, 0 new filesize/complexity findings)

🤖 Generated with Claude Code

…the export

B5: mute and solo, on groups and tracks (track mute already shipped by A2 —
nothing to build there).

Group mute — persisted as data-hidden on the <hf-audio-group> element itself
(never written onto members, per design doc §2.1's state-restoration
warning). Studio action reuses B7's generic setAudioGroupAttribute
(setQuiet/setLive split) rather than duplicating toggleTimelineTrackHidden's
shape — same one-atomic-patch/one-undo-entry contract, already built for
exactly this purpose. Render: B4 already drops every member of a
data-hidden group (confirmed by a new audioMixer.test.ts case — no
production change needed there). Preview: a dedicated muteGain node
(groupInput -> [fx] -> muteGain -> output -> master) so a mute toggle
never fights scheduleVolumeLane's ramps on the same param — the same
hazard B7's volume fader was split out to avoid. Mid-playback toggles
sync via a new syncAudioGroupMute pass in init.ts (a group carries no
data-start, so it's invisible to the existing visibility-node query).
Members of a muted group render the strikethrough label treatment
(TimelineTrackPlainHeader's isGroupMuted, sourced from
TimelineElement.audioGroupHidden) — display only, no attribute touched.

Solo — "Hear only this": a new session-only store slice (audioSoloSlice,
soloed: ReadonlySet<string> of clip/group ids, never track numbers, never
serialized). Predicate (isAudibleUnderSolo, packages/core/src/audioGroups.ts
so both the store and the preview transport share one definition): an
element is audible while any solo is active only if it or its own group is
soloed. "Siblings, never ancestors" lives in the graph, not the predicate —
solo gain is a per-element stage only; group buses are never attenuated by
solo, so a soloed member's path through its group stays open by
construction. Preview: a dedicated per-element soloGain in
webAudioTransport.ts (parallel to the mute mechanics), pushed via
window.__hf.setAudioSolo — a direct call, not an attribute write, so it
can't ride the visibility-diff path mute uses. media.ts's HTMLMedia
fallback folds the same predicate into its per-tick volume computation
(the same seam A2 used for data-hidden). Half-lit group indicator
(isGroupHalfLitUnderSolo) for "not soloed itself, but a member is".
Exclusive-by-default toggle, ⌘/Ctrl-click to add/remove, TimelineSoloButton
(⌗) beside mute on both track and group headers. Transport-bar banner
("Hearing only <label> — your export is not affected", Clear button) added
in PlayerControls.tsx, reading labels straight off the live preview DOM.

Export-safety, the most important property here: toggling/adding/clearing
solo never calls setAttribute/removeAttribute on any element and never
invokes the project save path (both asserted directly via spies in
audioSoloSlice.test.ts) — solo cannot reach an export by construction, not
by convention.

Also: extracted useHydrateActiveCompPathFromUrl out of App.tsx (a
pre-existing, unrelated effect) to stay under the 600-line filesize cap
after wiring useAudioSoloBridge in; and fixed a circular dependency the
solo-banner wiring introduced (useAudioSoloBridge.ts now imports
usePlayerStore from its concrete module instead of the player/ barrel,
which re-exports PlayerControls.tsx — the barrel path is what closed the
cycle).

Gates: bun run build clean; packages/core full suite 2379/2379; packages/
studio full suite 4276/4294 (18 pre-existing todo); packages/engine
audioMixer.grouping.test.ts 5/5; oxfmt/oxlint clean on all 23 touched
files; fallow clean (0 new circular deps, 0 new filesize/complexity
findings).

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