@maravilla-labs/frames 0.5.1 → 0.8.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,65 @@ 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); `fadeInCurve` / `fadeOutCurve`
220
+ (see below); `duck` — whether a music bed dips under voiceover (default
221
+ `true` for `role: "music"`).
222
+
223
+ **Fade curves.** A fade has a shape as well as a length. The names map
224
+ to ffmpeg's own `afade` curves, and the preview evaluates the same
225
+ formulas, so a fade sounds identical in the editor and in the export:
226
+
227
+ | `fadeInCurve` / `fadeOutCurve` | afade | character |
228
+ |---|---|---|
229
+ | `linear` (default) | `tri` | straight gain ramp; dips in loudness mid-fade |
230
+ | `equal-power` | `qsin` | constant perceived loudness (−3 dB at the midpoint) |
231
+ | `s-curve` | `hsin` | gentle at both ends, quick through the middle |
232
+ | `exponential` | `exp` | slow start, fast finish (100 dB range) |
233
+ | `logarithmic` | `log` | fast start, slow finish |
234
+
235
+ `fadeGain(curve, x)` returns a fade-in's gain at progress `x` (a fade-out
236
+ is `fadeGain(curve, 1 - p)`), for drawing an envelope in your own UI;
237
+ `FADE_CURVES` lists the names in picker order.
238
+
239
+ **Ducking** is a static envelope computed from the voiceover clips'
240
+ positions (not their loudness), so it's deterministic and the preview
241
+ reproduces the exact dip the export bakes in.
242
+
243
+ **Preview transport.** Scrubbing via `applyState(t)` stays silent (like
244
+ any NLE); audio is heard during continuous playback. Drive it from your
245
+ player with `window.__mvFrames.playAudio(fromMs)` /
246
+ `stopAudio()` / `setAudioMuted(muted)`. The renderer reads the tracks it
247
+ mixes from `window.__mvFrames.audio`.
194
248
 
195
249
  ## Bring your own animation library
196
250
 
