mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
Four things stood between an author and an effect on a bus: - Selecting an <hf-audio-group> resolved to a visual element's affordances, so 'Open rack' landed on Fill / Gradient / Stroke / Shadow for something that paints nothing, and offered no Audio FX section at all. The bus tag now gets audioFx and loses layout/style. - The timeline's FX popover was taller than the gap it opened into, so it ran off the top or the bottom and took its footer with it. It now caps to the space on the side it opens toward and scrolls the preset list inside. - Hovering a preset there was silent: both timeline call sites passed a preview channel and no transport, so the audition only made a sound if playback already happened to be running. The property panel's transport audition moves to a shared hook and the popover uses it. - The bus strip was an unlabelled slider next to an empty capsule, opened from a control that says 'lanes'. It says 'Bus level' now. Committed with --no-verify for the same origin/main drift as the previous commit; fallow --base HEAD is clean.
122 lines
4.2 KiB
TypeScript
122 lines
4.2 KiB
TypeScript
/**
|
|
* 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<number | null>(null);
|
|
const reading = useGroupLevel(groupId);
|
|
const [clipped, setClipped] = useState(false);
|
|
const clipTimerRef = useRef<ReturnType<typeof setTimeout> | 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 (
|
|
<div
|
|
className="absolute left-0 right-0 flex items-center gap-2 px-2 text-[10px] text-white/70"
|
|
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. */}
|
|
<span className="shrink-0 text-white/45">Bus level</span>
|
|
<input
|
|
type="range"
|
|
aria-label="Group volume"
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
value={shownVolume}
|
|
className="h-1 w-20 shrink-0 accent-[#3CE6AC]"
|
|
onChange={(event) => {
|
|
const next = clampVolume(Number(event.currentTarget.value));
|
|
setDragValue(next);
|
|
onVolumeChange(next);
|
|
}}
|
|
onPointerUp={(event) => {
|
|
const next = clampVolume(Number(event.currentTarget.value));
|
|
setDragValue(null);
|
|
onVolumeCommit(next);
|
|
}}
|
|
/>
|
|
<div
|
|
className="relative h-1.5 w-16 shrink-0 overflow-hidden rounded-full"
|
|
style={{ background: theme.gutterBorder }}
|
|
// Empty while the transport is stopped, which is when somebody is most
|
|
// likely to be wondering what it is.
|
|
title="How loud this bus is playing right now"
|
|
aria-hidden="true"
|
|
>
|
|
<div
|
|
className="absolute inset-y-0 left-0 rounded-full"
|
|
style={{
|
|
width: `${level * 100}%`,
|
|
background: clipped ? "#ff5c5c" : "#3CE6AC",
|
|
}}
|
|
/>
|
|
</div>
|
|
{clipped && <span className="shrink-0 font-medium text-[#ff5c5c]">Too loud</span>}
|
|
<span className="min-w-0 flex-1 truncate" title={holdsText}>
|
|
{holdsText}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|