Files
hyperframes/packages/core/src/runtime
Vance IngallsandClaude Sonnet 5 0d26072e6c feat(studio,core): mute groups, and hear-only-this that cannot reach the export (#3291)
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>
2026-08-21 11:19:53 -07:00
..

Hyperframe Runtime Engine

This folder owns the runtime that powers preview and producer parity.

Current Direction

  • Runtime source of truth is converging on hyperframe.ts.
  • Build produces:
    • dist/hyperframe.runtime.iife.js (browser bootstrap)
    • dist/hyperframe.runtime.mjs (tooling/tests)
    • dist/hyperframe.manifest.json (version + sha256 + artifact map)
  • FE owns iframe runtime injection.
  • BE persists raw generated HTML without injecting runtime scripts.
  • Producer validates pinned runtime checksum from manifest before render.

Runtime Contract (Stable Surface)

Globals:

  • window.__player
  • window.__playerReady
  • window.__renderReady
  • window.__timelines
  • window.__clipManifest

postMessage:

  • parent -> runtime control:
    • source: "hf-parent"
    • type: "control"
    • actions: play, pause, seek, set-muted, set-playback-rate, enable-pick-mode, disable-pick-mode
  • runtime -> parent events:
    • source: "hf-preview"
    • type: "state" and type: "timeline"
    • type: "ready" — emitted once when installRuntimeControlBridge registers the control-message listener. The parent uses it to replay current playback state (set-muted, set-volume, set-playback-rate) so any control message sent before the listener was installed isn't lost. Emitted again on every iframe reload because the new runtime instance starts with no state.

Determinism baseline:

  • renderSeek is the producer-canonical seek path.
  • 30fps quantization and readiness gates are correctness requirements.

Build

bun run --filter @hyperframes/core build:hyperframes-runtime

Security Expectations

  • Runtime bootstrap URL must be version-pinned and host-allowlisted.
  • Iframe bridge payloads must be schema-validated.
  • Unsafe URL schemes (javascript: and unapproved data:) are rejected.
  • Fail closed if runtime bootstrap/handshake is not healthy.

Product Editing Model

  • Primary mode: prompt + element picking.
  • Secondary mode: manual precision controls.
  • Avoid timeline-first manual workflows as default product path.