@pixerate/flair 0.28.0 → 0.29.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +48 -6
  3. package/dist/catalog/index.d.ts +11 -6
  4. package/dist/catalog/index.js +1 -1
  5. package/dist/{chunk-4LKAPRFM.js → chunk-6GFOLHCT.js} +920 -34
  6. package/dist/chunk-6GFOLHCT.js.map +1 -0
  7. package/dist/{chunk-JL6TI6II.js → chunk-7KYQ4R76.js} +2 -2
  8. package/dist/chunk-7KYQ4R76.js.map +1 -0
  9. package/dist/{chunk-STKEKXCK.js → chunk-AUH3PEFW.js} +2 -2
  10. package/dist/{chunk-PFGGPIMT.js → chunk-DA4UUSDO.js} +58 -3
  11. package/dist/chunk-DA4UUSDO.js.map +1 -0
  12. package/dist/{chunk-RDU63CDO.js → chunk-HDYBY3PQ.js} +380 -1
  13. package/dist/chunk-HDYBY3PQ.js.map +1 -0
  14. package/dist/{chunk-VLQLRKGW.js → chunk-KZYZX3V5.js} +260 -33
  15. package/dist/chunk-KZYZX3V5.js.map +1 -0
  16. package/dist/{chunk-GXP4YLAP.js → chunk-MBKNTXQX.js} +9 -9
  17. package/dist/chunk-MBKNTXQX.js.map +1 -0
  18. package/dist/{chunk-H6VDFBIO.js → chunk-SMR2MJXB.js} +156 -5
  19. package/dist/chunk-SMR2MJXB.js.map +1 -0
  20. package/dist/{chunk-YBGRXVAE.js → chunk-X3C2VFTM.js} +3 -3
  21. package/dist/{chunk-CK6EV37I.js → chunk-ZULFHADW.js} +69 -6
  22. package/dist/chunk-ZULFHADW.js.map +1 -0
  23. package/dist/cli.js +6 -6
  24. package/dist/editor/index.d.ts +3 -3
  25. package/dist/editor/index.js +3 -3
  26. package/dist/{effect-GGP66JI6.js → effect-YBWDNIYG.js} +10 -10
  27. package/dist/embed/index.d.ts +36 -8
  28. package/dist/embed/index.js +5 -5
  29. package/dist/{frames-VEcqa40g.d.ts → frames-CGJudAbu.d.ts} +11 -1
  30. package/dist/index.d.ts +69 -6
  31. package/dist/index.js +35 -7
  32. package/dist/index.js.map +1 -1
  33. package/dist/node.d.ts +4 -4
  34. package/dist/node.js +9 -9
  35. package/dist/react/index.d.ts +4 -4
  36. package/dist/react/index.js +7 -7
  37. package/dist/react/index.js.map +1 -1
  38. package/dist/{recorder-CDPPTJMG.js → recorder-TYMEZHOT.js} +3 -3
  39. package/dist/{renderer-teaa98RE.d.ts → renderer-tk1nbVkW.d.ts} +252 -2
  40. package/dist/svelte/index.d.ts +4 -4
  41. package/dist/svelte/index.js +7 -7
  42. package/dist/svelte/index.js.map +1 -1
  43. package/dist/{svg-BhRboN9q.d.ts → svg-DaKRPA9v.d.ts} +39 -7
  44. package/dist/{types-DUOMxZaq.d.ts → types-wh28fXW4.d.ts} +2 -2
  45. package/dist/vector/index.d.ts +2 -2
  46. package/dist/vector/index.js +5 -3
  47. package/package.json +1 -1
  48. package/dist/chunk-4LKAPRFM.js.map +0 -1
  49. package/dist/chunk-CK6EV37I.js.map +0 -1
  50. package/dist/chunk-GXP4YLAP.js.map +0 -1
  51. package/dist/chunk-H6VDFBIO.js.map +0 -1
  52. package/dist/chunk-JL6TI6II.js.map +0 -1
  53. package/dist/chunk-PFGGPIMT.js.map +0 -1
  54. package/dist/chunk-RDU63CDO.js.map +0 -1
  55. package/dist/chunk-VLQLRKGW.js.map +0 -1
  56. /package/dist/{chunk-STKEKXCK.js.map → chunk-AUH3PEFW.js.map} +0 -0
  57. /package/dist/{chunk-YBGRXVAE.js.map → chunk-X3C2VFTM.js.map} +0 -0
  58. /package/dist/{effect-GGP66JI6.js.map → effect-YBWDNIYG.js.map} +0 -0
  59. /package/dist/{recorder-CDPPTJMG.js.map → recorder-TYMEZHOT.js.map} +0 -0
