/** * The audio group model: a named bucket of audio tracks that shares a label, * an FX chain, and automation. Membership is held by the member (`data-audio-group` * pointing at a group id), not by the group nesting its members, so a track * dropped from the DOM simply disappears from the group on the next resolve — * nothing dangles. * * Parse-only: this module answers "what groups exist and who is in them," and * nothing here routes or sums audio yet. */ import { HF_AUDIO_FX_ATTR } from "./audioFx.js"; import { HF_AUDIO_AUTOMATION_ATTR } from "./audioAutomation.js"; export const HF_AUDIO_GROUP_TAG = "hf-audio-group"; export const HF_AUDIO_GROUP_ATTR = "data-audio-group"; export interface HfAudioGroup { id: string; /** `data-label`, falling back to the id when absent. */ label: string; /** Member element ids, in document order. */ memberIds: string[]; /** Serialised FX chain JSON from the group element's `data-fx-chain`, when set. */ fxChain?: string; /** Serialised automation JSON from the group element's `data-automation`, when set. */ automation?: string; /** The group element's `data-volume`, defaulting to 1 when absent or there is no group element. */ volume: number; /** * The group element's `data-hidden`. Render drops every member rather than * zeroing them (RULES: mute-by-drop, never mute-by-volume-0) — B5 defines * the UI for this; this field just makes the read available now. */ hidden: boolean; } /** * A group element's `data-volume`, defaulting to 1 for a missing, unparseable * or absent element. Shared so the preview bus and `resolveAudioGroups` (which * the render reads) cannot drift: the export was ~8 dB quieter than what was * auditioned for exactly as long as the preview ignored this. */ export function readAudioGroupVolume(el: Element | null | undefined): number { const raw = el?.getAttribute("data-volume"); const parsed = raw ? parseFloat(raw) : 1; return Number.isFinite(parsed) ? parsed : 1; } function buildGroup(id: string, memberIds: string[], el: Element | undefined): HfAudioGroup { const fxChain = el?.getAttribute(HF_AUDIO_FX_ATTR); const automation = el?.getAttribute(HF_AUDIO_AUTOMATION_ATTR); return { id, label: el?.getAttribute("data-label") || id, memberIds, ...(fxChain ? { fxChain } : {}), ...(automation ? { automation } : {}), volume: readAudioGroupVolume(el), hidden: el?.hasAttribute("data-hidden") ?? false, }; } /** * Every group with at least one member, resolved from the live document. * * A group with members but no `` element still resolves * (label = id) so a hand-authored composition degrades gracefully. Audio * only in v1 — a `data-audio-group` on a `