diff --git a/packages/studio/src/components/editor/TimelineFxPopover.tsx b/packages/studio/src/components/editor/TimelineFxPopover.tsx index 949587d34..ab55b62dc 100644 --- a/packages/studio/src/components/editor/TimelineFxPopover.tsx +++ b/packages/studio/src/components/editor/TimelineFxPopover.tsx @@ -58,10 +58,6 @@ export interface TimelineFxPopoverProps { /** Select the target the way clicking it in the timeline does, and ensure * the property panel's Audio FX group is expanded. */ onOpenRack: () => void; - /** Why hovering a preset here will make no sound — a muted bus, a track - * silenced by someone else's solo. Without this the shelf auditions into - * silence and reads as broken rather than as muted. */ - silentReason?: string | null; } export function TimelineFxPopover({ @@ -73,7 +69,6 @@ export function TimelineFxPopover({ onChainPreview, onAuditionTransport, onOpenRack, - silentReason, }: TimelineFxPopoverProps) { const rootRef = useRef(null); const { audition, clearAudition, storedChain } = useFxAudition( @@ -120,11 +115,6 @@ export function TimelineFxPopover({ onKeyDown={onKeyDown} onPointerDown={(event) => event.stopPropagation()} > - {silentReason ? ( -

- {silentReason} -

- ) : null} {/* The list scrolls; the footer below stays put. `min-h-0` is load-bearing — a flex child defaults to min-height:auto and would refuse to shrink, pushing the footer out of the popover instead of scrolling. */} diff --git a/packages/studio/src/components/editor/useAuditionTransport.test.ts b/packages/studio/src/components/editor/useAuditionTransport.test.ts deleted file mode 100644 index 9e9b87fa8..000000000 --- a/packages/studio/src/components/editor/useAuditionTransport.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { auditionStart } from "./useAuditionTransport"; - -const SPANS = [ - { start: 2, duration: 7 }, - { start: 18, duration: 7 }, -]; - -describe("auditionStart", () => { - // Nothing to aim at — the property panel's rack passes no spans, and it keeps - // its old behaviour: play from wherever the author left the playhead. - it("stays put when there are no spans", () => { - expect(auditionStart(undefined, 0)).toBeNull(); - expect(auditionStart([], 0)).toBeNull(); - }); - - // Already inside the clip: moving the playhead here would be the UI taking a - // decision it was not asked for, and it would cost the author their place for - // no gain. - it("stays put when the playhead is already inside a span", () => { - expect(auditionStart(SPANS, 2)).toBeNull(); - expect(auditionStart(SPANS, 8.9)).toBeNull(); - }); - - // The bug this exists for: hovering a preset at 0:00 on a group whose members - // start at 0:02 played silence under the effect. - it("jumps to the next span when the playhead is before or between them", () => { - expect(auditionStart(SPANS, 0)).toBe(2); - expect(auditionStart(SPANS, 9)).toBe(18); - }); - - // Past everything, wrap to the first rather than play out the tail in silence. - it("wraps to the first span when the playhead is past them all", () => { - expect(auditionStart(SPANS, 40)).toBe(2); - }); -}); diff --git a/packages/studio/src/components/editor/useAuditionTransport.ts b/packages/studio/src/components/editor/useAuditionTransport.ts index 150768af7..16975da47 100644 --- a/packages/studio/src/components/editor/useAuditionTransport.ts +++ b/packages/studio/src/components/editor/useAuditionTransport.ts @@ -5,9 +5,8 @@ * while the transport is paused — so a paused author hovering a preset heard * nothing at all and the affordance only worked mid-playback. Extracted from * `useFxLevelling`, where the property panel's rack owned it privately, because - * the timeline's FX popover needs exactly the same behaviour and had none: its - * two call sites passed a preview channel and no transport, so hovering there - * was silent by construction. + * the timeline's FX popover must render the preset shelf "exactly as FxSection + * renders it — same props" (runbook C1 §2) and was passing no transport at all. */ import { useRef } from "react"; @@ -15,33 +14,7 @@ import { useRef } from "react"; // timeline in, and the timeline's FX button imports this hook — a cycle. import { usePlayerStore } from "../../player/store/playerStore"; -/** A clip the audition is meant to be heard through. */ -export interface AuditionSpan { - start: number; - duration: number; -} - -/** - * Where to start playing so the preset is actually audible, or null to stay put. - * - * Playing "from the playhead" is only useful when the thing being auditioned is - * sounding there. A group whose members start at 0:02 and a clip parked at 0:36 - * both play silence under a preset hovered at 0:00 — the transport runs, the - * chain is in the graph, and the author hears the rest of the mix unchanged, - * which reads as the audition being broken. So: inside a span, stay; otherwise - * jump to the next one, or wrap to the first when the playhead is past them all. - */ -export function auditionStart( - spans: readonly AuditionSpan[] | undefined, - at: number, -): number | null { - if (!spans || spans.length === 0) return null; - if (spans.some((span) => at >= span.start && at < span.start + span.duration)) return null; - const starts = spans.map((span) => span.start).sort((a, b) => a - b); - return starts.find((start) => start > at) ?? starts[0] ?? null; -} - -export function useAuditionTransport(): (on: boolean, spans?: readonly AuditionSpan[]) => void { +export function useAuditionTransport(): (on: boolean) => void { /** * Where the playhead was when an audition started the transport, so leaving * can put it back. Null means this audition did not start playback — the @@ -54,15 +27,11 @@ export function useAuditionTransport(): (on: boolean, spans?: readonly AuditionS * that, and stopping their transport because they passed over a preset would * be the UI taking a decision that was not offered to it. */ - return (on: boolean, spans?: readonly AuditionSpan[]): void => { + return (on: boolean): void => { const store = usePlayerStore.getState(); if (on) { if (store.isPlaying || auditionReturn.current !== null) return; auditionReturn.current = store.currentTime; - // Recorded first, so leaving returns to where the author actually was - // rather than to the clip this jumped to. - const from = auditionStart(spans, store.currentTime); - if (from !== null) store.requestSeek(from); store.requestPlayback(true); return; } diff --git a/packages/studio/src/player/components/TimelineFxButton.tsx b/packages/studio/src/player/components/TimelineFxButton.tsx index b6e17ff13..8aa7ba4ce 100644 --- a/packages/studio/src/player/components/TimelineFxButton.tsx +++ b/packages/studio/src/player/components/TimelineFxButton.tsx @@ -18,10 +18,7 @@ import { } from "@hyperframes/core/audio-fx"; import type { HfAudioNameKind } from "@hyperframes/core/audio-carve"; import { TimelineFxPopover } from "../../components/editor/TimelineFxPopover.js"; -import { - useAuditionTransport, - type AuditionSpan, -} from "../../components/editor/useAuditionTransport.js"; +import { useAuditionTransport } from "../../components/editor/useAuditionTransport.js"; function parseFxChainOrEmpty(raw: string | undefined): HfAudioFxChain { if (!raw) return { version: 1, nodes: [] }; @@ -39,12 +36,6 @@ interface TimelineFxButtonChainProps { fxChainRaw: string | undefined; onChainChange: (next: HfAudioFxChain) => void; onChainPreview?: (next: HfAudioFxChain) => void; - /** The clips this chain is heard through, so an audition can start where they - * actually sound instead of playing silence from a playhead parked before - * the first one. */ - auditionSpans?: readonly AuditionSpan[]; - /** Why an audition here will be silent (excluded by someone else's solo). */ - silentReason?: string | null; /** Whether this target is muted right now. */ isMuted?: boolean; /** Set this target's mute on the running graph WITHOUT touching the document, @@ -161,10 +152,9 @@ export function TimelineFxButton(props: TimelineFxButtonProps) { if (on) borrowedMute.current = props.isMuted === true; if (borrowedMute.current) props.onSetMutedLive?.(!on); if (!on) borrowedMute.current = false; - transport(on, props.auditionSpans); + transport(on); }} onOpenRack={props.onOpenRack} - silentReason={props.silentReason} />, document.body, )} diff --git a/packages/studio/src/player/components/TimelineGroupBusStrip.tsx b/packages/studio/src/player/components/TimelineGroupBusStrip.tsx index 73a065b54..9944e61e5 100644 --- a/packages/studio/src/player/components/TimelineGroupBusStrip.tsx +++ b/packages/studio/src/player/components/TimelineGroupBusStrip.tsx @@ -73,10 +73,10 @@ export function TimelineGroupBusStrip({ style={{ top: TRACK_H, height: STRIP_H }} > {/* Named, because unnamed it reads as an unexplained slider next to an - empty capsule: the row opens off a control labelled "lanes", so the - first question it has to answer is what it IS. Two words, no dB and no - numeric readout — that part of the casual-user rule stands. */} - Bus level + empty capsule. "Volume" is the design mockup's own label (groups doc + §5) — B7's list is slider, bar, "Holds …", "⚠ Too loud" and NOTHING + else, and the vocabulary rule bans "bus" from the product outright. */} + Volume
- {clipped && Too loud} + {clipped && ⚠ Too loud} {holdsText} diff --git a/packages/studio/src/player/components/TimelineGroupHeader.tsx b/packages/studio/src/player/components/TimelineGroupHeader.tsx index 62b2fa5d9..9c17d6931 100644 --- a/packages/studio/src/player/components/TimelineGroupHeader.tsx +++ b/packages/studio/src/player/components/TimelineGroupHeader.tsx @@ -3,7 +3,6 @@ import type { HfAudioFxChain } from "@hyperframes/core/audio-fx"; import { TRACK_H } from "./timelineLayout"; import type { TimelineTheme } from "./timelineTheme"; import { TimelineFxButton } from "./TimelineFxButton"; -import type { AuditionSpan } from "../../components/editor/useAuditionTransport.js"; interface TimelineGroupHeaderProps { label: string; @@ -28,10 +27,6 @@ interface TimelineGroupHeaderProps { fxChain?: string; onFxChainChange: (next: HfAudioFxChain) => void; onFxChainPreview?: (next: HfAudioFxChain) => void; - /** Member clips, so hovering a preset auditions where the group sounds. */ - auditionSpans?: readonly AuditionSpan[]; - /** Why an audition here will be silent (excluded by someone else's solo). */ - silentReason?: string | null; /** Set the group mute on the running graph only, so an audition can lift it. */ onSetMutedLive?: (muted: boolean) => void; onOpenFxRack: () => void; @@ -59,8 +54,6 @@ export function TimelineGroupHeader({ fxChain, onFxChainChange, onFxChainPreview, - auditionSpans, - silentReason, onSetMutedLive, onOpenFxRack, columnWidth, @@ -156,8 +149,6 @@ export function TimelineGroupHeader({ fxChainRaw={fxChain} onChainChange={onFxChainChange} onChainPreview={onFxChainPreview} - auditionSpans={auditionSpans} - silentReason={silentReason} isMuted={hidden} onSetMutedLive={onSetMutedLive} onOpenRack={onOpenFxRack} diff --git a/packages/studio/src/player/components/TimelineGroupRow.tsx b/packages/studio/src/player/components/TimelineGroupRow.tsx index df8e1cf26..171244f56 100644 --- a/packages/studio/src/player/components/TimelineGroupRow.tsx +++ b/packages/studio/src/player/components/TimelineGroupRow.tsx @@ -86,12 +86,6 @@ export function TimelineGroupRow({ // reading (and rendering) as muted throughout. const setGroupMutedLive = (muted: boolean) => onSetAudioGroupAttributeLive?.(group.id, "data-hidden", muted ? "" : null); - // Solo is not ours to borrow the same way — it is a statement about every - // other track, and lifting it would silence the one the author soloed. Say so - // instead, or the shelf auditions into silence and reads as broken. - const silencedBySolo = - soloed.size > 0 && !soloed.has(group.id) && !memberIds.some((id) => soloed.has(id)); - const silentReason = silencedBySolo ? "Another track is soloed — presets here are silent." : null; const openGroupFxRack = () => { const target = domEditActions?.previewIframeRef.current?.contentDocument?.getElementById( group.id, @@ -139,8 +133,6 @@ export function TimelineGroupRow({ fxChain={group.fxChain} onFxChainChange={(next) => writeGroupFxChain(next, false)} onFxChainPreview={(next) => writeGroupFxChain(next, true)} - auditionSpans={memberElements} - silentReason={silentReason} onSetMutedLive={setGroupMutedLive} onOpenFxRack={openGroupFxRack} // Same width as every other row's header. The group row needs a real diff --git a/packages/studio/src/player/components/TimelineTrackHeader.tsx b/packages/studio/src/player/components/TimelineTrackHeader.tsx index 9b470c931..0f90e1603 100644 --- a/packages/studio/src/player/components/TimelineTrackHeader.tsx +++ b/packages/studio/src/player/components/TimelineTrackHeader.tsx @@ -490,23 +490,13 @@ export function TimelineTrackHeader({ trackKind={classifyAudioName(singleAudioClip.id, singleAudioClip.src)} onChainChange={(next) => writeClipFxChain(singleAudioClip, next, false)} onChainPreview={(next) => writeClipFxChain(singleAudioClip, next, true)} - auditionSpans={[singleAudioClip]} - // Mute is borrowed for the hover and put back (see the group row); - // solo is not ours to lift, so it gets said out loud instead. + // Muted, an audition is silent — so the hover lifts the mute on + // the running graph and puts it back on the way out, the same + // borrow-and-return it already does with the playhead. isMuted={isTrackHidden} onSetMutedLive={(muted) => onSetElementAttributeLive?.(singleAudioClip, "data-hidden", muted ? "" : null) } - // The clip's own mute is borrowed above. A mute that lives - // somewhere else — the bus this clip hangs off, or another - // track's solo — is not this row's to lift, so it gets said. - silentReason={ - singleAudioClip.audioGroupHidden - ? "This clip's group is muted — unmute the group to hear presets." - : soloed.size > 0 && !(soloTargetId !== null && soloed.has(soloTargetId)) - ? "Another track is soloed — presets here are silent." - : null - } onOpenRack={() => openClipFxRack(singleAudioClip)} /> )}