@@ -1,17 +1,18 @@
1
1
  import {
2
2
  ShaderFrames,
3
3
  ShaderRenderer
4
- } from "./chunk-H6VDFBIO.js";
4
+ } from "./chunk-SMR2MJXB.js";
5
5
  import {
6
+ analyseAudioBuffer,
6
7
  seekVideo
7
- } from "./chunk-RDU63CDO.js";
8
+ } from "./chunk-HDYBY3PQ.js";
8
9
  import {
9
10
  effectDefaults,
10
11
  effectInputs,
11
12
  getEffect,
12
13
  renderVectorEffect,
13
14
  textImage
14
- } from "./chunk-4LKAPRFM.js";
15
+ } from "./chunk-6GFOLHCT.js";
15
16
  import {
16
17
  parseColor
17
18
  } from "./chunk-VYOMXE6I.js";
@@ -19,9 +20,15 @@ import {
19
20
  SvgPlayer,
20
21
  svgDuration,
21
22
  svgView
22
- } from "./chunk-PFGGPIMT.js";
23
+ } from "./chunk-DA4UUSDO.js";
23
24
 
24
25
  // src/export/mp4mux.ts
26
+ var AAC_RATES = [96e3, 88200, 64e3, 48e3, 44100, 32e3, 24e3, 22050, 16e3, 12e3, 11025, 8e3, 7350];
27
+ function aacConfig(sampleRate, channels) {
28
+ const index = AAC_RATES.indexOf(sampleRate);
29
+ if (index < 0) throw new Error(`AAC has no standard rate of ${sampleRate} Hz; resample to 48000 or 44100.`);
30
+ return Uint8Array.of(2 << 3 | index >> 1, (index & 1) << 7 | channels << 3);
31
+ }
25
32
  var MOVIE_TIMESCALE = 1e3;
26
33
  var UINT32 = 4294967296;
27
34
  var ascii = (s) => Uint8Array.from(s, (c) => c.charCodeAt(0));
@@ -98,19 +105,80 @@ function stts(ticks) {
98
105
  for (const [count, delta] of runs) w.u32(count).u32(delta);
99
106
  }));
100
107
  }
