@clipkit/runtime 1.0.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/LICENSE +54 -0
- package/README.md +98 -0
- package/dist/animation/easings.d.ts +9 -0
- package/dist/animation/easings.d.ts.map +1 -0
- package/dist/animation/easings.js +230 -0
- package/dist/animation/easings.js.map +1 -0
- package/dist/animation/expr.d.ts +44 -0
- package/dist/animation/expr.d.ts.map +1 -0
- package/dist/animation/expr.js +236 -0
- package/dist/animation/expr.js.map +1 -0
- package/dist/animation/keyframes.d.ts +23 -0
- package/dist/animation/keyframes.d.ts.map +1 -0
- package/dist/animation/keyframes.js +117 -0
- package/dist/animation/keyframes.js.map +1 -0
- package/dist/animation/motion-path.d.ts +18 -0
- package/dist/animation/motion-path.d.ts.map +1 -0
- package/dist/animation/motion-path.js +269 -0
- package/dist/animation/motion-path.js.map +1 -0
- package/dist/animation/noise1d.d.ts +5 -0
- package/dist/animation/noise1d.d.ts.map +1 -0
- package/dist/animation/noise1d.js +27 -0
- package/dist/animation/noise1d.js.map +1 -0
- package/dist/animation/presets.d.ts +60 -0
- package/dist/animation/presets.d.ts.map +1 -0
- package/dist/animation/presets.js +221 -0
- package/dist/animation/presets.js.map +1 -0
- package/dist/assets/cache.d.ts +18 -0
- package/dist/assets/cache.d.ts.map +1 -0
- package/dist/assets/cache.js +56 -0
- package/dist/assets/cache.js.map +1 -0
- package/dist/assets/fonts.d.ts +20 -0
- package/dist/assets/fonts.d.ts.map +1 -0
- package/dist/assets/fonts.js +127 -0
- package/dist/assets/fonts.js.map +1 -0
- package/dist/assets/loader.d.ts +18 -0
- package/dist/assets/loader.d.ts.map +1 -0
- package/dist/assets/loader.js +87 -0
- package/dist/assets/loader.js.map +1 -0
- package/dist/assets/lut.d.ts +5 -0
- package/dist/assets/lut.d.ts.map +1 -0
- package/dist/assets/lut.js +77 -0
- package/dist/assets/lut.js.map +1 -0
- package/dist/assets/media-time.d.ts +31 -0
- package/dist/assets/media-time.d.ts.map +1 -0
- package/dist/assets/media-time.js +65 -0
- package/dist/assets/media-time.js.map +1 -0
- package/dist/assets/mp4-frame-source.d.ts +44 -0
- package/dist/assets/mp4-frame-source.d.ts.map +1 -0
- package/dist/assets/mp4-frame-source.js +387 -0
- package/dist/assets/mp4-frame-source.js.map +1 -0
- package/dist/audio/encoder.d.ts +31 -0
- package/dist/audio/encoder.d.ts.map +1 -0
- package/dist/audio/encoder.js +96 -0
- package/dist/audio/encoder.js.map +1 -0
- package/dist/audio/fades.d.ts +16 -0
- package/dist/audio/fades.d.ts.map +1 -0
- package/dist/audio/fades.js +43 -0
- package/dist/audio/fades.js.map +1 -0
- package/dist/audio/limiter.d.ts +8 -0
- package/dist/audio/limiter.d.ts.map +1 -0
- package/dist/audio/limiter.js +39 -0
- package/dist/audio/limiter.js.map +1 -0
- package/dist/audio/loader.d.ts +6 -0
- package/dist/audio/loader.d.ts.map +1 -0
- package/dist/audio/loader.js +42 -0
- package/dist/audio/loader.js.map +1 -0
- package/dist/audio/mixer.d.ts +17 -0
- package/dist/audio/mixer.d.ts.map +1 -0
- package/dist/audio/mixer.js +204 -0
- package/dist/audio/mixer.js.map +1 -0
- package/dist/audio/varispeed.d.ts +24 -0
- package/dist/audio/varispeed.d.ts.map +1 -0
- package/dist/audio/varispeed.js +114 -0
- package/dist/audio/varispeed.js.map +1 -0
- package/dist/audio/wav.d.ts +6 -0
- package/dist/audio/wav.d.ts.map +1 -0
- package/dist/audio/wav.js +62 -0
- package/dist/audio/wav.js.map +1 -0
- package/dist/backend/backend.d.ts +579 -0
- package/dist/backend/backend.d.ts.map +1 -0
- package/dist/backend/backend.js +17 -0
- package/dist/backend/backend.js.map +1 -0
- package/dist/backend/webgl-backend.d.ts +97 -0
- package/dist/backend/webgl-backend.d.ts.map +1 -0
- package/dist/backend/webgl-backend.js +2142 -0
- package/dist/backend/webgl-backend.js.map +1 -0
- package/dist/backend/webgpu-backend.d.ts +121 -0
- package/dist/backend/webgpu-backend.d.ts.map +1 -0
- package/dist/backend/webgpu-backend.js +2481 -0
- package/dist/backend/webgpu-backend.js.map +1 -0
- package/dist/compositor/bitfont.d.ts +8 -0
- package/dist/compositor/bitfont.d.ts.map +1 -0
- package/dist/compositor/bitfont.js +52 -0
- package/dist/compositor/bitfont.js.map +1 -0
- package/dist/compositor/camera.d.ts +5 -0
- package/dist/compositor/camera.d.ts.map +1 -0
- package/dist/compositor/camera.js +114 -0
- package/dist/compositor/camera.js.map +1 -0
- package/dist/compositor/color.d.ts +26 -0
- package/dist/compositor/color.d.ts.map +1 -0
- package/dist/compositor/color.js +189 -0
- package/dist/compositor/color.js.map +1 -0
- package/dist/compositor/element-renderers/caption.d.ts +4 -0
- package/dist/compositor/element-renderers/caption.d.ts.map +1 -0
- package/dist/compositor/element-renderers/caption.js +376 -0
- package/dist/compositor/element-renderers/caption.js.map +1 -0
- package/dist/compositor/element-renderers/group.d.ts +12 -0
- package/dist/compositor/element-renderers/group.d.ts.map +1 -0
- package/dist/compositor/element-renderers/group.js +259 -0
- package/dist/compositor/element-renderers/group.js.map +1 -0
- package/dist/compositor/element-renderers/image.d.ts +4 -0
- package/dist/compositor/element-renderers/image.d.ts.map +1 -0
- package/dist/compositor/element-renderers/image.js +97 -0
- package/dist/compositor/element-renderers/image.js.map +1 -0
- package/dist/compositor/element-renderers/lit.d.ts +6 -0
- package/dist/compositor/element-renderers/lit.d.ts.map +1 -0
- package/dist/compositor/element-renderers/lit.js +82 -0
- package/dist/compositor/element-renderers/lit.js.map +1 -0
- package/dist/compositor/element-renderers/particles.d.ts +4 -0
- package/dist/compositor/element-renderers/particles.d.ts.map +1 -0
- package/dist/compositor/element-renderers/particles.js +212 -0
- package/dist/compositor/element-renderers/particles.js.map +1 -0
- package/dist/compositor/element-renderers/shape.d.ts +4 -0
- package/dist/compositor/element-renderers/shape.d.ts.map +1 -0
- package/dist/compositor/element-renderers/shape.js +171 -0
- package/dist/compositor/element-renderers/shape.js.map +1 -0
- package/dist/compositor/element-renderers/svg.d.ts +4 -0
- package/dist/compositor/element-renderers/svg.d.ts.map +1 -0
- package/dist/compositor/element-renderers/svg.js +210 -0
- package/dist/compositor/element-renderers/svg.js.map +1 -0
- package/dist/compositor/element-renderers/text.d.ts +25 -0
- package/dist/compositor/element-renderers/text.d.ts.map +1 -0
- package/dist/compositor/element-renderers/text.js +1358 -0
- package/dist/compositor/element-renderers/text.js.map +1 -0
- package/dist/compositor/element-renderers/video.d.ts +12 -0
- package/dist/compositor/element-renderers/video.d.ts.map +1 -0
- package/dist/compositor/element-renderers/video.js +109 -0
- package/dist/compositor/element-renderers/video.js.map +1 -0
- package/dist/compositor/fit.d.ts +18 -0
- package/dist/compositor/fit.d.ts.map +1 -0
- package/dist/compositor/fit.js +106 -0
- package/dist/compositor/fit.js.map +1 -0
- package/dist/compositor/lighting.d.ts +63 -0
- package/dist/compositor/lighting.d.ts.map +1 -0
- package/dist/compositor/lighting.js +141 -0
- package/dist/compositor/lighting.js.map +1 -0
- package/dist/compositor/mat4.d.ts +88 -0
- package/dist/compositor/mat4.d.ts.map +1 -0
- package/dist/compositor/mat4.js +245 -0
- package/dist/compositor/mat4.js.map +1 -0
- package/dist/compositor/project.d.ts +24 -0
- package/dist/compositor/project.d.ts.map +1 -0
- package/dist/compositor/project.js +105 -0
- package/dist/compositor/project.js.map +1 -0
- package/dist/compositor/render-context.d.ts +194 -0
- package/dist/compositor/render-context.d.ts.map +1 -0
- package/dist/compositor/render-context.js +10 -0
- package/dist/compositor/render-context.js.map +1 -0
- package/dist/compositor/resolve.d.ts +80 -0
- package/dist/compositor/resolve.d.ts.map +1 -0
- package/dist/compositor/resolve.js +276 -0
- package/dist/compositor/resolve.js.map +1 -0
- package/dist/compositor/scene.d.ts +10 -0
- package/dist/compositor/scene.d.ts.map +1 -0
- package/dist/compositor/scene.js +658 -0
- package/dist/compositor/scene.js.map +1 -0
- package/dist/compositor/transform.d.ts +73 -0
- package/dist/compositor/transform.d.ts.map +1 -0
- package/dist/compositor/transform.js +229 -0
- package/dist/compositor/transform.js.map +1 -0
- package/dist/compositor/unit.d.ts +27 -0
- package/dist/compositor/unit.d.ts.map +1 -0
- package/dist/compositor/unit.js +74 -0
- package/dist/compositor/unit.js.map +1 -0
- package/dist/encoder/exporter.d.ts +95 -0
- package/dist/encoder/exporter.d.ts.map +1 -0
- package/dist/encoder/exporter.js +341 -0
- package/dist/encoder/exporter.js.map +1 -0
- package/dist/encoder/index.d.ts +3 -0
- package/dist/encoder/index.d.ts.map +1 -0
- package/dist/encoder/index.js +2 -0
- package/dist/encoder/index.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +32 -0
- package/dist/logger.js.map +1 -0
- package/dist/runtime.d.ts +216 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +1012 -0
- package/dist/runtime.js.map +1 -0
- package/dist/svg/morph.d.ts +6 -0
- package/dist/svg/morph.d.ts.map +1 -0
- package/dist/svg/morph.js +62 -0
- package/dist/svg/morph.js.map +1 -0
- package/dist/svg/svg-renderer.d.ts +18 -0
- package/dist/svg/svg-renderer.d.ts.map +1 -0
- package/dist/svg/svg-renderer.js +142 -0
- package/dist/svg/svg-renderer.js.map +1 -0
- package/dist/text/caption-chunk.d.ts +17 -0
- package/dist/text/caption-chunk.d.ts.map +1 -0
- package/dist/text/caption-chunk.js +76 -0
- package/dist/text/caption-chunk.js.map +1 -0
- package/dist/text/font-atlas.d.ts +63 -0
- package/dist/text/font-atlas.d.ts.map +1 -0
- package/dist/text/font-atlas.js +225 -0
- package/dist/text/font-atlas.js.map +1 -0
- package/dist/text/measure.d.ts +38 -0
- package/dist/text/measure.d.ts.map +1 -0
- package/dist/text/measure.js +164 -0
- package/dist/text/measure.js.map +1 -0
- package/dist/text/text-animation.d.ts +52 -0
- package/dist/text/text-animation.d.ts.map +1 -0
- package/dist/text/text-animation.js +133 -0
- package/dist/text/text-animation.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
// Worker-compatible, deterministic video frame source.
|
|
2
|
+
//
|
|
3
|
+
// HTMLVideoElement bundles demux + decode + a playback clock — but it
|
|
4
|
+
// doesn't exist in workers, and its seeking is approximate. This class
|
|
5
|
+
// rebuilds the pipeline from parts that DO exist in workers:
|
|
6
|
+
//
|
|
7
|
+
// fetch(url) → mp4box demux (sample table + codec config)
|
|
8
|
+
// → VideoDecoder (WebCodecs)
|
|
9
|
+
// → getFrame(t) returns the exact VideoFrame for time t
|
|
10
|
+
//
|
|
11
|
+
// Determinism is the point: preview frame N is pixel-identical to
|
|
12
|
+
// exported frame N, because both decode the same sample.
|
|
13
|
+
//
|
|
14
|
+
// Decode model — PIPELINED, not batch-per-seek:
|
|
15
|
+
// - One persistent decoder. Samples are fed in decode order through a
|
|
16
|
+
// cursor (`nextFeedIndex`); outputs land in a frame cache keyed by
|
|
17
|
+
// sample index.
|
|
18
|
+
// - getFrame(t) keeps the feed cursor FEED_AHEAD samples past the
|
|
19
|
+
// target, so during linear playback each request feeds ~1 new
|
|
20
|
+
// sample (amortized one hardware decode per frame — real-time).
|
|
21
|
+
// - Only a backward seek or a large forward jump resets the decoder
|
|
22
|
+
// to the target's preceding keyframe. flush() happens only at
|
|
23
|
+
// end-of-stream.
|
|
24
|
+
// - Decoded frames behind the playhead are evicted + closed.
|
|
25
|
+
//
|
|
26
|
+
// v1 limits (documented in PARITY-PLAN.md):
|
|
27
|
+
// - MP4 containers only (mp4box). Other containers fall back to the
|
|
28
|
+
// host's main-thread pump.
|
|
29
|
+
// - The whole file is fetched up front. Fine for clip-length assets;
|
|
30
|
+
// long-form needs ranged fetches + incremental demux.
|
|
31
|
+
import { createFile, DataStream, Endianness } from 'mp4box';
|
|
32
|
+
import { getLogger } from '../logger.js';
|
|
33
|
+
/** Samples kept fed beyond the target — the decode pipeline depth. */
|
|
34
|
+
const FEED_AHEAD = 12;
|
|
35
|
+
/** Cached frames kept behind the target (handles tiny backwards jitter). */
|
|
36
|
+
const CACHE_BEHIND = 2;
|
|
37
|
+
/**
|
|
38
|
+
* Cached frames kept behind the target while playing BACKWARD
|
|
39
|
+
* (time_remap reverse). H.264 only decodes forward from a keyframe,
|
|
40
|
+
* so reverse playback re-decodes a GOP chunk and then serves the next
|
|
41
|
+
* ~24 descending requests from cache — one GOP decode per chunk
|
|
42
|
+
* instead of per frame, with memory bounded to the window.
|
|
43
|
+
*/
|
|
44
|
+
const REVERSE_CACHE = 24;
|
|
45
|
+
/** Forward jump (in samples) beyond which we reset to a keyframe instead of decoding through. */
|
|
46
|
+
const FORWARD_RESET_THRESHOLD = 90;
|
|
47
|
+
/** Safety timeout for a single frame request. */
|
|
48
|
+
const FRAME_TIMEOUT_MS = 2000;
|
|
49
|
+
export class Mp4FrameSource {
|
|
50
|
+
width;
|
|
51
|
+
height;
|
|
52
|
+
/** Media duration in seconds. */
|
|
53
|
+
duration;
|
|
54
|
+
/** Samples in decode order, with composition times. */
|
|
55
|
+
samples;
|
|
56
|
+
/** Indices of keyframes within `samples`, ascending. */
|
|
57
|
+
keyIndices;
|
|
58
|
+
/** Decoder-output timestamp → sample index (composition order lookup). */
|
|
59
|
+
tsToIndex = new Map();
|
|
60
|
+
config;
|
|
61
|
+
decoder = null;
|
|
62
|
+
/** Next sample index (decode order) to feed the decoder. */
|
|
63
|
+
nextFeedIndex = Number.POSITIVE_INFINITY;
|
|
64
|
+
/** True after an end-of-stream flush — next decode needs a reset. */
|
|
65
|
+
flushed = false;
|
|
66
|
+
/** Decoded frames, keyed by sample index. Evicted + closed as the window moves. */
|
|
67
|
+
cache = new Map();
|
|
68
|
+
/** Cache window [lo, hi] — outputs outside it are closed on arrival. */
|
|
69
|
+
windowLo = 0;
|
|
70
|
+
windowHi = Number.POSITIVE_INFINITY;
|
|
71
|
+
/** Previous request's target — direction detection for the window shape. */
|
|
72
|
+
lastTarget = -1;
|
|
73
|
+
/** Pending getFrame waiters, keyed by sample index. */
|
|
74
|
+
waiters = new Map();
|
|
75
|
+
/** Serializes getFrame calls. */
|
|
76
|
+
opChain = Promise.resolve();
|
|
77
|
+
disposed = false;
|
|
78
|
+
static isSupported() {
|
|
79
|
+
return typeof VideoDecoder !== 'undefined';
|
|
80
|
+
}
|
|
81
|
+
constructor(config, samples, width, height, duration) {
|
|
82
|
+
this.config = config;
|
|
83
|
+
this.samples = samples;
|
|
84
|
+
this.width = width;
|
|
85
|
+
this.height = height;
|
|
86
|
+
this.duration = duration;
|
|
87
|
+
this.keyIndices = [];
|
|
88
|
+
for (let i = 0; i < samples.length; i++) {
|
|
89
|
+
if (samples[i].isKey)
|
|
90
|
+
this.keyIndices.push(i);
|
|
91
|
+
this.tsToIndex.set(samples[i].chunk.timestamp, i);
|
|
92
|
+
}
|
|
93
|
+
if (this.keyIndices.length === 0 || this.keyIndices[0] !== 0) {
|
|
94
|
+
// Defensive: treat the first sample as a sync point — most
|
|
95
|
+
// encoders emit a keyframe first; without one we can't decode.
|
|
96
|
+
this.keyIndices.unshift(0);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Fetch + demux + verify the codec is decodable. Throws on any failure. */
|
|
100
|
+
static async load(url) {
|
|
101
|
+
if (!Mp4FrameSource.isSupported()) {
|
|
102
|
+
throw new Error('WebCodecs VideoDecoder unavailable in this context');
|
|
103
|
+
}
|
|
104
|
+
// 10s fetch timeout (mirrors loadImage): a dead video host (e.g. the retired
|
|
105
|
+
// MDN flower.mp4) otherwise dangles a connection through preload.
|
|
106
|
+
const ac = new AbortController();
|
|
107
|
+
const timer = setTimeout(() => ac.abort(), 10000);
|
|
108
|
+
let response;
|
|
109
|
+
try {
|
|
110
|
+
response = await fetch(url, { mode: 'cors', signal: ac.signal });
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
}
|
|
115
|
+
if (!response.ok)
|
|
116
|
+
throw new Error(`fetch failed (${response.status})`);
|
|
117
|
+
const buffer = (await response.arrayBuffer());
|
|
118
|
+
buffer.fileStart = 0;
|
|
119
|
+
const mp4 = createFile();
|
|
120
|
+
const { info, rawSamples } = await new Promise((resolve, reject) => {
|
|
121
|
+
const collected = [];
|
|
122
|
+
let trackInfo = null;
|
|
123
|
+
mp4.onError = (e) => reject(new Error(`mp4box: ${e}`));
|
|
124
|
+
mp4.onReady = (i) => {
|
|
125
|
+
if (i.videoTracks.length === 0) {
|
|
126
|
+
reject(new Error('no video track in container'));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
trackInfo = i;
|
|
130
|
+
const track = i.videoTracks[0];
|
|
131
|
+
mp4.setExtractionOptions(track.id, null, { nbSamples: track.nb_samples });
|
|
132
|
+
mp4.start();
|
|
133
|
+
};
|
|
134
|
+
mp4.onSamples = (_trackId, _user, samples) => {
|
|
135
|
+
collected.push(...samples);
|
|
136
|
+
const expected = trackInfo?.videoTracks[0]?.nb_samples ?? Infinity;
|
|
137
|
+
if (collected.length >= expected) {
|
|
138
|
+
resolve({ info: trackInfo, rawSamples: collected });
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
mp4.appendBuffer(buffer);
|
|
142
|
+
mp4.flush();
|
|
143
|
+
});
|
|
144
|
+
const track = info.videoTracks[0];
|
|
145
|
+
const description = extractDescription(mp4, track.id);
|
|
146
|
+
const config = {
|
|
147
|
+
codec: track.codec,
|
|
148
|
+
codedWidth: track.video?.width ?? track.track_width,
|
|
149
|
+
codedHeight: track.video?.height ?? track.track_height,
|
|
150
|
+
...(description ? { description } : {}),
|
|
151
|
+
};
|
|
152
|
+
const support = await VideoDecoder.isConfigSupported(config);
|
|
153
|
+
if (!support.supported) {
|
|
154
|
+
throw new Error(`codec ${track.codec} not decodable here`);
|
|
155
|
+
}
|
|
156
|
+
const samples = rawSamples.map((s) => ({
|
|
157
|
+
time: s.cts / s.timescale,
|
|
158
|
+
isKey: s.is_sync,
|
|
159
|
+
chunk: new EncodedVideoChunk({
|
|
160
|
+
type: s.is_sync ? 'key' : 'delta',
|
|
161
|
+
timestamp: Math.round((s.cts / s.timescale) * 1_000_000),
|
|
162
|
+
duration: Math.round((s.duration / s.timescale) * 1_000_000),
|
|
163
|
+
data: s.data,
|
|
164
|
+
}),
|
|
165
|
+
}));
|
|
166
|
+
if (samples.length === 0)
|
|
167
|
+
throw new Error('container has zero video samples');
|
|
168
|
+
const duration = info.duration / info.timescale;
|
|
169
|
+
return new Mp4FrameSource(config, samples, config.codedWidth, config.codedHeight, duration > 0 ? duration : samples[samples.length - 1].time);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The decoded frame covering media time `t` (seconds). The RETURNED
|
|
173
|
+
* FRAME IS OWNED BY THE SOURCE — upload it, don't close it; it's
|
|
174
|
+
* closed when the cache window moves past it. Returns null after
|
|
175
|
+
* dispose or on decode failure.
|
|
176
|
+
*/
|
|
177
|
+
getFrame(t) {
|
|
178
|
+
const run = this.opChain.then(() => this.#getFrameInner(t));
|
|
179
|
+
// Keep the chain alive through failures.
|
|
180
|
+
this.opChain = run.catch(() => undefined);
|
|
181
|
+
return run;
|
|
182
|
+
}
|
|
183
|
+
async #getFrameInner(t) {
|
|
184
|
+
if (this.disposed)
|
|
185
|
+
return null;
|
|
186
|
+
const target = this.#sampleIndexAt(t);
|
|
187
|
+
const backward = this.lastTarget >= 0 && target < this.lastTarget;
|
|
188
|
+
this.lastTarget = target;
|
|
189
|
+
// Move the eviction window and close frames OUTSIDE it — both
|
|
190
|
+
// sides, or reverse playback (descending targets) would keep every
|
|
191
|
+
// frame it ever decoded and leak the whole clip as GPU surfaces.
|
|
192
|
+
// The window keeps a deep tail BEHIND the target when stepping
|
|
193
|
+
// backward (so descending requests amortize one GOP re-decode
|
|
194
|
+
// across REVERSE_CACHE cache hits) and a shallow one going forward.
|
|
195
|
+
const prevWindowLo = this.windowLo;
|
|
196
|
+
this.windowLo = Math.max(0, target - (backward ? REVERSE_CACHE : CACHE_BEHIND));
|
|
197
|
+
this.windowHi = Math.min(this.samples.length - 1, target + FEED_AHEAD);
|
|
198
|
+
for (const [idx, frame] of this.cache) {
|
|
199
|
+
if (idx < this.windowLo || idx > this.windowHi) {
|
|
200
|
+
frame.close();
|
|
201
|
+
this.cache.delete(idx);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const feedEnd = this.windowHi;
|
|
205
|
+
const cached = this.cache.get(target);
|
|
206
|
+
if (cached) {
|
|
207
|
+
// Keep the pipeline primed for the frames that follow.
|
|
208
|
+
this.#feedThrough(feedEnd, /*hasWaiter*/ false);
|
|
209
|
+
return cached;
|
|
210
|
+
}
|
|
211
|
+
this.#ensureDecoderFor(target, prevWindowLo);
|
|
212
|
+
const result = await new Promise((resolve) => {
|
|
213
|
+
const list = this.waiters.get(target) ?? [];
|
|
214
|
+
list.push(resolve);
|
|
215
|
+
this.waiters.set(target, list);
|
|
216
|
+
this.#feedThrough(feedEnd, /*hasWaiter*/ true);
|
|
217
|
+
// Safety net: a stuck decoder shouldn't stall produce forever.
|
|
218
|
+
setTimeout(() => {
|
|
219
|
+
const pending = this.waiters.get(target);
|
|
220
|
+
if (pending && pending.includes(resolve)) {
|
|
221
|
+
this.waiters.set(target, pending.filter((w) => w !== resolve));
|
|
222
|
+
resolve(this.cache.get(target) ?? null);
|
|
223
|
+
}
|
|
224
|
+
}, FRAME_TIMEOUT_MS);
|
|
225
|
+
});
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Make sure the decoder exists and its feed cursor can reach
|
|
230
|
+
* `target`. Reset to the target's preceding keyframe when:
|
|
231
|
+
* - there's no usable decoder (never started, errored, or flushed
|
|
232
|
+
* at end-of-stream), or
|
|
233
|
+
* - the target sits in a region this run already evicted
|
|
234
|
+
* (`target < prevWindowLo` — backward seek past the cache), or
|
|
235
|
+
* - the feed cursor is already past the target by more than the
|
|
236
|
+
* pipeline depth (an uncached target can never emerge from this
|
|
237
|
+
* run — without a reset the waiter would just hit its timeout), or
|
|
238
|
+
* - the jump forward is large enough that decoding through is more
|
|
239
|
+
* work than restarting at the target's keyframe.
|
|
240
|
+
*
|
|
241
|
+
* Crucially NOT a reset: target fed but output still pending in the
|
|
242
|
+
* decoder (the normal in-flight case during playback) — feeding a
|
|
243
|
+
* few samples ahead makes it emit. Produces are serialized, so at
|
|
244
|
+
* most one waiter exists and it always belongs to the current call.
|
|
245
|
+
*/
|
|
246
|
+
#ensureDecoderFor(target, prevWindowLo) {
|
|
247
|
+
const needsReset = !this.decoder ||
|
|
248
|
+
this.decoder.state === 'closed' ||
|
|
249
|
+
this.flushed ||
|
|
250
|
+
this.nextFeedIndex === Number.POSITIVE_INFINITY ||
|
|
251
|
+
target < prevWindowLo ||
|
|
252
|
+
this.nextFeedIndex > target + FEED_AHEAD ||
|
|
253
|
+
target - this.nextFeedIndex > FORWARD_RESET_THRESHOLD;
|
|
254
|
+
if (!needsReset)
|
|
255
|
+
return;
|
|
256
|
+
if (this.decoder && this.decoder.state !== 'closed')
|
|
257
|
+
this.decoder.close();
|
|
258
|
+
const decoder = new VideoDecoder({
|
|
259
|
+
output: (frame) => this.#onFrame(frame),
|
|
260
|
+
error: (e) => {
|
|
261
|
+
getLogger().warn('VideoDecoder error:', e.message);
|
|
262
|
+
this.#failPendingWaiters();
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
decoder.configure(this.config);
|
|
266
|
+
this.decoder = decoder;
|
|
267
|
+
this.flushed = false;
|
|
268
|
+
this.nextFeedIndex = this.#keyframeBefore(target);
|
|
269
|
+
}
|
|
270
|
+
/** Feed samples up to `end` (decode order). Flush only at end-of-stream. */
|
|
271
|
+
#feedThrough(end, hasWaiter) {
|
|
272
|
+
const decoder = this.decoder;
|
|
273
|
+
if (!decoder || decoder.state === 'closed' || this.flushed)
|
|
274
|
+
return;
|
|
275
|
+
while (this.nextFeedIndex <= end && this.nextFeedIndex < this.samples.length) {
|
|
276
|
+
decoder.decode(this.samples[this.nextFeedIndex].chunk);
|
|
277
|
+
this.nextFeedIndex += 1;
|
|
278
|
+
}
|
|
279
|
+
// End of stream with someone waiting: flush to drain the decoder's
|
|
280
|
+
// reorder buffer (the last few frames only emit on flush).
|
|
281
|
+
if (hasWaiter && this.nextFeedIndex >= this.samples.length) {
|
|
282
|
+
this.flushed = true;
|
|
283
|
+
decoder.flush().catch(() => {
|
|
284
|
+
/* reset on next request */
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
#onFrame(frame) {
|
|
289
|
+
if (this.disposed) {
|
|
290
|
+
frame.close();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const idx = this.tsToIndex.get(frame.timestamp);
|
|
294
|
+
if (idx === undefined || idx < this.windowLo || idx > this.windowHi) {
|
|
295
|
+
frame.close();
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
const previous = this.cache.get(idx);
|
|
299
|
+
if (previous)
|
|
300
|
+
previous.close();
|
|
301
|
+
this.cache.set(idx, frame);
|
|
302
|
+
}
|
|
303
|
+
if (idx !== undefined) {
|
|
304
|
+
const waiting = this.waiters.get(idx);
|
|
305
|
+
if (waiting) {
|
|
306
|
+
this.waiters.delete(idx);
|
|
307
|
+
const resolved = this.cache.get(idx) ?? null;
|
|
308
|
+
for (const w of waiting)
|
|
309
|
+
w(resolved);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
#failPendingWaiters() {
|
|
314
|
+
for (const [, waiting] of this.waiters) {
|
|
315
|
+
for (const w of waiting)
|
|
316
|
+
w(null);
|
|
317
|
+
}
|
|
318
|
+
this.waiters.clear();
|
|
319
|
+
}
|
|
320
|
+
/** Index of the sample whose composition window covers time t. */
|
|
321
|
+
#sampleIndexAt(t) {
|
|
322
|
+
const clamped = Math.max(0, Math.min(t, this.samples[this.samples.length - 1].time));
|
|
323
|
+
// Samples are in decode order; composition order ≈ decode order for
|
|
324
|
+
// streams without B-frame reordering (our common case). Linear-scan
|
|
325
|
+
// fallback keeps B-frame streams correct at small cost.
|
|
326
|
+
let best = 0;
|
|
327
|
+
let bestTime = -Infinity;
|
|
328
|
+
for (let i = 0; i < this.samples.length; i++) {
|
|
329
|
+
const st = this.samples[i].time;
|
|
330
|
+
if (st <= clamped && st > bestTime) {
|
|
331
|
+
bestTime = st;
|
|
332
|
+
best = i;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return best;
|
|
336
|
+
}
|
|
337
|
+
#keyframeBefore(index) {
|
|
338
|
+
let best = this.keyIndices[0];
|
|
339
|
+
for (const k of this.keyIndices) {
|
|
340
|
+
if (k <= index)
|
|
341
|
+
best = k;
|
|
342
|
+
else
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
return best;
|
|
346
|
+
}
|
|
347
|
+
dispose() {
|
|
348
|
+
this.disposed = true;
|
|
349
|
+
this.#failPendingWaiters();
|
|
350
|
+
for (const frame of this.cache.values())
|
|
351
|
+
frame.close();
|
|
352
|
+
this.cache.clear();
|
|
353
|
+
if (this.decoder && this.decoder.state !== 'closed')
|
|
354
|
+
this.decoder.close();
|
|
355
|
+
this.decoder = null;
|
|
356
|
+
this.samples = [];
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Pull the codec-private description (avcC / hvcC / vpcC / av1C box
|
|
361
|
+
* payload) out of the sample-description entry — VideoDecoder needs it
|
|
362
|
+
* for H.264/H.265 in MP4 (the "avcC extradata" convention).
|
|
363
|
+
*/
|
|
364
|
+
function extractDescription(mp4, trackId) {
|
|
365
|
+
try {
|
|
366
|
+
const trak = mp4.getTrackById(trackId);
|
|
367
|
+
for (const entry of trak.mdia.minf.stbl.stsd.entries) {
|
|
368
|
+
const box = entry.avcC ?? entry.hvcC ?? entry.vpcC ?? entry.av1C;
|
|
369
|
+
if (box) {
|
|
370
|
+
// ⚠ Endianness lives on the `Endianness` export in current
|
|
371
|
+
// mp4box builds — `DataStream.BIG_ENDIAN` is undefined there,
|
|
372
|
+
// and passing undefined silently serializes LITTLE-endian,
|
|
373
|
+
// byte-swapping the avcC and making VideoDecoder reject the
|
|
374
|
+
// config. Cost us a debugging session; don't regress this.
|
|
375
|
+
const stream = new DataStream(undefined, 0, Endianness.BIG_ENDIAN);
|
|
376
|
+
box.write(stream);
|
|
377
|
+
// Skip the 8-byte box header (size + fourcc).
|
|
378
|
+
return new Uint8Array(stream.buffer, 8);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
getLogger().debug('extractDescription failed:', err instanceof Error ? err.message : String(err));
|
|
384
|
+
}
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
//# sourceMappingURL=mp4-frame-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mp4-frame-source.js","sourceRoot":"","sources":["../../src/assets/mp4-frame-source.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Muxer, ArrayBufferTarget } from 'mp4-muxer';
|
|
2
|
+
export interface AudioEncodeOptions {
|
|
3
|
+
/** AAC bitrate in bits/second. Default 128 kbps. */
|
|
4
|
+
bitrate?: number;
|
|
5
|
+
/** Codec string. Default 'mp4a.40.2' (AAC-LC). */
|
|
6
|
+
codec?: string;
|
|
7
|
+
/** Samples per AudioData chunk. Default 1024 (typical AAC frame size). */
|
|
8
|
+
chunkSize?: number;
|
|
9
|
+
}
|
|
10
|
+
/** A muxer codec id paired with the matching WebCodecs encoder codec string. */
|
|
11
|
+
export interface PickedAudioCodec {
|
|
12
|
+
/** mp4-muxer / webm-muxer audio codec id. */
|
|
13
|
+
muxer: 'aac' | 'opus';
|
|
14
|
+
/** WebCodecs AudioEncoder codec string. */
|
|
15
|
+
encoder: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Pick an audio codec this environment's WebCodecs AudioEncoder can actually
|
|
19
|
+
* encode. AAC ('mp4a.40.2') is preferred (most universal in MP4), but
|
|
20
|
+
* Chromium on Linux ships NO AAC encoder — only macOS/Windows borrow the OS
|
|
21
|
+
* one — so on the Linux render container it falls back to Opus, which Chromium
|
|
22
|
+
* encodes everywhere (and which MP4/WebM both mux). Returns null when no audio
|
|
23
|
+
* encoder exists at all, so the caller can render silently instead of throwing
|
|
24
|
+
* "Unsupported codec type" mid-export.
|
|
25
|
+
*/
|
|
26
|
+
export declare function pickAudioCodec(sampleRate: number, numberOfChannels: number, bitrate?: number): Promise<PickedAudioCodec | null>;
|
|
27
|
+
/**
|
|
28
|
+
* Encode an AudioBuffer through WebCodecs and feed chunks into the muxer.
|
|
29
|
+
*/
|
|
30
|
+
export declare function encodeAudioBuffer(buffer: AudioBuffer, muxer: Muxer<ArrayBufferTarget>, options?: AudioEncodeOptions): Promise<void>;
|
|
31
|
+
//# sourceMappingURL=encoder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoder.d.ts","sourceRoot":"","sources":["../../src/audio/encoder.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAG1D,MAAM,WAAW,kBAAkB;IACjC,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD,gFAAgF;AAChF,MAAM,WAAW,gBAAgB;IAC/B,6CAA6C;IAC7C,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC;IACtB,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAClC,UAAU,EAAE,MAAM,EAClB,gBAAgB,EAAE,MAAM,EACxB,OAAO,SAAkB,GACxB,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAoBlC;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,KAAK,CAAC,iBAAiB,CAAC,EAC/B,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAyDf"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// WebCodecs AudioEncoder integration for the exporter.
|
|
2
|
+
//
|
|
3
|
+
// Takes a rendered AudioBuffer (output of `mixSourceAudio`), chunks it into
|
|
4
|
+
// AudioData frames, encodes via AudioEncoder (AAC-LC), and writes the
|
|
5
|
+
// resulting EncodedAudioChunks into the muxer's audio track.
|
|
6
|
+
import { getLogger } from '../logger.js';
|
|
7
|
+
const DEFAULT_BITRATE = 128_000;
|
|
8
|
+
const DEFAULT_CODEC = 'mp4a.40.2';
|
|
9
|
+
const DEFAULT_CHUNK_SIZE = 1024;
|
|
10
|
+
/**
|
|
11
|
+
* Pick an audio codec this environment's WebCodecs AudioEncoder can actually
|
|
12
|
+
* encode. AAC ('mp4a.40.2') is preferred (most universal in MP4), but
|
|
13
|
+
* Chromium on Linux ships NO AAC encoder — only macOS/Windows borrow the OS
|
|
14
|
+
* one — so on the Linux render container it falls back to Opus, which Chromium
|
|
15
|
+
* encodes everywhere (and which MP4/WebM both mux). Returns null when no audio
|
|
16
|
+
* encoder exists at all, so the caller can render silently instead of throwing
|
|
17
|
+
* "Unsupported codec type" mid-export.
|
|
18
|
+
*/
|
|
19
|
+
export async function pickAudioCodec(sampleRate, numberOfChannels, bitrate = DEFAULT_BITRATE) {
|
|
20
|
+
if (typeof AudioEncoder === 'undefined')
|
|
21
|
+
return null;
|
|
22
|
+
const candidates = [
|
|
23
|
+
{ muxer: 'aac', encoder: 'mp4a.40.2' },
|
|
24
|
+
{ muxer: 'opus', encoder: 'opus' },
|
|
25
|
+
];
|
|
26
|
+
for (const c of candidates) {
|
|
27
|
+
try {
|
|
28
|
+
const { supported } = await AudioEncoder.isConfigSupported({
|
|
29
|
+
codec: c.encoder,
|
|
30
|
+
sampleRate,
|
|
31
|
+
numberOfChannels,
|
|
32
|
+
bitrate,
|
|
33
|
+
});
|
|
34
|
+
if (supported)
|
|
35
|
+
return c;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* malformed/unsupported codec string → try the next candidate */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Encode an AudioBuffer through WebCodecs and feed chunks into the muxer.
|
|
45
|
+
*/
|
|
46
|
+
export async function encodeAudioBuffer(buffer, muxer, options = {}) {
|
|
47
|
+
if (typeof AudioEncoder === 'undefined') {
|
|
48
|
+
throw new Error('WebCodecs AudioEncoder is not available in this environment');
|
|
49
|
+
}
|
|
50
|
+
const bitrate = options.bitrate ?? DEFAULT_BITRATE;
|
|
51
|
+
const codec = options.codec ?? DEFAULT_CODEC;
|
|
52
|
+
const chunkSize = options.chunkSize ?? DEFAULT_CHUNK_SIZE;
|
|
53
|
+
const sampleRate = buffer.sampleRate;
|
|
54
|
+
const channels = buffer.numberOfChannels;
|
|
55
|
+
const encoder = new AudioEncoder({
|
|
56
|
+
output: (chunk, metadata) => {
|
|
57
|
+
muxer.addAudioChunk(chunk, metadata);
|
|
58
|
+
},
|
|
59
|
+
error: (error) => {
|
|
60
|
+
getLogger().error('AudioEncoder error:', error.message);
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
encoder.configure({
|
|
64
|
+
codec,
|
|
65
|
+
sampleRate,
|
|
66
|
+
numberOfChannels: channels,
|
|
67
|
+
bitrate,
|
|
68
|
+
});
|
|
69
|
+
// Interleave channels into a single Float32Array per chunk.
|
|
70
|
+
const totalFrames = buffer.length;
|
|
71
|
+
const channelData = [];
|
|
72
|
+
for (let ch = 0; ch < channels; ch++)
|
|
73
|
+
channelData.push(buffer.getChannelData(ch));
|
|
74
|
+
for (let offset = 0; offset < totalFrames; offset += chunkSize) {
|
|
75
|
+
const frames = Math.min(chunkSize, totalFrames - offset);
|
|
76
|
+
const interleaved = new Float32Array(frames * channels);
|
|
77
|
+
for (let i = 0; i < frames; i++) {
|
|
78
|
+
for (let ch = 0; ch < channels; ch++) {
|
|
79
|
+
interleaved[i * channels + ch] = channelData[ch][offset + i];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const audioData = new AudioData({
|
|
83
|
+
format: 'f32',
|
|
84
|
+
sampleRate,
|
|
85
|
+
numberOfFrames: frames,
|
|
86
|
+
numberOfChannels: channels,
|
|
87
|
+
timestamp: Math.round((offset / sampleRate) * 1_000_000), // microseconds
|
|
88
|
+
data: interleaved,
|
|
89
|
+
});
|
|
90
|
+
encoder.encode(audioData);
|
|
91
|
+
audioData.close();
|
|
92
|
+
}
|
|
93
|
+
await encoder.flush();
|
|
94
|
+
encoder.close();
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=encoder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoder.js","sourceRoot":"","sources":["../../src/audio/encoder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface FadePoint {
|
|
2
|
+
/** Timeline seconds since the element became active. */
|
|
3
|
+
tau: number;
|
|
4
|
+
/** Envelope gain in [0, 1] (multiply with the element's volume gain). */
|
|
5
|
+
gain: number;
|
|
6
|
+
}
|
|
7
|
+
/** Envelope value at timeline offset τ. */
|
|
8
|
+
export declare function fadeGainAt(tau: number, trackLength: number, fadeIn: number, fadeOut: number): number;
|
|
9
|
+
/**
|
|
10
|
+
* Corner points of the envelope from `startTau` (inclusive) through the
|
|
11
|
+
* end of the track. The first point is the value AT startTau (for
|
|
12
|
+
* setValueAtTime); subsequent points are linearRamp targets. When both
|
|
13
|
+
* fades are 0 the result is a single constant point.
|
|
14
|
+
*/
|
|
15
|
+
export declare function fadeBreakpoints(startTau: number, trackLength: number, fadeIn: number, fadeOut: number): FadePoint[];
|
|
16
|
+
//# sourceMappingURL=fades.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fades.d.ts","sourceRoot":"","sources":["../../src/audio/fades.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,SAAS;IACxB,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAC;IACZ,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;CACd;AAED,2CAA2C;AAC3C,wBAAgB,UAAU,CACxB,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GACd,MAAM,CAKR;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GACd,SAAS,EAAE,CASb"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Audio fade envelope — ONE piecewise-linear definition shared by the
|
|
2
|
+
// preview scheduler (realtime GainNode ramps) and the export mixer
|
|
3
|
+
// (OfflineAudioContext ramps), so preview and export are gain-identical.
|
|
4
|
+
//
|
|
5
|
+
// The envelope runs in TIMELINE seconds over the element's visible
|
|
6
|
+
// window [0, trackLength]:
|
|
7
|
+
//
|
|
8
|
+
// g(τ) = min(1, τ / fade_in) × min(1, (trackLength − τ) / fade_out)
|
|
9
|
+
//
|
|
10
|
+
// (each factor is 1 when its fade is 0/absent). When the fades don't
|
|
11
|
+
// overlap, g is piecewise linear and ramping between its corner points
|
|
12
|
+
// reproduces it exactly; when fade_in + fade_out > trackLength the
|
|
13
|
+
// overlap region is quadratic and the linear ramps are a close
|
|
14
|
+
// approximation through the same corners. fadeBreakpoints returns the
|
|
15
|
+
// corners from a given starting offset, ready for setValueAtTime +
|
|
16
|
+
// linearRampToValueAtTime.
|
|
17
|
+
/** Envelope value at timeline offset τ. */
|
|
18
|
+
export function fadeGainAt(tau, trackLength, fadeIn, fadeOut) {
|
|
19
|
+
let g = 1;
|
|
20
|
+
if (fadeIn > 0)
|
|
21
|
+
g *= Math.min(1, Math.max(0, tau / fadeIn));
|
|
22
|
+
if (fadeOut > 0)
|
|
23
|
+
g *= Math.min(1, Math.max(0, (trackLength - tau) / fadeOut));
|
|
24
|
+
return g;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Corner points of the envelope from `startTau` (inclusive) through the
|
|
28
|
+
* end of the track. The first point is the value AT startTau (for
|
|
29
|
+
* setValueAtTime); subsequent points are linearRamp targets. When both
|
|
30
|
+
* fades are 0 the result is a single constant point.
|
|
31
|
+
*/
|
|
32
|
+
export function fadeBreakpoints(startTau, trackLength, fadeIn, fadeOut) {
|
|
33
|
+
const corners = new Set([startTau, trackLength]);
|
|
34
|
+
if (fadeIn > 0)
|
|
35
|
+
corners.add(Math.min(fadeIn, trackLength));
|
|
36
|
+
if (fadeOut > 0)
|
|
37
|
+
corners.add(Math.max(0, trackLength - fadeOut));
|
|
38
|
+
return [...corners]
|
|
39
|
+
.filter((tau) => tau >= startTau)
|
|
40
|
+
.sort((a, b) => a - b)
|
|
41
|
+
.map((tau) => ({ tau, gain: fadeGainAt(tau, trackLength, fadeIn, fadeOut) }));
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=fades.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fades.js","sourceRoot":"","sources":["../../src/audio/fades.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,mEAAmE;AACnE,yEAAyE;AACzE,EAAE;AACF,mEAAmE;AACnE,2BAA2B;AAC3B,EAAE;AACF,sEAAsE;AACtE,EAAE;AACF,qEAAqE;AACrE,uEAAuE;AACvE,mEAAmE;AACnE,+DAA+D;AAC/D,sEAAsE;AACtE,mEAAmE;AACnE,2BAA2B;AAS3B,2CAA2C;AAC3C,MAAM,UAAU,UAAU,CACxB,GAAW,EACX,WAAmB,EACnB,MAAc,EACd,OAAe;IAEf,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,IAAI,MAAM,GAAG,CAAC;QAAE,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC;IAC5D,IAAI,OAAO,GAAG,CAAC;QAAE,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,WAAW,GAAG,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAC9E,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAC7B,QAAgB,EAChB,WAAmB,EACnB,MAAc,EACd,OAAe;IAEf,MAAM,OAAO,GAAG,IAAI,GAAG,CAAS,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC;IACzD,IAAI,MAAM,GAAG,CAAC;QAAE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;IAC3D,IAAI,OAAO,GAAG,CAAC;QAAE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC;IAEjE,OAAO,CAAC,GAAG,OAAO,CAAC;SAChB,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,QAAQ,CAAC;SAChC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;SACrB,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;AAClF,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create the master limiter node. Works on both AudioContext (preview) and
|
|
3
|
+
* OfflineAudioContext (export). Route the summed master gain through this,
|
|
4
|
+
* then to the destination — and tap meters POST-limiter so they show what
|
|
5
|
+
* you actually hear.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createMasterLimiter(ctx: BaseAudioContext): DynamicsCompressorNode;
|
|
8
|
+
//# sourceMappingURL=limiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limiter.d.ts","sourceRoot":"","sources":["../../src/audio/limiter.ts"],"names":[],"mappings":"AAyBA;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,gBAAgB,GAAG,sBAAsB,CAQjF"}
|