/** * B7: the group's own volume slider + a living level bar + "Holds …" — the * bus, not the mechanism. No dB numbers, no peak-hold readout, no routing * row (groups doc §5, casual-user section) — a slider, a bar that moves with * the sound, and the words "⚠ Too loud" when it clips. */ import { useEffect, useRef, useState } from "react"; import { useGroupLevel } from "../../hooks/useGroupLevel"; import { STRIP_H, TRACK_H } from "./timelineLayout"; import type { TimelineTheme } from "./timelineTheme"; /** How long "Too loud" stays lit after the last clipped block. */ const CLIP_HOLD_MS = 2000; /** * Unity is the ceiling because unity is what the pipeline honours: the render * puts every track volume through its own `clampVolume` ([0,1]) before building * the filter, and the preview bus clamps to match. A fader travelling to 2.0 * therefore spent its top half writing `data-volume` values that BOTH ends * discard — the control promised +6 dB and nothing delivered it. * * Raising the ceiling instead would mean changing the render's shared clamp for * every track, not just group buses; that is a mixer decision, not a slider one. */ function clampVolume(value: number): number { return Math.min(1, Math.max(0, value)); } interface TimelineGroupBusStripProps { groupId: string; volume: number; memberLabels: readonly string[]; onVolumeChange: (value: number) => void; onVolumeCommit: (value: number) => void; theme: TimelineTheme; } export function TimelineGroupBusStrip({ groupId, volume, memberLabels, onVolumeChange, onVolumeCommit, theme, }: TimelineGroupBusStripProps) { const [dragValue, setDragValue] = useState(null); const reading = useGroupLevel(groupId); const [clipped, setClipped] = useState(false); const clipTimerRef = useRef | null>(null); useEffect(() => { if (!reading?.clipped) return; setClipped(true); if (clipTimerRef.current) clearTimeout(clipTimerRef.current); clipTimerRef.current = setTimeout(() => setClipped(false), CLIP_HOLD_MS); }, [reading?.clipped]); useEffect( () => () => { if (clipTimerRef.current) clearTimeout(clipTimerRef.current); }, [], ); const shownVolume = dragValue ?? volume; const level = Math.min(1, reading?.level ?? 0); const holdsText = memberLabels.length > 0 ? `Holds ${memberLabels.join(", ")}` : "Holds nothing yet"; return (
{/* Named, because unnamed it reads as an unexplained slider next to an 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 { const next = clampVolume(Number(event.currentTarget.value)); setDragValue(next); onVolumeChange(next); }} onPointerUp={(event) => { const next = clampVolume(Number(event.currentTarget.value)); setDragValue(null); onVolumeCommit(next); }} /> ); }