Files
hyperframes/packages/studio/src/player/components/timelineCallbacks.ts
T
Vance IngallsandClaude Sonnet 5 a11883e6d1 feat(studio,core): a volume and a living meter on the group row
B7: the group bus strip — droppable, and deliberately minimal per the
casual-user design constraints (groups doc §5): a volume slider, a level
bar that moves with the sound, and the words "Too loud" when it clips. No
dB numbers, no peak-hold readout, no routing row.

Transport (core): groupInput() now routes each group through input -> [FX
chain or dry passthrough] -> output -> master, with one AnalyserNode per
group tapped off `output` (post-FX, so the meter reads what the bus
actually outputs) — fftSize 256, level not spectrum. groupLevel(groupId)
returns RMS-ish level 0..1 + a clipped flag off a reused per-group buffer
(no per-frame allocation), or null when the group is idle/unknown. The
runtime posts group-levels messages only while playing, piggybacking the
existing message channel rather than adding a new poll loop.

Studio: groupLevels.ts is a plain pub-sub store (mirrors liveTime.ts's
shape) fed by useTimelinePlayer's message handler via
parseGroupLevelsMessage; useGroupLevel throttles re-renders to ~33ms.
TimelineGroupBusStrip renders in the group row's own `∿` lane area
(STRIP_H, already sized in B2's row-height pipeline) — drag writes live
via onSetAudioGroupAttributeLive, release commits one undo entry via
onSetAudioGroupAttributeQuiet (packages/studio/src/hooks/
timelineAudioGroupVolume.ts, extracted from timelineTrackVisibility.ts to
stay under the 600-line cap; mirrors FxParamRow's live/commit split).
"Too loud" holds for ~2s after the last clipped block, tracked in the
component, not the transport. volumeByGroup mirrors labelByGroup in
useTimelineTrackDerivations.ts so the strip's slider round-trips the
group's own data-volume.

Fixed two pre-existing group-routing tests in webAudioTransport.test.ts
that hardcoded gain-node creation order/count — B7 inserts an extra
`output` gain node between the group's input and master (for the meter to
tap), which shifted node indices the tests asserted on directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 02:12:22 -07:00

100 lines
4.0 KiB
TypeScript

// fallow-ignore-file code-duplication
// fallow-ignore-file dead-code
import type { TimelineElement } from "../store/playerStore";
import type { TimelineMoveOperation } from "../../hooks/timelineMoveAdapter";
import type { BlockedTimelineEditIntent } from "./timelineEditing";
import type { PropertyGroupName } from "@hyperframes/core/gsap-parser";
import type { TimelineKeyframeTarget } from "./timelineKeyframeIdentity";
export interface TimelinePropertyGroupKeyframeToggle {
animationId: string;
propertyGroup: PropertyGroupName;
tweenPercentage: number;
properties: Record<string, number | string>;
remove: boolean;
}
/**
* Shared callback signatures for timeline editing operations.
* Used by NLELayout, Timeline, and any component that passes through
* the standard set of timeline mutation handlers.
*/
export interface TimelineDropCallbacks {
onFileDrop?: (
files: File[],
placement?: { start: number; track: number },
) => Promise<void> | void;
onAssetDrop?: (
assetPath: string,
placement: { start: number; track: number },
) => Promise<void> | void;
onBlockDrop?: (
blockName: string,
placement: { start: number; track: number },
) => Promise<void> | void;
onCompositionDrop?: (
sourcePath: string,
placement: { start: number; track: number },
) => Promise<void> | void;
}
export interface TimelineEditCallbacks {
onMoveElement?: (
element: TimelineElement,
updates: Pick<TimelineElement, "start" | "track">,
) => Promise<void> | void;
/** Atomic multi-clip move (single undo) for main-track ripple + track-insert.
* `coalesceKey` (drag-commit gesture id) merges the move history entry with a
* lane change's follow-up z-reorder entry into one undo step; `coalesceMs`
* widens that entry's fold window when a server round-trip separates the
* gesture's records (per-gesture-unique keys keep the fold gesture-scoped). */
onMoveElements?: (
edits: Array<{ element: TimelineElement; updates: Pick<TimelineElement, "start" | "track"> }>,
coalesceKey?: string,
operation?: TimelineMoveOperation,
coalesceMs?: number,
) => Promise<void> | void;
onResizeElement?: (
element: TimelineElement,
updates: Pick<TimelineElement, "start" | "duration" | "playbackStart">,
) => Promise<void> | void;
onResizeElements?: (
changes: Array<{
element: TimelineElement;
start: number;
duration: number;
playbackStart?: number;
}>,
options?: { coalesceKey?: string },
) => Promise<void> | void;
onToggleTrackHidden?: (track: number, hidden: boolean) => Promise<void> | void;
/** B7's bus strip: live-write the group's own attribute while dragging. */
onSetAudioGroupAttributeLive?: (groupId: string, attr: string, value: string | null) => void;
/** ...and persist one undo entry on release. */
onSetAudioGroupAttributeQuiet?: (
groupId: string,
attr: string,
value: string | null,
label: string,
) => Promise<void>;
onBlockedEditAttempt?: (element: TimelineElement, intent: BlockedTimelineEditIntent) => void;
onSplitElement?: (element: TimelineElement, splitTime: number) => Promise<void> | void;
onRazorSplit?: (element: TimelineElement, splitTime: number) => Promise<void> | void;
onRazorSplitAll?: (splitTime: number) => Promise<void> | void;
onDeleteKeyframe?: (elementId: string, keyframe: TimelineKeyframeTarget) => void;
onDeleteAllKeyframes?: (element: TimelineElement, animationId?: string) => void;
onMoveKeyframeToPlayhead?: (element: TimelineElement, keyframe: TimelineKeyframeTarget) => void;
/** Drag-to-retime: `keyframe` identifies the dragged keyframe (its percentage
* is clip-relative), `toClipPercentage` is the neighbour-clamped drop. */
onMoveKeyframe?: (
elementId: string,
keyframe: TimelineKeyframeTarget,
toClipPercentage: number,
) => Promise<boolean>;
onToggleKeyframeAtPlayhead?: (element: TimelineElement) => void;
onTogglePropertyGroupKeyframe?: (
element: TimelineElement,
target: TimelinePropertyGroupKeyframeToggle,
) => Promise<void> | void;
}