@hyperframes/engine 0.6.55 → 0.6.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/services/audioMixer.d.ts.map +1 -1
- package/dist/services/audioMixer.js +146 -40
- package/dist/services/audioMixer.js.map +1 -1
- package/dist/services/audioVolumeEnvelope.d.ts +25 -0
- package/dist/services/audioVolumeEnvelope.d.ts.map +1 -0
- package/dist/services/audioVolumeEnvelope.js +123 -0
- package/dist/services/audioVolumeEnvelope.js.map +1 -0
- package/package.json +2 -2
- package/src/services/audioMixer.test.ts +115 -0
- package/src/services/audioMixer.ts +162 -44
- package/src/services/audioVolumeEnvelope.test.ts +176 -0
- package/src/services/audioVolumeEnvelope.ts +138 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audioMixer.d.ts","sourceRoot":"","sources":["../../src/services/audioMixer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAIjE,OAAO,KAAK,EAAE,YAAY,EAAc,SAAS,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"audioMixer.d.ts","sourceRoot":"","sources":["../../src/services/audioMixer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAIjE,OAAO,KAAK,EAAE,YAAY,EAAc,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAGjF,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAyJrE,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,EAAE,CAuD/D;AAmPD,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,YAAY,EAAE,EACxB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,WAAW,EACpB,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,sBAAsB,GAAG,WAAW,CAAC,CAAC,EAC1E,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,SAAS,CAAC,CA2HpB"}
|
|
@@ -12,6 +12,7 @@ import { DEFAULT_CONFIG } from "../config.js";
|
|
|
12
12
|
import { runFfmpeg } from "../utils/runFfmpeg.js";
|
|
13
13
|
import { unwrapTemplate } from "../utils/htmlTemplate.js";
|
|
14
14
|
import { resolveProjectRelativeSrc } from "./videoFrameExtractor.js";
|
|
15
|
+
import { applyVolumeEnvelopeToWav } from "./audioVolumeEnvelope.js";
|
|
15
16
|
function clampVolume(volume) {
|
|
16
17
|
if (!Number.isFinite(volume))
|
|
17
18
|
return 1;
|
|
@@ -23,10 +24,83 @@ function formatFilterNumber(value) {
|
|
|
23
24
|
function escapeExpressionCommas(expression) {
|
|
24
25
|
return expression.replace(/\\/g, "\\\\").replace(/,/g, "\\,");
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Upper bound on volume-automation keyframes folded into the FFmpeg `volume`
|
|
29
|
+
* expression. The expression nests one `if(lt(...))` per keyframe, and
|
|
30
|
+
* FFmpeg's expression evaluator has a finite nesting depth: past ~95 levels
|
|
31
|
+
* (build-dependent — lower on some Linux ffmpeg builds) `volume=...:eval=frame`
|
|
32
|
+
* fails filter-graph init, which fails the whole mix and drops the audio track
|
|
33
|
+
* entirely. The 60 Hz timeline probe routinely emits 100–300 keyframes for a
|
|
34
|
+
* multi-second fade (GH #1066 follow-up: a 171-keyframe GSAP fade rendered with
|
|
35
|
+
* no audio). 32 segments keeps a wide safety margin and is far more resolution
|
|
36
|
+
* than a piecewise-linear volume envelope needs.
|
|
37
|
+
*/
|
|
38
|
+
const MAX_VOLUME_SEGMENTS = 32;
|
|
39
|
+
/**
|
|
40
|
+
* Volume delta below which a keyframe is collinear enough to drop. Kept tight
|
|
41
|
+
* (0.5% linear) so the rendered piecewise-linear envelope tracks the GSAP curve
|
|
42
|
+
* the browser plays in preview to within ~0.2 dB across the audible range — well
|
|
43
|
+
* under the ~1 dB loudness JND, so render stays WYSIWYG with preview. A full
|
|
44
|
+
* ease-in/ease-out fade still reduces to ~25 segments, inside MAX_VOLUME_SEGMENTS.
|
|
45
|
+
*/
|
|
46
|
+
const VOLUME_SIMPLIFY_EPSILON = 0.005;
|
|
47
|
+
/**
|
|
48
|
+
* Reduce a sorted keyframe list to a perceptually-equivalent piecewise-linear
|
|
49
|
+
* envelope with a bounded segment count.
|
|
50
|
+
*
|
|
51
|
+
* Ramer–Douglas–Peucker drops control points lying within
|
|
52
|
+
* `VOLUME_SIMPLIFY_EPSILON` of the line through their neighbours (a linear fade
|
|
53
|
+
* collapses to its two endpoints; an eased fade to a handful). A uniform
|
|
54
|
+
* downsample backstop then bounds pathological inputs (e.g. audio-rate volume
|
|
55
|
+
* oscillation) to `MAX_VOLUME_SEGMENTS`. Endpoints are always preserved so the
|
|
56
|
+
* envelope still spans the full clip.
|
|
57
|
+
*/
|
|
58
|
+
function simplifyVolumeKeyframes(keyframes) {
|
|
59
|
+
if (keyframes.length < 3)
|
|
60
|
+
return keyframes;
|
|
61
|
+
const keep = new Array(keyframes.length).fill(false);
|
|
62
|
+
keep[0] = true;
|
|
63
|
+
keep[keyframes.length - 1] = true;
|
|
64
|
+
const stack = [[0, keyframes.length - 1]];
|
|
65
|
+
while (stack.length > 0) {
|
|
66
|
+
const [startIndex, endIndex] = stack.pop();
|
|
67
|
+
const start = keyframes[startIndex];
|
|
68
|
+
const end = keyframes[endIndex];
|
|
69
|
+
const span = end.time - start.time;
|
|
70
|
+
let maxDistance = VOLUME_SIMPLIFY_EPSILON;
|
|
71
|
+
let splitIndex = -1;
|
|
72
|
+
for (let i = startIndex + 1; i < endIndex; i += 1) {
|
|
73
|
+
const point = keyframes[i];
|
|
74
|
+
const interpolated = span === 0
|
|
75
|
+
? start.volume
|
|
76
|
+
: start.volume + ((end.volume - start.volume) * (point.time - start.time)) / span;
|
|
77
|
+
const distance = Math.abs(point.volume - interpolated);
|
|
78
|
+
if (distance > maxDistance) {
|
|
79
|
+
maxDistance = distance;
|
|
80
|
+
splitIndex = i;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (splitIndex !== -1) {
|
|
84
|
+
keep[splitIndex] = true;
|
|
85
|
+
stack.push([startIndex, splitIndex], [splitIndex, endIndex]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const simplified = keyframes.filter((_, i) => keep[i]);
|
|
89
|
+
if (simplified.length <= MAX_VOLUME_SEGMENTS)
|
|
90
|
+
return simplified;
|
|
91
|
+
const step = (simplified.length - 1) / (MAX_VOLUME_SEGMENTS - 1);
|
|
92
|
+
const sampled = [];
|
|
93
|
+
for (let i = 0; i < MAX_VOLUME_SEGMENTS; i += 1) {
|
|
94
|
+
const point = simplified[Math.round(i * step)];
|
|
95
|
+
if (sampled.length === 0 || point.time > sampled.at(-1).time)
|
|
96
|
+
sampled.push(point);
|
|
97
|
+
}
|
|
98
|
+
return sampled;
|
|
99
|
+
}
|
|
100
|
+
function buildVolumeExpression(track, ignoreKeyframes = false) {
|
|
27
101
|
const trimDuration = track.end - track.start;
|
|
28
102
|
const staticVolume = clampVolume(track.volume);
|
|
29
|
-
const keyframes = (track.volumeKeyframes ?? [])
|
|
103
|
+
const keyframes = (ignoreKeyframes ? [] : (track.volumeKeyframes ?? []))
|
|
30
104
|
.filter((keyframe) => Number.isFinite(keyframe.time) && Number.isFinite(keyframe.volume))
|
|
31
105
|
.map((keyframe) => ({
|
|
32
106
|
time: Math.max(0, Math.min(trimDuration, keyframe.time - track.start)),
|
|
@@ -48,13 +122,17 @@ function buildVolumeExpression(track) {
|
|
|
48
122
|
deduped.push(keyframe);
|
|
49
123
|
}
|
|
50
124
|
}
|
|
51
|
-
|
|
52
|
-
|
|
125
|
+
// Collapse the densely-sampled probe output to a bounded piecewise-linear
|
|
126
|
+
// envelope. Without this, the nested-if expression below grows one level per
|
|
127
|
+
// keyframe and overflows FFmpeg's expression evaluator (see MAX_VOLUME_SEGMENTS).
|
|
128
|
+
const simplified = simplifyVolumeKeyframes(deduped);
|
|
129
|
+
if (simplified.length === 1) {
|
|
130
|
+
return `volume=${formatFilterNumber(simplified[0].volume)}`;
|
|
53
131
|
}
|
|
54
|
-
let expression = formatFilterNumber(
|
|
55
|
-
for (let i =
|
|
56
|
-
const current =
|
|
57
|
-
const next =
|
|
132
|
+
let expression = formatFilterNumber(simplified.at(-1).volume);
|
|
133
|
+
for (let i = simplified.length - 2; i >= 0; i -= 1) {
|
|
134
|
+
const current = simplified[i];
|
|
135
|
+
const next = simplified[i + 1];
|
|
58
136
|
const currentTime = formatFilterNumber(current.time);
|
|
59
137
|
const nextTime = formatFilterNumber(next.time);
|
|
60
138
|
const currentVolume = formatFilterNumber(current.volume);
|
|
@@ -240,36 +318,52 @@ async function mixAudioTracks(tracks, outputPath, totalDuration, signal, config)
|
|
|
240
318
|
const outputDir = dirname(outputPath);
|
|
241
319
|
if (!existsSync(outputDir))
|
|
242
320
|
mkdirSync(outputDir, { recursive: true });
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
321
|
+
const buildArgs = (ignoreAutomation) => {
|
|
322
|
+
const inputs = [];
|
|
323
|
+
const filterParts = [];
|
|
324
|
+
tracks.forEach((track, i) => {
|
|
325
|
+
inputs.push("-i", track.srcPath);
|
|
326
|
+
const delayMs = Math.round(track.start * 1000);
|
|
327
|
+
const trimDuration = track.end - track.start;
|
|
328
|
+
const volumeFilter = buildVolumeExpression(track, ignoreAutomation);
|
|
329
|
+
filterParts.push(`[${i}:a]atrim=0:${trimDuration},${volumeFilter},adelay=${delayMs}|${delayMs},apad=whole_dur=${totalDuration}[a${i}]`);
|
|
330
|
+
});
|
|
331
|
+
const mixInputs = tracks.map((_, i) => `[a${i}]`).join("");
|
|
332
|
+
const weights = tracks.map(() => "1").join(" ");
|
|
333
|
+
const mixFilter = `${mixInputs}amix=inputs=${tracks.length}:duration=longest:dropout_transition=0:normalize=0:weights='${weights}'[mixed]`;
|
|
334
|
+
const postMixGainFilter = `[mixed]volume=${masterOutputGain}[out]`;
|
|
335
|
+
const fullFilter = [...filterParts, mixFilter, postMixGainFilter].join(";");
|
|
336
|
+
return [
|
|
337
|
+
...inputs,
|
|
338
|
+
"-filter_complex",
|
|
339
|
+
fullFilter,
|
|
340
|
+
"-map",
|
|
341
|
+
"[out]",
|
|
342
|
+
"-acodec",
|
|
343
|
+
"aac",
|
|
344
|
+
"-b:a",
|
|
345
|
+
"192k",
|
|
346
|
+
"-t",
|
|
347
|
+
String(totalDuration),
|
|
348
|
+
"-y",
|
|
349
|
+
outputPath,
|
|
350
|
+
];
|
|
351
|
+
};
|
|
352
|
+
let result = await runFfmpeg(buildArgs(false), { signal, timeout: ffmpegProcessTimeout });
|
|
353
|
+
// Defense in depth: volume automation is folded into an FFmpeg `volume`
|
|
354
|
+
// expression whose evaluator limits are build-dependent (see
|
|
355
|
+
// MAX_VOLUME_SEGMENTS). If that ever fails the mix, retry once without the
|
|
356
|
+
// automation so the track renders at its base volume rather than being
|
|
357
|
+
// dropped from the output entirely — a missing fade beats missing audio.
|
|
358
|
+
let degradedAutomation = false;
|
|
359
|
+
const hasAutomation = tracks.some((track) => (track.volumeKeyframes?.length ?? 0) > 0);
|
|
360
|
+
if (!result.success && !signal?.aborted && hasAutomation) {
|
|
361
|
+
const retry = await runFfmpeg(buildArgs(true), { signal, timeout: ffmpegProcessTimeout });
|
|
362
|
+
if (retry.success) {
|
|
363
|
+
result = retry;
|
|
364
|
+
degradedAutomation = true;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
273
367
|
if (signal?.aborted) {
|
|
274
368
|
return {
|
|
275
369
|
success: false,
|
|
@@ -293,6 +387,9 @@ async function mixAudioTracks(tracks, outputPath, totalDuration, signal, config)
|
|
|
293
387
|
outputPath,
|
|
294
388
|
durationMs: result.durationMs,
|
|
295
389
|
tracksProcessed: tracks.length,
|
|
390
|
+
error: degradedAutomation
|
|
391
|
+
? "Volume automation exceeded this ffmpeg build's expression limits; rendered at base volume"
|
|
392
|
+
: undefined,
|
|
296
393
|
};
|
|
297
394
|
}
|
|
298
395
|
export async function processCompositionAudio(elements, baseDir, workDir, outputPath, totalDuration, signal, config, compiledDir) {
|
|
@@ -355,6 +452,14 @@ export async function processCompositionAudio(elements, baseDir, workDir, output
|
|
|
355
452
|
}
|
|
356
453
|
audioSrcPath = trimmedPath;
|
|
357
454
|
}
|
|
455
|
+
// Primary volume-automation path: bake the envelope into the PCM samples
|
|
456
|
+
// (sample-accurate, no keyframe ceiling). If the WAV isn't the expected
|
|
457
|
+
// 16-bit PCM, fall back to the ffmpeg expression path by leaving the
|
|
458
|
+
// keyframes on the track for buildVolumeExpression to handle.
|
|
459
|
+
let bakedEnvelope = false;
|
|
460
|
+
if (element.volumeKeyframes && element.volumeKeyframes.length > 0) {
|
|
461
|
+
bakedEnvelope = applyVolumeEnvelopeToWav(audioSrcPath, element.volumeKeyframes, element.start, element.volume ?? 1.0);
|
|
462
|
+
}
|
|
358
463
|
tracks.push({
|
|
359
464
|
id: element.id,
|
|
360
465
|
srcPath: audioSrcPath,
|
|
@@ -362,8 +467,9 @@ export async function processCompositionAudio(elements, baseDir, workDir, output
|
|
|
362
467
|
end: element.end,
|
|
363
468
|
mediaStart: element.mediaStart,
|
|
364
469
|
duration: element.end - element.start,
|
|
365
|
-
|
|
366
|
-
|
|
470
|
+
// Gain is already in the samples when baked, so mix at unity.
|
|
471
|
+
volume: bakedEnvelope ? 1.0 : (element.volume ?? 1.0),
|
|
472
|
+
volumeKeyframes: bakedEnvelope ? undefined : element.volumeKeyframes,
|
|
367
473
|
});
|
|
368
474
|
}
|
|
369
475
|
catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audioMixer.js","sourceRoot":"","sources":["../../src/services/audioMixer.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"audioMixer.js","sourceRoot":"","sources":["../../src/services/audioMixer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sample-accurate volume automation.
|
|
3
|
+
*
|
|
4
|
+
* The audio mixer's primary path for time-varying volume bakes the envelope
|
|
5
|
+
* directly into the prepared PCM rather than encoding it as an FFmpeg `volume`
|
|
6
|
+
* expression. The expression approach nests one `if(lt(t,...))` per keyframe and
|
|
7
|
+
* overflows FFmpeg's expression evaluator past ~95 levels (a dense GSAP fade
|
|
8
|
+
* emits hundreds of keyframes), which fails the whole mix and drops the audio
|
|
9
|
+
* track. Multiplying the samples in-house has no such ceiling, is exact at every
|
|
10
|
+
* sample, and keeps the downstream ffmpeg `amix`/AAC encode untouched — so the
|
|
11
|
+
* output (and the golden baselines) only change where a fade is actually applied.
|
|
12
|
+
*
|
|
13
|
+
* The prepared tracks are always `pcm_s16le`, 48 kHz, stereo (see
|
|
14
|
+
* `prepareAudioTrack` / `extractAudioFromVideo`). Anything else is rejected so
|
|
15
|
+
* the caller can fall back to the expression path rather than corrupting audio.
|
|
16
|
+
*/
|
|
17
|
+
import type { AudioVolumeKeyframe } from "./audioMixer.types.js";
|
|
18
|
+
/**
|
|
19
|
+
* Multiply a prepared WAV's samples by a time-varying gain envelope in place.
|
|
20
|
+
*
|
|
21
|
+
* @returns `true` if the envelope was applied; `false` if the file isn't the
|
|
22
|
+
* expected 16-bit PCM (caller should fall back to the expression path).
|
|
23
|
+
*/
|
|
24
|
+
export declare function applyVolumeEnvelopeToWav(wavPath: string, keyframes: AudioVolumeKeyframe[], trackStart: number, baseVolume: number): boolean;
|
|
25
|
+
//# sourceMappingURL=audioVolumeEnvelope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audioVolumeEnvelope.d.ts","sourceRoot":"","sources":["../../src/services/audioVolumeEnvelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAyDjE;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,mBAAmB,EAAE,EAChC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAkDT"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sample-accurate volume automation.
|
|
3
|
+
*
|
|
4
|
+
* The audio mixer's primary path for time-varying volume bakes the envelope
|
|
5
|
+
* directly into the prepared PCM rather than encoding it as an FFmpeg `volume`
|
|
6
|
+
* expression. The expression approach nests one `if(lt(t,...))` per keyframe and
|
|
7
|
+
* overflows FFmpeg's expression evaluator past ~95 levels (a dense GSAP fade
|
|
8
|
+
* emits hundreds of keyframes), which fails the whole mix and drops the audio
|
|
9
|
+
* track. Multiplying the samples in-house has no such ceiling, is exact at every
|
|
10
|
+
* sample, and keeps the downstream ffmpeg `amix`/AAC encode untouched — so the
|
|
11
|
+
* output (and the golden baselines) only change where a fade is actually applied.
|
|
12
|
+
*
|
|
13
|
+
* The prepared tracks are always `pcm_s16le`, 48 kHz, stereo (see
|
|
14
|
+
* `prepareAudioTrack` / `extractAudioFromVideo`). Anything else is rejected so
|
|
15
|
+
* the caller can fall back to the expression path rather than corrupting audio.
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync, renameSync, writeFileSync } from "fs";
|
|
18
|
+
import { randomBytes } from "crypto";
|
|
19
|
+
import { normaliseEnvelope } from "@hyperframes/core/media-volume-envelope";
|
|
20
|
+
const PCM_FORMAT = 1; // WAVE_FORMAT_PCM
|
|
21
|
+
const SUPPORTED_BITS = 16;
|
|
22
|
+
/**
|
|
23
|
+
* Locate the `fmt ` and `data` chunks and validate the format we know how to edit.
|
|
24
|
+
*
|
|
25
|
+
* Scans every chunk rather than assuming an ordering: the loop always advances
|
|
26
|
+
* past a chunk's body (using its declared size), so `data` may precede `fmt `
|
|
27
|
+
* and trailing chunks (LIST/fact/etc.) are skipped harmlessly. Returns null on
|
|
28
|
+
* anything unexpected so the caller falls back to the expression path.
|
|
29
|
+
*/
|
|
30
|
+
function parseWavLayout(buffer) {
|
|
31
|
+
if (buffer.length < 12 || buffer.toString("ascii", 0, 4) !== "RIFF")
|
|
32
|
+
return null;
|
|
33
|
+
if (buffer.toString("ascii", 8, 12) !== "WAVE")
|
|
34
|
+
return null;
|
|
35
|
+
let offset = 12;
|
|
36
|
+
let fmt = null;
|
|
37
|
+
let data = null;
|
|
38
|
+
while (offset + 8 <= buffer.length) {
|
|
39
|
+
const chunkId = buffer.toString("ascii", offset, offset + 4);
|
|
40
|
+
const chunkSize = buffer.readUInt32LE(offset + 4);
|
|
41
|
+
const body = offset + 8;
|
|
42
|
+
if (chunkId === "fmt " && body + 16 <= buffer.length) {
|
|
43
|
+
if (buffer.readUInt16LE(body) !== PCM_FORMAT)
|
|
44
|
+
return null;
|
|
45
|
+
fmt = {
|
|
46
|
+
numChannels: buffer.readUInt16LE(body + 2),
|
|
47
|
+
sampleRate: buffer.readUInt32LE(body + 4),
|
|
48
|
+
bitsPerSample: buffer.readUInt16LE(body + 14),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
else if (chunkId === "data") {
|
|
52
|
+
data = { offset: body, size: Math.min(chunkSize, buffer.length - body) };
|
|
53
|
+
}
|
|
54
|
+
// Chunks are word-aligned: an odd size carries a trailing pad byte.
|
|
55
|
+
offset = body + chunkSize + (chunkSize % 2);
|
|
56
|
+
}
|
|
57
|
+
if (!fmt || !data)
|
|
58
|
+
return null;
|
|
59
|
+
if (fmt.bitsPerSample !== SUPPORTED_BITS || fmt.numChannels < 1)
|
|
60
|
+
return null;
|
|
61
|
+
return {
|
|
62
|
+
numChannels: fmt.numChannels,
|
|
63
|
+
sampleRate: fmt.sampleRate,
|
|
64
|
+
dataOffset: data.offset,
|
|
65
|
+
dataSize: data.size,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Multiply a prepared WAV's samples by a time-varying gain envelope in place.
|
|
70
|
+
*
|
|
71
|
+
* @returns `true` if the envelope was applied; `false` if the file isn't the
|
|
72
|
+
* expected 16-bit PCM (caller should fall back to the expression path).
|
|
73
|
+
*/
|
|
74
|
+
export function applyVolumeEnvelopeToWav(wavPath, keyframes, trackStart, baseVolume) {
|
|
75
|
+
const envelope = normaliseEnvelope(keyframes, trackStart, baseVolume);
|
|
76
|
+
if (envelope.length === 0)
|
|
77
|
+
return false;
|
|
78
|
+
try {
|
|
79
|
+
const buffer = readFileSync(wavPath);
|
|
80
|
+
const layout = parseWavLayout(buffer);
|
|
81
|
+
if (!layout)
|
|
82
|
+
return false;
|
|
83
|
+
const { numChannels, sampleRate, dataOffset, dataSize } = layout;
|
|
84
|
+
const bytesPerSample = SUPPORTED_BITS / 8;
|
|
85
|
+
const frameBytes = numChannels * bytesPerSample;
|
|
86
|
+
const frameCount = Math.floor(dataSize / frameBytes);
|
|
87
|
+
// Maintain an incremental segment cursor so the per-frame envelope lookup
|
|
88
|
+
// is O(N+M) overall, not O(N×M). interpolateVolumeGain restarts from 0 on
|
|
89
|
+
// each call — fine for the preview path (one call per RAF tick) but not for
|
|
90
|
+
// the PCM path (one call per sample, 48k×duration frames total).
|
|
91
|
+
let segment = 0;
|
|
92
|
+
for (let frame = 0; frame < frameCount; frame += 1) {
|
|
93
|
+
const time = frame / sampleRate;
|
|
94
|
+
while (segment < envelope.length - 2 && time >= envelope[segment + 1].time)
|
|
95
|
+
segment += 1;
|
|
96
|
+
const a = envelope[segment];
|
|
97
|
+
const b = envelope[segment + 1] ?? a;
|
|
98
|
+
const span = b.time - a.time;
|
|
99
|
+
const progress = span <= 0 ? 0 : Math.min(1, Math.max(0, (time - a.time) / span));
|
|
100
|
+
const gain = a.volume + (b.volume - a.volume) * progress;
|
|
101
|
+
const base = dataOffset + frame * frameBytes;
|
|
102
|
+
for (let channel = 0; channel < numChannels; channel += 1) {
|
|
103
|
+
const at = base + channel * bytesPerSample;
|
|
104
|
+
const scaled = Math.round(buffer.readInt16LE(at) * gain);
|
|
105
|
+
buffer.writeInt16LE(scaled < -32768 ? -32768 : scaled > 32767 ? 32767 : scaled, at);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Write to a uniquely-named sibling then atomically rename over the
|
|
109
|
+
// original. The random name avoids following a pre-planted symlink at a
|
|
110
|
+
// predictable path, and the rename means a crash mid-write can't leave a
|
|
111
|
+
// truncated WAV for the downstream mix.
|
|
112
|
+
const tempPath = `${wavPath}.${randomBytes(6).toString("hex")}.tmp`;
|
|
113
|
+
writeFileSync(tempPath, buffer);
|
|
114
|
+
renameSync(tempPath, wavPath);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Any read/parse/write failure → leave the file untouched and let the
|
|
119
|
+
// caller fall back to the ffmpeg expression path rather than losing audio.
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=audioVolumeEnvelope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audioVolumeEnvelope.js","sourceRoot":"","sources":["../../src/services/audioVolumeEnvelope.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/engine",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.57",
|
|
4
4
|
"description": "Seekable web page to video rendering engine (Puppeteer + FFmpeg)",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"linkedom": "^0.18.12",
|
|
22
22
|
"puppeteer": "^24.0.0",
|
|
23
23
|
"puppeteer-core": "^24.39.1",
|
|
24
|
-
"@hyperframes/core": "^0.6.
|
|
24
|
+
"@hyperframes/core": "^0.6.57"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^25.0.10",
|
|
@@ -108,6 +108,121 @@ describe("processCompositionAudio", () => {
|
|
|
108
108
|
expect(filter).toContain("adelay=2000|2000");
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
it("bounds expression nesting for dense keyframe automation without dropping the envelope", async () => {
|
|
112
|
+
const baseDir = mkdtempSync(join(tmpdir(), "hf-audio-base-"));
|
|
113
|
+
const workDir = mkdtempSync(join(tmpdir(), "hf-audio-work-"));
|
|
114
|
+
tempDirs.push(baseDir, workDir);
|
|
115
|
+
|
|
116
|
+
writeFileSync(join(baseDir, "bgm.wav"), "stub");
|
|
117
|
+
|
|
118
|
+
// Mirrors the 60 Hz timeline probe: a 10s eased fade emits hundreds of
|
|
119
|
+
// keyframes. The nested-if volume expression must not grow one level per
|
|
120
|
+
// keyframe — past ~95 levels FFmpeg fails filter-graph init and the audio
|
|
121
|
+
// track is dropped entirely (GH #1066 follow-up).
|
|
122
|
+
const keyframes = Array.from({ length: 300 }, (_, i) => {
|
|
123
|
+
const time = (i / 299) * 10;
|
|
124
|
+
const volume =
|
|
125
|
+
time < 3 ? 0.8 * (time / 3) ** 2 : time < 7 ? 0.8 : 0.8 * (1 - (time - 7) / 3) ** 2;
|
|
126
|
+
return { time, volume };
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const result = await processCompositionAudio(
|
|
130
|
+
[
|
|
131
|
+
{
|
|
132
|
+
id: "bgm",
|
|
133
|
+
src: "bgm.wav",
|
|
134
|
+
start: 0,
|
|
135
|
+
end: 10,
|
|
136
|
+
mediaStart: 0,
|
|
137
|
+
layer: 0,
|
|
138
|
+
volume: 0,
|
|
139
|
+
volumeKeyframes: keyframes,
|
|
140
|
+
type: "audio",
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
baseDir,
|
|
144
|
+
workDir,
|
|
145
|
+
join(baseDir, "out.m4a"),
|
|
146
|
+
10,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(result.success).toBe(true);
|
|
150
|
+
|
|
151
|
+
const mixArgs = runFfmpegMock.mock.calls[1]?.[0];
|
|
152
|
+
const filterIndex = mixArgs.indexOf("-filter_complex");
|
|
153
|
+
const filter = mixArgs[filterIndex + 1];
|
|
154
|
+
|
|
155
|
+
// One nested `if(lt(...))` is emitted per segment; cap it well under the
|
|
156
|
+
// FFmpeg evaluator's nesting limit (MAX_VOLUME_SEGMENTS = 32).
|
|
157
|
+
const nestingDepth = (filter.match(/if\(lt\(t/g) ?? []).length;
|
|
158
|
+
expect(nestingDepth).toBeGreaterThan(1);
|
|
159
|
+
expect(nestingDepth).toBeLessThan(32);
|
|
160
|
+
|
|
161
|
+
// The simplified envelope still spans the clip: silent start, audible peak.
|
|
162
|
+
expect(filter).toContain(":eval=frame");
|
|
163
|
+
expect(filter).toMatch(/volume=if\(lt\(t\\,[0-9.]+\)\\,0\+/);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("falls back to a static-volume mix instead of dropping audio when the automated mix fails", async () => {
|
|
167
|
+
const baseDir = mkdtempSync(join(tmpdir(), "hf-audio-base-"));
|
|
168
|
+
const workDir = mkdtempSync(join(tmpdir(), "hf-audio-work-"));
|
|
169
|
+
tempDirs.push(baseDir, workDir);
|
|
170
|
+
|
|
171
|
+
writeFileSync(join(baseDir, "bgm.wav"), "stub");
|
|
172
|
+
|
|
173
|
+
// Simulate an ffmpeg build that rejects the automation expression: the
|
|
174
|
+
// first mix attempt fails, the static-volume retry succeeds. (prepare =
|
|
175
|
+
// call 0, automated mix = call 1, fallback mix = call 2.)
|
|
176
|
+
runFfmpegMock
|
|
177
|
+
.mockImplementationOnce(async () => ({
|
|
178
|
+
success: true,
|
|
179
|
+
durationMs: 1,
|
|
180
|
+
stderr: "",
|
|
181
|
+
exitCode: 0,
|
|
182
|
+
}))
|
|
183
|
+
.mockImplementationOnce(async () => ({
|
|
184
|
+
success: false,
|
|
185
|
+
durationMs: 1,
|
|
186
|
+
stderr: "Error initializing filters",
|
|
187
|
+
exitCode: 234,
|
|
188
|
+
}));
|
|
189
|
+
|
|
190
|
+
const result = await processCompositionAudio(
|
|
191
|
+
[
|
|
192
|
+
{
|
|
193
|
+
id: "bgm",
|
|
194
|
+
src: "bgm.wav",
|
|
195
|
+
start: 0,
|
|
196
|
+
end: 5,
|
|
197
|
+
mediaStart: 0,
|
|
198
|
+
layer: 0,
|
|
199
|
+
volume: 0.8,
|
|
200
|
+
volumeKeyframes: [
|
|
201
|
+
{ time: 0, volume: 0.8 },
|
|
202
|
+
{ time: 5, volume: 0 },
|
|
203
|
+
],
|
|
204
|
+
type: "audio",
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
baseDir,
|
|
208
|
+
workDir,
|
|
209
|
+
join(baseDir, "out.m4a"),
|
|
210
|
+
5,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
expect(result.success).toBe(true);
|
|
214
|
+
expect(result.tracksProcessed).toBe(1);
|
|
215
|
+
expect(runFfmpegMock).toHaveBeenCalledTimes(3);
|
|
216
|
+
// Degradation is surfaced, not silent — the track rendered at base volume.
|
|
217
|
+
expect(result.error).toMatch(/base volume/i);
|
|
218
|
+
|
|
219
|
+
// The fallback mix omits the automation expression (base volume only).
|
|
220
|
+
const fallbackArgs = runFfmpegMock.mock.calls[2]?.[0];
|
|
221
|
+
const fallbackFilter = fallbackArgs[fallbackArgs.indexOf("-filter_complex") + 1];
|
|
222
|
+
expect(fallbackFilter).not.toContain(":eval=frame");
|
|
223
|
+
expect(fallbackFilter).toContain("volume=0.8");
|
|
224
|
+
});
|
|
225
|
+
|
|
111
226
|
it("prepares percent-encoded non-Latin audio srcs from decoded filesystem paths", async () => {
|
|
112
227
|
const baseDir = mkdtempSync(join(tmpdir(), "hf-audio-base-"));
|
|
113
228
|
const workDir = mkdtempSync(join(tmpdir(), "hf-audio-work-"));
|
|
@@ -14,6 +14,7 @@ import { runFfmpeg } from "../utils/runFfmpeg.js";
|
|
|
14
14
|
import { unwrapTemplate } from "../utils/htmlTemplate.js";
|
|
15
15
|
import { resolveProjectRelativeSrc } from "./videoFrameExtractor.js";
|
|
16
16
|
import type { AudioElement, AudioTrack, MixResult } from "./audioMixer.types.js";
|
|
17
|
+
import { applyVolumeEnvelopeToWav } from "./audioVolumeEnvelope.js";
|
|
17
18
|
|
|
18
19
|
export type { AudioElement, MixResult } from "./audioMixer.types.js";
|
|
19
20
|
|
|
@@ -30,10 +31,89 @@ function escapeExpressionCommas(expression: string): string {
|
|
|
30
31
|
return expression.replace(/\\/g, "\\\\").replace(/,/g, "\\,");
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Upper bound on volume-automation keyframes folded into the FFmpeg `volume`
|
|
36
|
+
* expression. The expression nests one `if(lt(...))` per keyframe, and
|
|
37
|
+
* FFmpeg's expression evaluator has a finite nesting depth: past ~95 levels
|
|
38
|
+
* (build-dependent — lower on some Linux ffmpeg builds) `volume=...:eval=frame`
|
|
39
|
+
* fails filter-graph init, which fails the whole mix and drops the audio track
|
|
40
|
+
* entirely. The 60 Hz timeline probe routinely emits 100–300 keyframes for a
|
|
41
|
+
* multi-second fade (GH #1066 follow-up: a 171-keyframe GSAP fade rendered with
|
|
42
|
+
* no audio). 32 segments keeps a wide safety margin and is far more resolution
|
|
43
|
+
* than a piecewise-linear volume envelope needs.
|
|
44
|
+
*/
|
|
45
|
+
const MAX_VOLUME_SEGMENTS = 32;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Volume delta below which a keyframe is collinear enough to drop. Kept tight
|
|
49
|
+
* (0.5% linear) so the rendered piecewise-linear envelope tracks the GSAP curve
|
|
50
|
+
* the browser plays in preview to within ~0.2 dB across the audible range — well
|
|
51
|
+
* under the ~1 dB loudness JND, so render stays WYSIWYG with preview. A full
|
|
52
|
+
* ease-in/ease-out fade still reduces to ~25 segments, inside MAX_VOLUME_SEGMENTS.
|
|
53
|
+
*/
|
|
54
|
+
const VOLUME_SIMPLIFY_EPSILON = 0.005;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Reduce a sorted keyframe list to a perceptually-equivalent piecewise-linear
|
|
58
|
+
* envelope with a bounded segment count.
|
|
59
|
+
*
|
|
60
|
+
* Ramer–Douglas–Peucker drops control points lying within
|
|
61
|
+
* `VOLUME_SIMPLIFY_EPSILON` of the line through their neighbours (a linear fade
|
|
62
|
+
* collapses to its two endpoints; an eased fade to a handful). A uniform
|
|
63
|
+
* downsample backstop then bounds pathological inputs (e.g. audio-rate volume
|
|
64
|
+
* oscillation) to `MAX_VOLUME_SEGMENTS`. Endpoints are always preserved so the
|
|
65
|
+
* envelope still spans the full clip.
|
|
66
|
+
*/
|
|
67
|
+
function simplifyVolumeKeyframes(
|
|
68
|
+
keyframes: { time: number; volume: number }[],
|
|
69
|
+
): { time: number; volume: number }[] {
|
|
70
|
+
if (keyframes.length < 3) return keyframes;
|
|
71
|
+
|
|
72
|
+
const keep = new Array<boolean>(keyframes.length).fill(false);
|
|
73
|
+
keep[0] = true;
|
|
74
|
+
keep[keyframes.length - 1] = true;
|
|
75
|
+
const stack: [number, number][] = [[0, keyframes.length - 1]];
|
|
76
|
+
while (stack.length > 0) {
|
|
77
|
+
const [startIndex, endIndex] = stack.pop()!;
|
|
78
|
+
const start = keyframes[startIndex]!;
|
|
79
|
+
const end = keyframes[endIndex]!;
|
|
80
|
+
const span = end.time - start.time;
|
|
81
|
+
let maxDistance = VOLUME_SIMPLIFY_EPSILON;
|
|
82
|
+
let splitIndex = -1;
|
|
83
|
+
for (let i = startIndex + 1; i < endIndex; i += 1) {
|
|
84
|
+
const point = keyframes[i]!;
|
|
85
|
+
const interpolated =
|
|
86
|
+
span === 0
|
|
87
|
+
? start.volume
|
|
88
|
+
: start.volume + ((end.volume - start.volume) * (point.time - start.time)) / span;
|
|
89
|
+
const distance = Math.abs(point.volume - interpolated);
|
|
90
|
+
if (distance > maxDistance) {
|
|
91
|
+
maxDistance = distance;
|
|
92
|
+
splitIndex = i;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (splitIndex !== -1) {
|
|
96
|
+
keep[splitIndex] = true;
|
|
97
|
+
stack.push([startIndex, splitIndex], [splitIndex, endIndex]);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const simplified = keyframes.filter((_, i) => keep[i]);
|
|
102
|
+
if (simplified.length <= MAX_VOLUME_SEGMENTS) return simplified;
|
|
103
|
+
|
|
104
|
+
const step = (simplified.length - 1) / (MAX_VOLUME_SEGMENTS - 1);
|
|
105
|
+
const sampled: { time: number; volume: number }[] = [];
|
|
106
|
+
for (let i = 0; i < MAX_VOLUME_SEGMENTS; i += 1) {
|
|
107
|
+
const point = simplified[Math.round(i * step)]!;
|
|
108
|
+
if (sampled.length === 0 || point.time > sampled.at(-1)!.time) sampled.push(point);
|
|
109
|
+
}
|
|
110
|
+
return sampled;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildVolumeExpression(track: AudioTrack, ignoreKeyframes = false): string {
|
|
34
114
|
const trimDuration = track.end - track.start;
|
|
35
115
|
const staticVolume = clampVolume(track.volume);
|
|
36
|
-
const keyframes = (track.volumeKeyframes ?? [])
|
|
116
|
+
const keyframes = (ignoreKeyframes ? [] : (track.volumeKeyframes ?? []))
|
|
37
117
|
.filter((keyframe) => Number.isFinite(keyframe.time) && Number.isFinite(keyframe.volume))
|
|
38
118
|
.map((keyframe) => ({
|
|
39
119
|
time: Math.max(0, Math.min(trimDuration, keyframe.time - track.start)),
|
|
@@ -57,14 +137,19 @@ function buildVolumeExpression(track: AudioTrack): string {
|
|
|
57
137
|
}
|
|
58
138
|
}
|
|
59
139
|
|
|
60
|
-
|
|
61
|
-
|
|
140
|
+
// Collapse the densely-sampled probe output to a bounded piecewise-linear
|
|
141
|
+
// envelope. Without this, the nested-if expression below grows one level per
|
|
142
|
+
// keyframe and overflows FFmpeg's expression evaluator (see MAX_VOLUME_SEGMENTS).
|
|
143
|
+
const simplified = simplifyVolumeKeyframes(deduped);
|
|
144
|
+
|
|
145
|
+
if (simplified.length === 1) {
|
|
146
|
+
return `volume=${formatFilterNumber(simplified[0]!.volume)}`;
|
|
62
147
|
}
|
|
63
148
|
|
|
64
|
-
let expression = formatFilterNumber(
|
|
65
|
-
for (let i =
|
|
66
|
-
const current =
|
|
67
|
-
const next =
|
|
149
|
+
let expression = formatFilterNumber(simplified.at(-1)!.volume);
|
|
150
|
+
for (let i = simplified.length - 2; i >= 0; i -= 1) {
|
|
151
|
+
const current = simplified[i]!;
|
|
152
|
+
const next = simplified[i + 1]!;
|
|
68
153
|
const currentTime = formatFilterNumber(current.time);
|
|
69
154
|
const nextTime = formatFilterNumber(next.time);
|
|
70
155
|
const currentVolume = formatFilterNumber(current.volume);
|
|
@@ -299,42 +384,58 @@ async function mixAudioTracks(
|
|
|
299
384
|
const outputDir = dirname(outputPath);
|
|
300
385
|
if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
|
|
301
386
|
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const mixInputs = tracks.map((_, i) => `[a${i}]`).join("");
|
|
316
|
-
const weights = tracks.map(() => "1").join(" ");
|
|
317
|
-
const mixFilter = `${mixInputs}amix=inputs=${tracks.length}:duration=longest:dropout_transition=0:normalize=0:weights='${weights}'[mixed]`;
|
|
318
|
-
const postMixGainFilter = `[mixed]volume=${masterOutputGain}[out]`;
|
|
319
|
-
const fullFilter = [...filterParts, mixFilter, postMixGainFilter].join(";");
|
|
387
|
+
const buildArgs = (ignoreAutomation: boolean): string[] => {
|
|
388
|
+
const inputs: string[] = [];
|
|
389
|
+
const filterParts: string[] = [];
|
|
390
|
+
tracks.forEach((track, i) => {
|
|
391
|
+
inputs.push("-i", track.srcPath);
|
|
392
|
+
const delayMs = Math.round(track.start * 1000);
|
|
393
|
+
const trimDuration = track.end - track.start;
|
|
394
|
+
const volumeFilter = buildVolumeExpression(track, ignoreAutomation);
|
|
395
|
+
filterParts.push(
|
|
396
|
+
`[${i}:a]atrim=0:${trimDuration},${volumeFilter},adelay=${delayMs}|${delayMs},apad=whole_dur=${totalDuration}[a${i}]`,
|
|
397
|
+
);
|
|
398
|
+
});
|
|
320
399
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
"
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
400
|
+
const mixInputs = tracks.map((_, i) => `[a${i}]`).join("");
|
|
401
|
+
const weights = tracks.map(() => "1").join(" ");
|
|
402
|
+
const mixFilter = `${mixInputs}amix=inputs=${tracks.length}:duration=longest:dropout_transition=0:normalize=0:weights='${weights}'[mixed]`;
|
|
403
|
+
const postMixGainFilter = `[mixed]volume=${masterOutputGain}[out]`;
|
|
404
|
+
const fullFilter = [...filterParts, mixFilter, postMixGainFilter].join(";");
|
|
405
|
+
|
|
406
|
+
return [
|
|
407
|
+
...inputs,
|
|
408
|
+
"-filter_complex",
|
|
409
|
+
fullFilter,
|
|
410
|
+
"-map",
|
|
411
|
+
"[out]",
|
|
412
|
+
"-acodec",
|
|
413
|
+
"aac",
|
|
414
|
+
"-b:a",
|
|
415
|
+
"192k",
|
|
416
|
+
"-t",
|
|
417
|
+
String(totalDuration),
|
|
418
|
+
"-y",
|
|
419
|
+
outputPath,
|
|
420
|
+
];
|
|
421
|
+
};
|
|
336
422
|
|
|
337
|
-
|
|
423
|
+
let result = await runFfmpeg(buildArgs(false), { signal, timeout: ffmpegProcessTimeout });
|
|
424
|
+
|
|
425
|
+
// Defense in depth: volume automation is folded into an FFmpeg `volume`
|
|
426
|
+
// expression whose evaluator limits are build-dependent (see
|
|
427
|
+
// MAX_VOLUME_SEGMENTS). If that ever fails the mix, retry once without the
|
|
428
|
+
// automation so the track renders at its base volume rather than being
|
|
429
|
+
// dropped from the output entirely — a missing fade beats missing audio.
|
|
430
|
+
let degradedAutomation = false;
|
|
431
|
+
const hasAutomation = tracks.some((track) => (track.volumeKeyframes?.length ?? 0) > 0);
|
|
432
|
+
if (!result.success && !signal?.aborted && hasAutomation) {
|
|
433
|
+
const retry = await runFfmpeg(buildArgs(true), { signal, timeout: ffmpegProcessTimeout });
|
|
434
|
+
if (retry.success) {
|
|
435
|
+
result = retry;
|
|
436
|
+
degradedAutomation = true;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
338
439
|
|
|
339
440
|
if (signal?.aborted) {
|
|
340
441
|
return {
|
|
@@ -360,6 +461,9 @@ async function mixAudioTracks(
|
|
|
360
461
|
outputPath,
|
|
361
462
|
durationMs: result.durationMs,
|
|
362
463
|
tracksProcessed: tracks.length,
|
|
464
|
+
error: degradedAutomation
|
|
465
|
+
? "Volume automation exceeded this ffmpeg build's expression limits; rendered at base volume"
|
|
466
|
+
: undefined,
|
|
363
467
|
};
|
|
364
468
|
}
|
|
365
469
|
|
|
@@ -452,6 +556,19 @@ export async function processCompositionAudio(
|
|
|
452
556
|
audioSrcPath = trimmedPath;
|
|
453
557
|
}
|
|
454
558
|
|
|
559
|
+
// Primary volume-automation path: bake the envelope into the PCM samples
|
|
560
|
+
// (sample-accurate, no keyframe ceiling). If the WAV isn't the expected
|
|
561
|
+
// 16-bit PCM, fall back to the ffmpeg expression path by leaving the
|
|
562
|
+
// keyframes on the track for buildVolumeExpression to handle.
|
|
563
|
+
let bakedEnvelope = false;
|
|
564
|
+
if (element.volumeKeyframes && element.volumeKeyframes.length > 0) {
|
|
565
|
+
bakedEnvelope = applyVolumeEnvelopeToWav(
|
|
566
|
+
audioSrcPath,
|
|
567
|
+
element.volumeKeyframes,
|
|
568
|
+
element.start,
|
|
569
|
+
element.volume ?? 1.0,
|
|
570
|
+
);
|
|
571
|
+
}
|
|
455
572
|
tracks.push({
|
|
456
573
|
id: element.id,
|
|
457
574
|
srcPath: audioSrcPath,
|
|
@@ -459,8 +576,9 @@ export async function processCompositionAudio(
|
|
|
459
576
|
end: element.end,
|
|
460
577
|
mediaStart: element.mediaStart,
|
|
461
578
|
duration: element.end - element.start,
|
|
462
|
-
|
|
463
|
-
|
|
579
|
+
// Gain is already in the samples when baked, so mix at unity.
|
|
580
|
+
volume: bakedEnvelope ? 1.0 : (element.volume ?? 1.0),
|
|
581
|
+
volumeKeyframes: bakedEnvelope ? undefined : element.volumeKeyframes,
|
|
464
582
|
});
|
|
465
583
|
} catch (err: unknown) {
|
|
466
584
|
errors.push(`Error: ${element.id} — ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { applyVolumeEnvelopeToWav } from "./audioVolumeEnvelope.js";
|
|
6
|
+
|
|
7
|
+
const SAMPLE_RATE = 48000;
|
|
8
|
+
const CHANNELS = 2;
|
|
9
|
+
|
|
10
|
+
/** Build a PCM s16le stereo WAV whose every sample equals `value`. */
|
|
11
|
+
function writeConstantWav(path: string, frames: number, value: number): void {
|
|
12
|
+
const bytesPerSample = 2;
|
|
13
|
+
const dataSize = frames * CHANNELS * bytesPerSample;
|
|
14
|
+
const buffer = Buffer.alloc(44 + dataSize);
|
|
15
|
+
buffer.write("RIFF", 0, "ascii");
|
|
16
|
+
buffer.writeUInt32LE(36 + dataSize, 4);
|
|
17
|
+
buffer.write("WAVE", 8, "ascii");
|
|
18
|
+
buffer.write("fmt ", 12, "ascii");
|
|
19
|
+
buffer.writeUInt32LE(16, 16);
|
|
20
|
+
buffer.writeUInt16LE(1, 20); // PCM
|
|
21
|
+
buffer.writeUInt16LE(CHANNELS, 22);
|
|
22
|
+
buffer.writeUInt32LE(SAMPLE_RATE, 24);
|
|
23
|
+
buffer.writeUInt32LE(SAMPLE_RATE * CHANNELS * bytesPerSample, 28);
|
|
24
|
+
buffer.writeUInt16LE(CHANNELS * bytesPerSample, 32);
|
|
25
|
+
buffer.writeUInt16LE(16, 34);
|
|
26
|
+
buffer.write("data", 36, "ascii");
|
|
27
|
+
buffer.writeUInt32LE(dataSize, 40);
|
|
28
|
+
for (let i = 0; i < frames * CHANNELS; i += 1) buffer.writeInt16LE(value, 44 + i * 2);
|
|
29
|
+
writeFileSync(path, buffer);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sampleAt(path: string, frame: number, channel = 0): number {
|
|
33
|
+
const buffer = readFileSync(path);
|
|
34
|
+
return buffer.readInt16LE(44 + (frame * CHANNELS + channel) * 2);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("applyVolumeEnvelopeToWav", () => {
|
|
38
|
+
const dirs: string[] = [];
|
|
39
|
+
const tmp = () => {
|
|
40
|
+
const d = mkdtempSync(join(tmpdir(), "hf-env-"));
|
|
41
|
+
dirs.push(d);
|
|
42
|
+
return d;
|
|
43
|
+
};
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("applies a linear fade sample-accurately", () => {
|
|
49
|
+
const path = join(tmp(), "a.wav");
|
|
50
|
+
const frames = SAMPLE_RATE; // 1 second
|
|
51
|
+
writeConstantWav(path, frames, 10000);
|
|
52
|
+
|
|
53
|
+
// Fade 0 -> 1 over the full second.
|
|
54
|
+
const applied = applyVolumeEnvelopeToWav(
|
|
55
|
+
path,
|
|
56
|
+
[
|
|
57
|
+
{ time: 0, volume: 0 },
|
|
58
|
+
{ time: 1, volume: 1 },
|
|
59
|
+
],
|
|
60
|
+
0,
|
|
61
|
+
0,
|
|
62
|
+
);
|
|
63
|
+
expect(applied).toBe(true);
|
|
64
|
+
|
|
65
|
+
expect(sampleAt(path, 0)).toBe(0); // gain 0
|
|
66
|
+
expect(sampleAt(path, frames / 2)).toBeCloseTo(5000, -2); // gain ~0.5
|
|
67
|
+
expect(sampleAt(path, frames - 1)).toBeGreaterThan(9900); // gain ~1
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("offsets keyframes by the track start (composition time -> track-relative)", () => {
|
|
71
|
+
const path = join(tmp(), "b.wav");
|
|
72
|
+
const frames = SAMPLE_RATE;
|
|
73
|
+
writeConstantWav(path, frames, 10000);
|
|
74
|
+
|
|
75
|
+
// Track starts at 5s; the fade runs from comp-time 5s..6s -> wav 0s..1s.
|
|
76
|
+
applyVolumeEnvelopeToWav(
|
|
77
|
+
path,
|
|
78
|
+
[
|
|
79
|
+
{ time: 5, volume: 0 },
|
|
80
|
+
{ time: 6, volume: 1 },
|
|
81
|
+
],
|
|
82
|
+
5,
|
|
83
|
+
0,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(sampleAt(path, 0)).toBe(0);
|
|
87
|
+
expect(sampleAt(path, frames / 2)).toBeCloseTo(5000, -2);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("holds base volume before the first keyframe and the last value after", () => {
|
|
91
|
+
const path = join(tmp(), "c.wav");
|
|
92
|
+
const frames = SAMPLE_RATE * 3; // 3 seconds
|
|
93
|
+
writeConstantWav(path, frames, 10000);
|
|
94
|
+
|
|
95
|
+
// Base 0.8 held until a fade-out begins at 2s.
|
|
96
|
+
applyVolumeEnvelopeToWav(
|
|
97
|
+
path,
|
|
98
|
+
[
|
|
99
|
+
{ time: 2, volume: 0.8 },
|
|
100
|
+
{ time: 3, volume: 0 },
|
|
101
|
+
],
|
|
102
|
+
0,
|
|
103
|
+
0.8,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
expect(sampleAt(path, SAMPLE_RATE)).toBeCloseTo(8000, -2); // 1s: base 0.8
|
|
107
|
+
expect(sampleAt(path, frames - 1)).toBeLessThan(200); // 3s: faded to ~0
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("handles thousands of keyframes without failing (no expression ceiling)", () => {
|
|
111
|
+
const path = join(tmp(), "d.wav");
|
|
112
|
+
const frames = SAMPLE_RATE * 2;
|
|
113
|
+
writeConstantWav(path, frames, 10000);
|
|
114
|
+
|
|
115
|
+
const keyframes = Array.from({ length: 5000 }, (_, i) => ({
|
|
116
|
+
time: (i / 4999) * 2,
|
|
117
|
+
volume: Math.abs(Math.sin(i / 50)),
|
|
118
|
+
}));
|
|
119
|
+
expect(applyVolumeEnvelopeToWav(path, keyframes, 0, 0)).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("parses chunks in any order (data before fmt)", () => {
|
|
123
|
+
const path = join(tmp(), "order.wav");
|
|
124
|
+
const frames = 4;
|
|
125
|
+
const dataSize = frames * CHANNELS * 2;
|
|
126
|
+
// Lay the data chunk before fmt to exercise order-independent scanning.
|
|
127
|
+
const buffer = Buffer.alloc(12 + (8 + dataSize) + (8 + 16));
|
|
128
|
+
buffer.write("RIFF", 0, "ascii");
|
|
129
|
+
buffer.writeUInt32LE(buffer.length - 8, 4);
|
|
130
|
+
buffer.write("WAVE", 8, "ascii");
|
|
131
|
+
let o = 12;
|
|
132
|
+
buffer.write("data", o, "ascii");
|
|
133
|
+
buffer.writeUInt32LE(dataSize, o + 4);
|
|
134
|
+
for (let i = 0; i < frames * CHANNELS; i += 1) buffer.writeInt16LE(10000, o + 8 + i * 2);
|
|
135
|
+
o += 8 + dataSize;
|
|
136
|
+
buffer.write("fmt ", o, "ascii");
|
|
137
|
+
buffer.writeUInt32LE(16, o + 4);
|
|
138
|
+
buffer.writeUInt16LE(1, o + 8);
|
|
139
|
+
buffer.writeUInt16LE(CHANNELS, o + 10);
|
|
140
|
+
buffer.writeUInt32LE(SAMPLE_RATE, o + 12);
|
|
141
|
+
buffer.writeUInt16LE(16, o + 22);
|
|
142
|
+
writeFileSync(path, buffer);
|
|
143
|
+
|
|
144
|
+
expect(applyVolumeEnvelopeToWav(path, [{ time: 0, volume: 0 }], 0, 0)).toBe(true);
|
|
145
|
+
expect(readFileSync(path).readInt16LE(12 + 8)).toBe(0); // first sample muted
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("rejects non-16-bit PCM so the caller can fall back", () => {
|
|
149
|
+
const path = join(tmp(), "e.wav");
|
|
150
|
+
// 24-bit PCM header (bitsPerSample = 24); body contents are irrelevant.
|
|
151
|
+
const buffer = Buffer.alloc(44);
|
|
152
|
+
buffer.write("RIFF", 0, "ascii");
|
|
153
|
+
buffer.write("WAVE", 8, "ascii");
|
|
154
|
+
buffer.write("fmt ", 12, "ascii");
|
|
155
|
+
buffer.writeUInt32LE(16, 16);
|
|
156
|
+
buffer.writeUInt16LE(1, 20);
|
|
157
|
+
buffer.writeUInt16LE(CHANNELS, 22);
|
|
158
|
+
buffer.writeUInt32LE(SAMPLE_RATE, 24);
|
|
159
|
+
buffer.writeUInt16LE(24, 34);
|
|
160
|
+
buffer.write("data", 36, "ascii");
|
|
161
|
+
buffer.writeUInt32LE(0, 40);
|
|
162
|
+
writeFileSync(path, buffer);
|
|
163
|
+
|
|
164
|
+
expect(
|
|
165
|
+
applyVolumeEnvelopeToWav(
|
|
166
|
+
path,
|
|
167
|
+
[
|
|
168
|
+
{ time: 0, volume: 0 },
|
|
169
|
+
{ time: 1, volume: 1 },
|
|
170
|
+
],
|
|
171
|
+
0,
|
|
172
|
+
0,
|
|
173
|
+
),
|
|
174
|
+
).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sample-accurate volume automation.
|
|
3
|
+
*
|
|
4
|
+
* The audio mixer's primary path for time-varying volume bakes the envelope
|
|
5
|
+
* directly into the prepared PCM rather than encoding it as an FFmpeg `volume`
|
|
6
|
+
* expression. The expression approach nests one `if(lt(t,...))` per keyframe and
|
|
7
|
+
* overflows FFmpeg's expression evaluator past ~95 levels (a dense GSAP fade
|
|
8
|
+
* emits hundreds of keyframes), which fails the whole mix and drops the audio
|
|
9
|
+
* track. Multiplying the samples in-house has no such ceiling, is exact at every
|
|
10
|
+
* sample, and keeps the downstream ffmpeg `amix`/AAC encode untouched — so the
|
|
11
|
+
* output (and the golden baselines) only change where a fade is actually applied.
|
|
12
|
+
*
|
|
13
|
+
* The prepared tracks are always `pcm_s16le`, 48 kHz, stereo (see
|
|
14
|
+
* `prepareAudioTrack` / `extractAudioFromVideo`). Anything else is rejected so
|
|
15
|
+
* the caller can fall back to the expression path rather than corrupting audio.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync, renameSync, writeFileSync } from "fs";
|
|
19
|
+
import { randomBytes } from "crypto";
|
|
20
|
+
import type { AudioVolumeKeyframe } from "./audioMixer.types.js";
|
|
21
|
+
import { normaliseEnvelope } from "@hyperframes/core/media-volume-envelope";
|
|
22
|
+
|
|
23
|
+
const PCM_FORMAT = 1; // WAVE_FORMAT_PCM
|
|
24
|
+
const SUPPORTED_BITS = 16;
|
|
25
|
+
|
|
26
|
+
interface WavLayout {
|
|
27
|
+
numChannels: number;
|
|
28
|
+
sampleRate: number;
|
|
29
|
+
dataOffset: number;
|
|
30
|
+
dataSize: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Locate the `fmt ` and `data` chunks and validate the format we know how to edit.
|
|
35
|
+
*
|
|
36
|
+
* Scans every chunk rather than assuming an ordering: the loop always advances
|
|
37
|
+
* past a chunk's body (using its declared size), so `data` may precede `fmt `
|
|
38
|
+
* and trailing chunks (LIST/fact/etc.) are skipped harmlessly. Returns null on
|
|
39
|
+
* anything unexpected so the caller falls back to the expression path.
|
|
40
|
+
*/
|
|
41
|
+
function parseWavLayout(buffer: Buffer): WavLayout | null {
|
|
42
|
+
if (buffer.length < 12 || buffer.toString("ascii", 0, 4) !== "RIFF") return null;
|
|
43
|
+
if (buffer.toString("ascii", 8, 12) !== "WAVE") return null;
|
|
44
|
+
|
|
45
|
+
let offset = 12;
|
|
46
|
+
let fmt: { numChannels: number; sampleRate: number; bitsPerSample: number } | null = null;
|
|
47
|
+
let data: { offset: number; size: number } | null = null;
|
|
48
|
+
|
|
49
|
+
while (offset + 8 <= buffer.length) {
|
|
50
|
+
const chunkId = buffer.toString("ascii", offset, offset + 4);
|
|
51
|
+
const chunkSize = buffer.readUInt32LE(offset + 4);
|
|
52
|
+
const body = offset + 8;
|
|
53
|
+
if (chunkId === "fmt " && body + 16 <= buffer.length) {
|
|
54
|
+
if (buffer.readUInt16LE(body) !== PCM_FORMAT) return null;
|
|
55
|
+
fmt = {
|
|
56
|
+
numChannels: buffer.readUInt16LE(body + 2),
|
|
57
|
+
sampleRate: buffer.readUInt32LE(body + 4),
|
|
58
|
+
bitsPerSample: buffer.readUInt16LE(body + 14),
|
|
59
|
+
};
|
|
60
|
+
} else if (chunkId === "data") {
|
|
61
|
+
data = { offset: body, size: Math.min(chunkSize, buffer.length - body) };
|
|
62
|
+
}
|
|
63
|
+
// Chunks are word-aligned: an odd size carries a trailing pad byte.
|
|
64
|
+
offset = body + chunkSize + (chunkSize % 2);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!fmt || !data) return null;
|
|
68
|
+
if (fmt.bitsPerSample !== SUPPORTED_BITS || fmt.numChannels < 1) return null;
|
|
69
|
+
return {
|
|
70
|
+
numChannels: fmt.numChannels,
|
|
71
|
+
sampleRate: fmt.sampleRate,
|
|
72
|
+
dataOffset: data.offset,
|
|
73
|
+
dataSize: data.size,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Multiply a prepared WAV's samples by a time-varying gain envelope in place.
|
|
79
|
+
*
|
|
80
|
+
* @returns `true` if the envelope was applied; `false` if the file isn't the
|
|
81
|
+
* expected 16-bit PCM (caller should fall back to the expression path).
|
|
82
|
+
*/
|
|
83
|
+
export function applyVolumeEnvelopeToWav(
|
|
84
|
+
wavPath: string,
|
|
85
|
+
keyframes: AudioVolumeKeyframe[],
|
|
86
|
+
trackStart: number,
|
|
87
|
+
baseVolume: number,
|
|
88
|
+
): boolean {
|
|
89
|
+
const envelope = normaliseEnvelope(keyframes, trackStart, baseVolume);
|
|
90
|
+
if (envelope.length === 0) return false;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const buffer = readFileSync(wavPath);
|
|
94
|
+
const layout = parseWavLayout(buffer);
|
|
95
|
+
if (!layout) return false;
|
|
96
|
+
|
|
97
|
+
const { numChannels, sampleRate, dataOffset, dataSize } = layout;
|
|
98
|
+
const bytesPerSample = SUPPORTED_BITS / 8;
|
|
99
|
+
const frameBytes = numChannels * bytesPerSample;
|
|
100
|
+
const frameCount = Math.floor(dataSize / frameBytes);
|
|
101
|
+
|
|
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;
|
|
107
|
+
for (let frame = 0; frame < frameCount; frame += 1) {
|
|
108
|
+
const time = frame / sampleRate;
|
|
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;
|
|
116
|
+
|
|
117
|
+
const base = dataOffset + frame * frameBytes;
|
|
118
|
+
for (let channel = 0; channel < numChannels; channel += 1) {
|
|
119
|
+
const at = base + channel * bytesPerSample;
|
|
120
|
+
const scaled = Math.round(buffer.readInt16LE(at) * gain);
|
|
121
|
+
buffer.writeInt16LE(scaled < -32768 ? -32768 : scaled > 32767 ? 32767 : scaled, at);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Write to a uniquely-named sibling then atomically rename over the
|
|
126
|
+
// original. The random name avoids following a pre-planted symlink at a
|
|
127
|
+
// predictable path, and the rename means a crash mid-write can't leave a
|
|
128
|
+
// truncated WAV for the downstream mix.
|
|
129
|
+
const tempPath = `${wavPath}.${randomBytes(6).toString("hex")}.tmp`;
|
|
130
|
+
writeFileSync(tempPath, buffer);
|
|
131
|
+
renameSync(tempPath, wavPath);
|
|
132
|
+
return true;
|
|
133
|
+
} catch {
|
|
134
|
+
// Any read/parse/write failure → leave the file untouched and let the
|
|
135
|
+
// caller fall back to the ffmpeg expression path rather than losing audio.
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|