mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-11 19:09:20 +00:00
fix(core): sample the volume lane at clip-local time, the way the render bakes it
A lane's `t` is "seconds from the start of the clip" — HfAutomationPoint says so, and the render honours it: prepareAudioTrack cuts the wav with `-ss mediaStart`, so its t=0 IS the clip's start, and normaliseEnvelope subtracts trackStart. Preview fed `relTime` instead, which is MEDIA time: it carries mediaStart, scales by playbackRate and wraps on a loop. So the same envelope played somewhere else than it rendered. `data-media-start` past the last point held the final value from the first frame and never faded; `playbackRate: 2` ran the envelope at double speed in preview only; a looping clip restarted the envelope every lap in preview while the render baked it once. The FX lanes shipped in this same stack already use clip-local elapsed. There is one time base for automation, and this makes the volume lane use it.
This commit is contained in:
@@ -365,6 +365,37 @@ describe("syncRuntimeMedia", () => {
|
||||
expect(only).toBeCloseTo(0.55, 5);
|
||||
});
|
||||
|
||||
/**
|
||||
* 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
|
||||
* normaliseEnvelope subtracts trackStart. Preview used to sample at MEDIA
|
||||
* time — mediaStart included, scaled by playbackRate, wrapped on a loop — so
|
||||
* the same envelope played somewhere else than it rendered.
|
||||
*/
|
||||
it("samples the lane at clip-local time, the way the render bakes it", () => {
|
||||
const trimmed = (t: number) => {
|
||||
const clip = createMockClip({ start: 0, end: 10, volume: 0.55, mediaStart: 30 });
|
||||
Object.defineProperty(clip.el, "readyState", { value: 4, writable: true });
|
||||
clip.el.setAttribute("data-automation", DUCK);
|
||||
let seen = -1;
|
||||
syncRuntimeMedia({
|
||||
clips: [clip],
|
||||
timeSeconds: t,
|
||||
playing: true,
|
||||
playbackRate: 1,
|
||||
onElementVolume: (_el, v) => {
|
||||
seen = v;
|
||||
},
|
||||
});
|
||||
return seen;
|
||||
};
|
||||
// `data-media-start="30"` on a clip whose lane holds 0.8 until t=2 then
|
||||
// ducks to 0.1 by t=3. At media time the playhead is already 30 s past the
|
||||
// last point, so preview held 0.1 from the first frame and never ducked.
|
||||
expect(trimmed(1)).toBeCloseTo(0.8, 5);
|
||||
expect(trimmed(5)).toBeCloseTo(0.1, 5);
|
||||
});
|
||||
|
||||
it("supersedes keyframes probed from the timeline", () => {
|
||||
// Both present: the lane is the explicit one, and `lint` warns about it.
|
||||
const clip = createMockClip({ start: 0, end: 10, volume: 0.55 });
|
||||
|
||||
@@ -274,7 +274,15 @@ export function syncRuntimeMedia(params: {
|
||||
// An explicit volume lane owns the fader. It is checked before the probed
|
||||
// keyframes because the two would otherwise fight, and it is the one the
|
||||
// author drew — `lint` warns when a track carries both.
|
||||
const laneGain = elementVolumeLaneGain(el, relTime);
|
||||
// Clip-local, NOT `relTime`. A lane's `t` is "seconds from the start of
|
||||
// the clip" (see HfAutomationPoint), and the render honours that: the wav
|
||||
// is already cut with `-ss mediaStart`, so its t=0 IS the clip's start.
|
||||
// `relTime` is MEDIA time — it carries mediaStart, scales by playbackRate
|
||||
// and wraps on a loop — so feeding it here played the envelope at a
|
||||
// different position than it renders, or ran it off the end entirely on a
|
||||
// trimmed clip. The FX lanes on this same feature use clip-local elapsed;
|
||||
// there is one time base, and this is it.
|
||||
const laneGain = elementVolumeLaneGain(el, params.timeSeconds - clip.start);
|
||||
if (laneGain !== null) {
|
||||
authorVolume = clampVolume(laneGain);
|
||||
} else if (clip.volumeKeyframes && clip.volumeKeyframes.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user