108
+ var descriptor = (tag, ...content) => {
109
+ const body = fields((w) => content.forEach((c) => w.bytes(c)));
110
+ if (body.length > 127) throw new Error("An esds descriptor this long is not written here.");
111
+ return fields((w) => w.u8(tag).u8(body.length).bytes(body));
112
+ };
113
+ function oneChunk(samples, offset, wide) {
114
+ const stsz = fullBox("stsz", 0, 0, fields((w) => {
115
+ w.u32(0).u32(samples.length);
116
+ for (const s of samples) w.u32(s.data.length);
117
+ }));
118
+ const stsc = fullBox("stsc", 0, 0, fields((w) => samples.length ? w.u32(1).u32(1).u32(samples.length).u32(1) : w.u32(0)));
119
+ const stco = wide ? fullBox("co64", 0, 0, fields((w) => w.u32(1).u64(offset))) : fullBox("stco", 0, 0, fields((w) => w.u32(1).u32(offset)));
120
+ return [stsz, stsc, stco];
121
+ }
122
+ function audioTrak(audio, offset, wide) {
123
+ const timescale = audio.sampleRate;
124
+ const ticks = sampleTicks(audio.samples, timescale);
125
+ const trackTicks = ticks.reduce((a, b) => a + b, 0);
126
+ const movieTicks = Math.round(trackTicks * MOVIE_TIMESCALE / timescale);
127
+ const tkhd = fullBox("tkhd", 0, 3, fields(
128
+ (w) => w.u32(0).u32(0).u32(2).u32(0).u32(movieTicks).zeros(8).u16(0).u16(1).u16(256).u16(0).bytes(MATRIX).u32(0).u32(0)
129
+ ));
130
+ const mdhd = fullBox("mdhd", 0, 0, fields((w) => w.u32(0).u32(0).u32(timescale).u32(trackTicks).u16(21956).u16(0)));
131
+ const hdlr = fullBox("hdlr", 0, 0, fields((w) => w.u32(0).bytes(ascii("soun")).zeros(12).bytes(ascii("SoundHandler\0"))));
132
+ const smhd = fullBox("smhd", 0, 0, new Uint8Array(4));
133
+ const dinf = box("dinf", fullBox("dref", 0, 0, fields((w) => w.u32(1)), fullBox("url ", 0, 1)));
134
+ const bitrate = Math.round(audio.samples.reduce((n, s) => n + s.data.length, 0) * 8 / Math.max(1e-3, trackTicks / timescale));
135
+ const esds = fullBox(
136
+ "esds",
137
+ 0,
138
+ 0,
139
+ descriptor(
140
+ 3,
141
+ // ES_Descriptor
142
+ fields((w) => w.u16(2).u8(0)),
143
+ // ES id, flags
144
+ descriptor(
145
+ 4,
146
+ // DecoderConfigDescriptor
147
+ fields((w) => w.u8(64).u8(21).u8(0).u16(0).u32(bitrate).u32(bitrate)),
148
+ // MPEG-4 audio, audio stream, buffer size, bitrates
149
+ descriptor(5, audio.config ?? aacConfig(audio.sampleRate, audio.channels))
150
+ // DecoderSpecificInfo
151
+ ),
152
+ descriptor(6, Uint8Array.of(2))
153
+ // SLConfigDescriptor
154
+ )
155
+ );
156
+ const mp4a = box(
157
+ "mp4a",
158
+ fields((w) => w.zeros(6).u16(1).zeros(8).u16(audio.channels).u16(16).u16(0).u16(0).u32(Math.min(audio.sampleRate, 65535) * 65536)),
159
+ esds
160
+ );
161
+ const stsd = fullBox("stsd", 0, 0, fields((w) => w.u32(1)), mp4a);
162
+ const stbl = box("stbl", stsd, stts(ticks), ...oneChunk(audio.samples, offset, wide));
163
+ const trak = box("trak", tkhd, box("mdia", mdhd, hdlr, box("minf", smhd, dinf, stbl)));
164
+ return { trak, movieTicks };
165
+ }
101
166
  function moov(options, ticks, dataOffset, wide) {
102
167
  const { width, height, avcC, samples } = options;
103
168
  const timescale = options.timescale ?? 9e4;
104
169
  const trackTicks = ticks.reduce((a, b) => a + b, 0);
105
- const movieTicks = Math.round(trackTicks * MOVIE_TIMESCALE / timescale);
170
+ const videoTicks = Math.round(trackTicks * MOVIE_TIMESCALE / timescale);
171
+ const videoBytes = samples.reduce((n, s) => n + s.data.length, 0);
172
+ const sound = options.audio?.samples.length ? audioTrak(options.audio, dataOffset + videoBytes, wide) : void 0;
173
+ const movieTicks = Math.max(videoTicks, sound?.movieTicks ?? 0);
106
174
  const mvhd = fullBox("mvhd", 0, 0, fields(
107
- (w) => w.u32(0).u32(0).u32(MOVIE_TIMESCALE).u32(movieTicks).u32(65536).u16(256).zeros(10).bytes(MATRIX).zeros(24).u32(2)
175
+ (w) => w.u32(0).u32(0).u32(MOVIE_TIMESCALE).u32(movieTicks).u32(65536).u16(256).zeros(10).bytes(MATRIX).zeros(24).u32(sound ? 3 : 2)
108
176
  // pre_defined, next track id
109
177
  ));
110
178
  const tkhd = fullBox("tkhd", 0, 3, fields(
111
179
  (w) => (
112
180
  // enabled, in the movie
113
- w.u32(0).u32(0).u32(1).u32(0).u32(movieTicks).zeros(8).u16(0).u16(0).u16(0).u16(0).bytes(MATRIX).u32(width * 65536).u32(height * 65536)
181
+ w.u32(0).u32(0).u32(1).u32(0).u32(videoTicks).zeros(8).u16(0).u16(0).u16(0).u16(0).bytes(MATRIX).u32(width * 65536).u32(height * 65536)
114
182
  )
115
183
  ));
116
184
  const mdhd = fullBox("mdhd", 0, 0, fields((w) => w.u32(0).u32(0).u32(timescale).u32(trackTicks).u16(21956).u16(0)));
@@ -131,16 +199,10 @@ function moov(options, ticks, dataOffset, wide) {
131
199
  w.u32(keys.length);
132
200
  for (const k of keys) w.u32(k);
133
201
  }))];
