@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/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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
package/dist/audio.d.ts
ADDED
|
@@ -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":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,YAAY,EACZ,cAAc,EACd,aAAa,GAGd,MAAM,cAAc,CAAC;AA2BtB,SAAS,mBAAmB;IAC1B,IAAI,OAAO,MAAM,KAAK,WAAW;QAAE,OAAO,IAAI,CAAC;IAC/C,OAAO,CACL,MAAM,CAAC,YAAY;QAClB,MAAiE;aAC/D,kBAAkB;QACrB,IAAI,CACL,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,WAAW;IACd,MAAM,CAAsB;IAC5B,IAAI,CAAa;IACjB,GAAG,GAAwB,IAAI,CAAC;IAChC,MAAM,GAAoB,IAAI,CAAC;IAC/B,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;IACzC,MAAM,GAA4B,EAAE,CAAC;IACrC,KAAK,GAAG,KAAK,CAAC;IAEtB,YAAY,MAA2B,EAAE,OAAmB,YAAY;QACtE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,mEAAmE;IAC3D,iBAAiB;QACvB,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM;aACrB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC;aACrC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACX,KAAK,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI;YAClB,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI;SACvC,CAAC,CAAC;aACF,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;QAClC,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IAED,uEAAuE;IAC/D,KAAK,CAAC,aAAa;QACzB,MAAM,IAAI,GAAG,mBAAmB,EAAE,CAAC;QACnC,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;YACd,IAAI,CAAC,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;YACtB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC;YACpC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAC5C,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;QACrB,MAAM,OAAO,CAAC,GAAG,CACf,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;YAC1B,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;gBAAE,OAAO;YACpC,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;gBAChC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;gBACrC,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;gBAC3C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YAC/B,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,kEAAkE;gBAClE,sCAAsC;gBACtC,OAAO,CAAC,IAAI,CAAC,oDAAoD,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,CAAC,CAAC;YAClF,CAAC;QACH,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;IAED,mEAAmE;IACnE,KAAK,CAAC,QAAQ,CAAC,MAAc;QAC3B,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;QAC3B,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO;QACtC,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,2EAA2E;QAC3E,4EAA4E;QAC5E,4EAA4E;QAC5E,iDAAiD;QACjD,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,WAAW,EAAE,CAAC;YACnC,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;YAC1B,CAAC;YAAC,MAAM,CAAC;gBACP,uDAAuD;YACzD,CAAC;QACH,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;QACrB,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAAC;QACzB,MAAM,QAAQ,GAAG,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC,iCAAiC;QAC1E,MAAM,QAAQ,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAC1C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAE5C,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACxC,IAAI,CAAC,GAAG;gBAAE,SAAS;YAEnB,MAAM,SAAS,GAAG,GAAG,CAAC,QAAQ,GAAG,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;YACnD,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;YACvD,MAAM,QAAQ,GAAG,KAAK,CAAC,EAAE,GAAG,KAAK,CAAC;YAClC,IAAI,EAAE,GAAG,IAAI,IAAI,QAAQ;gBAAE,SAAS,CAAC,yBAAyB;YAE9D,0EAA0E;YAC1E,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;YAChD,MAAM,QAAQ,GAAG,CAAC,SAAS,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;YAChD,MAAM,YAAY,GAAG,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,SAAS,GAAG,KAAK,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;YAClE,MAAM,YAAY,GAAG,CAAC,QAAQ,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC;YACnD,IAAI,YAAY,IAAI,CAAC;gBAAE,SAAS;YAEhC,8DAA8D;YAC9D,MAAM,KAAK,GAAG,CAAC,WAAmB,EAAE,EAAE,CACpC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,GAAG,CAAC,WAAW,GAAG,EAAE,CAAC,CAAC,CAAC;YAEpD,MAAM,QAAQ,GAAG,GAAG,CAAC,UAAU,EAAE,CAAC;YAClC,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;YAExC,IAAI,IAAI,GAAc,QAAQ,CAAC;YAC/B,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,IAAI,KAAK,CAAC,IAAI,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;gBAC5D,MAAM,QAAQ,GAAG,GAAG,CAAC,UAAU,EAAE,CAAC;gBAClC,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;gBACtD,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;gBAC3B,IAAI,GAAG,QAAQ,CAAC;YAClB,CAAC;YACD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAE1B,qEAAqE;YACrE,uEAAuE;YACvE,eAAe;YACf,MAAM,IAAI,GAAG,GAAG,CAAC,kBAAkB,EAAE,CAAC;YACtC,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC;YAClB,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YACzF,IAAI,KAAK,EAAE,CAAC;gBACV,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;gBAC1B,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YACjC,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YACzB,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,QAAQ,EAAE,YAAY,EAAE,YAAY,CAAC,CAAC;YAC5D,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;IAED,2DAA2D;IACnD,UAAU,CAChB,CAAW,EACX,KAAwB,EACxB,KAAsC;QAEtC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,KAAK,OAAO,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;QACnE,MAAM,QAAQ,GAAG,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC;QACjC,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC;QACtC,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;QACxC,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,IAAI,CAAC,CAAC;QAClC,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,KAAK,CAAC,GAAG,IAAI,CAAC;QAEzC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;QACpE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,IAAI,EAAE,KAAK,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC;QACpE,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACnC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;YAC5E,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IAED,yEAAyE;IACjE,SAAS,CACf,CAAW,EACX,IAAY,EACZ,QAAmB,EACnB,KAAsC;QAEtC,MAAM,MAAM,GAAG,cAAc,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QACzD,IAAI,KAAK,GAAG,IAAI,CAAC;QACjB,KAAK,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;YACtC,IAAI,KAAK,EAAE,CAAC;gBACV,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;gBAC7C,KAAK,GAAG,KAAK,CAAC;YAChB,CAAC;iBAAM,CAAC;gBACN,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;YACxD,CAAC;QACH,CAAC;IACH,CAAC;IAED,uDAAuD;IACvD,IAAI;QACF,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAC/B,IAAI,CAAC;gBACH,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,CAAC;YAAC,MAAM,CAAC;gBACP,qBAAqB;YACvB,CAAC;QACH,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;IACnB,CAAC;IAED,QAAQ,CAAC,KAAc;QACrB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,IAAI,CAAC,MAAM;YAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1D,CAAC;CACF;AAED,SAAS,OAAO,CAAC,CAAS;IACxB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACnC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,MAAyD;IAQnF,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC;IACzC,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC;IAC3C,OAAO;QACL,EAAE,EAAE,GAAG,GAAG,GAAG,GAAG,IAAI,EAAE,mCAAmC;QACzD,EAAE,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,EAAE,oCAAoC;QAC5D,WAAW,EAAE,KAAK,GAAG,EAAE,EAAE,yBAAyB;QAClD,GAAG,EAAE,CAAC,GAAG,KAAK,GAAG,CAAC,EAAE,2BAA2B;QAC/C,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,GAAG,GAAG,CAAC,EAAE,iBAAiB;QAC9C,CAAC,EAAE,CAAC,GAAG,KAAK,GAAG,CAAC,EAAE,iBAAiB;KACpC,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,CAAS;IAC1B,MAAM,CAAC,GAAG,IAAI,CAAC;IACf,MAAM,KAAK,GAAG,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC;IAClC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAChC,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,eAAe,CACtB,GAAiB,EACjB,MAAyD;IAEzD,MAAM,CAAC,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAC9B,MAAM,EAAE,GAAG,GAAG,CAAC,kBAAkB,EAAE,CAAC;IACpC,EAAE,CAAC,IAAI,GAAG,UAAU,CAAC;IACrB,EAAE,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC;IAC1B,MAAM,EAAE,GAAG,GAAG,CAAC,kBAAkB,EAAE,CAAC;IACpC,EAAE,CAAC,IAAI,GAAG,SAAS,CAAC;IACpB,EAAE,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC;IAC1B,MAAM,KAAK,GAAG,GAAG,CAAC,kBAAkB,EAAE,CAAC;IACvC,KAAK,CAAC,IAAI,GAAG,UAAU,CAAC;IACxB,KAAK,CAAC,SAAS,CAAC,KAAK,GAAG,GAAG,CAAC;IAC5B,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,WAAW,CAAC;IACjC,MAAM,GAAG,GAAG,GAAG,CAAC,UAAU,EAAE,CAAC;IAC7B,GAAG,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC;IACvB,MAAM,MAAM,GAAG,GAAG,CAAC,gBAAgB,EAAE,CAAC;IACtC,MAAM,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9B,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC;IACzB,MAAM,IAAI,GAAG,GAAG,CAAC,UAAU,EAAE,CAAC;IAC9B,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC;IACzB,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACf,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAClB,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACnB,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACpB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACrB,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AACrC,CAAC"}
|
|
@@ -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"}
|
package/dist/ducking.js
ADDED
|
@@ -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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"}
|