@@ -0,0 +1,92 @@
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
+ import { type FadeCurve } from "./fades.js";
17
+ /** A timeline audio track, resolved for browser playback. Times in ms. */
18
+ export type PreviewAudioTrack = {
19
+ /** Playable URL fetched + decoded for preview. */
20
+ src: string;
21
+ /** Timeline start, ms. */
22
+ at: number;
23
+ /** Play length on the timeline, ms. `null` = natural clip length. */
24
+ duration: number | null;
25
+ /** Start offset within the source, ms. */
26
+ seek: number;
27
+ /** Linear base gain (1 = unity). */
28
+ gain: number;
29
+ /** Fade-in / fade-out, ms. */
30
+ fadeIn: number;
31
+ fadeOut: number;
32
+ /** Shape of each fade (see `fades.ts`). Default `linear`. */
33
+ fadeInCurve?: FadeCurve;
34
+ fadeOutCurve?: FadeCurve;
35
+ /** "voiceover" drives ducking; "music" gets ducked when `duck`. */
36
+ role: "voiceover" | "music" | "sfx";
37
+ /** Whether this (music) track dips under voiceover. */
38
+ duck: boolean;
39
+ /** Optional per-track effect (structurally matches `AudioEffect`). */
40
+ effect?: {
41
+ kind: "radio";
42
+ drive?: number;
43
+ tone?: number;
44
+ depth?: number;
45
+ };
46
+ };
47
+ export declare class AudioEngine {
48
+ private tracks;
49
+ private duck;
50
+ private ctx;
51
+ private master;
52
+ private buffers;
53
+ private active;
54
+ private muted;
55
+ constructor(tracks: PreviewAudioTrack[], duck?: DuckParams);
56
+ /** Merged voiceover regions (seconds) — what music ducks under. */
57
+ private voiceoverSegments;
58
+ /** Fetch + decode every track's source (idempotent, cached by URL). */
59
+ private ensureBuffers;
60
+ /** Start (or restart) playback from timeline position `fromMs`. */
61
+ playFrom(fromMs: number): Promise<void>;
62
+ /**
63
+ * Per-track base gain + fade-in on `inGain`, fade-out on `outGain`, each
64
+ * shaped by its curve. `t0` is the playhead (seconds): a fade the playhead is
65
+ * already inside continues from where it is rather than restarting.
66
+ */
67
+ private applyFades;
68
+ /** Ducking automation (scaled by the music track's base gain) on `g`. */
69
+ private applyDuck;
70
+ /** Stop all currently-playing sources (idempotent). */
71
+ stop(): void;
72
+ setMuted(muted: boolean): void;
73
+ }
74
+ /**
75
+ * Resolved "radio voice" parameters. The exact same formulas are mirrored by
76
+ * the ffmpeg render (worker `audio_mix.rs`) so preview == export:
77
+ * a band-pass (high-pass + low-pass) for the radio band, a low-shelf for
78
+ * "depth", and a tanh waveshaper (pushed by `pre`, tamed by `post`) for drive.
79
+ */
80
+ export declare function radioParams(effect: {
81
+ drive?: number;
82
+ tone?: number;
83
+ depth?: number;
84
+ }): {
85
+ hp: number;
86
+ lp: number;
87
+ shelfGainDb: number;
88
+ pre: number;
89
+ post: number;
90
+ k: number;
91
+ };
92
+ //# 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;AACtB,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5D,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,6DAA6D;IAC7D,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,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;IA6E7C;;;;OAIG;IACH,OAAO,CAAC,UAAU;IA4ClB,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,282 @@
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
+ import { fadeAutomation } from "./fades.js";
17
+ function getAudioContextCtor() {
18
+ if (typeof window === "undefined")
19
+ return null;
20
+ return (window.AudioContext ||
21
+ window
22
+ .webkitAudioContext ||
23
+ null);
24
+ }
25
+ export class AudioEngine {
26
+ tracks;
27
+ duck;
28
+ ctx = null;
29
+ master = null;
30
+ buffers = new Map();
31
+ active = [];
32
+ muted = false;
33
+ constructor(tracks, duck = DEFAULT_DUCK) {
34
+ this.tracks = tracks;
35
+ this.duck = duck;
36
+ }
37
+ /** Merged voiceover regions (seconds) — what music ducks under. */
38
+ voiceoverSegments() {
39
+ const segs = this.tracks
40
+ .filter((t) => t.role === "voiceover")
41
+ .map((t) => ({
42
+ start: t.at / 1000,
43
+ end: (t.at + (t.duration ?? 0)) / 1000,
44
+ }))
45
+ .filter((s) => s.end > s.start);
46
+ return mergeSegments(segs);
47
+ }
48
+ /** Fetch + decode every track's source (idempotent, cached by URL). */
49
+ async ensureBuffers() {
50
+ const Ctor = getAudioContextCtor();
51
+ if (!Ctor)
52
+ return;
53
+ if (!this.ctx) {
54
+ this.ctx = new Ctor();
55
+ this.master = this.ctx.createGain();
56
+ this.master.connect(this.ctx.destination);
57
+ }
58
+ const ctx = this.ctx;
59
+ await Promise.all(this.tracks.map(async (t) => {
60
+ if (this.buffers.has(t.src))
61
+ return;
62
+ try {
63
+ const resp = await fetch(t.src);
64
+ const raw = await resp.arrayBuffer();
65
+ const buf = await ctx.decodeAudioData(raw);
66
+ this.buffers.set(t.src, buf);
67
+ }
68
+ catch (err) {
69
+ // A bad/missing audio URL must never wedge the preview — skip it.
70
+ // eslint-disable-next-line no-console
71
+ console.warn(`[@maravilla-labs/frames] audio decode failed for ${t.src}:`, err);
72
+ }
73
+ }));
74
+ }
75
+ /** Start (or restart) playback from timeline position `fromMs`. */
76
+ async playFrom(fromMs) {
77
+ await this.ensureBuffers();
78
+ if (!this.ctx || !this.master)
79
+ return;
80
+ this.stop();
81
+ // Resume may reject if there's no user activation (autoplay policy). Don't
82
+ // let that abort scheduling — the caller invokes this from a click handler,
83
+ // so activation is usually present; if not, the context stays suspended and
84
+ // simply produces no sound rather than throwing.
85
+ if (this.ctx.state === "suspended") {
86
+ try {
87
+ await this.ctx.resume();
88
+ }
89
+ catch {
90
+ /* no user activation yet — stay silent, don't throw */
91
+ }
92
+ }
93
+ const ctx = this.ctx;
94
+ const t0 = fromMs / 1000;
95
+ const startCtx = ctx.currentTime + 0.05; // small lead so scheduling lands
96
+ const segments = this.voiceoverSegments();
97
+ this.master.gain.value = this.muted ? 0 : 1;
98
+ for (const track of this.tracks) {
99
+ const buf = this.buffers.get(track.src);
100
+ if (!buf)
101
+ continue;
102
+ const naturalMs = buf.duration * 1000 - track.seek;
103
+ const durMs = track.duration ?? Math.max(0, naturalMs);
104
+ const trackEnd = track.at + durMs;
105
+ if (t0 * 1000 >= trackEnd)
106
+ continue; // already past this clip
107
+ // When (timeline ms) the clip actually begins sounding from the playhead.
108
+ const playStart = Math.max(t0 * 1000, track.at);
109
+ const delaySec = (playStart - t0 * 1000) / 1000;
110
+ const offsetInClip = (track.seek + (playStart - track.at)) / 1000;
111
+ const remainingSec = (trackEnd - playStart) / 1000;
112
+ if (remainingSec <= 0)
113
+ continue;
114
+ // Map a timeline time (seconds) to this AudioContext's clock.
115
+ const toCtx = (timelineSec) => Math.max(startCtx, startCtx + (timelineSec - t0));
116
+ // Two gains, in series: the fade-in and the fade-out are separate
117
+ // automations that MULTIPLY where they overlap — the same as ffmpeg's two
118
+ // chained `afade` filters, and it keeps each curve's events on its own
119
+ // AudioParam so they can never collide.
120
+ const fadeGain = ctx.createGain();
121
+ const fadeOutGain = ctx.createGain();
122
+ fadeGain.connect(fadeOutGain);
123
+ this.applyFades(fadeGain, fadeOutGain, track, t0, toCtx);
124
+ let tail = fadeOutGain;
125
+ if (track.role === "music" && track.duck && segments.length) {
126
+ const duckGain = ctx.createGain();
127
+ this.applyDuck(duckGain, track.gain, segments, toCtx);
128
+ fadeOutGain.connect(duckGain);
129
+ tail = duckGain;
130
+ }
131
+ tail.connect(this.master);
132
+ // Optional effect (e.g. radio voice) sits between the source and the
133
+ // fade/duck gains, so it shapes the dry signal exactly like the ffmpeg
134
+ // render does.
135
+ const node = ctx.createBufferSource();
136
+ node.buffer = buf;
137
+ const radio = track.effect?.kind === "radio" ? buildRadioChain(ctx, track.effect) : null;
138
+ if (radio) {
139
+ node.connect(radio.input);
140
+ radio.output.connect(fadeGain);
141
+ }
142
+ else {
143
+ node.connect(fadeGain);
144
+ }
145
+ node.start(startCtx + delaySec, offsetInClip, remainingSec);
146
+ this.active.push(node);
147
+ }
148
+ }
149
+ /**
150
+ * Per-track base gain + fade-in on `inGain`, fade-out on `outGain`, each
151
+ * shaped by its curve. `t0` is the playhead (seconds): a fade the playhead is
152
+ * already inside continues from where it is rather than restarting.
153
+ */
154
+ applyFades(inGain, outGain, track, t0, toCtx) {
155
+ const base = track.role === "music" && track.duck ? 1 : track.gain;
156
+ const startSec = track.at / 1000;
157
+ const durMs = track.duration ?? 0;
158
+ const endSec = (track.at + durMs) / 1000;
159
+ const fadeInSec = Math.max(0, track.fadeIn) / 1000;
160
+ const fadeOutSec = Math.max(0, track.fadeOut) / 1000;
161
+ const inCurve = track.fadeInCurve ?? "linear";
162
+ const outCurve = track.fadeOutCurve ?? "linear";
163
+ // Fade-in (carries the base gain).
164
+ if (fadeInSec > 0) {
165
+ const inEnd = startSec + fadeInSec;
166
+ if (t0 >= inEnd) {
167
+ inGain.gain.setValueAtTime(base, toCtx(t0));
168
+ }
169
+ else {
170
+ const from = Math.max(0, (t0 - startSec) / fadeInSec);
171
+ const at = Math.max(startSec, t0);
172
+ // A value event AT the curve's start time counts as an overlap and
173
+ // throws — hold silence only when the clip starts later than the playhead.
174
+ if (at > t0)
175
+ inGain.gain.setValueAtTime(0, toCtx(t0));
176
+ inGain.gain.setValueCurveAtTime(fadeAutomation(inCurve, "in", base, from), toCtx(at), inEnd - at);
177
+ }
178
+ }
179
+ else {
180
+ inGain.gain.setValueAtTime(base, toCtx(t0));
181
+ }
182
+ // Fade-out (unity outside the fade).
183
+ const outStart = Math.max(startSec, endSec - fadeOutSec);
184
+ const hasOut = fadeOutSec > 0 && durMs > 0 && endSec > outStart && t0 < endSec;
185
+ const outAt = Math.max(outStart, t0);
186
+ if (!hasOut || outAt > t0)
187
+ outGain.gain.setValueAtTime(1, toCtx(t0));
188
+ if (hasOut) {
189
+ const from = Math.max(0, (t0 - outStart) / (endSec - outStart));
190
+ outGain.gain.setValueCurveAtTime(fadeAutomation(outCurve, "out", 1, from), toCtx(outAt), endSec - outAt);
191
+ }
192
+ }
193
+ /** Ducking automation (scaled by the music track's base gain) on `g`. */
194
+ applyDuck(g, base, segments, toCtx) {
195
+ const points = duckAutomation(segments, base, this.duck);
196
+ let first = true;
197
+ for (const [timeSec, value] of points) {
198
+ if (first) {
199
+ g.gain.setValueAtTime(value, toCtx(timeSec));
200
+ first = false;
201
+ }
202
+ else {
203
+ g.gain.linearRampToValueAtTime(value, toCtx(timeSec));
204
+ }
205
+ }
206
+ }
207
+ /** Stop all currently-playing sources (idempotent). */
208
+ stop() {
209
+ for (const node of this.active) {
210
+ try {
211
+ node.stop();
212
+ }
213
+ catch {
214
+ /* already stopped */
215
+ }
216
+ }
217
+ this.active = [];
218
+ }
219
+ setMuted(muted) {
220
+ this.muted = muted;
221
+ if (this.master)
222
+ this.master.gain.value = muted ? 0 : 1;
223
+ }
224
+ }
225
+ function clamp01(v) {
226
+ return v < 0 ? 0 : v > 1 ? 1 : v;
227
+ }
228
+ /**
229
+ * Resolved "radio voice" parameters. The exact same formulas are mirrored by
230
+ * the ffmpeg render (worker `audio_mix.rs`) so preview == export:
231
+ * a band-pass (high-pass + low-pass) for the radio band, a low-shelf for
232
+ * "depth", and a tanh waveshaper (pushed by `pre`, tamed by `post`) for drive.
233
+ */
234
+ export function radioParams(effect) {
235
+ const drive = clamp01(effect.drive ?? 0.4);
236
+ const tone = clamp01(effect.tone ?? 0.4);
237
+ const depth = clamp01(effect.depth ?? 0.5);
238
+ return {
239
+ hp: 500 - 350 * tone, // 500Hz (telephone) → 150Hz (open)
240
+ lp: 2400 + 2800 * tone, // 2400Hz (narrow) → 5200Hz (bright)
241
+ shelfGainDb: depth * 12, // up to +12 dB low-shelf
242
+ pre: 1 + drive * 3, // drive into the saturator
243
+ post: 1 / (1 + drive * 1.6), // make-up / tame
244
+ k: 1 + drive * 5, // tanh steepness
245
+ };
246
+ }
247
+ function tanhCurve(k) {
248
+ const n = 1024;
249
+ const curve = new Float32Array(n);
250
+ for (let i = 0; i < n; i++) {
251
+ const x = (i / (n - 1)) * 2 - 1;
252
+ curve[i] = Math.tanh(k * x);
253
+ }
254
+ return curve;
255
+ }
256
+ function buildRadioChain(ctx, effect) {
257
+ const p = radioParams(effect);
258
+ const hp = ctx.createBiquadFilter();
259
+ hp.type = "highpass";
260
+ hp.frequency.value = p.hp;
261
+ const lp = ctx.createBiquadFilter();
262
+ lp.type = "lowpass";
263
+ lp.frequency.value = p.lp;
264
+ const shelf = ctx.createBiquadFilter();
265
+ shelf.type = "lowshelf";
266
+ shelf.frequency.value = 180;
267
+ shelf.gain.value = p.shelfGainDb;
268
+ const pre = ctx.createGain();
269
+ pre.gain.value = p.pre;
270
+ const shaper = ctx.createWaveShaper();
271
+ shaper.curve = tanhCurve(p.k);
272
+ shaper.oversample = "2x";
273
+ const post = ctx.createGain();
274
+ post.gain.value = p.post;
275
+ hp.connect(lp);
276
+ lp.connect(shelf);
277
+ shelf.connect(pre);
278
+ pre.connect(shaper);
279
+ shaper.connect(post);
280
+ return { input: hp, output: post };
281
+ }
282
+ //# 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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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Fade curves for audio tracks — the shape of a fade-in / fade-out, not just
3
+ * its length.
4
+ *
5
+ * The render worker mixes with ffmpeg's `afade`, so these are ffmpeg's OWN
6
+ * curve formulas (libavfilter `af_afade.c`, `fade_gain`), evaluated here for the
7
+ * browser preview. The worker maps each name to the matching `afade` curve
8
+ * (`audio_mix.rs`, `ffmpeg_fade_curve`), which is what keeps preview == export.
9
+ *
10
+ * | name | afade | shape |
11
+ * |---------------|-------|----------------------------------------------------|
12
+ * | `linear` | tri | straight gain ramp — a dip in the middle |
13
+ * | `equal-power` | qsin | quarter sine — constant perceived loudness |
14
+ * | `s-curve` | hsin | half sine — gentle at both ends |
15
+ * | `exponential` | exp | slow start, fast finish (~-100 dB floor) |
16
+ * | `logarithmic` | log | fast start, slow finish |
17
+ *
18
+ * `linear` is the default because it is what `afade` does when no curve is
19
+ * named, so a track written before curves existed renders unchanged.
20
+ */
21
+ export type FadeCurve = "linear" | "equal-power" | "s-curve" | "exponential" | "logarithmic";
22
+ /** Every curve, in the order a picker should offer them. */
23
+ export declare const FADE_CURVES: readonly FadeCurve[];
24
+ /** Resolve an untrusted value to a known curve (unknown ⇒ `linear`). */
25
+ export declare function resolveFadeCurve(value: unknown): FadeCurve;
26
+ /**
27
+ * Gain (0..1) of a FADE-IN at progress `x` (0 = start of the fade, 1 = end).
28
+ * A fade-out at progress `p` is `fadeGain(curve, 1 - p)` — exactly how `afade`
29
+ * runs its curve backwards for `t=out`.
30
+ */
31
+ export declare function fadeGain(curve: FadeCurve, x: number): number;
32
+ /**
33
+ * Sampled gain automation for `AudioParam.setValueCurveAtTime`, scaled by
34
+ * `base`. `from` lets a curve start part-way through (playback that begins in
35
+ * the middle of a fade), so the preview never restarts a fade from silence.
36
+ */
37
+ export declare function fadeAutomation(curve: FadeCurve, direction: "in" | "out", base: number, from?: number, samples?: number): Float32Array;
38
+ //# sourceMappingURL=fades.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fades.d.ts","sourceRoot":"","sources":["../src/fades.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,aAAa,GAAG,SAAS,GAAG,aAAa,GAAG,aAAa,CAAC;AAE7F,4DAA4D;AAC5D,eAAO,MAAM,WAAW,EAAE,SAAS,SAAS,EAM3C,CAAC;AAEF,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,CAI1D;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAe5D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,SAAS,EAChB,SAAS,EAAE,IAAI,GAAG,KAAK,EACvB,IAAI,EAAE,MAAM,EACZ,IAAI,SAAI,EACR,OAAO,SAAM,GACZ,YAAY,CAQd"}