mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
Renders what B3 already routes in preview: a group's members sub-mix into
one PCM WAV at full composition length (adelay already places each member
at its composition position, so the group WAV's t=0 IS composition time),
run through the group's own FX chain and automation via the same
applyAudioFxChain/envelope-bake path a member uses, then fold into the flat
track list as one processed AudioTrack — the final mixAudioTracks call
never has to know groups exist.
Gain law verified against plans/spikes/amix-nesting-spike.sh (brought over
from the plans branch, along with audioMixer.grouping.test.ts, since both
were committed there and never merged to origin/main — every step branch in
this stack descends from origin/main): the sub-mix's own amix prefers
normalize=0 (nulls exactly against a flat mix), falling back to per-node
compensation by the group's OWN member count only when this ffmpeg build's
amix rejects the option. Carrying any other count into a nested amix node
is the exact +2.499 dB silent failure the spike measured — confirmed by a
manual mutation check (wrong-count compensation landed 3.5 dB hot, exactly
20*log10(3/2) for a 2-member group compensated as 3; reverted after
confirming the level test catches it).
A group element carrying data-hidden drops every member before the sub-mix
ever runs (RULES: mute-by-drop, never mute-by-volume-0) — parseAudioElements
now resolves groups once per parse and skips hidden-group members the same
way it already skips data-hidden ancestors.
HfAudioGroup (packages/core/src/audioGroups.ts, from B1) gains fxChain,
automation, volume and hidden, read off the group element the same way
resolveAudioGroups already reads data-label — audioGroups.test.ts updated
for the wider shape plus new coverage for the added reads.
it.todo("mixes a grouped composition at the same level as the ungrouped
one") is now a real, passing test; two more added per the step doc (FX
routing isolation, member-level envelope survives grouping).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
161 lines
5.9 KiB
TypeScript
161 lines
5.9 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import {
|
|
audioGroupOf,
|
|
HF_AUDIO_GROUP_ATTR,
|
|
resolveAudioGroups,
|
|
resolveCarveSourceIds,
|
|
} from "./audioGroups.js";
|
|
|
|
beforeEach(() => {
|
|
document.body.innerHTML = "";
|
|
});
|
|
|
|
describe("resolveAudioGroups", () => {
|
|
it("returns one group of two members plus ignores an ungrouped track", () => {
|
|
document.body.innerHTML = `
|
|
<hf-audio-group id="voiceover" data-label="Voiceover"></hf-audio-group>
|
|
<audio id="vo-1" data-audio-group="voiceover"></audio>
|
|
<audio id="vo-2" data-audio-group="voiceover"></audio>
|
|
<audio id="sfx-1"></audio>
|
|
`;
|
|
const groups = resolveAudioGroups(document);
|
|
expect(groups).toEqual([
|
|
{
|
|
id: "voiceover",
|
|
label: "Voiceover",
|
|
memberIds: ["vo-1", "vo-2"],
|
|
volume: 1,
|
|
hidden: false,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("resolves from member tags alone when the group element is absent, label = id", () => {
|
|
document.body.innerHTML = `
|
|
<audio id="vo-1" data-audio-group="narration"></audio>
|
|
`;
|
|
const groups = resolveAudioGroups(document);
|
|
expect(groups).toEqual([
|
|
{ id: "narration", label: "narration", memberIds: ["vo-1"], volume: 1, hidden: false },
|
|
]);
|
|
});
|
|
|
|
it("ignores data-audio-group on the group element itself (groups do not nest)", () => {
|
|
document.body.innerHTML = `
|
|
<hf-audio-group id="outer" data-audio-group="outer"></hf-audio-group>
|
|
<audio id="vo-1" data-audio-group="outer"></audio>
|
|
`;
|
|
const groups = resolveAudioGroups(document);
|
|
expect(groups).toEqual([
|
|
{ id: "outer", label: "outer", memberIds: ["vo-1"], volume: 1, hidden: false },
|
|
]);
|
|
expect(audioGroupOf(document.getElementById("outer") as Element)).toBeNull();
|
|
});
|
|
|
|
it("drops a member removed from the DOM on re-resolve — nothing dangles", () => {
|
|
document.body.innerHTML = `
|
|
<audio id="vo-1" data-audio-group="voiceover"></audio>
|
|
<audio id="vo-2" data-audio-group="voiceover"></audio>
|
|
`;
|
|
expect(resolveAudioGroups(document)[0].memberIds).toEqual(["vo-1", "vo-2"]);
|
|
|
|
document.getElementById("vo-2")?.remove();
|
|
expect(resolveAudioGroups(document)[0].memberIds).toEqual(["vo-1"]);
|
|
});
|
|
|
|
it("ignores a data-audio-group on a video element (audio only in v1)", () => {
|
|
document.body.innerHTML = `<video id="v-1" data-audio-group="voiceover"></video>`;
|
|
expect(resolveAudioGroups(document)).toEqual([]);
|
|
});
|
|
|
|
it("reads the group element's fx chain, automation, volume and hidden", () => {
|
|
document.body.innerHTML = `
|
|
<hf-audio-group id="voiceover" data-fx-chain='{"version":1,"nodes":[]}' data-automation='{"lanes":[]}' data-volume="0.5" data-hidden></hf-audio-group>
|
|
<audio id="vo-1" data-audio-group="voiceover"></audio>
|
|
`;
|
|
const groups = resolveAudioGroups(document);
|
|
expect(groups).toEqual([
|
|
{
|
|
id: "voiceover",
|
|
label: "voiceover",
|
|
memberIds: ["vo-1"],
|
|
fxChain: '{"version":1,"nodes":[]}',
|
|
automation: '{"lanes":[]}',
|
|
volume: 0.5,
|
|
hidden: true,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("defaults volume to 1 and hidden to false when a group element exists but carries neither", () => {
|
|
document.body.innerHTML = `
|
|
<hf-audio-group id="voiceover"></hf-audio-group>
|
|
<audio id="vo-1" data-audio-group="voiceover"></audio>
|
|
`;
|
|
const [group] = resolveAudioGroups(document);
|
|
expect(group?.volume).toBe(1);
|
|
expect(group?.hidden).toBe(false);
|
|
expect(group?.fxChain).toBeUndefined();
|
|
expect(group?.automation).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("audioGroupOf", () => {
|
|
it("reads the member's group id", () => {
|
|
document.body.innerHTML = `<audio id="vo-1" data-audio-group="voiceover"></audio>`;
|
|
expect(audioGroupOf(document.getElementById("vo-1") as Element)).toBe("voiceover");
|
|
});
|
|
|
|
it("returns null when the attribute is absent", () => {
|
|
document.body.innerHTML = `<audio id="vo-1"></audio>`;
|
|
expect(audioGroupOf(document.getElementById("vo-1") as Element)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("resolveCarveSourceIds", () => {
|
|
it("expands a group id to its current members", () => {
|
|
document.body.innerHTML = `
|
|
<audio id="vo-1" data-audio-group="voiceover"></audio>
|
|
<audio id="vo-2" data-audio-group="voiceover"></audio>
|
|
`;
|
|
expect(resolveCarveSourceIds(document, ["voiceover"])).toEqual(["vo-1", "vo-2"]);
|
|
});
|
|
|
|
it("picks up a member added to the group after the carve was set (analysis-time, not frozen)", () => {
|
|
document.body.innerHTML = `
|
|
<audio id="vo-1" data-audio-group="voiceover"></audio>
|
|
<audio id="vo-2" data-audio-group="voiceover"></audio>
|
|
`;
|
|
expect(resolveCarveSourceIds(document, ["voiceover"])).toEqual(["vo-1", "vo-2"]);
|
|
document.body.insertAdjacentHTML(
|
|
"beforeend",
|
|
`<audio id="vo-3" data-audio-group="voiceover"></audio>`,
|
|
);
|
|
expect(resolveCarveSourceIds(document, ["voiceover"])).toEqual(["vo-1", "vo-2", "vo-3"]);
|
|
});
|
|
|
|
it("passes through a plain clip id that still exists", () => {
|
|
document.body.innerHTML = `<audio id="vo-1"></audio>`;
|
|
expect(resolveCarveSourceIds(document, ["vo-1"])).toEqual(["vo-1"]);
|
|
});
|
|
|
|
it("drops an id that resolves to nothing — a deleted clip, an empty or vanished group", () => {
|
|
document.body.innerHTML = `<audio id="vo-1"></audio>`;
|
|
expect(resolveCarveSourceIds(document, ["vo-1", "deleted", "no-such-group"])).toEqual(["vo-1"]);
|
|
});
|
|
|
|
it("dedupes and preserves first-seen order across a mix of group and plain ids", () => {
|
|
document.body.innerHTML = `
|
|
<audio id="vo-1" data-audio-group="voiceover"></audio>
|
|
<audio id="vo-2" data-audio-group="voiceover"></audio>
|
|
`;
|
|
expect(resolveCarveSourceIds(document, ["voiceover", "vo-1"])).toEqual(["vo-1", "vo-2"]);
|
|
});
|
|
});
|
|
|
|
describe(HF_AUDIO_GROUP_ATTR, () => {
|
|
it("is the attribute name membership is keyed on", () => {
|
|
expect(HF_AUDIO_GROUP_ATTR).toBe("data-audio-group");
|
|
});
|
|
});
|