Skip to content

Commit 1d73ae1

Browse files
committed
fix(core,engine): guard volume probe cache and restore PCM cursor
Two perf fixes caught in #1118 review: 1. Cache guard: probeAndCacheVolumeKeyframes now short-circuits when the element is already in volumeKeyframeCache. Without the guard every bindMediaMetadataListeners call (every 30 RAF ticks) re-probed all bound elements — N elements × full-composition timeline seeks at 60 Hz regardless of whether keyframes were already known. bindRootTimelineIfAvailable still clears the cache on a new timeline capture so keyframes stay fresh when the composition is rebound. 2. PCM cursor: audioVolumeEnvelope.ts had the incremental segment cursor (O(N+M) overall) before #1118 extracted the interpolation into interpolateVolumeGain. The shared function restarts from segment=0 on each call — fine for the preview path (one call per RAF tick) but O(N×M) for the PCM path (one call per sample: 48 kHz × duration). Napkin math: a 10-min render went from ~30M to ~460M ops. Restored the inline incremental scan in the engine bake loop; engine now only imports normaliseEnvelope from core.
1 parent dcd68ae commit 1d73ae1

2 files changed

Lines changed: 17 additions & 4 deletions

File tree

packages/core/src/runtime/init.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -967,9 +967,10 @@ export function initSandboxRuntimeModular(): void {
967967
mediaDurationFloorSeconds: resolution.mediaDurationFloorSeconds ?? null,
968968
},
969969
});
970-
// (Re-)probe all already-bound media elements now that a timeline is available.
971-
// Elements bound before this point couldn't be probed in bindMediaMetadataListeners.
970+
// (Re-)probe all already-bound media elements against the new timeline.
971+
// Clear the cache first so elements probed against a prior timeline get fresh keyframes.
972972
for (const el of metadataBoundMedia) {
973+
volumeKeyframeCache.delete(el);
973974
probeAndCacheVolumeKeyframes(el);
974975
}
975976
return true;
@@ -1283,6 +1284,7 @@ export function initSandboxRuntimeModular(): void {
12831284
};
12841285

12851286
const probeAndCacheVolumeKeyframes = (mediaEl: HTMLMediaElement) => {
1287+
if (volumeKeyframeCache.has(mediaEl)) return;
12861288
probeAndCacheElementVolume(
12871289
mediaEl,
12881290
state.capturedTimeline,

packages/engine/src/services/audioVolumeEnvelope.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import { readFileSync, renameSync, writeFileSync } from "fs";
1919
import { randomBytes } from "crypto";
2020
import type { AudioVolumeKeyframe } from "./audioMixer.types.js";
21-
import { normaliseEnvelope, interpolateVolumeGain } from "@hyperframes/core/media-volume-envelope";
21+
import { normaliseEnvelope } from "@hyperframes/core/media-volume-envelope";
2222

2323
const PCM_FORMAT = 1; // WAVE_FORMAT_PCM
2424
const SUPPORTED_BITS = 16;
@@ -99,9 +99,20 @@ export function applyVolumeEnvelopeToWav(
9999
const frameBytes = numChannels * bytesPerSample;
100100
const frameCount = Math.floor(dataSize / frameBytes);
101101

102+
// Maintain an incremental segment cursor so the per-frame envelope lookup
103+
// is O(N+M) overall, not O(N×M). interpolateVolumeGain restarts from 0 on
104+
// each call — fine for the preview path (one call per RAF tick) but not for
105+
// the PCM path (one call per sample, 48k×duration frames total).
106+
let segment = 0;
102107
for (let frame = 0; frame < frameCount; frame += 1) {
103108
const time = frame / sampleRate;
104-
const gain = interpolateVolumeGain(envelope, time);
109+
while (segment < envelope.length - 2 && time >= envelope[segment + 1]!.time) segment += 1;
110+
111+
const a = envelope[segment]!;
112+
const b = envelope[segment + 1] ?? a;
113+
const span = b.time - a.time;
114+
const progress = span <= 0 ? 0 : Math.min(1, Math.max(0, (time - a.time) / span));
115+
const gain = a.volume + (b.volume - a.volume) * progress;
105116

106117
const base = dataOffset + frame * frameBytes;
107118
for (let channel = 0; channel < numChannels; channel += 1) {

0 commit comments

Comments
 (0)