134
- const stsz = fullBox("stsz", 0, 0, fields((w) => {
135
- w.u32(0).u32(samples.length);
136
- for (const s of samples) w.u32(s.data.length);
137
- }));
138
- const stsc = fullBox("stsc", 0, 0, fields((w) => samples.length ? w.u32(1).u32(1).u32(samples.length).u32(1) : w.u32(0)));
139
- const stco = wide ? fullBox("co64", 0, 0, fields((w) => w.u32(1).u64(dataOffset))) : fullBox("stco", 0, 0, fields((w) => w.u32(1).u32(dataOffset)));
140
- const stbl = box("stbl", stsd, stts(ticks), ...stss, stsz, stsc, stco);
202
+ const stbl = box("stbl", stsd, stts(ticks), ...stss, ...oneChunk(samples, dataOffset, wide));
141
203
  const minf = box("minf", vmhd, dinf, stbl);
142
204
  const trak = box("trak", tkhd, box("mdia", mdhd, hdlr, minf));
143
- return box("moov", mvhd, trak);
205
+ return box("moov", mvhd, trak, ...sound ? [sound.trak] : []);
144
206
  }
145
207
  function muxMp4(options) {
146
208
  const { samples } = options;
@@ -148,7 +210,8 @@ function muxMp4(options) {
148
210
  if (!samples[0].key) throw new Error("An MP4's first frame must be a keyframe.");
149
211
  const ftyp = box("ftyp", ascii("isom"), fields((w) => w.u32(512)), ascii("isomiso2avc1mp41"));
150
212
  const ticks = sampleTicks(samples, options.timescale ?? 9e4);
151
- const payload = samples.reduce((n, s) => n + s.data.length, 0);
213
+ const all = [...samples, ...options.audio?.samples ?? []];
214
+ const payload = all.reduce((n, s) => n + s.data.length, 0);
152
215
  const large = payload + 8 >= UINT32;
153
216
  const mdatHeader = large ? 16 : 8;
154
217
  const wide = ftyp.length + moov(options, ticks, 0, large).length + mdatHeader + payload >= UINT32;
@@ -162,7 +225,7 @@ function muxMp4(options) {
162
225
  const out = new Uint8Array(head.length + payload);
163
226
  out.set(head);
164
227
  let at = head.length;
165
- for (const s of samples) {
228
+ for (const s of all) {
166
229
  out.set(s.data, at);
167
230
  at += s.data.length;
168
231
  }
@@ -170,6 +233,16 @@ function muxMp4(options) {
170
233
  }
171
234
 
172
235
  // src/export/webmmux.ts
236
+ function opusHead(sampleRate, channels, preSkip = 312) {
237
+ const b = new Uint8Array(19);
238
+ b.set(new TextEncoder().encode("OpusHead"));
239
+ const v = new DataView(b.buffer);
240
+ v.setUint8(8, 1);
241
+ v.setUint8(9, channels);
242
+ v.setUint16(10, preSkip, true);
243
+ v.setUint32(12, sampleRate, true);
244
+ return b;
245
+ }
173
246
  var ID = {
174
247
  EBML: 440786851,
175
248
  EBMLVersion: 17030,
@@ -193,6 +266,12 @@ var ID = {
193
266
  FlagLacing: 156,
194
267
  CodecID: 134,
195
268
  DefaultDuration: 2352003,
269
+ CodecPrivate: 25506,
270
+ CodecDelay: 22186,
271
+ SeekPreRoll: 22203,
272
+ Audio: 225,
273
+ SamplingFrequency: 181,
274
+ Channels: 159,
196
275
  MaxBlockAdditionID: 21998,
197
276
  Video: 224,
198
277
  PixelWidth: 176,
@@ -255,9 +334,9 @@ var float = (id, n) => {
255
334
  return el(id, b);
256
335
  };
257
336
  var str = (id, s) => el(id, new TextEncoder().encode(s));
258
- function blockHeader(offset, flags) {
337
+ function blockHeader(offset, flags, track = 1) {
259
338
  const b = new Uint8Array(4);
260
- b[0] = 129;
339
+ b[0] = 128 | track;
261
340
  new DataView(b.buffer).setInt16(1, offset);
262
341
  b[3] = flags;
263
342
  return b;
@@ -267,6 +346,7 @@ function muxWebm(options) {
267
346
  if (!frames.length) throw new Error("A WebM needs at least one frame.");
268
347
  if (!frames[0].key) throw new Error("A WebM's first frame must be a keyframe.");
269
348
  const alpha = frames.some((f) => f.alpha);
349
+ const sound = options.audio?.packets.length ? options.audio : void 0;
270
350
  const ms = (us) => Math.round(us / 1e3);
271
351
  const last = frames[frames.length - 1];
272
352
  const frameUs = fps ? 1e6 / fps : frames.length > 1 ? (last.timestamp - frames[0].timestamp) / (frames.length - 1) : 1e6 / 30;
@@ -302,7 +382,8 @@ function muxWebm(options) {
302
382
  ...fps ? [uint(ID.DefaultDuration, Math.round(1e9 / fps))] : [],
303
383
  ...alpha ? [uint(ID.MaxBlockAdditionID, 1)] : [],
304
384
  el(ID.Video, uint(ID.PixelWidth, width), uint(ID.PixelHeight, height), ...alpha ? [uint(ID.AlphaMode, 1)] : [])
305
- )
385
+ ),
386
+ ...sound ? [soundTrack(sound)] : []
306
387
  );
307
388
  const clusters = [];
308
389
  let blocks = [];
@@ -312,11 +393,26 @@ function muxWebm(options) {
312
393
  if (blocks.length) clusters.push(el(ID.Cluster, uint(ID.Timestamp, clusterMs), ...blocks));
313
394
  blocks = [];
314
395
  };
315
- frames.forEach((frame, i) => {
316
- const at = ms(frame.timestamp);
317
- if (i === 0 || frame.key || at - clusterMs > MAX_OFFSET) {
396
+ const items = [...frames.map((frame) => ({ frame })), ...(sound?.packets ?? []).map((packet) => ({ packet }))];
397
+ const timeOf = (item) => (item.frame ?? item.packet).timestamp;
398
+ items.sort((a, b) => timeOf(a) - timeOf(b) || (a.frame ? -1 : 0) - (b.frame ? -1 : 0));
399
+ let started = false;
400
+ for (const item of items) {
401
+ const at = ms(timeOf(item));
402
+ const frame = item.frame;
403
+ if (!frame) {
404
+ if (!started || at - clusterMs > MAX_OFFSET) {
405
+ close();
406
+ clusterMs = at;
407
+ started = true;
408
+ }
409
+ blocks.push(el(ID.SimpleBlock, blockHeader(at - clusterMs, 128, 2), item.packet.data));
410
+ continue;
411
+ }
412
+ if (!started || frame.key || at - clusterMs > MAX_OFFSET) {
318
413
  close();
319
414
  clusterMs = at;
415
+ started = true;
320
416
  }
321
417
  const offset = at - clusterMs;
322
418
  if (frame.alpha) {
@@ -332,11 +428,28 @@ function muxWebm(options) {
332
428
  blocks.push(el(ID.SimpleBlock, blockHeader(offset, frame.key ? 128 : 0), frame.data));
333
429
  }
334
430
  previousMs = at;
335
- });
431
+ }
336
432
  close();
337
433
  const segment = el(ID.Segment, info, tracks, ...clusters);
338
434
  return concat([header, segment]);
339
435
  }
436
+ function soundTrack(sound) {
437
+ const head = sound.codecPrivate?.length ? sound.codecPrivate : opusHead(sound.sampleRate, sound.channels);
438
+ const preSkip = head.length >= 12 ? head[10] | head[11] << 8 : 312;
439
+ return el(
440
+ ID.TrackEntry,
441
+ uint(ID.TrackNumber, 2),
442
+ uint(ID.TrackUID, 2),
443
+ uint(ID.TrackType, 2),
444
+ // audio
445
+ uint(ID.FlagLacing, 0),
446
+ str(ID.CodecID, "A_OPUS"),
447
+ el(ID.CodecPrivate, head),
448
+ uint(ID.CodecDelay, Math.round(preSkip * 1e9 / 48e3)),
449
+ uint(ID.SeekPreRoll, 8e7),
450
+ el(ID.Audio, float(ID.SamplingFrequency, sound.sampleRate), uint(ID.Channels, sound.channels))
451
+ );
452
+ }
340
453
  function signed(n) {
341
454
  for (let len = 1; len <= 8; len++) {
342
455
  if (n >= -(2 ** (8 * len - 1)) && n < 2 ** (8 * len - 1)) {
@@ -423,6 +536,16 @@ function zipStore(entries) {
423
536
  }
424
537
 
425
538
  // src/embed/record.ts
539
+ var AUDIO_RATE = 48e3;
540
+ async function audioConfig(format) {
541
+ if (typeof AudioEncoder === "undefined" || typeof AudioData === "undefined") return void 0;
542
+ const config = { codec: format === "mp4" ? "mp4a.40.2" : "opus", sampleRate: AUDIO_RATE, numberOfChannels: 2, bitrate: 128e3 };
543
+ try {
544
+ return (await AudioEncoder.isConfigSupported(config)).supported ? config : void 0;
545
+ } catch {
546
+ return void 0;
547
+ }
548
+ }
426
549
  var DEFAULT_SECONDS = 4;
427
550
  var yieldToBrowser = () => new Promise((resolve) => setTimeout(resolve, 0));
428
551
  var formats = (list) => list.map((f) => `"${f}"`).join(", ").replace(/, ([^,]*)$/, " or $1");
@@ -538,7 +661,24 @@ async function recordingPlan(effect, options = {}) {
538
661
  width = w;
539
662
  height = h;
540
663
  }
541
- return { effect: def, format, width, height, fps, duration, start, frames: Math.max(1, Math.round(duration * fps)), alpha, ...encoder ? { encoder } : {} };
664
+ let audioEncoder;
665
+ if (options.audio !== void 0 && options.audioTrack !== false && (format === "mp4" || format === "webm")) {
666
+ audioEncoder = await audioConfig(format);
667
+ if (!audioEncoder && format === "mp4" && !options.format) {
668
+ const webm = await videoConfig("webm", width, height, fps, encoder.bitrate ?? Math.round(width * height * fps * 0.15));
669
+ const opus = webm && await audioConfig("webm");
670
+ if (webm && opus) {
671
+ format = "webm";
672
+ encoder = webm;
673
+ audioEncoder = opus;
674
+ }
675
+ }
676
+ if (!audioEncoder) {
677
+ const codec = format === "mp4" ? "AAC" : "Opus";
678
+ throw new Error(`This browser cannot encode ${codec} audio, so it cannot put the sound in ${format === "mp4" ? "an mp4" : "a webm"}; ${format === "mp4" ? 'record "webm" (Opus), or ' : ""}pass audioTrack: false to record the picture alone (the sound still drives it).`);
679
+ }
680
+ }
681
+ return { effect: def, format, width, height, fps, duration, start, frames: Math.max(1, Math.round(duration * fps)), alpha, ...encoder ? { encoder } : {}, ...audioEncoder ? { audioEncoder } : {} };
542
682
  }
543
683
  function defaultDuration(def, options) {
544
684
  if (def.kind === "shader") return def.loop ?? DEFAULT_SECONDS;
@@ -551,7 +691,7 @@ function effectValues(def, options) {
551
691
  const s = subjectValues(def, options.subject);
552
692
  return { values: { ...values, ...s.values }, live: s.live };
553
693
  }
554
- function shaderSource(plan, values, live, options) {
694
+ function shaderSource(plan, values, live, options, sound) {
555
695
  const def = plan.effect;
556
696
  const frames = new ShaderFrames({ source: def.source, values, width: plan.width, height: plan.height, alpha: plan.alpha || Boolean(def.transparent) });
557
697
  const videos = [];
@@ -569,7 +709,7 @@ function shaderSource(plan, values, live, options) {
569
709
  await options.beforeFrame?.(time, index);
570
710
  await frames.ready();
571
711
  const pointer = typeof script === "function" ? script(time) : script;
572
- frames.render(time, { frame: index, delta: 1 / plan.fps, ...pointer });
712
+ frames.render(time, { frame: index, delta: 1 / plan.fps, ...pointer, ...sound ? { audio: sound.at(time) } : {} });
573
713
  frames.readPixels(out);
574
714
  },
575
715
  dispose() {
@@ -682,7 +822,64 @@ function collector(config, fail) {
682
822
  out.encoder.configure(config);
683
823
  return out;
684
824
  }
685
- function videoEncoder(plan) {
825
+ async function decodeAudio(url) {
826
+ if (typeof OfflineAudioContext === "undefined") throw new Error("This browser has no Web Audio (OfflineAudioContext), so it cannot decode the audio.");
827
+ let bytes;
828
+ try {
829
+ const response = await fetch(url);
830
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
831
+ bytes = await response.arrayBuffer();
832
+ } catch (e) {
833
+ throw new Error(`The audio at ${url} could not be loaded (${e.message}); a file from another site needs CORS.`, { cause: e });
834
+ }
835
+ try {
836
+ return await new OfflineAudioContext(2, 1, AUDIO_RATE).decodeAudioData(bytes);
837
+ } catch (e) {
838
+ throw new Error(`The audio at ${url} could not be decoded; give it an mp3, AAC, Ogg, Opus, FLAC or WAV file the browser plays.`, { cause: e });
839
+ }
840
+ }
841
+ async function encodeSound(buffer, plan) {
842
+ const config = plan.audioEncoder;
843
+ const channels = config.numberOfChannels;
844
+ const length = Math.max(1, Math.round(plan.duration * AUDIO_RATE));
845
+ const offline = new OfflineAudioContext(channels, length, AUDIO_RATE);
846
+ const node = offline.createBufferSource();
847
+ node.buffer = buffer;
848
+ node.connect(offline.destination);
849
+ node.start(0, Math.min(plan.start, buffer.duration));
850
+ const rendered = await offline.startRendering();
851
+ const out = { codec: config.codec === "opus" ? "opus" : "aac", sampleRate: AUDIO_RATE, channels, chunks: [] };
852
+ let failure;
853
+ const encoder = new AudioEncoder({
854
+ output(chunk, metadata) {
855
+ const data = new Uint8Array(chunk.byteLength);
856
+ chunk.copyTo(data);
857
+ const desc = metadata?.decoderConfig?.description;
858
+ if (desc && !out.description) out.description = bytesOf(desc);
859
+ out.chunks.push({ data, timestamp: chunk.timestamp, ...chunk.duration ? { duration: chunk.duration } : {}, key: true });
860
+ },
861
+ error: (e) => failure ??= new Error(`The audio encoder failed: ${e.message}`, { cause: e })
862
+ });
863
+ encoder.configure(config);
864
+ const block = 4800;
865
+ for (let at = 0; at < length; at += block) {
866
+ const n = Math.min(block, length - at);
867
+ const planes = new Float32Array(n * channels);
868
+ for (let c = 0; c < channels; c++) planes.set(rendered.getChannelData(c).subarray(at, at + n), c * n);
869
+ const data = new AudioData({ format: "f32-planar", sampleRate: AUDIO_RATE, numberOfFrames: n, numberOfChannels: channels, timestamp: Math.round(at * 1e6 / AUDIO_RATE), data: planes });
870
+ try {
871
+ encoder.encode(data);
872
+ } finally {
873
+ data.close();
874
+ }
875
+ }
876
+ await encoder.flush();
877
+ encoder.close();
878
+ if (failure) throw failure;
879
+ out.chunks.sort((a, b) => a.timestamp - b.timestamp);
880
+ return out;
881
+ }
882
+ function videoEncoder(plan, sound) {
686
883
  const config = plan.encoder;
687
884
  let failure;
688
885
  const fail = (e) => failure ??= e;
@@ -721,9 +918,16 @@ function videoEncoder(plan) {
721
918
  for (const e of encoders) e.close();
722
919
  const byTime = (a, b) => a.timestamp - b.timestamp;
723
920
  const chunks = color.chunks.sort(byTime);
921
+ const audio = await sound;
724
922
  if (plan.format === "mp4") {
725
923
  if (!color.description) throw new Error("The H.264 encoder gave no decoder configuration (avcC), so the mp4 cannot be written.");
726
- const file2 = muxMp4({ width, height, avcC: color.description, samples: chunks });
924
+ const file2 = muxMp4({
925
+ width,
926
+ height,
927
+ avcC: color.description,
928
+ samples: chunks,
929
+ ...audio ? { audio: { sampleRate: audio.sampleRate, channels: audio.channels, ...audio.description ? { config: audio.description } : {}, samples: audio.chunks } } : {}
930
+ });
727
931
  return new Blob([file2], { type: "video/mp4" });
728
932
  }
729
933
  const alphas = new Map(alpha?.chunks.map((c) => [c.timestamp, c.data]));
@@ -733,7 +937,15 @@ function videoEncoder(plan) {
733
937
  return { ...c, ...a ? { alpha: a } : {} };
734
938
  });
735
939
  const codec = config.codec.startsWith("vp8") ? "vp8" : "vp9";
736
- const file = muxWebm({ codec, width, height, frames, duration: plan.frames * frameUs, fps: plan.fps });
940
+ const file = muxWebm({
941
+ codec,
942
+ width,
943
+ height,
944
+ frames,
945
+ duration: plan.frames * frameUs,
946
+ fps: plan.fps,
947
+ ...audio ? { audio: { sampleRate: audio.sampleRate, channels: audio.channels, ...audio.description ? { codecPrivate: audio.description } : {}, packets: audio.chunks } } : {}
948
+ });
737
949
  return new Blob([file], { type: "video/webm" });
738
950
  },
739
951
  close() {
@@ -746,10 +958,14 @@ async function recordEffect(effect, options = {}) {
746
958
  const { signal, onProgress } = options;
747
959
  const { values, live } = effectValues(plan.effect, options);
748
960
  const background = (parseColor(options.background ?? (plan.effect.kind === "shader" ? "#000000" : "#ffffff")) ?? [0, 0, 0]).map((c) => Math.round(c * 255));
749
- const source = plan.effect.kind === "shader" ? shaderSource(plan, values, live, options) : vectorSource(plan, values, options);
961
+ const buffer = options.audio === void 0 ? void 0 : typeof options.audio === "string" ? await decodeAudio(options.audio) : options.audio;
962
+ const sound = buffer && plan.effect.kind === "shader" && effectInputs(plan.effect).accepts.includes("audio") ? analyseAudioBuffer(buffer) : void 0;
963
+ const track = buffer && plan.audioEncoder ? encodeSound(buffer, plan) : void 0;
964
+ track?.catch(() => void 0);
965
+ const source = plan.effect.kind === "shader" ? shaderSource(plan, values, live, options, sound) : vectorSource(plan, values, options);
750
966
  let encoder;
751
967
  try {
752
- encoder = plan.format === "gif" ? await gifEncoder(plan) : plan.format === "png" ? pngEncoder(plan) : videoEncoder(plan);
968
+ encoder = plan.format === "gif" ? await gifEncoder(plan) : plan.format === "png" ? pngEncoder(plan) : videoEncoder(plan, track);
753
969
  const rgba = new Uint8ClampedArray(plan.width * plan.height * 4);
754
970
  for (let i = 0; i < plan.frames; i++) {
755
971
  if (signal?.aborted) throw new DOMException("Cancelled", "AbortError");
@@ -792,6 +1008,12 @@ function subjectValues(effect, subject, slot) {
792
1008
  for (const s of Array.isArray(subject) ? subject : [subject]) {
793
1009
  const kind = kindOf(s);
794
1010
  const free = (slots) => slots.filter((x) => !used.has(x.name));
1011
+ if (kind === "audio") {
1012
+ if (!inputs.accepts.includes("audio"))
1013
+ throw new Error(`"${effect.id}" does not react to sound; findEffects({ accepts: ["audio"] }) lists the effects that do.`);
1014
+ out.audio = s.audio;
1015
+ continue;
1016
+ }
795
1017
  if (kind === "path" || kind === "text" && effect.kind === "vector") {
796
1018
  const holds = kind === "path" ? "path" : "copy";
797
1019
  const text = pick(free(inputs.texts), (t) => t.holds === holds, slot, effect, kind === "path" ? "path data" : "text");
@@ -819,11 +1041,13 @@ function mountEffect(host, effect, options = {}) {
819
1041
  if (!def) throw new Error(`There is no effect "${String(effect)}" in the catalog; findEffects() lists them.`);
820
1042
  let values = { ...effectDefaults(def, { ...options.preset ? { preset: options.preset } : {}, ...options.values ? { values: options.values } : {} }) };
821
1043
  let live = {};
1044
+ let audio;
822
1045
  const apply = (subject) => {
823
1046
  if (!subject) return;
824
1047
  const s = subjectValues(def, subject);
825
1048
  values = { ...values, ...s.values };
826
1049
  live = s.live;
1050
+ audio = s.audio;
827
1051
  };
828
1052
  apply(options.subject);
829
1053
  if (def.kind === "shader") {
@@ -840,15 +1064,18 @@ function mountEffect(host, effect, options = {}) {
840
1064
  for (const [name, source] of Object.entries(live)) renderer.setLiveSource(name, source);
841
1065
  };
842
1066
  setLive({});
1067
+ if (audio) renderer.setAudioSource(audio);
843
1068
  return {
844
1069
  effect: def,
845
1070
  element: renderer.canvas,
846
1071
  renderer,
847
1072
  setSubject(subject) {
848
1073
  const before = live;
1074
+ const heard = audio;
849
1075
  apply(subject);
850
1076
  renderer.setValues(values);
851
1077
  setLive(before);
1078
+ if (audio !== heard) renderer.setAudioSource(audio ?? null);
852
1079
  },
853
1080
  setValues(next) {
854
1081
  values = { ...values, ...next };
@@ -899,4 +1126,4 @@ export {
899
1126
  subjectValues,
900
1127
  mountEffect
901
1128
  };
902
- //# sourceMappingURL=chunk-VLQLRKGW.js.map
1129
+ //# sourceMappingURL=chunk-KZYZX3V5.js.map