@maravilla-labs/frames 0.5.0 → 0.7.0
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/README.md +42 -5
- package/dist/audio.d.ts +84 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/audio.js +248 -0
- package/dist/audio.js.map +1 -0
- package/dist/ducking.d.ts +53 -0
- package/dist/ducking.d.ts.map +1 -0
- package/dist/ducking.js +85 -0
- package/dist/ducking.js.map +1 -0
- package/dist/index.d.ts +89 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +122 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/audio.ts +313 -0
- package/src/ducking.ts +103 -0
- package/src/index.ts +238 -7
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* - Inside the Maravilla renderer, the timeline is stepped frame by
|
|
8
8
|
* frame to produce deterministic video output.
|
|
9
9
|
*/
|
|
10
|
+
export { AudioEngine } from "./audio.js";
|
|
11
|
+
export type { PreviewAudioTrack } from "./audio.js";
|
|
12
|
+
export { DEFAULT_DUCK, duckAutomation, duckMultiplier, mergeSegments, type DuckParams, type Segment, } from "./ducking.js";
|
|
10
13
|
export type AnimationInstr = {
|
|
11
14
|
/** Optional discriminator. Defaults to `"animation"`. */
|
|
12
15
|
kind?: "animation";
|
|
@@ -63,11 +66,85 @@ export type CssClassInstr = {
|
|
|
63
66
|
/** Class names to add, e.g. `["animate__animated", "animate__bounceInDown"]`. */
|
|
64
67
|
classes: string[];
|
|
65
68
|
};
|
|
66
|
-
|
|
69
|
+
/** Role of an audio track. `"voiceover"` is what music ducks *under*;
|
|
70
|
+
* `"music"` is the background bed that gets ducked; `"sfx"` is a one-shot
|
|
71
|
+
* that is neither ducked nor a ducking trigger. */
|
|
72
|
+
export type AudioRole = "voiceover" | "music" | "sfx";
|
|
73
|
+
/**
|
|
74
|
+
* A per-track audio effect. `"radio"` is a band-limited, saturated "deep
|
|
75
|
+
* radio/telephone voice" — applied identically in the browser preview (Web
|
|
76
|
+
* Audio filter chain, [`audio.ts`]) and in the render (ffmpeg, worker-side), so
|
|
77
|
+
* preview matches export. All knobs are 0..1.
|
|
78
|
+
*/
|
|
79
|
+
export type AudioEffect = {
|
|
80
|
+
kind: "radio";
|
|
81
|
+
/** Overdrive / saturation amount (0 = clean, 1 = crunchy). Default 0.4. */
|
|
82
|
+
drive?: number;
|
|
83
|
+
/** Band brightness (0 = narrow telephone band, 1 = wide/open). Default 0.4. */
|
|
84
|
+
tone?: number;
|
|
85
|
+
/** Low-end "depth" boost for a deeper voice. Default 0.5. */
|
|
86
|
+
depth?: number;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* An audio track on the timeline. Unlike the visual instructions it owns no
|
|
90
|
+
* DOM element — it's mixed into the final video by the render worker (ffmpeg)
|
|
91
|
+
* and played in the browser preview by the Web Audio engine ([`audio.ts`]).
|
|
92
|
+
* Multiple tracks coexist (e.g. an intro bed + a voiceover); music tracks
|
|
93
|
+
* dip under voiceover via static-envelope ducking (see [`ducking.ts`]).
|
|
94
|
+
*/
|
|
95
|
+
export type AudioInstr = {
|
|
96
|
+
/** Discriminator — required. */
|
|
97
|
+
kind: "audio";
|
|
98
|
+
/** When on the timeline the track starts, in milliseconds. */
|
|
99
|
+
at: number;
|
|
100
|
+
/** How long it plays from `at`, in ms. Defaults to the clip's natural length. */
|
|
101
|
+
duration?: number;
|
|
102
|
+
/** Start offset WITHIN the source file, in ms. Defaults to 0. */
|
|
103
|
+
seek?: number;
|
|
104
|
+
/**
|
|
105
|
+
* Playable URL for the browser preview (fetched + decoded via Web Audio).
|
|
106
|
+
* For a stored asset, a URL that resolves to it (e.g. `/_assets/<key>`).
|
|
107
|
+
*/
|
|
108
|
+
src: string;
|
|
109
|
+
/**
|
|
110
|
+
* Storage key the render worker fetches + stages to mix into the final
|
|
111
|
+
* video. Without it the track is preview-only (it can't be rendered).
|
|
112
|
+
*/
|
|
113
|
+
key?: string;
|
|
114
|
+
/** Track role. Defaults to `"music"`. */
|
|
115
|
+
role?: AudioRole;
|
|
116
|
+
/** Linear gain (1 = unity). Defaults to 1. */
|
|
117
|
+
gain?: number;
|
|
118
|
+
/** Fade-in / fade-out length from the clip edges, ms. Default 0. */
|
|
119
|
+
fadeIn?: number;
|
|
120
|
+
fadeOut?: number;
|
|
121
|
+
/** Whether a music track ducks under voiceover. Defaults to `role === "music"`. */
|
|
122
|
+
duck?: boolean;
|
|
123
|
+
/** Optional per-track effect (e.g. a "radio voice"). */
|
|
124
|
+
effect?: AudioEffect;
|
|
125
|
+
};
|
|
126
|
+
export type Instr = AnimationInstr | VideoInstr | CssClassInstr | AudioInstr;
|
|
67
127
|
export type TimelineSchema = {
|
|
68
128
|
duration: number;
|
|
69
129
|
instructions: Instr[];
|
|
70
130
|
};
|
|
131
|
+
/** An audio track as exposed to the render worker via `window.__mvFrames.audio`.
|
|
132
|
+
* The worker stages `key` and builds the ffmpeg mix; `duration === null`
|
|
133
|
+
* means "use the source's natural length". Mirrors [`AudioInstr`] with
|
|
134
|
+
* defaults resolved. */
|
|
135
|
+
export type AudioSlotInfo = {
|
|
136
|
+
/** Storage key the worker stages (absent ⇒ preview-only, not rendered). */
|
|
137
|
+
key?: string;
|
|
138
|
+
at: number;
|
|
139
|
+
duration: number | null;
|
|
140
|
+
seek: number;
|
|
141
|
+
gain: number;
|
|
142
|
+
role: AudioRole;
|
|
143
|
+
duck: boolean;
|
|
144
|
+
fadeIn: number;
|
|
145
|
+
fadeOut: number;
|
|
146
|
+
effect?: AudioEffect;
|
|
147
|
+
};
|
|
71
148
|
/** A `<video>` slot as exposed to the renderer engine (worker). The engine
|
|
72
149
|
* pre-extracts each clip to frames with ffmpeg and swaps the live `<video>`
|
|
73
150
|
* for the matching frame image per captured frame — so the live video is never
|
|
@@ -93,6 +170,17 @@ declare global {
|
|
|
93
170
|
/** Video slots in this timeline — read by the renderer engine to
|
|
94
171
|
* swap video frames out-of-band. Empty when no video. */
|
|
95
172
|
videos: VideoSlotInfo[];
|
|
173
|
+
/** Audio tracks in this timeline — read by the render worker to build
|
|
174
|
+
* the ffmpeg mix. Empty when no audio. */
|
|
175
|
+
audio: AudioSlotInfo[];
|
|
176
|
+
/** Preview transport (browser only; no-ops under the renderer). Start
|
|
177
|
+
* the audio mix from timeline position `fromMs`. The preview host calls
|
|
178
|
+
* this on play; scrubbing via `applyState` stays silent. */
|
|
179
|
+
playAudio(fromMs: number): void;
|
|
180
|
+
/** Stop preview audio playback. */
|
|
181
|
+
stopAudio(): void;
|
|
182
|
+
/** Mute / unmute preview audio without tearing down playback. */
|
|
183
|
+
setAudioMuted(muted: boolean): void;
|
|
96
184
|
};
|
|
97
185
|
/** Set true by the headless renderer when it captures (`__mvFramesRendererPresent`). */
|
|
98
186
|
__mvFramesRendererPresent?: boolean;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* - Inside the Maravilla renderer, the timeline is stepped frame by
|
|
8
8
|
* frame to produce deterministic video output.
|
|
9
9
|
*/
|
|
10
|
+
import { AudioEngine } from "./audio.js";
|
|
11
|
+
export { AudioEngine } from "./audio.js";
|
|
12
|
+
export { DEFAULT_DUCK, duckAutomation, duckMultiplier, mergeSegments, } from "./ducking.js";
|
|
10
13
|
/**
|
|
11
14
|
* Declare the page's timeline. **Safe to call as soon as your module runs** —
|
|
12
15
|
* it internally waits for render-blocking stylesheets and fonts to be ready
|
|
@@ -60,9 +63,61 @@ export function stylesheetsReady() {
|
|
|
60
63
|
})
|
|
61
64
|
.then(() => (document.fonts ? document.fonts.ready.then(() => undefined) : undefined));
|
|
62
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Seek `el` to `targetSec` and resolve only once the seeked frame has
|
|
68
|
+
* actually decoded — so a subsequent capture grabs the right frame instead
|
|
69
|
+
* of racing an in-flight seek.
|
|
70
|
+
*
|
|
71
|
+
* Resolution waits for the `seeked` event, then for one decoded video frame
|
|
72
|
+
* via `requestVideoFrameCallback` where available (fallback: a microtask, so
|
|
73
|
+
* we still yield to the decode). A timeout guards against `seeked` never
|
|
74
|
+
* firing (e.g. seeking to the exact current position, or an unsupported
|
|
75
|
+
* codec) so `applyState` can't hang the whole render. Already-satisfied
|
|
76
|
+
* seeks (`readyState` high enough and time already at target) resolve fast.
|
|
77
|
+
*/
|
|
78
|
+
function seekVideo(el, targetSec) {
|
|
79
|
+
const HAVE_CURRENT_DATA = 2;
|
|
80
|
+
// If we're already at (or within a frame of) the target with data ready,
|
|
81
|
+
// there's nothing to wait for — assigning currentTime won't fire `seeked`.
|
|
82
|
+
const alreadyThere = Math.abs(el.currentTime - targetSec) < 1e-3 && el.readyState >= HAVE_CURRENT_DATA;
|
|
83
|
+
el.currentTime = targetSec;
|
|
84
|
+
if (alreadyThere)
|
|
85
|
+
return Promise.resolve();
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
let done = false;
|
|
88
|
+
const finish = () => {
|
|
89
|
+
if (done)
|
|
90
|
+
return;
|
|
91
|
+
done = true;
|
|
92
|
+
el.removeEventListener("seeked", onSeeked);
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
resolve();
|
|
95
|
+
};
|
|
96
|
+
const afterSeeked = () => {
|
|
97
|
+
// Wait for one decoded frame to be presented when the API exists.
|
|
98
|
+
const rvfc = el.requestVideoFrameCallback;
|
|
99
|
+
if (typeof rvfc === "function") {
|
|
100
|
+
rvfc.call(el, () => finish());
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// No rVFC: yield a microtask so the decode can settle, then finish.
|
|
104
|
+
Promise.resolve().then(finish);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const onSeeked = () => afterSeeked();
|
|
108
|
+
el.addEventListener("seeked", onSeeked, { once: true });
|
|
109
|
+
// Guard: never let a missing `seeked` stall the render.
|
|
110
|
+
const timer = setTimeout(finish, 2000);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
63
113
|
function installTimeline(schema) {
|
|
64
114
|
const rendererPresent = typeof window !== "undefined" && window.__mvFramesRendererPresent === true;
|
|
65
|
-
|
|
115
|
+
// Audio tracks own no DOM element — split them out before the visual
|
|
116
|
+
// instructions hit `querySelector`. They're mixed by the worker (which
|
|
117
|
+
// reads `__mvFrames.audio`) and previewed by the Web Audio engine below.
|
|
118
|
+
const audioInstrs = schema.instructions.filter((i) => i.kind === "audio");
|
|
119
|
+
const visualInstrs = schema.instructions.filter((i) => i.kind !== "audio");
|
|
120
|
+
const slots = visualInstrs.flatMap((i) => {
|
|
66
121
|
const el = document.querySelector(i.selector);
|
|
67
122
|
if (!el)
|
|
68
123
|
throw new Error(`defineTimeline: no element matches ${i.selector}`);
|
|
@@ -159,9 +214,64 @@ function installTimeline(schema) {
|
|
|
159
214
|
src: s.src,
|
|
160
215
|
frameset: s.frameset,
|
|
161
216
|
}));
|
|
217
|
+
// Resolve audio instruction defaults once — shared by the worker-facing
|
|
218
|
+
// slot list and the browser preview engine so both see the same mix.
|
|
219
|
+
const audioResolved = audioInstrs.map((a) => {
|
|
220
|
+
const role = a.role ?? "music";
|
|
221
|
+
return {
|
|
222
|
+
src: a.src,
|
|
223
|
+
key: a.key,
|
|
224
|
+
at: a.at,
|
|
225
|
+
duration: a.duration ?? null,
|
|
226
|
+
seek: a.seek ?? 0,
|
|
227
|
+
gain: a.gain ?? 1,
|
|
228
|
+
role,
|
|
229
|
+
duck: a.duck ?? role === "music",
|
|
230
|
+
fadeIn: a.fadeIn ?? 0,
|
|
231
|
+
fadeOut: a.fadeOut ?? 0,
|
|
232
|
+
effect: a.effect,
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
// Worker-facing slots (read via CDP to build the ffmpeg mix). Drop tracks
|
|
236
|
+
// with no storage key — those are preview-only and can't be rendered.
|
|
237
|
+
const audio = audioResolved
|
|
238
|
+
.filter((a) => typeof a.key === "string" && a.key.length > 0)
|
|
239
|
+
.map(({ src: _src, ...slot }) => slot);
|
|
240
|
+
// Browser preview engine. Constructed whenever there are tracks AND we're in
|
|
241
|
+
// a real browser — INCLUDING under `__mvFramesRendererPresent` (the Studio
|
|
242
|
+
// preview iframe sets that flag so it can drive `applyState`, yet still wants
|
|
243
|
+
// audible playback). It's lazy — no AudioContext, no fetch until `playFrom`
|
|
244
|
+
// is called — so in the *headless* renderer (which only ever calls
|
|
245
|
+
// `applyState`, never `playAudio`) it stays completely inert and the real mix
|
|
246
|
+
// is done by ffmpeg. Construction is just field storage.
|
|
247
|
+
const audioEngine = typeof window !== "undefined" && typeof AudioContext !== "undefined" && audioResolved.length > 0
|
|
248
|
+
? new AudioEngine(audioResolved.map((a) => ({
|
|
249
|
+
src: a.src,
|
|
250
|
+
at: a.at,
|
|
251
|
+
duration: a.duration,
|
|
252
|
+
seek: a.seek,
|
|
253
|
+
gain: a.gain,
|
|
254
|
+
fadeIn: a.fadeIn,
|
|
255
|
+
fadeOut: a.fadeOut,
|
|
256
|
+
role: a.role,
|
|
257
|
+
duck: a.duck,
|
|
258
|
+
effect: a.effect,
|
|
259
|
+
})))
|
|
260
|
+
: null;
|
|
162
261
|
window.__mvFrames = {
|
|
163
262
|
videos,
|
|
263
|
+
audio,
|
|
264
|
+
playAudio(fromMs) {
|
|
265
|
+
void audioEngine?.playFrom(fromMs);
|
|
266
|
+
},
|
|
267
|
+
stopAudio() {
|
|
268
|
+
audioEngine?.stop();
|
|
269
|
+
},
|
|
270
|
+
setAudioMuted(muted) {
|
|
271
|
+
audioEngine?.setMuted(muted);
|
|
272
|
+
},
|
|
164
273
|
async applyState(t) {
|
|
274
|
+
const videoWaits = [];
|
|
165
275
|
for (const s of slots) {
|
|
166
276
|
if (s.kind === "css-pending")
|
|
167
277
|
continue; // only relevant in browser preview
|
|
@@ -170,18 +280,24 @@ function installTimeline(schema) {
|
|
|
170
280
|
// Seek the live <video> EXCEPT on the renderer's begin-frame-control
|
|
171
281
|
// capture path (where seeking crashes the headless tab — the engine
|
|
172
282
|
// swaps in pre-extracted frame images instead, flagged via
|
|
173
|
-
// `__mvFramesNoVideoSeek`).
|
|
174
|
-
// renderer's screenshot
|
|
175
|
-
//
|
|
283
|
+
// `__mvFramesNoVideoSeek`). On every live-seek path (editor, live
|
|
284
|
+
// preview, AND the renderer's screenshot capture) we must AWAIT the
|
|
285
|
+
// seeked frame's decode before resolving — otherwise a capture races
|
|
286
|
+
// an in-flight seek and grabs a stale/blank frame (and the per-frame
|
|
287
|
+
// loop spins, hitting the render wall-clock budget). `seekVideo`
|
|
288
|
+
// guards with a timeout so a missing `seeked` can't hang the render.
|
|
176
289
|
if (!window.__mvFramesNoVideoSeek) {
|
|
177
|
-
s.el
|
|
290
|
+
videoWaits.push(seekVideo(s.el, (s.seek + local) / 1000));
|
|
178
291
|
}
|
|
179
292
|
continue;
|
|
180
293
|
}
|
|
181
294
|
const local = clamp(t - s.at, 0, s.duration);
|
|
182
295
|
s.anim.currentTime = local;
|
|
183
296
|
}
|
|
184
|
-
await
|
|
297
|
+
await Promise.all([
|
|
298
|
+
document.fonts ? document.fonts.ready.then(() => undefined) : Promise.resolve(),
|
|
299
|
+
...videoWaits,
|
|
300
|
+
]);
|
|
185
301
|
},
|
|
186
302
|
};
|
|
187
303
|
if (!rendererPresent) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maravilla-labs/frames",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Tiny vanilla helper for declaring Web Animations API timelines that the Maravilla runtime renderer drives frame-accurately.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
package/src/audio.ts
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-side audio preview for a frames timeline.
|
|
3
|
+
*
|
|
4
|
+
* The render worker mixes audio with ffmpeg (the browser can't expose
|
|
5
|
+
* decoded audio to the capture pipeline), so this engine exists purely so
|
|
6
|
+
* the **editor / preview** plays the same mix the final video will have:
|
|
7
|
+
* multiple time-bound tracks (voiceover, background music, sfx), per-track
|
|
8
|
+
* gain + fades, and static-envelope ducking of music under voiceover (see
|
|
9
|
+
* [`ducking.ts`] — the exact curve the worker bakes in).
|
|
10
|
+
*
|
|
11
|
+
* Scrubbing (`applyState(t)`) stays silent by design — like every real NLE,
|
|
12
|
+
* audio is heard only during continuous playback. The preview host calls
|
|
13
|
+
* [`AudioEngine.playFrom`] on play and [`AudioEngine.stop`] on pause.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_DUCK,
|
|
18
|
+
duckAutomation,
|
|
19
|
+
mergeSegments,
|
|
20
|
+
type DuckParams,
|
|
21
|
+
type Segment,
|
|
22
|
+
} from "./ducking.js";
|
|
23
|
+
|
|
24
|
+
/** A timeline audio track, resolved for browser playback. Times in ms. */
|
|
25
|
+
export type PreviewAudioTrack = {
|
|
26
|
+
/** Playable URL fetched + decoded for preview. */
|
|
27
|
+
src: string;
|
|
28
|
+
/** Timeline start, ms. */
|
|
29
|
+
at: number;
|
|
30
|
+
/** Play length on the timeline, ms. `null` = natural clip length. */
|
|
31
|
+
duration: number | null;
|
|
32
|
+
/** Start offset within the source, ms. */
|
|
33
|
+
seek: number;
|
|
34
|
+
/** Linear base gain (1 = unity). */
|
|
35
|
+
gain: number;
|
|
36
|
+
/** Fade-in / fade-out, ms. */
|
|
37
|
+
fadeIn: number;
|
|
38
|
+
fadeOut: number;
|
|
39
|
+
/** "voiceover" drives ducking; "music" gets ducked when `duck`. */
|
|
40
|
+
role: "voiceover" | "music" | "sfx";
|
|
41
|
+
/** Whether this (music) track dips under voiceover. */
|
|
42
|
+
duck: boolean;
|
|
43
|
+
/** Optional per-track effect (structurally matches `AudioEffect`). */
|
|
44
|
+
effect?: { kind: "radio"; drive?: number; tone?: number; depth?: number };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type WindowAudioContext = typeof AudioContext;
|
|
48
|
+
|
|
49
|
+
function getAudioContextCtor(): WindowAudioContext | null {
|
|
50
|
+
if (typeof window === "undefined") return null;
|
|
51
|
+
return (
|
|
52
|
+
window.AudioContext ||
|
|
53
|
+
(window as unknown as { webkitAudioContext?: WindowAudioContext })
|
|
54
|
+
.webkitAudioContext ||
|
|
55
|
+
null
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class AudioEngine {
|
|
60
|
+
private tracks: PreviewAudioTrack[];
|
|
61
|
+
private duck: DuckParams;
|
|
62
|
+
private ctx: AudioContext | null = null;
|
|
63
|
+
private master: GainNode | null = null;
|
|
64
|
+
private buffers = new Map<string, AudioBuffer>();
|
|
65
|
+
private active: AudioBufferSourceNode[] = [];
|
|
66
|
+
private muted = false;
|
|
67
|
+
|
|
68
|
+
constructor(tracks: PreviewAudioTrack[], duck: DuckParams = DEFAULT_DUCK) {
|
|
69
|
+
this.tracks = tracks;
|
|
70
|
+
this.duck = duck;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Merged voiceover regions (seconds) — what music ducks under. */
|
|
74
|
+
private voiceoverSegments(): Segment[] {
|
|
75
|
+
const segs = this.tracks
|
|
76
|
+
.filter((t) => t.role === "voiceover")
|
|
77
|
+
.map((t) => ({
|
|
78
|
+
start: t.at / 1000,
|
|
79
|
+
end: (t.at + (t.duration ?? 0)) / 1000,
|
|
80
|
+
}))
|
|
81
|
+
.filter((s) => s.end > s.start);
|
|
82
|
+
return mergeSegments(segs);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Fetch + decode every track's source (idempotent, cached by URL). */
|
|
86
|
+
private async ensureBuffers(): Promise<void> {
|
|
87
|
+
const Ctor = getAudioContextCtor();
|
|
88
|
+
if (!Ctor) return;
|
|
89
|
+
if (!this.ctx) {
|
|
90
|
+
this.ctx = new Ctor();
|
|
91
|
+
this.master = this.ctx.createGain();
|
|
92
|
+
this.master.connect(this.ctx.destination);
|
|
93
|
+
}
|
|
94
|
+
const ctx = this.ctx;
|
|
95
|
+
await Promise.all(
|
|
96
|
+
this.tracks.map(async (t) => {
|
|
97
|
+
if (this.buffers.has(t.src)) return;
|
|
98
|
+
try {
|
|
99
|
+
const resp = await fetch(t.src);
|
|
100
|
+
const raw = await resp.arrayBuffer();
|
|
101
|
+
const buf = await ctx.decodeAudioData(raw);
|
|
102
|
+
this.buffers.set(t.src, buf);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
// A bad/missing audio URL must never wedge the preview — skip it.
|
|
105
|
+
// eslint-disable-next-line no-console
|
|
106
|
+
console.warn(`[@maravilla-labs/frames] audio decode failed for ${t.src}:`, err);
|
|
107
|
+
}
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Start (or restart) playback from timeline position `fromMs`. */
|
|
113
|
+
async playFrom(fromMs: number): Promise<void> {
|
|
114
|
+
await this.ensureBuffers();
|
|
115
|
+
if (!this.ctx || !this.master) return;
|
|
116
|
+
this.stop();
|
|
117
|
+
// Resume may reject if there's no user activation (autoplay policy). Don't
|
|
118
|
+
// let that abort scheduling — the caller invokes this from a click handler,
|
|
119
|
+
// so activation is usually present; if not, the context stays suspended and
|
|
120
|
+
// simply produces no sound rather than throwing.
|
|
121
|
+
if (this.ctx.state === "suspended") {
|
|
122
|
+
try {
|
|
123
|
+
await this.ctx.resume();
|
|
124
|
+
} catch {
|
|
125
|
+
/* no user activation yet — stay silent, don't throw */
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const ctx = this.ctx;
|
|
130
|
+
const t0 = fromMs / 1000;
|
|
131
|
+
const startCtx = ctx.currentTime + 0.05; // small lead so scheduling lands
|
|
132
|
+
const segments = this.voiceoverSegments();
|
|
133
|
+
this.master.gain.value = this.muted ? 0 : 1;
|
|
134
|
+
|
|
135
|
+
for (const track of this.tracks) {
|
|
136
|
+
const buf = this.buffers.get(track.src);
|
|
137
|
+
if (!buf) continue;
|
|
138
|
+
|
|
139
|
+
const naturalMs = buf.duration * 1000 - track.seek;
|
|
140
|
+
const durMs = track.duration ?? Math.max(0, naturalMs);
|
|
141
|
+
const trackEnd = track.at + durMs;
|
|
142
|
+
if (t0 * 1000 >= trackEnd) continue; // already past this clip
|
|
143
|
+
|
|
144
|
+
// When (timeline ms) the clip actually begins sounding from the playhead.
|
|
145
|
+
const playStart = Math.max(t0 * 1000, track.at);
|
|
146
|
+
const delaySec = (playStart - t0 * 1000) / 1000;
|
|
147
|
+
const offsetInClip = (track.seek + (playStart - track.at)) / 1000;
|
|
148
|
+
const remainingSec = (trackEnd - playStart) / 1000;
|
|
149
|
+
if (remainingSec <= 0) continue;
|
|
150
|
+
|
|
151
|
+
// Map a timeline time (seconds) to this AudioContext's clock.
|
|
152
|
+
const toCtx = (timelineSec: number) =>
|
|
153
|
+
Math.max(startCtx, startCtx + (timelineSec - t0));
|
|
154
|
+
|
|
155
|
+
const fadeGain = ctx.createGain();
|
|
156
|
+
this.applyFades(fadeGain, track, toCtx);
|
|
157
|
+
|
|
158
|
+
let tail: AudioNode = fadeGain;
|
|
159
|
+
if (track.role === "music" && track.duck && segments.length) {
|
|
160
|
+
const duckGain = ctx.createGain();
|
|
161
|
+
this.applyDuck(duckGain, track.gain, segments, toCtx);
|
|
162
|
+
fadeGain.connect(duckGain);
|
|
163
|
+
tail = duckGain;
|
|
164
|
+
}
|
|
165
|
+
tail.connect(this.master);
|
|
166
|
+
|
|
167
|
+
// Optional effect (e.g. radio voice) sits between the source and the
|
|
168
|
+
// fade/duck gains, so it shapes the dry signal exactly like the ffmpeg
|
|
169
|
+
// render does.
|
|
170
|
+
const node = ctx.createBufferSource();
|
|
171
|
+
node.buffer = buf;
|
|
172
|
+
const radio = track.effect?.kind === "radio" ? buildRadioChain(ctx, track.effect) : null;
|
|
173
|
+
if (radio) {
|
|
174
|
+
node.connect(radio.input);
|
|
175
|
+
radio.output.connect(fadeGain);
|
|
176
|
+
} else {
|
|
177
|
+
node.connect(fadeGain);
|
|
178
|
+
}
|
|
179
|
+
node.start(startCtx + delaySec, offsetInClip, remainingSec);
|
|
180
|
+
this.active.push(node);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Per-track base gain + fade-in/out automation on `g`. */
|
|
185
|
+
private applyFades(
|
|
186
|
+
g: GainNode,
|
|
187
|
+
track: PreviewAudioTrack,
|
|
188
|
+
toCtx: (timelineSec: number) => number,
|
|
189
|
+
): void {
|
|
190
|
+
const base = track.role === "music" && track.duck ? 1 : track.gain;
|
|
191
|
+
const startSec = track.at / 1000;
|
|
192
|
+
const fadeInSec = track.fadeIn / 1000;
|
|
193
|
+
const fadeOutSec = track.fadeOut / 1000;
|
|
194
|
+
const durMs = track.duration ?? 0;
|
|
195
|
+
const endSec = (track.at + durMs) / 1000;
|
|
196
|
+
|
|
197
|
+
g.gain.setValueAtTime(track.fadeIn > 0 ? 0 : base, toCtx(startSec));
|
|
198
|
+
if (track.fadeIn > 0) {
|
|
199
|
+
g.gain.linearRampToValueAtTime(base, toCtx(startSec + fadeInSec));
|
|
200
|
+
}
|
|
201
|
+
if (track.fadeOut > 0 && durMs > 0) {
|
|
202
|
+
g.gain.setValueAtTime(base, toCtx(Math.max(startSec, endSec - fadeOutSec)));
|
|
203
|
+
g.gain.linearRampToValueAtTime(0, toCtx(endSec));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Ducking automation (scaled by the music track's base gain) on `g`. */
|
|
208
|
+
private applyDuck(
|
|
209
|
+
g: GainNode,
|
|
210
|
+
base: number,
|
|
211
|
+
segments: Segment[],
|
|
212
|
+
toCtx: (timelineSec: number) => number,
|
|
213
|
+
): void {
|
|
214
|
+
const points = duckAutomation(segments, base, this.duck);
|
|
215
|
+
let first = true;
|
|
216
|
+
for (const [timeSec, value] of points) {
|
|
217
|
+
if (first) {
|
|
218
|
+
g.gain.setValueAtTime(value, toCtx(timeSec));
|
|
219
|
+
first = false;
|
|
220
|
+
} else {
|
|
221
|
+
g.gain.linearRampToValueAtTime(value, toCtx(timeSec));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Stop all currently-playing sources (idempotent). */
|
|
227
|
+
stop(): void {
|
|
228
|
+
for (const node of this.active) {
|
|
229
|
+
try {
|
|
230
|
+
node.stop();
|
|
231
|
+
} catch {
|
|
232
|
+
/* already stopped */
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
this.active = [];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
setMuted(muted: boolean): void {
|
|
239
|
+
this.muted = muted;
|
|
240
|
+
if (this.master) this.master.gain.value = muted ? 0 : 1;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function clamp01(v: number): number {
|
|
245
|
+
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Resolved "radio voice" parameters. The exact same formulas are mirrored by
|
|
250
|
+
* the ffmpeg render (worker `audio_mix.rs`) so preview == export:
|
|
251
|
+
* a band-pass (high-pass + low-pass) for the radio band, a low-shelf for
|
|
252
|
+
* "depth", and a tanh waveshaper (pushed by `pre`, tamed by `post`) for drive.
|
|
253
|
+
*/
|
|
254
|
+
export function radioParams(effect: { drive?: number; tone?: number; depth?: number }): {
|
|
255
|
+
hp: number;
|
|
256
|
+
lp: number;
|
|
257
|
+
shelfGainDb: number;
|
|
258
|
+
pre: number;
|
|
259
|
+
post: number;
|
|
260
|
+
k: number;
|
|
261
|
+
} {
|
|
262
|
+
const drive = clamp01(effect.drive ?? 0.4);
|
|
263
|
+
const tone = clamp01(effect.tone ?? 0.4);
|
|
264
|
+
const depth = clamp01(effect.depth ?? 0.5);
|
|
265
|
+
return {
|
|
266
|
+
hp: 500 - 350 * tone, // 500Hz (telephone) → 150Hz (open)
|
|
267
|
+
lp: 2400 + 2800 * tone, // 2400Hz (narrow) → 5200Hz (bright)
|
|
268
|
+
shelfGainDb: depth * 12, // up to +12 dB low-shelf
|
|
269
|
+
pre: 1 + drive * 3, // drive into the saturator
|
|
270
|
+
post: 1 / (1 + drive * 1.6), // make-up / tame
|
|
271
|
+
k: 1 + drive * 5, // tanh steepness
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function tanhCurve(k: number) {
|
|
276
|
+
const n = 1024;
|
|
277
|
+
const curve = new Float32Array(n);
|
|
278
|
+
for (let i = 0; i < n; i++) {
|
|
279
|
+
const x = (i / (n - 1)) * 2 - 1;
|
|
280
|
+
curve[i] = Math.tanh(k * x);
|
|
281
|
+
}
|
|
282
|
+
return curve;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function buildRadioChain(
|
|
286
|
+
ctx: AudioContext,
|
|
287
|
+
effect: { drive?: number; tone?: number; depth?: number },
|
|
288
|
+
): { input: AudioNode; output: AudioNode } {
|
|
289
|
+
const p = radioParams(effect);
|
|
290
|
+
const hp = ctx.createBiquadFilter();
|
|
291
|
+
hp.type = "highpass";
|
|
292
|
+
hp.frequency.value = p.hp;
|
|
293
|
+
const lp = ctx.createBiquadFilter();
|
|
294
|
+
lp.type = "lowpass";
|
|
295
|
+
lp.frequency.value = p.lp;
|
|
296
|
+
const shelf = ctx.createBiquadFilter();
|
|
297
|
+
shelf.type = "lowshelf";
|
|
298
|
+
shelf.frequency.value = 180;
|
|
299
|
+
shelf.gain.value = p.shelfGainDb;
|
|
300
|
+
const pre = ctx.createGain();
|
|
301
|
+
pre.gain.value = p.pre;
|
|
302
|
+
const shaper = ctx.createWaveShaper();
|
|
303
|
+
shaper.curve = tanhCurve(p.k);
|
|
304
|
+
shaper.oversample = "2x";
|
|
305
|
+
const post = ctx.createGain();
|
|
306
|
+
post.gain.value = p.post;
|
|
307
|
+
hp.connect(lp);
|
|
308
|
+
lp.connect(shelf);
|
|
309
|
+
shelf.connect(pre);
|
|
310
|
+
pre.connect(shaper);
|
|
311
|
+
shaper.connect(post);
|
|
312
|
+
return { input: hp, output: post };
|
|
313
|
+
}
|