mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix(studio,core,engine): close the defects a max-effort review found in the fixes
A review of the five fix commits found eleven real defects, including a regression one of them introduced. Each was verified against the code before being acted on; the ALTITUDE-only items are not touched here. REGRESSION, from "group rows survive a collapse". Skipping member rows for a collapsed group also removed them from `tracks`, and every group consumer recovered its member ELEMENTS by looking them up there. Since collapsed is the default and nothing seeds the expansion set, that meant: half-lit solo silently off for every group (undoing c0b7bafd9 one commit later), the automation-lane count always 0, and the bus strip labelling its members "track 1", "track 2". Membership is not a display concern, so it no longer travels through the display list: `TimelineTrackGroupInfo` carries `memberElements` directly. Group bus. `reanchor` wrote `fader.gain.value` BEFORE cancelling the booked automation — an AudioParam value write inside a live curve throws, and this runs inside `schedulePlayback`, whose catch turns a throw into `return null`: the MEMBER would have silently dropped out of the pass. Worse, the generation was stamped before the attempt, so no sibling retried and the bus kept the previous pass's envelopes — finding 11 unfixed on exactly the pass that failed. Now: clear first, stamp only on success, and isolate the call. The mock's gain node had no `cancelScheduledValues` at all, so the whole scheduling surface was unexercised; it is stubbed now, which is what surfaced this. `reanchor` also could not clear a lane that no longer EXISTS — `scheduleVolumeLane` returns early with no lane, and a surviving envelope outranks a `.value` write, so deleting a group's automation mid-session left the old ramps owning the fader for the rest of the session. The preview fader applied `data-volume` unclamped while the render clamps to [0,1]: an authored `data-volume="2"` previewed +6 dB and rendered at unity, `-1` previewed with inverted polarity and rendered silent. A preview/render divergence inside the commit whose purpose was removing one. Pitch shift. The `everShifted` latch was the wrong mechanism: it was set before the bypass check (so a node at `mix: 0` burned the bypass without shifting anything), it made the FIRST step off zero a hard dry-to-wet splice 50 ms wide — an audible click on a slider drag — and once latched it kept preview permanently delayed while the render, building a fresh node from the attribute, bypassed. Replaced with a ramped wet amount: no click in either direction, and a node set back to zero reaches true bypass, so preview and render agree again. Silent no-ops. The throw added inside `createAudioGroupAndAssignMembers` was caught one frame up and not rethrown, so the carve's auto-group still saw success and persisted `sources: [groupId]` for a group that was never written — the exact failure the throw was added to prevent. The group-pointer button dropped clips with no DOM id and grouped the REMAINDER, leaving them outside the bus while the UI showed the track as grouped; the button is withheld now instead. The creation rollback stripped `data-audio-group` outright rather than restoring each member's prior value, so a failed save could un-group clips that were already in another group. `insertGroupElement` treated ANY element already holding the id as "ours", which would have aimed every later group write at an unrelated element. `setAudioMuteHidden` rescheduled Web Audio mid-play without `stopAll()`. Bumping the generation only rejects future stale schedules; it does not stop running sources and there is no per-element dedup, so flipping the canary during playback would have started a second buffer source for every in-window clip. `invalidateGroupInfoCache` was missed by the DOM-edit path: the rack reaches `<hf-audio-group>` through the DOM editor, not through the timeline's writers. Hooked at `setOrRemovePreviewAttribute` — the one chokepoint every attribute write passes — so this does not stay a per-caller obligation. Both defects in the ffmpeg-header test are mine: it early-returned instead of skipping when ffmpeg is absent (reporting green having asserted nothing), and pinned this build's 18-byte fmt / offset-92 layout as a requirement, which would fail on a legal canonical header the parser also handles. Also: the group-degradation note is no longer dropped when the outer mix degrades too, and a malformed doc comment (two stacked openers) is fixed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
07203f59d1
commit
7393095be8
@@ -1,4 +1,3 @@
|
||||
import type { TimelineElement } from "../store/playerStore";
|
||||
import { usePlayerStore } from "../store/playerStore";
|
||||
import { isGroupHalfLitUnderSolo } from "../store/audioSoloSlice";
|
||||
import { runtimeAudioId } from "../lib/timelineElementHelpers";
|
||||
@@ -23,7 +22,6 @@ interface TimelineGroupRowProps {
|
||||
rowKey: number;
|
||||
group: TimelineTrackGroupInfo;
|
||||
logicalRow: TimelineLogicalRow;
|
||||
tracks: readonly (readonly [number, readonly TimelineElement[]])[];
|
||||
top: number;
|
||||
height: number;
|
||||
virtualized: boolean;
|
||||
@@ -42,7 +40,6 @@ export function TimelineGroupRow({
|
||||
rowKey,
|
||||
group,
|
||||
logicalRow,
|
||||
tracks,
|
||||
top,
|
||||
height,
|
||||
virtualized,
|
||||
@@ -54,11 +51,13 @@ export function TimelineGroupRow({
|
||||
toggleGroupExpanded,
|
||||
toggleLaneOwnerExpanded,
|
||||
}: TimelineGroupRowProps) {
|
||||
const memberElements = group.memberTracks.flatMap(
|
||||
(track) => tracks.find(([t]) => t === track)?.[1] ?? [],
|
||||
);
|
||||
// From the group, NOT from `tracks`: a collapsed group emits no member rows
|
||||
// into the display list, and every one of these reads silently degraded to
|
||||
// empty in that (default) state — half-lit solo went dark, the lane count
|
||||
// read 0, and the bus strip fell back to "track 1", "track 2".
|
||||
const memberElements = group.memberElements;
|
||||
const memberLabels = group.memberTracks.map((track, i) => {
|
||||
const owner = tracks.find(([t]) => t === track)?.[1]?.find((el) => el.audioGroup);
|
||||
const owner = memberElements.find((el) => el.track === track && el.audioGroup);
|
||||
return owner?.label ?? owner?.id ?? `track ${i + 1}`;
|
||||
});
|
||||
const isLaneOpen = expandedLaneOwnerIds.has(group.id);
|
||||
|
||||
@@ -163,7 +163,6 @@ export function TimelineLanes({
|
||||
rowKey={rowKey}
|
||||
group={group}
|
||||
logicalRow={groupLogicalRow}
|
||||
tracks={tracks}
|
||||
top={rowGeometry.getRowTop(row)}
|
||||
height={rowGeometry.getRowHeight(row)}
|
||||
virtualized={rowsVirtualized}
|
||||
|
||||
@@ -413,14 +413,22 @@ export function TimelineTrackHeader({
|
||||
const openClipFxRack = (clip: TimelineElement) => {
|
||||
void domEditActions?.handleTimelineElementSelect(clip);
|
||||
};
|
||||
// DOM ids, matching the carve picker's other caller — membership is read back
|
||||
// by `resolveAudioGroups`, which only ever sees the document. A clip with no
|
||||
// DOM id cannot be a member (resolveAudioGroups skips it), so a track holding
|
||||
// one cannot be grouped WHOLE — and grouping the rest would quietly leave
|
||||
// those clips outside the bus, past every fader, mute and effect, while the
|
||||
// UI showed the track as grouped. The button is withheld instead of acting on
|
||||
// a subset, which is also why the carve path's loud guard cannot catch this:
|
||||
// the unresolvable ids were filtered out before the call.
|
||||
const groupableClipIds = trackElements.map(runtimeAudioId);
|
||||
const canGroupWholeTrack =
|
||||
groupableClipIds.length >= 2 && groupableClipIds.every((id) => id !== null);
|
||||
const groupUngroupedClips = () => {
|
||||
const doc = domEditActions?.previewIframeRef.current?.contentDocument;
|
||||
if (!doc || !onGroupClips) return;
|
||||
// DOM ids, matching the carve picker's other caller — membership is read
|
||||
// back by `resolveAudioGroups`, which only ever sees the document.
|
||||
const clipIds = trackElements.map(runtimeAudioId).filter((id): id is string => id !== null);
|
||||
if (clipIds.length < 2) return;
|
||||
void onGroupClips(clipIds, mintGroupId(doc));
|
||||
if (!canGroupWholeTrack) return;
|
||||
void onGroupClips(groupableClipIds as string[], mintGroupId(doc));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -471,6 +479,7 @@ export function TimelineTrackHeader({
|
||||
{isAudioTrack &&
|
||||
clipCount > 1 &&
|
||||
!isTrackGrouped &&
|
||||
canGroupWholeTrack &&
|
||||
isCanaryEnabled("audio-fx-rack") &&
|
||||
isCanaryEnabled("audio-groups") && (
|
||||
<TimelineFxButton variant="group-pointer" onGroupClips={groupUngroupedClips} />
|
||||
|
||||
@@ -315,8 +315,10 @@ export function buildTimelineLogicalRows({
|
||||
items: [],
|
||||
});
|
||||
if (expandedLaneOwnerIds.has(group.id)) {
|
||||
const memberElements = group.memberTracks.flatMap((track) => trackMap.get(track) ?? []);
|
||||
for (const laneGroup of groupAutomationLanes(memberElements)) {
|
||||
// The group's own member list, not `trackMap`: a COLLAPSED group can have
|
||||
// its lane shelf open, and its members are absent from the display list —
|
||||
// so looking them up there emitted zero lane rows for exactly that case.
|
||||
for (const laneGroup of groupAutomationLanes(group.memberElements)) {
|
||||
rows.push({
|
||||
id: `${groupRowId}::${laneGroup.key}`,
|
||||
kind: "row",
|
||||
|
||||
@@ -16,6 +16,17 @@ export interface TimelineTrackGroupInfo {
|
||||
anchorKey: number;
|
||||
/** Member track numbers, ascending. */
|
||||
memberTracks: number[];
|
||||
/**
|
||||
* Every clip under this group, in member-track order — INDEPENDENT of whether
|
||||
* the group is expanded.
|
||||
*
|
||||
* Collapsing a group stops emitting its member rows into `tracks`, so anything
|
||||
* that recovered member elements by looking them up there got an empty list in
|
||||
* the default (collapsed) state — silently disabling half-lit solo, the
|
||||
* automation-lane count, and the bus strip's member labels. Membership is not
|
||||
* a display concern, so it does not travel through the display list.
|
||||
*/
|
||||
memberElements: TimelineElement[];
|
||||
/** The group element's `data-volume`, mirrored from a member's parse (B7's slider). */
|
||||
volume: number;
|
||||
/** The group element's `data-hidden`, mirrored from a member's parse (B5's group mute). */
|
||||
@@ -70,6 +81,7 @@ function buildGroupInfo(
|
||||
groupId: string,
|
||||
fallbackTrackNum: number,
|
||||
membership: GroupMembership,
|
||||
rawByTrack: ReadonlyMap<number, TimelineElement[]>,
|
||||
): TimelineTrackGroupInfo {
|
||||
const memberTracks = [...(membership.memberTracksByGroup.get(groupId) ?? [])].sort(
|
||||
(a, b) => a - b,
|
||||
@@ -80,6 +92,7 @@ function buildGroupInfo(
|
||||
label: membership.labelByGroup.get(groupId) ?? groupId,
|
||||
anchorKey: (memberTracks[0] ?? fallbackTrackNum) - 0.5,
|
||||
memberTracks,
|
||||
memberElements: memberTracks.flatMap((track) => rawByTrack.get(track) ?? []),
|
||||
volume: membership.volumeByGroup.get(groupId) ?? 1,
|
||||
hidden: membership.hiddenByGroup.get(groupId) ?? false,
|
||||
...(fxChain ? { fxChain } : {}),
|
||||
@@ -139,7 +152,7 @@ function groupTimelineTracks(
|
||||
}
|
||||
if (emitted.has(groupId)) continue;
|
||||
emitted.add(groupId);
|
||||
const info = buildGroupInfo(groupId, trackNum, membership);
|
||||
const info = buildGroupInfo(groupId, trackNum, membership, rawByTrack);
|
||||
groups.push(info);
|
||||
emitGroupRows(info, rawByTrack, trackGroupOf, tracks, expandedGroupIds.has(groupId));
|
||||
}
|
||||
|
||||
@@ -88,6 +88,18 @@ describe("collapsed audio groups", () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
// Membership is not a display concern. Half-lit solo, the automation-lane
|
||||
// count and the bus strip's member labels all read the group's members, and
|
||||
// all three silently degraded to empty when those were recovered from the
|
||||
// display list — which a collapsed group does not appear in. Collapsed is the
|
||||
// default, so that was every group until someone opened it.
|
||||
it("carries its member elements even while collapsed", () => {
|
||||
const { layout, unmount } = renderGrouped();
|
||||
expect(layout.trackOrder).toEqual([-0.5]); // collapsed: no member rows
|
||||
expect(layout.groups[0]!.memberElements.map((el) => el.id)).toEqual(["voice-1", "voice-2"]);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("emits the member rows once the group is expanded", () => {
|
||||
usePlayerStore.setState({ expandedGroupIds: new Set(["voiceover"]) });
|
||||
const { layout, unmount } = renderGrouped();
|
||||
|
||||
Reference in New Issue
Block a user