feat(studio,core): mute groups, and hear-only-this that cannot reach the export - #3291
Open
vanceingalls wants to merge 1 commit into
Open
feat(studio,core): mute groups, and hear-only-this that cannot reach the export#3291vanceingalls wants to merge 1 commit into
vanceingalls wants to merge 1 commit into
Conversation
…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>
7 tasks
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.
B5 — mute and solo, on groups and tracks
Stacks on and depends on:
Group mute
Persisted as
data-hiddenon the<hf-audio-group>element itself (never written onto members — design doc §2.1's state-restoration warning). Studio action reuses B7's genericsetAudioGroupAttribute(setQuiet/setLive) rather than duplicatingtoggleTimelineTrackHidden's shape. Render: B4 already drops every member of adata-hiddengroup — confirmed by a newaudioMixer.test.tscase, no production change needed. Preview: a dedicatedmuteGainnode (groupInput -> [fx] -> muteGain -> output -> master) so a mute toggle never fightsscheduleVolumeLane's ramps on the same param. Mid-playback toggles sync via a newsyncAudioGroupMutepass ininit.ts(a group carries nodata-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 predicateisAudibleUnderSolo(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-elementsoloGaininwebAudioTransport.ts, pushed viawindow.__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/removeAttributeon any element and never invokes the project save path — both asserted directly via spies inaudioSoloSlice.test.ts. Solo cannot reach an export by construction.Incidental fixes
useHydrateActiveCompPathFromUrlout ofApp.tsx(pre-existing, unrelated effect) to stay under the 600-line filesize cap after wiring the solo bridge in.useAudioSoloBridge.tsnow importsusePlayerStorefrom its concrete module instead of theplayer/barrel (which re-exportsPlayerControls.tsx— that's what closed the cycle).Gates
bun run buildcleanpackages/corefull suite: 2379/2379packages/studiofull suite: 4276/4294 (18 pre-existing todo)packages/engineaudioMixer.grouping.test.ts: 5/5🤖 Generated with Claude Code