Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions packages/core/src/runtime/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,37 @@ describe("initSandboxRuntimeModular", () => {
},
);

it("keeps a boosted clip legal on the element when the bridge sets volume", () => {
// `data-volume` may hold up to 12 dB of authored gain. `el.volume` is
// spec-pinned to [0,1] and THROWS outside it, so assigning the product raw
// aborted the loop — every element after the boosted one kept its old
// volume, and the bridge's own state said otherwise.
document.body.innerHTML =
`<div data-composition-id="main" data-root="true">` +
`<audio data-start="0" data-volume="3.98"></audio>` +
`<audio data-start="0" data-volume="0.5"></audio>` +
`</div>`;
window.__timelines = {};
initSandboxRuntimeModular();

const [boosted, quiet] = Array.from(document.querySelectorAll("audio"));
if (!boosted || !quiet) throw new Error("expected both clips");
// Sentinels, so the assertions cannot be satisfied by what the runtime
// already applied while starting up.
boosted.volume = 0.2;
quiet.volume = 0.1;

window.dispatchEvent(
new MessageEvent("message", {
data: { source: "hf-parent", type: "control", action: "set-volume", volume: 1 },
}),
);

expect(boosted.volume).toBe(1);
// The clip after the boosted one is what a throw mid-loop strands.
expect(quiet.volume).toBeCloseTo(0.5, 5);
});

afterEach(() => {
window.__hfRuntimeTeardown?.();
document.body.innerHTML = "";
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/runtime/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3159,7 +3159,12 @@ export function initSandboxRuntimeModular(): void {
if (!(el instanceof HTMLMediaElement)) continue;
const parsed = parseFloat(el.dataset.volume ?? "");
const clipVolume = Number.isFinite(parsed) ? parsed : 1;
el.volume = clipVolume * volume;
// `data-volume` carries authored gain, which goes above unity now that
// the ceiling is 12 dB — and `el.volume` is spec-pinned to [0,1], so
// assigning the product raw THROWS IndexSizeError and takes the rest of
// the loop with it. The element carries the legal part; the boost above
// unity belongs to Web Audio, which already has it from `setVolume`.
el.volume = Math.max(0, Math.min(1, clipVolume * volume));
}
},
onSetMediaOutputMuted: (muted) => {
Expand Down
21 changes: 21 additions & 0 deletions packages/core/src/runtime/media.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,27 @@ describe("syncRuntimeMedia", () => {
expect(only).toBeCloseTo(0.55, 5);
});

it("sends boosted author gain to Web Audio while keeping the native element legal", () => {
const clip = createMockClip({ start: 0, end: 10, volume: 3.98 });
Object.defineProperty(clip.el, "readyState", { value: 4, writable: true });
let transportGain = -1;

syncRuntimeMedia({
clips: [clip],
timeSeconds: 1,
playing: true,
playbackRate: 1,
// Third arg is the authored gain, which is the one the transport wants;
// the second is the element's, which the spec pins to [0,1].
onElementVolume: (_el, _effectiveVolume, authorVolume) => {
transportGain = authorVolume;
},
});

expect(transportGain).toBeCloseTo(3.98, 5);
expect(clip.el.volume).toBe(1);
});

/**
* The render bakes the lane at CLIP-LOCAL time: prepareAudioTrack already
* cut the wav with `-ss mediaStart`, so its t=0 is the clip's start, and
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/runtime/webAudioTransport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,15 @@ describe("WebAudioTransport", () => {
});

describe("schedulePlayback timing", () => {
it("keeps author boost above unity on the per-element gain node", async () => {
const { transport, mock, gen } = setupTransport(100);

await transport.schedulePlayback(mockEl, mockBuffer, 0, 0, 0, 1, gen);
transport.setElementVolume(mockEl, 3.98);

expect(mock.gainNode.gain.value).toBeCloseTo(3.98, 5);
});

it("starts in-progress clips immediately with correct buffer offset", async () => {
const { transport, mock, gen } = setupTransport(100);

Expand Down
Loading