diff --git a/packages/studio/src/player/components/TimelineGroupRow.tsx b/packages/studio/src/player/components/TimelineGroupRow.tsx index 5d88b4a6c..7dfc0526a 100644 --- a/packages/studio/src/player/components/TimelineGroupRow.tsx +++ b/packages/studio/src/player/components/TimelineGroupRow.tsx @@ -1,6 +1,7 @@ import type { TimelineElement } from "../store/playerStore"; import { usePlayerStore } from "../store/playerStore"; import { isGroupHalfLitUnderSolo } from "../store/audioSoloSlice"; +import { runtimeAudioId } from "../lib/timelineElementHelpers"; import { HF_AUDIO_FX_ATTR, serializeAudioFxChain, @@ -65,7 +66,10 @@ export function TimelineGroupRow({ const domEditActions = useDomEditActionsContextOptional(); const soloed = usePlayerStore((s) => s.soloed); const toggleSolo = usePlayerStore((s) => s.toggleSolo); - const memberIds = memberElements.map((el) => el.key ?? el.id); + // Bare DOM ids: this list is compared against the `soloed` set, which the + // runtime matches on `el.id` (see `runtimeAudioId`). Store keys here made the + // half-lit state unreachable — soloing a member lit nothing on its group. + const memberIds = memberElements.map(runtimeAudioId).filter((id): id is string => id !== null); const writeGroupFxChain = (next: HfAudioFxChain, live: boolean) => { const value = next.nodes.length ? serializeAudioFxChain(next) : null; if (live) onSetAudioGroupAttributeLive?.(group.id, HF_AUDIO_FX_ATTR, value); diff --git a/packages/studio/src/player/lib/runtimeAudioId.test.ts b/packages/studio/src/player/lib/runtimeAudioId.test.ts index f3f5d65ef..9c9150b10 100644 --- a/packages/studio/src/player/lib/runtimeAudioId.test.ts +++ b/packages/studio/src/player/lib/runtimeAudioId.test.ts @@ -15,6 +15,7 @@ import { describe, expect, it } from "vitest"; import { audioGroupOf, isAudibleUnderSolo, + isGroupHalfLitUnderSolo, resolveAudioGroups, } from "@hyperframes/core/audio-groups"; import { parseTimelineFromDOM } from "./timelineDOM"; @@ -95,6 +96,24 @@ describe("group membership ids cross into the runtime", () => { for (const id of memberIds) expect(clipIds).toContain(id); }); + // TimelineGroupRow's half-lit state ("some of what's under here still + // plays") compares its member list against the same soloed set. Built from + // store keys it never matched, so soloing a member lit nothing on its group. + it("half-lights the group when one member is soloed", () => { + const doc = docWith(COMPOSITION); + const members = parseTimelineFromDOM(doc, 30).filter((el) => el.audioGroup === "voiceover"); + const memberIds = members.map(runtimeAudioId).filter((id): id is string => id !== null); + expect(memberIds).toEqual(["voice-1", "voice-2"]); + + const soloed = new Set(["voice-1"]); + expect(isGroupHalfLitUnderSolo(soloed, "voiceover", memberIds)).toBe(true); + // Store keys are the shape that silently failed. + const storeKeys = members.map((el) => el.key ?? el.id); + expect(isGroupHalfLitUnderSolo(soloed, "voiceover", storeKeys)).toBe(false); + // Soloing the group itself is lit, not half-lit. + expect(isGroupHalfLitUnderSolo(new Set(["voiceover"]), "voiceover", memberIds)).toBe(false); + }); + it("an element with no DOM id is not groupable or soloable", () => { const doc = docWith(`