@maravilla-labs/frames 0.5.1 → 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 CHANGED
@@ -186,11 +186,48 @@ doesn't block it.
186
186
  <video class="intro" src="/clips/intro.mp4" preload="auto" muted playsinline></video>
187
187
  ```
188
188
 
189
- If the same composition will be rendered to a final video by the
190
- Maravilla runtime, the audio track of the embedded `<video>` is not
191
- captured (browsers don't expose decoded audio to the rendering API).
192
- Use the `audio` option on `FRAMES.render(...)` to mix a soundtrack
193
- into the final output instead.
189
+ The audio track of an embedded `<video>` is not captured (browsers
190
+ don't expose decoded audio to the rendering API). Add a `kind: "audio"`
191
+ instruction (below) instead or, for a single pre-mixed soundtrack, the
192
+ `audio` option on `FRAMES.render(...)`.
193
+
194
+ ## Audio
195
+
196
+ Declare time-bound audio tracks right in the timeline. They are mixed
197
+ into the final video by the renderer (multi-track ffmpeg, server-side)
198
+ and played in the browser preview by a built-in Web Audio engine — so
199
+ preview matches render.
200
+
201
+ ```ts
202
+ defineTimeline({
203
+ duration: 12000,
204
+ instructions: [
205
+ // Background music bed for the whole piece.
206
+ { kind: "audio", at: 0, duration: 12000, role: "music",
207
+ src: "/audio/bed.m4a", key: "audio/bed.m4a", fadeIn: 400, fadeOut: 800 },
208
+ // A voiceover from 3s; music automatically ducks beneath it.
209
+ { kind: "audio", at: 3000, role: "voiceover",
210
+ src: "/audio/vo.m4a", key: "audio/vo.m4a" },
211
+ ],
212
+ });
213
+ ```
214
+
215
+ Fields: `at` / `duration` / `seek` (ms, like video); `src` — a URL the
216
+ browser preview fetches + decodes; `key` — the storage key the renderer
217
+ stages to mix (omit for preview-only tracks); `role` —
218
+ `"voiceover" | "music" | "sfx"` (default `"music"`); `gain` (linear,
219
+ default 1); `fadeIn` / `fadeOut` (ms); `duck` — whether a music bed dips
220
+ under voiceover (default `true` for `role: "music"`).
221
+
222
+ **Ducking** is a static envelope computed from the voiceover clips'
223
+ positions (not their loudness), so it's deterministic and the preview
224
+ reproduces the exact dip the export bakes in.
225
+
226
+ **Preview transport.** Scrubbing via `applyState(t)` stays silent (like
227
+ any NLE); audio is heard during continuous playback. Drive it from your
228
+ player with `window.__mvFrames.playAudio(fromMs)` /
229
+ `stopAudio()` / `setAudioMuted(muted)`. The renderer reads the tracks it
230
+ mixes from `window.__mvFrames.audio`.
194
231
 
195
232
  ## Bring your own animation library
196
233
 
@@ -0,0 +1,84 @@
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
+ import { type DuckParams } from "./ducking.js";
16
+ /** A timeline audio track, resolved for browser playback. Times in ms. */
17
+ export type PreviewAudioTrack = {
18
+ /** Playable URL fetched + decoded for preview. */
19
+ src: string;
20
+ /** Timeline start, ms. */
21
+ at: number;
22
+ /** Play length on the timeline, ms. `null` = natural clip length. */
23
+ duration: number | null;
24
+ /** Start offset within the source, ms. */
25
+ seek: number;
26
+ /** Linear base gain (1 = unity). */
27
+ gain: number;
28
+ /** Fade-in / fade-out, ms. */
29
+ fadeIn: number;
30
+ fadeOut: number;
31
+ /** "voiceover" drives ducking; "music" gets ducked when `duck`. */
32
+ role: "voiceover" | "music" | "sfx";
33
+ /** Whether this (music) track dips under voiceover. */
34
+ duck: boolean;
35
+ /** Optional per-track effect (structurally matches `AudioEffect`). */
36
+ effect?: {
37
+ kind: "radio";
38
+ drive?: number;
39
+ tone?: number;
40
+ depth?: number;
41
+ };
42
+ };
43
+ export declare class AudioEngine {
44
+ private tracks;
45
+ private duck;
46
+ private ctx;
47
+ private master;
48
+ private buffers;
49
+ private active;
50
+ private muted;
51
+ constructor(tracks: PreviewAudioTrack[], duck?: DuckParams);
52
+ /** Merged voiceover regions (seconds) — what music ducks under. */
53
+ private voiceoverSegments;
54
+ /** Fetch + decode every track's source (idempotent, cached by URL). */
55
+ private ensureBuffers;
56
+ /** Start (or restart) playback from timeline position `fromMs`. */
57
+ playFrom(fromMs: number): Promise<void>;
58
+ /** Per-track base gain + fade-in/out automation on `g`. */
59
+ private applyFades;
60
+ /** Ducking automation (scaled by the music track's base gain) on `g`. */
61
+ private applyDuck;
62
+ /** Stop all currently-playing sources (idempotent). */
63
+ stop(): void;
64
+ setMuted(muted: boolean): void;
65
+ }
66
+ /**
67
+ * Resolved "radio voice" parameters. The exact same formulas are mirrored by
68
+ * the ffmpeg render (worker `audio_mix.rs`) so preview == export:
69
+ * a band-pass (high-pass + low-pass) for the radio band, a low-shelf for
70
+ * "depth", and a tanh waveshaper (pushed by `pre`, tamed by `post`) for drive.
71
+ */
72
+ export declare function radioParams(effect: {
73
+ drive?: number;
74
+ tone?: number;
75
+ depth?: number;
76
+ }): {
77
+ hp: number;
78
+ lp: number;
79
+ shelfGainDb: number;
80
+ pre: number;
81
+ post: number;
82
+ k: number;
83
+ };
84
+ //# sourceMappingURL=audio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio.d.ts","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAIL,KAAK,UAAU,EAEhB,MAAM,cAAc,CAAC;AAEtB,0EAA0E;AAC1E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,kDAAkD;IAClD,GAAG,EAAE,MAAM,CAAC;IACZ,0BAA0B;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,qEAAqE;IACrE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,IAAI,EAAE,WAAW,GAAG,OAAO,GAAG,KAAK,CAAC;IACpC,uDAAuD;IACvD,IAAI,EAAE,OAAO,CAAC;IACd,sEAAsE;IACtE,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3E,CAAC;AAcF,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,GAAG,CAA6B;IACxC,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,OAAO,CAAkC;IACjD,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,KAAK,CAAS;gBAEV,MAAM,EAAE,iBAAiB,EAAE,EAAE,IAAI,GAAE,UAAyB;IAKxE,mEAAmE;IACnE,OAAO,CAAC,iBAAiB;IAWzB,uEAAuE;YACzD,aAAa;IA0B3B,mEAAmE;IAC7D,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAuE7C,2DAA2D;IAC3D,OAAO,CAAC,UAAU;IAsBlB,yEAAyE;IACzE,OAAO,CAAC,SAAS;IAkBjB,uDAAuD;IACvD,IAAI,IAAI,IAAI;IAWZ,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;CAI/B;AAMD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IACtF,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,CAAC,EAAE,MAAM,CAAC;CACX,CAYA"}
package/dist/audio.js ADDED
@@ -0,0 +1,248 @@
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
+ import { DEFAULT_DUCK, duckAutomation, mergeSegments, } from "./ducking.js";
16
+ function getAudioContextCtor() {
17
+ if (typeof window === "undefined")
18
+ return null;
19
+ return (window.AudioContext ||
20
+ window
21
+ .webkitAudioContext ||
22
+ null);
23
+ }
24
+ export class AudioEngine {
25
+ tracks;
26
+ duck;
27
+ ctx = null;
28
+ master = null;
29
+ buffers = new Map();
30
+ active = [];
31
+ muted = false;
32
+ constructor(tracks, duck = DEFAULT_DUCK) {
33
+ this.tracks = tracks;
34
+ this.duck = duck;
35
+ }
36
+ /** Merged voiceover regions (seconds) — what music ducks under. */
37
+ voiceoverSegments() {
38
+ const segs = this.tracks
39
+ .filter((t) => t.role === "voiceover")
40
+ .map((t) => ({
41
+ start: t.at / 1000,
42
+ end: (t.at + (t.duration ?? 0)) / 1000,
43
+ }))
44
+ .filter((s) => s.end > s.start);
45
+ return mergeSegments(segs);
46
+ }
47
+ /** Fetch + decode every track's source (idempotent, cached by URL). */
48
+ async ensureBuffers() {
49
+ const Ctor = getAudioContextCtor();
50
+ if (!Ctor)
51
+ return;
52
+ if (!this.ctx) {
53
+ this.ctx = new Ctor();
54
+ this.master = this.ctx.createGain();
55
+ this.master.connect(this.ctx.destination);
56
+ }
57
+ const ctx = this.ctx;
58
+ await Promise.all(this.tracks.map(async (t) => {
59
+ if (this.buffers.has(t.src))
60
+ return;
61
+ try {
62
+ const resp = await fetch(t.src);
63
+ const raw = await resp.arrayBuffer();
64
+ const buf = await ctx.decodeAudioData(raw);
65
+ this.buffers.set(t.src, buf);
66
+ }
67
+ catch (err) {
68
+ // A bad/missing audio URL must never wedge the preview — skip it.
69
+ // eslint-disable-next-line no-console
70
+ console.warn(`[@maravilla-labs/frames] audio decode failed for ${t.src}:`, err);
71
+ }
72
+ }));
73
+ }
74
+ /** Start (or restart) playback from timeline position `fromMs`. */
75
+ async playFrom(fromMs) {
76
+ await this.ensureBuffers();
77
+ if (!this.ctx || !this.master)
78
+ return;
79
+ this.stop();
80
+ // Resume may reject if there's no user activation (autoplay policy). Don't
81
+ // let that abort scheduling — the caller invokes this from a click handler,
82
+ // so activation is usually present; if not, the context stays suspended and
83
+ // simply produces no sound rather than throwing.
84
+ if (this.ctx.state === "suspended") {
85
+ try {
86
+ await this.ctx.resume();
87
+ }
88
+ catch {
89
+ /* no user activation yet — stay silent, don't throw */
90
+ }
91
+ }
92
+ const ctx = this.ctx;
93
+ const t0 = fromMs / 1000;
94
+ const startCtx = ctx.currentTime + 0.05; // small lead so scheduling lands
95
+ const segments = this.voiceoverSegments();
96
+ this.master.gain.value = this.muted ? 0 : 1;
97
+ for (const track of this.tracks) {
98
+ const buf = this.buffers.get(track.src);
99
+ if (!buf)
100
+ continue;
101
+ const naturalMs = buf.duration * 1000 - track.seek;
102
+ const durMs = track.duration ?? Math.max(0, naturalMs);
103
+ const trackEnd = track.at + durMs;
104
+ if (t0 * 1000 >= trackEnd)
105
+ continue; // already past this clip
106
+ // When (timeline ms) the clip actually begins sounding from the playhead.
107
+ const playStart = Math.max(t0 * 1000, track.at);
108
+ const delaySec = (playStart - t0 * 1000) / 1000;
109
+ const offsetInClip = (track.seek + (playStart - track.at)) / 1000;
110
+ const remainingSec = (trackEnd - playStart) / 1000;
111
+ if (remainingSec <= 0)
112
+ continue;
113
+ // Map a timeline time (seconds) to this AudioContext's clock.
114
+ const toCtx = (timelineSec) => Math.max(startCtx, startCtx + (timelineSec - t0));
115
+ const fadeGain = ctx.createGain();
116
+ this.applyFades(fadeGain, track, toCtx);
117
+ let tail = fadeGain;
118
+ if (track.role === "music" && track.duck && segments.length) {
119
+ const duckGain = ctx.createGain();
120
+ this.applyDuck(duckGain, track.gain, segments, toCtx);
121
+ fadeGain.connect(duckGain);
122
+ tail = duckGain;
123
+ }
124
+ tail.connect(this.master);
125
+ // Optional effect (e.g. radio voice) sits between the source and the
126
+ // fade/duck gains, so it shapes the dry signal exactly like the ffmpeg
127
+ // render does.
128
+ const node = ctx.createBufferSource();
129
+ node.buffer = buf;
130
+ const radio = track.effect?.kind === "radio" ? buildRadioChain(ctx, track.effect) : null;
131
+ if (radio) {
132
+ node.connect(radio.input);
133
+ radio.output.connect(fadeGain);
134
+ }
135
+ else {
136
+ node.connect(fadeGain);
137
+ }
138
+ node.start(startCtx + delaySec, offsetInClip, remainingSec);
139
+ this.active.push(node);
140
+ }
141
+ }
142
+ /** Per-track base gain + fade-in/out automation on `g`. */
143
+ applyFades(g, track, toCtx) {
144
+ const base = track.role === "music" && track.duck ? 1 : track.gain;
145
+ const startSec = track.at / 1000;
146
+ const fadeInSec = track.fadeIn / 1000;
147
+ const fadeOutSec = track.fadeOut / 1000;
148
+ const durMs = track.duration ?? 0;
149
+ const endSec = (track.at + durMs) / 1000;
150
+ g.gain.setValueAtTime(track.fadeIn > 0 ? 0 : base, toCtx(startSec));
151
+ if (track.fadeIn > 0) {
152
+ g.gain.linearRampToValueAtTime(base, toCtx(startSec + fadeInSec));
153
+ }
154
+ if (track.fadeOut > 0 && durMs > 0) {
155
+ g.gain.setValueAtTime(base, toCtx(Math.max(startSec, endSec - fadeOutSec)));
156
+ g.gain.linearRampToValueAtTime(0, toCtx(endSec));
157
+ }
158
+ }
159
+ /** Ducking automation (scaled by the music track's base gain) on `g`. */
160
+ applyDuck(g, base, segments, toCtx) {
161
+ const points = duckAutomation(segments, base, this.duck);
162
+ let first = true;
163
+ for (const [timeSec, value] of points) {
164
+ if (first) {
165
+ g.gain.setValueAtTime(value, toCtx(timeSec));
166
+ first = false;
167
+ }
168
+ else {
169
+ g.gain.linearRampToValueAtTime(value, toCtx(timeSec));
170
+ }
171
+ }
172
+ }
173
+ /** Stop all currently-playing sources (idempotent). */
174
+ stop() {
175
+ for (const node of this.active) {
176
+ try {
177
+ node.stop();
178
+ }
179
+ catch {
180
+ /* already stopped */
181
+ }
182
+ }
183
+ this.active = [];
184
+ }
185
+ setMuted(muted) {
186
+ this.muted = muted;
187
+ if (this.master)
188
+ this.master.gain.value = muted ? 0 : 1;
189
+ }
190
+ }
191
+ function clamp01(v) {
192
+ return v < 0 ? 0 : v > 1 ? 1 : v;
193
+ }
194
+ /**
195
+ * Resolved "radio voice" parameters. The exact same formulas are mirrored by
196
+ * the ffmpeg render (worker `audio_mix.rs`) so preview == export:
197
+ * a band-pass (high-pass + low-pass) for the radio band, a low-shelf for
198
+ * "depth", and a tanh waveshaper (pushed by `pre`, tamed by `post`) for drive.
199
+ */
200
+ export function radioParams(effect) {
201
+ const drive = clamp01(effect.drive ?? 0.4);
202
+ const tone = clamp01(effect.tone ?? 0.4);
203
+ const depth = clamp01(effect.depth ?? 0.5);
204
+ return {
205
+ hp: 500 - 350 * tone, // 500Hz (telephone) → 150Hz (open)
206
+ lp: 2400 + 2800 * tone, // 2400Hz (narrow) → 5200Hz (bright)
207
+ shelfGainDb: depth * 12, // up to +12 dB low-shelf
208
+ pre: 1 + drive * 3, // drive into the saturator
209
+ post: 1 / (1 + drive * 1.6), // make-up / tame
210
+ k: 1 + drive * 5, // tanh steepness
211
+ };
212
+ }
213
+ function tanhCurve(k) {
214
+ const n = 1024;
215
+ const curve = new Float32Array(n);
216
+ for (let i = 0; i < n; i++) {
217
+ const x = (i / (n - 1)) * 2 - 1;
218
+ curve[i] = Math.tanh(k * x);
219
+ }
220
+ return curve;
221
+ }
222
+ function buildRadioChain(ctx, effect) {
223
+ const p = radioParams(effect);
224
+ const hp = ctx.createBiquadFilter();
225
+ hp.type = "highpass";
226
+ hp.frequency.value = p.hp;
227
+ const lp = ctx.createBiquadFilter();
228
+ lp.type = "lowpass";
229
+ lp.frequency.value = p.lp;
230
+ const shelf = ctx.createBiquadFilter();
231
+ shelf.type = "lowshelf";
232
+ shelf.frequency.value = 180;
233
+ shelf.gain.value = p.shelfGainDb;
234
+ const pre = ctx.createGain();
235
+ pre.gain.value = p.pre;
236
+ const shaper = ctx.createWaveShaper();
237
+ shaper.curve = tanhCurve(p.k);
238
+ shaper.oversample = "2x";
239
+ const post = ctx.createGain();
240
+ post.gain.value = p.post;
241
+ hp.connect(lp);
242
+ lp.connect(shelf);
243
+ shelf.connect(pre);
244
+ pre.connect(shaper);
245
+ shaper.connect(post);
246
+ return { input: hp, output: post };
247
+ }
248
+ //# sourceMappingURL=audio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio.js","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Static-envelope ducking — the single source of truth for "music dips
3
+ * under voiceover" shared by the browser preview ([`audio.ts`]) and the
4
+ * Rust render worker (which mirrors this math when it builds the ffmpeg
5
+ * `volume` expression).
6
+ *
7
+ * The model is deliberately *static*: the dip is computed purely from the
8
+ * voiceover clips' timeline positions (`at` / `duration`), NOT from the
9
+ * voiceover's actual loudness. That makes it deterministic — the same
10
+ * timeline produces the same envelope every render — and, crucially, lets
11
+ * the browser preview reproduce the exact gain curve the final mp4 will
12
+ * have (a sidechain compressor could not be mirrored frame-for-frame in
13
+ * Web Audio).
14
+ *
15
+ * All times here are in **seconds** (Web Audio + ffmpeg both want seconds);
16
+ * the DSL stores milliseconds, so callers convert at the boundary.
17
+ */
18
+ /** Tunable ducking parameters. Defaults match the shorts editor. */
19
+ export type DuckParams = {
20
+ /** Music multiplier while voiceover is active. `0.25` ≈ −12 dB. */
21
+ level: number;
22
+ /** Linear ramp into / out of the dip, seconds. */
23
+ ramp: number;
24
+ };
25
+ export declare const DEFAULT_DUCK: DuckParams;
26
+ /** A half-open `[start, end)` interval on the timeline, seconds. */
27
+ export type Segment = {
28
+ start: number;
29
+ end: number;
30
+ };
31
+ /**
32
+ * Merge overlapping / touching voiceover segments into a disjoint, sorted
33
+ * set. Merging is what makes the preview automation and the ffmpeg
34
+ * expression agree even when voiceover clips overlap: a union of regions
35
+ * is unambiguous, whereas per-clip ramps could otherwise fight each other.
36
+ */
37
+ export declare function mergeSegments(segments: Segment[]): Segment[];
38
+ /**
39
+ * The ducking multiplier at time `t` (seconds) for a music track, given the
40
+ * merged voiceover `segments`. `1` = full volume, `level` = fully ducked,
41
+ * with linear ramps of width `ramp` on each edge. Used by the preview to
42
+ * sample the curve and by tests to assert the Rust expression matches.
43
+ */
44
+ export declare function duckMultiplier(t: number, segments: Segment[], duck?: DuckParams): number;
45
+ /**
46
+ * Gain-automation breakpoints `[time, value]` for a music track's
47
+ * `GainNode`, scaled by `base` gain. The preview replays these with
48
+ * `setValueAtTime` / `linearRampToValueAtTime` so the dip in the player
49
+ * matches the dip baked into the render. Times are absolute timeline
50
+ * seconds.
51
+ */
52
+ export declare function duckAutomation(segments: Segment[], base: number, duck?: DuckParams): Array<[number, number]>;
53
+ //# sourceMappingURL=ducking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ducking.d.ts","sourceRoot":"","sources":["../src/ducking.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,oEAAoE;AACpE,MAAM,MAAM,UAAU,GAAG;IACvB,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,UAAwC,CAAC;AAEpE,oEAAoE;AACpE,MAAM,MAAM,OAAO,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAErD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAc5D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,CAAC,EAAE,MAAM,EACT,QAAQ,EAAE,OAAO,EAAE,EACnB,IAAI,GAAE,UAAyB,GAC9B,MAAM,CAOR;AAUD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,OAAO,EAAE,EACnB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,UAAyB,GAC9B,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAWzB"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Static-envelope ducking — the single source of truth for "music dips
3
+ * under voiceover" shared by the browser preview ([`audio.ts`]) and the
4
+ * Rust render worker (which mirrors this math when it builds the ffmpeg
5
+ * `volume` expression).
6
+ *
7
+ * The model is deliberately *static*: the dip is computed purely from the
8
+ * voiceover clips' timeline positions (`at` / `duration`), NOT from the
9
+ * voiceover's actual loudness. That makes it deterministic — the same
10
+ * timeline produces the same envelope every render — and, crucially, lets
11
+ * the browser preview reproduce the exact gain curve the final mp4 will
12
+ * have (a sidechain compressor could not be mirrored frame-for-frame in
13
+ * Web Audio).
14
+ *
15
+ * All times here are in **seconds** (Web Audio + ffmpeg both want seconds);
16
+ * the DSL stores milliseconds, so callers convert at the boundary.
17
+ */
18
+ export const DEFAULT_DUCK = { level: 0.25, ramp: 0.25 };
19
+ /**
20
+ * Merge overlapping / touching voiceover segments into a disjoint, sorted
21
+ * set. Merging is what makes the preview automation and the ffmpeg
22
+ * expression agree even when voiceover clips overlap: a union of regions
23
+ * is unambiguous, whereas per-clip ramps could otherwise fight each other.
24
+ */
25
+ export function mergeSegments(segments) {
26
+ const sorted = segments
27
+ .filter((s) => s.end > s.start)
28
+ .sort((a, b) => a.start - b.start);
29
+ const out = [];
30
+ for (const s of sorted) {
31
+ const last = out[out.length - 1];
32
+ if (last && s.start <= last.end) {
33
+ last.end = Math.max(last.end, s.end);
34
+ }
35
+ else {
36
+ out.push({ ...s });
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+ /**
42
+ * The ducking multiplier at time `t` (seconds) for a music track, given the
43
+ * merged voiceover `segments`. `1` = full volume, `level` = fully ducked,
44
+ * with linear ramps of width `ramp` on each edge. Used by the preview to
45
+ * sample the curve and by tests to assert the Rust expression matches.
46
+ */
47
+ export function duckMultiplier(t, segments, duck = DEFAULT_DUCK) {
48
+ let m = 1;
49
+ for (const { start, end } of segments) {
50
+ const seg = segMultiplier(t, start, end, duck);
51
+ if (seg < m)
52
+ m = seg;
53
+ }
54
+ return m;
55
+ }
56
+ function segMultiplier(t, s, e, duck) {
57
+ const { level: d, ramp: r } = duck;
58
+ if (t <= s - r || t >= e + r)
59
+ return 1;
60
+ if (t < s)
61
+ return 1 - (1 - d) * (t - (s - r)) / r; // ramp down
62
+ if (t <= e)
63
+ return d; // hold
64
+ return d + (1 - d) * (t - e) / r; // ramp up
65
+ }
66
+ /**
67
+ * Gain-automation breakpoints `[time, value]` for a music track's
68
+ * `GainNode`, scaled by `base` gain. The preview replays these with
69
+ * `setValueAtTime` / `linearRampToValueAtTime` so the dip in the player
70
+ * matches the dip baked into the render. Times are absolute timeline
71
+ * seconds.
72
+ */
73
+ export function duckAutomation(segments, base, duck = DEFAULT_DUCK) {
74
+ const merged = mergeSegments(segments);
75
+ const points = [[0, base]];
76
+ const { level: d, ramp: r } = duck;
77
+ for (const { start, end } of merged) {
78
+ points.push([Math.max(0, start - r), base]); // begin ramp down
79
+ points.push([start, base * d]); // fully ducked
80
+ points.push([end, base * d]); // hold to clip end
81
+ points.push([end + r, base]); // ramp back up
82
+ }
83
+ return points;
84
+ }
85
+ //# sourceMappingURL=ducking.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ducking.js","sourceRoot":"","sources":["../src/ducking.ts"],"names":[],"mappings":"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"}