mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix: preserve plateaus in sampled audio automation (#2863)
* fix: preserve audio automation plateaus * fix(audio): align automation probe windows
This commit is contained in:
@@ -4,6 +4,7 @@ import { mkdtempSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { parseHTML } from "linkedom";
|
||||
import { interpolateVolumeGain } from "@hyperframes/core/media-volume-envelope";
|
||||
import { defaultLogger } from "../logger.js";
|
||||
import {
|
||||
collectExternalAssets,
|
||||
@@ -1763,6 +1764,58 @@ h1 { font-size: 2rem; }`;
|
||||
});
|
||||
|
||||
describe("discoverAudioVolumeAutomationFromTimeline", () => {
|
||||
it("emits plateau boundaries around a sampled volume change", async () => {
|
||||
class TestAudioElement {
|
||||
id = "music";
|
||||
dataset = { start: "0", duration: "3", volume: "0.8" };
|
||||
volume = 0.8;
|
||||
}
|
||||
class TestVideoElement {}
|
||||
|
||||
const audio = new TestAudioElement();
|
||||
const previousWindow = globalThis.window;
|
||||
const previousDocument = globalThis.document;
|
||||
const previousAudioElement = globalThis.HTMLAudioElement;
|
||||
const previousVideoElement = globalThis.HTMLVideoElement;
|
||||
|
||||
globalThis.window = {
|
||||
__timelines: {
|
||||
root: {
|
||||
totalTime: (time: number) => {
|
||||
audio.volume = time < 1.05 ? 0.8 : 0.2;
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
globalThis.document = {
|
||||
querySelector: (selector: string) =>
|
||||
selector === "[data-composition-id]"
|
||||
? { getAttribute: (name: string) => (name === "data-composition-id" ? "root" : null) }
|
||||
: null,
|
||||
getElementById: (id: string) => (id === "music" ? audio : null),
|
||||
} as any;
|
||||
globalThis.HTMLAudioElement = TestAudioElement as any;
|
||||
globalThis.HTMLVideoElement = TestVideoElement as any;
|
||||
|
||||
try {
|
||||
const page = {
|
||||
evaluate: async (fn: (arg: unknown) => unknown, arg: unknown) => fn(arg),
|
||||
} as any;
|
||||
|
||||
const [automation] = await discoverAudioVolumeAutomationFromTimeline(page, ["music"], 3, 10);
|
||||
|
||||
expect(automation?.keyframes).toContainEqual({ time: 1, volume: 0.8 });
|
||||
expect(automation?.keyframes).toContainEqual({ time: 1.1, volume: 0.2 });
|
||||
expect(interpolateVolumeGain(automation?.keyframes ?? [], 0.5)).toBeCloseTo(0.8, 6);
|
||||
expect(interpolateVolumeGain(automation?.keyframes ?? [], 1.5)).toBeCloseTo(0.2, 6);
|
||||
} finally {
|
||||
globalThis.window = previousWindow;
|
||||
globalThis.document = previousDocument;
|
||||
globalThis.HTMLAudioElement = previousAudioElement;
|
||||
globalThis.HTMLVideoElement = previousVideoElement;
|
||||
}
|
||||
});
|
||||
|
||||
it("prefers runtime duration over stale data-end while sampling video-derived audio", async () => {
|
||||
class TestAudioElement {}
|
||||
class TestVideoElement {
|
||||
|
||||
@@ -2150,20 +2150,33 @@ export async function discoverAudioVolumeAutomationFromTimeline(
|
||||
}
|
||||
|
||||
const keyframes: { time: number; volume: number }[] = [];
|
||||
for (let t = sampleStart; t <= sampleEnd + 0.000001; t += step) {
|
||||
const boundedTime = Math.min(sampleEnd, t);
|
||||
seekTl(boundedTime);
|
||||
let previousSample: { time: number; volume: number } | undefined;
|
||||
for (let t = sampleStart; t <= sampleEnd + 0.000001; t = Math.min(sampleEnd, t + step)) {
|
||||
seekTl(t);
|
||||
const rawVolume = Number(el.volume);
|
||||
if (!Number.isFinite(rawVolume)) continue;
|
||||
const volume = Math.max(0, Math.min(1, rawVolume));
|
||||
const last = keyframes.at(-1);
|
||||
if (!last || Math.abs(last.volume - volume) > 0.0001 || boundedTime === sampleEnd) {
|
||||
keyframes.push({
|
||||
time: Number(boundedTime.toFixed(6)),
|
||||
volume: Number(volume.toFixed(6)),
|
||||
});
|
||||
if (!Number.isFinite(rawVolume)) {
|
||||
if (t === sampleEnd) break;
|
||||
continue;
|
||||
}
|
||||
if (boundedTime === sampleEnd) break;
|
||||
const volume = Math.max(0, Math.min(1, rawVolume));
|
||||
const sample = {
|
||||
time: Number(t.toFixed(6)),
|
||||
volume: Number(volume.toFixed(6)),
|
||||
};
|
||||
const last = keyframes.at(-1);
|
||||
if (!last || Math.abs(last.volume - volume) > 0.0001) {
|
||||
// Retain the preceding real sample when compression omitted a flat
|
||||
// run. Continuous ramps already have that sample as their last
|
||||
// keyframe, so their interpolation remains unchanged.
|
||||
if (last && previousSample && previousSample.time > last.time) {
|
||||
keyframes.push(previousSample);
|
||||
}
|
||||
keyframes.push(sample);
|
||||
} else if (t === sampleEnd && sample.time > last.time) {
|
||||
keyframes.push(sample);
|
||||
}
|
||||
previousSample = sample;
|
||||
if (t === sampleEnd) break;
|
||||
}
|
||||
|
||||
const staticAttr = Number.parseFloat(el.dataset.volume ?? "");
|
||||
|
||||
Reference in New Issue
Block a user