mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8052f3e68f
commit
e1c9b50948
@@ -70,16 +70,24 @@ function resolveClipTag(clip: ClipManifestClip): string {
|
||||
|
||||
// One `<hf-audio-group>` scan per document, not per clip — resolveAudioGroups
|
||||
// walks the whole tree, and a parse touches every clip in it.
|
||||
const groupLabelCache = new WeakMap<Document, Map<string, string>>();
|
||||
const groupInfoCache = new WeakMap<Document, Map<string, { label: string; volume: number }>>();
|
||||
|
||||
function groupLabelFor(doc: Document | null | undefined, groupId: string): string {
|
||||
if (!doc) return groupId;
|
||||
let labels = groupLabelCache.get(doc);
|
||||
if (!labels) {
|
||||
labels = new Map(resolveAudioGroups(doc).map((group) => [group.id, group.label]));
|
||||
groupLabelCache.set(doc, labels);
|
||||
function groupInfoFor(
|
||||
doc: Document | null | undefined,
|
||||
groupId: string,
|
||||
): { label: string; volume: number } {
|
||||
if (!doc) return { label: groupId, volume: 1 };
|
||||
let info = groupInfoCache.get(doc);
|
||||
if (!info) {
|
||||
info = new Map(
|
||||
resolveAudioGroups(doc).map((group) => [
|
||||
group.id,
|
||||
{ label: group.label, volume: group.volume },
|
||||
]),
|
||||
);
|
||||
groupInfoCache.set(doc, info);
|
||||
}
|
||||
return labels.get(groupId) ?? groupId;
|
||||
return info.get(groupId) ?? { label: groupId, volume: 1 };
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
@@ -156,7 +164,9 @@ export function createTimelineElementFromManifestClip(params: {
|
||||
const audioGroup = hostEl.getAttribute("data-audio-group");
|
||||
if (audioGroup) {
|
||||
entry.audioGroup = audioGroup;
|
||||
entry.audioGroupLabel = groupLabelFor(doc ?? hostEl.ownerDocument, audioGroup);
|
||||
const info = groupInfoFor(doc ?? hostEl.ownerDocument, audioGroup);
|
||||
entry.audioGroupLabel = info.label;
|
||||
entry.audioGroupVolume = info.volume;
|
||||
}
|
||||
const fxChain = hostEl.getAttribute("data-fx-chain");
|
||||
if (fxChain) entry.fxChain = fxChain;
|
||||
@@ -379,7 +389,9 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
|
||||
const domAudioGroup = el.getAttribute("data-audio-group");
|
||||
if (domAudioGroup) {
|
||||
entry.audioGroup = domAudioGroup;
|
||||
entry.audioGroupLabel = groupLabelFor(doc, domAudioGroup);
|
||||
const domGroupInfo = groupInfoFor(doc, domAudioGroup);
|
||||
entry.audioGroupLabel = domGroupInfo.label;
|
||||
entry.audioGroupVolume = domGroupInfo.volume;
|
||||
}
|
||||
|
||||
// Sub-compositions
|
||||
|
||||
Reference in New Issue
Block a user