Skip to content
Merged
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
6 changes: 4 additions & 2 deletions packages/core/src/runtime/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -967,9 +967,10 @@ export function initSandboxRuntimeModular(): void {
mediaDurationFloorSeconds: resolution.mediaDurationFloorSeconds ?? null,
},
});
// (Re-)probe all already-bound media elements now that a timeline is available.
// Elements bound before this point couldn't be probed in bindMediaMetadataListeners.
// (Re-)probe all already-bound media elements against the new timeline.
// Clear the cache first so elements probed against a prior timeline get fresh keyframes.
for (const el of metadataBoundMedia) {
volumeKeyframeCache.delete(el);
probeAndCacheVolumeKeyframes(el);
}
return true;
Expand Down Expand Up @@ -1283,6 +1284,7 @@ export function initSandboxRuntimeModular(): void {
};

const probeAndCacheVolumeKeyframes = (mediaEl: HTMLMediaElement) => {
if (volumeKeyframeCache.has(mediaEl)) return;
probeAndCacheElementVolume(
mediaEl,
state.capturedTimeline,
Expand Down
15 changes: 13 additions & 2 deletions packages/engine/src/services/audioVolumeEnvelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import { readFileSync, renameSync, writeFileSync } from "fs";
import { randomBytes } from "crypto";
import type { AudioVolumeKeyframe } from "./audioMixer.types.js";
import { normaliseEnvelope, interpolateVolumeGain } from "@hyperframes/core/media-volume-envelope";
import { normaliseEnvelope } from "@hyperframes/core/media-volume-envelope";

const PCM_FORMAT = 1; // WAVE_FORMAT_PCM
const SUPPORTED_BITS = 16;
Expand Down Expand Up @@ -99,9 +99,20 @@ export function applyVolumeEnvelopeToWav(
const frameBytes = numChannels * bytesPerSample;
const frameCount = Math.floor(dataSize / frameBytes);

// Maintain an incremental segment cursor so the per-frame envelope lookup
// is O(N+M) overall, not O(N×M). interpolateVolumeGain restarts from 0 on
// each call — fine for the preview path (one call per RAF tick) but not for
// the PCM path (one call per sample, 48k×duration frames total).
let segment = 0;
for (let frame = 0; frame < frameCount; frame += 1) {
const time = frame / sampleRate;
const gain = interpolateVolumeGain(envelope, time);
while (segment < envelope.length - 2 && time >= envelope[segment + 1]!.time) segment += 1;

const a = envelope[segment]!;
const b = envelope[segment + 1] ?? a;
const span = b.time - a.time;
const progress = span <= 0 ? 0 : Math.min(1, Math.max(0, (time - a.time) / span));
const gain = a.volume + (b.volume - a.volume) * progress;

const base = dataOffset + frame * frameBytes;
for (let channel = 0; channel < numChannels; channel += 1) {
Expand Down
Loading