mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
fix(audio): harden group routing and audition
This commit is contained in:
@@ -101,6 +101,37 @@ function mount(dataAttributes: Record<string, string>, alone = false, voices = 2
|
||||
return { host, onSetAttributeQuiet, onSetAttributeLive };
|
||||
}
|
||||
|
||||
function mountGroup(memberStart: number) {
|
||||
const bus = document.createElement("hf-audio-group");
|
||||
bus.id = "voiceover";
|
||||
document.body.append(bus);
|
||||
const member = document.createElement("audio");
|
||||
member.id = "vo-1";
|
||||
member.setAttribute("data-audio-group", "voiceover");
|
||||
member.setAttribute("data-start", String(memberStart));
|
||||
member.setAttribute("data-duration", "5");
|
||||
document.body.append(member);
|
||||
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const selection = {
|
||||
dataAttributes: { "fx-chain": CHAIN },
|
||||
id: "voiceover",
|
||||
element: bus,
|
||||
tagName: "hf-audio-group",
|
||||
} as unknown as DomEditSelection;
|
||||
act(() => {
|
||||
createRoot(host).render(
|
||||
<AudioFxGroup
|
||||
element={selection}
|
||||
onSetAttributeQuiet={vi.fn()}
|
||||
onSetAttributeLive={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
return host;
|
||||
}
|
||||
|
||||
const rowFor = (host: HTMLElement, label: string): HTMLElement | null => {
|
||||
for (const row of Array.from(host.querySelectorAll<HTMLElement>(".hf-fx-row"))) {
|
||||
if (row.querySelector(".hf-fx-label")?.textContent === label) return row;
|
||||
@@ -565,6 +596,20 @@ describe("AudioFxGroup dynamic carve", () => {
|
||||
expect(store().playbackRequest?.returnTo).toBe(42);
|
||||
});
|
||||
|
||||
it("seeks a group audition to the next member span", () => {
|
||||
act(() =>
|
||||
usePlayerStore.setState({
|
||||
isPlaying: false,
|
||||
currentTime: 2,
|
||||
requestedSeekTime: null,
|
||||
}),
|
||||
);
|
||||
const host = mountGroup(10);
|
||||
hoverPreset(host);
|
||||
expect(store().requestedSeekTime).toBe(10);
|
||||
leaveShelf(host);
|
||||
});
|
||||
|
||||
it("leaves a transport the author started alone", () => {
|
||||
// Stopping their playback because they passed over a preset would be the
|
||||
// panel taking a decision nobody offered it.
|
||||
|
||||
@@ -48,9 +48,39 @@ import { useFxChainObserved } from "./useFxChainObserved.js";
|
||||
import { useFxCarve } from "./useFxCarve.js";
|
||||
import { audioFxSignalPath } from "./audioFxSignalPath.js";
|
||||
import type { AuditionSpan } from "./useAuditionTransport.js";
|
||||
import { resolveAudioGroups } from "@hyperframes/core/audio-groups";
|
||||
import {
|
||||
HF_AUDIO_GROUP_ATTR,
|
||||
HF_AUDIO_GROUP_TAG,
|
||||
resolveAudioGroups,
|
||||
} from "@hyperframes/core/audio-groups";
|
||||
import { useFxLevelling } from "./useFxLevelling.js";
|
||||
|
||||
function auditionSpan(startRaw: string | undefined, durationRaw: string | undefined) {
|
||||
const start = Number.parseFloat(startRaw ?? "");
|
||||
const duration = Number.parseFloat(durationRaw ?? "");
|
||||
return Number.isFinite(start) && Number.isFinite(duration) && duration > 0
|
||||
? { start, duration }
|
||||
: null;
|
||||
}
|
||||
|
||||
/** The selected clip, or every current member when the selected rack is a bus. */
|
||||
function auditionSpansFor(element: DomEditSelection): AuditionSpan[] {
|
||||
const own = auditionSpan(element.dataAttributes?.["start"], element.dataAttributes?.["duration"]);
|
||||
if (own) return [own];
|
||||
if (element.tagName?.toLowerCase() !== HF_AUDIO_GROUP_TAG || !element.id) return [];
|
||||
const doc = element.element?.ownerDocument;
|
||||
if (!doc) return [];
|
||||
return [...doc.querySelectorAll(`audio[${HF_AUDIO_GROUP_ATTR}]`)]
|
||||
.filter((member) => member.getAttribute(HF_AUDIO_GROUP_ATTR) === element.id)
|
||||
.flatMap((member) => {
|
||||
const span = auditionSpan(
|
||||
member.getAttribute("data-start") ?? undefined,
|
||||
member.getAttribute("data-duration") ?? undefined,
|
||||
);
|
||||
return span ? [span] : [];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridges the FX panel to the element/attribute world. Chain and carve are
|
||||
* serialised onto the element the way colour grading carries its config, so
|
||||
@@ -254,18 +284,10 @@ export function AudioFxGroup({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [element, storeElements]);
|
||||
|
||||
/**
|
||||
* The clip this rack belongs to, so hovering a preset auditions where it
|
||||
* sounds. A group's rack reaches this file too, but a group has no span of
|
||||
* its own — its members carry the audio, and this panel does not see them,
|
||||
* so it passes none and the transport plays from the playhead as before.
|
||||
*/
|
||||
const auditionSpans = useMemo((): AuditionSpan[] => {
|
||||
const start = Number.parseFloat(element.dataAttributes?.["start"] ?? "");
|
||||
const duration = Number.parseFloat(element.dataAttributes?.["duration"] ?? "");
|
||||
if (!Number.isFinite(start) || !Number.isFinite(duration) || duration <= 0) return [];
|
||||
return [{ start, duration }];
|
||||
}, [element]);
|
||||
// A bus has no span of its own, so resolve the live members. The
|
||||
// `storeElements` subscription above rerenders this panel when membership
|
||||
// changes without changing the selection.
|
||||
const auditionSpans = auditionSpansFor(element);
|
||||
|
||||
const { carvedAgainstBy, sourceOptions, setCarve } = useFxCarve(
|
||||
element,
|
||||
|
||||
Reference in New Issue
Block a user