@pexip/media-processor 16.7.1 → 17.1.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 (69) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +1 -7
  3. package/dist/main/audio.d.ts +123 -0
  4. package/dist/main/audio.js +653 -0
  5. package/dist/main/benchUtils.d.ts +12 -0
  6. package/dist/main/benchUtils.js +34 -0
  7. package/dist/main/generator.d.ts +4 -0
  8. package/dist/main/generator.js +4 -0
  9. package/dist/main/index.d.ts +17 -0
  10. package/dist/main/index.js +17 -0
  11. package/dist/main/math.d.ts +41 -0
  12. package/dist/main/math.js +57 -0
  13. package/dist/main/path.d.ts +102 -0
  14. package/dist/main/path.js +103 -0
  15. package/dist/main/process.d.ts +239 -0
  16. package/dist/main/process.js +364 -0
  17. package/dist/main/processor.d.ts +5 -0
  18. package/dist/main/processor.js +4 -0
  19. package/dist/main/transformer.d.ts +1 -0
  20. package/dist/main/transformer.js +4 -0
  21. package/dist/main/tsconfig.tsbuildinfo +1 -0
  22. package/dist/main/typeGuards.d.ts +5 -0
  23. package/dist/main/typeGuards.js +24 -0
  24. package/dist/main/types.d.ts +342 -0
  25. package/dist/main/types.js +1 -0
  26. package/dist/main/utils.d.ts +173 -0
  27. package/dist/main/utils.js +364 -0
  28. package/dist/main/video/canvasRenderUtils.d.ts +22 -0
  29. package/dist/main/video/canvasRenderUtils.js +198 -0
  30. package/dist/main/video/canvasTransform.d.ts +8 -0
  31. package/dist/main/video/canvasTransform.js +173 -0
  32. package/dist/main/video/constants.d.ts +10 -0
  33. package/dist/main/video/constants.js +12 -0
  34. package/dist/main/video/index.d.ts +9 -0
  35. package/dist/main/video/index.js +9 -0
  36. package/dist/main/video/load.d.ts +17 -0
  37. package/dist/main/video/load.js +47 -0
  38. package/dist/main/video/segmenters/index.d.ts +1 -0
  39. package/dist/main/video/segmenters/index.js +1 -0
  40. package/dist/main/video/segmenters/mediapipe.d.ts +13 -0
  41. package/dist/main/video/segmenters/mediapipe.js +85 -0
  42. package/dist/main/video/transformer.d.ts +10 -0
  43. package/dist/main/video/transformer.js +56 -0
  44. package/dist/main/video/typeGuards.d.ts +2 -0
  45. package/dist/main/video/typeGuards.js +13 -0
  46. package/dist/main/video/types.d.ts +98 -0
  47. package/dist/main/video/types.js +20 -0
  48. package/dist/main/video/utils.d.ts +102 -0
  49. package/dist/main/video/utils.js +476 -0
  50. package/dist/main/video/video.d.ts +19 -0
  51. package/dist/main/video/video.js +48 -0
  52. package/dist/main/video/videoStreamTrackProcessor.d.ts +14 -0
  53. package/dist/main/video/videoStreamTrackProcessor.js +82 -0
  54. package/dist/main/visual.d.ts +80 -0
  55. package/dist/main/visual.js +135 -0
  56. package/dist/main/workletNodes.d.ts +2 -0
  57. package/dist/main/workletNodes.js +3 -0
  58. package/dist/workers/index.d.ts +0 -0
  59. package/dist/workers/index.js +1 -0
  60. package/dist/workers/tsconfig.tsbuildinfo +1 -0
  61. package/dist/worklets/denoise.worklet.d.ts +1 -0
  62. package/dist/worklets/denoise.worklet.js +1 -2
  63. package/dist/worklets/tsconfig.tsbuildinfo +1 -0
  64. package/dist/worklets/types.d.ts +52 -0
  65. package/dist/worklets/types.js +0 -0
  66. package/package.json +11 -9
  67. package/dist/index.d.ts +0 -1129
  68. package/dist/index.mjs +0 -2441
  69. package/dist/worklets/denoise.worklet.js.map +0 -7
@@ -0,0 +1,653 @@
1
+ import { copyByteBufferToFloatBuffer, processAverageVolume } from './process';
2
+ import { createDenoiseWorkletNode } from './workletNodes';
3
+ import { createAnalyserNode, createChannelSplitterNode, createGainNode, createMediaStreamAudioDestinationNode, createMediaStreamAudioSourceNode, createMediaStreamAudioClone, createDelayNode, muteToGain, stopStreamTracks, createMediaElementSourceNode, createChannelMergerNode, } from './utils';
4
+ import { isAudioParam, isAudioNodeInit } from './typeGuards';
5
+ /**
6
+ * A function to create `AudioContext` using constructor or factory function
7
+ * depends on the browser supports
8
+ *
9
+ * @param options - @see {@link AudioContextOptions}
10
+ *
11
+ * @internal
12
+ */
13
+ export const createAudioContext = (options) => {
14
+ try {
15
+ return new AudioContext(options);
16
+ }
17
+ catch {
18
+ return new window.webkitAudioContext(options);
19
+ }
20
+ };
21
+ /**
22
+ * Resume the stream whenever interrupted
23
+ *
24
+ * @param audioContext - AudioContext
25
+ *
26
+ * @alpha
27
+ */
28
+ export function resumeAudioOnInterruption(audioContext) {
29
+ const resumeInterrupted = () => {
30
+ // Resume the stream whenever Safari has interrupted it
31
+ if (audioContext.state === 'interrupted') {
32
+ void audioContext.resume();
33
+ }
34
+ };
35
+ audioContext.addEventListener('statechange', resumeInterrupted);
36
+ return () => {
37
+ audioContext.removeEventListener('statechange', resumeInterrupted);
38
+ };
39
+ }
40
+ /**
41
+ * Resume the AudioContext whenever the source track is unmuted
42
+ *
43
+ * @param audioContext - The `AudioContext` to resume
44
+ *
45
+ * @alpha
46
+ */
47
+ export const resumeAudioOnUnmute = (context) =>
48
+ /**
49
+ * @param track - The source track to listen on the `unmute` event
50
+ */
51
+ (track) => {
52
+ const resume = () => {
53
+ if (context.state === 'suspended') {
54
+ void context.resume();
55
+ }
56
+ };
57
+ track.addEventListener('unmute', resume);
58
+ return () => {
59
+ track.removeEventListener('unmute', resume);
60
+ };
61
+ };
62
+ /**
63
+ * Wrap GainNode with mute function
64
+ *
65
+ * @param context - @see {@link AudioContext}
66
+ * @param muteInit - initial mute state of the node
67
+ *
68
+ * @internal
69
+ */
70
+ const createGainWithMute = (context, muteInit = false) => {
71
+ let mute = muteInit;
72
+ const gain = createGainNode(context, {
73
+ gain: muteToGain(mute),
74
+ channelCountMode: 'explicit',
75
+ });
76
+ const disconnect = gain.disconnect.bind(gain);
77
+ const connect = gain.connect.bind(gain);
78
+ return {
79
+ get node() {
80
+ return gain;
81
+ },
82
+ get mute() {
83
+ return mute;
84
+ },
85
+ set mute(shouldMute) {
86
+ mute = shouldMute;
87
+ const time = context.currentTime;
88
+ if (isFinite(time)) {
89
+ gain.gain.setValueAtTime(muteToGain(shouldMute), time);
90
+ }
91
+ else {
92
+ throw new Error('Set gain with non-finite time value');
93
+ }
94
+ },
95
+ connect,
96
+ disconnect,
97
+ };
98
+ };
99
+ /**
100
+ * Wrap AnalyserNode with polyfill and able to get average volume
101
+ *
102
+ * @param context - @see {@link AudioContext}
103
+ * @param options - @see {@link AnalyserOptions}
104
+ *
105
+ * @internal
106
+ */
107
+ const createAnalyzer = (context, options) => {
108
+ const analyser = createAnalyserNode(context, options);
109
+ const getByteFrequencyData = analyser.getByteFrequencyData.bind(analyser);
110
+ const getByteTimeDomainData = analyser.getByteTimeDomainData.bind(analyser);
111
+ const getFloatFrequencyData = analyser.getFloatFrequencyData.bind(analyser);
112
+ const connect = analyser.connect.bind(analyser);
113
+ const disconnect = analyser.disconnect.bind(analyser);
114
+ let byteBuffer;
115
+ /**
116
+ * Safari does not support getFloatTimeDomainData API, thus polyfill is
117
+ * needed
118
+ *
119
+ * According to https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/getFloatTimeDomainData
120
+ */
121
+ const getFloatTimeDomainData = (buffer) => {
122
+ if ('getFloatTimeDomainData' in AnalyserNode.prototype) {
123
+ return analyser.getFloatTimeDomainData(buffer);
124
+ }
125
+ // Use getByteTimeDomainData and convert the result to the buffer
126
+ if (!byteBuffer) {
127
+ byteBuffer = new Uint8Array(buffer.length);
128
+ }
129
+ analyser.getByteTimeDomainData(byteBuffer);
130
+ return copyByteBufferToFloatBuffer(byteBuffer, buffer);
131
+ };
132
+ const getAverageVolume = (buffer) => {
133
+ getFloatTimeDomainData(buffer);
134
+ return processAverageVolume(Array.from(buffer));
135
+ };
136
+ return {
137
+ get node() {
138
+ return analyser;
139
+ },
140
+ get frequencyBinCount() {
141
+ return analyser.frequencyBinCount;
142
+ },
143
+ get fftSize() {
144
+ return analyser.fftSize;
145
+ },
146
+ set fftSize(size) {
147
+ analyser.fftSize = size;
148
+ },
149
+ get minDecibels() {
150
+ return analyser.minDecibels;
151
+ },
152
+ set minDecibels(decibels) {
153
+ analyser.minDecibels = decibels;
154
+ },
155
+ get maxDecibels() {
156
+ return analyser.maxDecibels;
157
+ },
158
+ set maxDecibels(decibels) {
159
+ analyser.maxDecibels = decibels;
160
+ },
161
+ get smoothingTimeConstant() {
162
+ return analyser.smoothingTimeConstant;
163
+ },
164
+ set smoothingTimeConstant(constant) {
165
+ analyser.smoothingTimeConstant = constant;
166
+ },
167
+ getByteTimeDomainData,
168
+ getByteFrequencyData,
169
+ getFloatFrequencyData,
170
+ getFloatTimeDomainData,
171
+ getAverageVolume,
172
+ connect,
173
+ disconnect,
174
+ };
175
+ };
176
+ /**
177
+ * Subscribe MessagePort message from an AudioWorkletNode
178
+ *
179
+ * @param workletNode - the node to subscribe
180
+ * @param options - can pass a message handler here to handle the message
181
+ */
182
+ export const subscribeWorkletNode = (workletNode, { messageHandler, errorHandler } = {}) => {
183
+ const subscribeMessage = (event) => {
184
+ messageHandler?.(event.data);
185
+ };
186
+ const subscribeToPortError = (event) => {
187
+ if (errorHandler) {
188
+ errorHandler(event);
189
+ }
190
+ };
191
+ const subscribeToProcessorError = (event) => {
192
+ if (errorHandler) {
193
+ errorHandler(event);
194
+ }
195
+ };
196
+ workletNode.port.addEventListener('message', subscribeMessage);
197
+ workletNode.port.start();
198
+ workletNode.addEventListener('processorerror', subscribeToProcessorError);
199
+ workletNode.port.addEventListener('messageerror', subscribeToPortError);
200
+ return () => {
201
+ workletNode.port.postMessage({ type: 'release' });
202
+ workletNode.port.removeEventListener('message', subscribeMessage);
203
+ workletNode.removeEventListener('processorerror', subscribeToProcessorError);
204
+ workletNode.port.removeEventListener('messageerror', subscribeToPortError);
205
+ };
206
+ };
207
+ /**
208
+ * Offset factor to compensate real-time audio processing since `setTimeout` is
209
+ * not guaranteed to execute at provided delay and we need this adjustment
210
+ * factor to compensate the delay to get a result similar what we did with
211
+ * `AudioWorklet`.
212
+ */
213
+ const TIMEOUT_DELAY_ADJUSTMENT = 0.5;
214
+ /**
215
+ * Subscribe to a timeout loop to get the data from Analyzer
216
+ *
217
+ * @param analyzer - the analyzer to subscribe
218
+ * @param options - message handler, etc.
219
+ */
220
+ export const subscribeTimeoutAnalyzerNode = (analyzer, { messageHandler, updateFrequency = 2.0 }) => {
221
+ // A timeout-based approach
222
+ const timeoutMs = updateFrequency * 1000 * TIMEOUT_DELAY_ADJUSTMENT;
223
+ let timeoutId = 0;
224
+ let stopTimeout = false;
225
+ const clearTimeout = () => {
226
+ if (timeoutId) {
227
+ window.clearTimeout(timeoutId);
228
+ timeoutId = 0;
229
+ }
230
+ };
231
+ const process = () => {
232
+ messageHandler(analyzer);
233
+ clearTimeout();
234
+ if (!stopTimeout) {
235
+ timeoutId = window.setTimeout(process, timeoutMs);
236
+ }
237
+ };
238
+ process();
239
+ return () => {
240
+ stopTimeout = true;
241
+ clearTimeout();
242
+ };
243
+ };
244
+ function createBaseAudioNode(name, create, release) {
245
+ const props = {
246
+ name,
247
+ node: undefined,
248
+ audioNode: undefined,
249
+ outputs: new WeakSet(),
250
+ };
251
+ const createNode = (context, prevNode) => {
252
+ const [audioNode, node] = create(context, prevNode);
253
+ props.audioNode = audioNode;
254
+ props.node = node;
255
+ return [audioNode, node];
256
+ };
257
+ const releaseNode = () => {
258
+ release?.();
259
+ props.outputs = new WeakSet();
260
+ props.audioNode?.disconnect();
261
+ props.audioNode = undefined;
262
+ };
263
+ const connectNode = (param) => {
264
+ if (!param) {
265
+ return;
266
+ }
267
+ if (!props.audioNode) {
268
+ throw new Error('Source AudioNode is not initialized');
269
+ }
270
+ const [destination, output, input] = Array.isArray(param)
271
+ ? param
272
+ : [param];
273
+ if (isAudioParam(destination)) {
274
+ props.outputs.add(destination);
275
+ return props.audioNode.connect(destination, output);
276
+ }
277
+ if (isAudioNodeInit(destination)) {
278
+ if (!destination.audioNode) {
279
+ throw new Error('Destination AudioNode is not initialized');
280
+ }
281
+ props.outputs.add(destination);
282
+ return props.audioNode.connect(destination.audioNode, output, input);
283
+ }
284
+ };
285
+ const disconnectNode = (destInit) => {
286
+ if (!destInit) {
287
+ return;
288
+ }
289
+ if (!props.audioNode) {
290
+ throw new Error('AudioNode is not initialized');
291
+ }
292
+ const [destination, output, input] = Array.isArray(destInit)
293
+ ? destInit
294
+ : [destInit];
295
+ if (destination && !props.outputs.has(destination)) {
296
+ return; // There is no connection between the nodes
297
+ }
298
+ if (isAudioParam(destination)) {
299
+ props.outputs.delete(destination);
300
+ if (output !== undefined) {
301
+ return props.audioNode.disconnect(destination, output);
302
+ }
303
+ return props.audioNode.disconnect(destination);
304
+ }
305
+ if (isAudioNodeInit(destination)) {
306
+ if (!destination.audioNode) {
307
+ throw new Error('Destination AudioNode is not initialized');
308
+ }
309
+ props.outputs.delete(destination);
310
+ if (output !== undefined) {
311
+ if (input !== undefined) {
312
+ return props.audioNode.disconnect(destination.audioNode, output, input);
313
+ }
314
+ return props.audioNode.disconnect(destination.audioNode, output);
315
+ }
316
+ return props.audioNode.disconnect(destination.audioNode);
317
+ }
318
+ props.outputs = new WeakSet();
319
+ props.audioNode.disconnect();
320
+ };
321
+ const hasConnectedTo = init => {
322
+ return props.outputs.has(init);
323
+ };
324
+ return Object.assign(props, {
325
+ create: createNode,
326
+ connect: connectNode,
327
+ disconnect: disconnectNode,
328
+ release: releaseNode,
329
+ hasConnectedTo,
330
+ toJSON: () => ({
331
+ name: props.name,
332
+ node: props.node,
333
+ audioNode: props.audioNode,
334
+ }),
335
+ });
336
+ }
337
+ /**
338
+ * Create a MediaStreamAudioSourceNodeInit
339
+ *
340
+ * @param mediaStream - Source MediaStream
341
+ * @param shouldResetEnabled - Whether or not to enable the cloned track
342
+ */
343
+ export const createStreamSourceGraphNode = (mediaStream, shouldResetEnabled = true) => {
344
+ let stream = undefined;
345
+ let unsubscribeUnmutes = undefined;
346
+ return createBaseAudioNode('source', context => {
347
+ stream = createMediaStreamAudioClone(mediaStream);
348
+ // Enabled the track for the cloned stream
349
+ if (shouldResetEnabled) {
350
+ stream
351
+ .getAudioTracks()
352
+ .forEach(track => (track.enabled = true));
353
+ }
354
+ unsubscribeUnmutes = mediaStream
355
+ .getAudioTracks()
356
+ .map(resumeAudioOnUnmute(context));
357
+ const node = createMediaStreamAudioSourceNode(context, {
358
+ mediaStream: stream,
359
+ });
360
+ return [node, node];
361
+ }, () => {
362
+ stopStreamTracks(stream);
363
+ unsubscribeUnmutes?.forEach(unsubscribe => unsubscribe());
364
+ stream = undefined;
365
+ });
366
+ };
367
+ /**
368
+ * Create a MediaStreamAudioSourceNodeInit
369
+ *
370
+ * @param mediaStream - Source MediaStream
371
+ */
372
+ export const createMediaElementSourceGraphNode = (mediaElement) => {
373
+ return createBaseAudioNode('source', context => {
374
+ const node = createMediaElementSourceNode(context, {
375
+ mediaElement,
376
+ });
377
+ return [node, node];
378
+ });
379
+ };
380
+ /**
381
+ * Create an analyzer node with push-based subscription
382
+ */
383
+ export const createAnalyzerSubscribableGraphNode = ({ messageHandler, updateFrequency = 2.0, ...analyserOptions }) => {
384
+ let unsubscribe;
385
+ return createBaseAudioNode('analyzer', context => {
386
+ const node = createAnalyzer(context, analyserOptions);
387
+ // Subscribe to the timeout callback
388
+ unsubscribe = subscribeTimeoutAnalyzerNode(node, {
389
+ messageHandler,
390
+ updateFrequency,
391
+ });
392
+ return [node.node, node];
393
+ }, () => {
394
+ unsubscribe?.();
395
+ unsubscribe = undefined;
396
+ });
397
+ };
398
+ /**
399
+ * Create a noise suppression node
400
+ *
401
+ * @param data - WebAssembly source
402
+ */
403
+ export const createDenoiseWorkletGraphNode = (data, messageHandler) => {
404
+ let unsubscribe;
405
+ return createBaseAudioNode('denoise', (context, prevNode) => {
406
+ const sampleRate = context.sampleRate;
407
+ const channelCount = prevNode?.channelCount ?? 1;
408
+ const node = createDenoiseWorkletNode(context, {
409
+ outputChannelCount: [channelCount],
410
+ processorOptions: {
411
+ data,
412
+ sampleRate,
413
+ shouldSendVAD: !!messageHandler,
414
+ },
415
+ });
416
+ unsubscribe = subscribeWorkletNode(node, { messageHandler });
417
+ return [node, node];
418
+ }, () => {
419
+ unsubscribe?.();
420
+ unsubscribe = undefined;
421
+ });
422
+ };
423
+ /**
424
+ * Create a GainNodeInit
425
+ *
426
+ * @param mute - initial mute state
427
+ */
428
+ export const createGainGraphNode = (mute) => {
429
+ return createBaseAudioNode('gain', context => {
430
+ const node = createGainWithMute(context, mute);
431
+ return [node.node, node];
432
+ });
433
+ };
434
+ /**
435
+ * Create an AnalyzerNodeInit
436
+ *
437
+ * @param options - @see {@link AnalyserOptions}
438
+ */
439
+ export const createAnalyzerGraphNode = (options) => {
440
+ return createBaseAudioNode('analyzer', context => {
441
+ const node = createAnalyzer(context, options);
442
+ return [node.node, node];
443
+ });
444
+ };
445
+ /**
446
+ * Create a MediaStreamAudioDestinationNodeInit
447
+ */
448
+ export const createStreamDestinationGraphNode = (options) => {
449
+ let node = undefined;
450
+ return createBaseAudioNode('destination', context => {
451
+ node = createMediaStreamAudioDestinationNode(context, options);
452
+ return [node, node];
453
+ }, () => {
454
+ stopStreamTracks(node?.stream);
455
+ node = undefined;
456
+ });
457
+ };
458
+ /**
459
+ * Create an `AudioDestinationNode`
460
+ */
461
+ export const createAudioDestinationGraphNode = () => {
462
+ return createBaseAudioNode('destination', context => {
463
+ const node = context.destination;
464
+ return [node, node];
465
+ });
466
+ };
467
+ /**
468
+ * Create a `DelayNode`
469
+ *
470
+ * @param options - @see DelayOptions
471
+ */
472
+ export const createDelayGraphNode = (options) => {
473
+ return createBaseAudioNode('delay', context => {
474
+ const node = createDelayNode(context, options);
475
+ return [node, node];
476
+ });
477
+ };
478
+ /**
479
+ * Create a ChannelSplitterNode
480
+ *
481
+ * @param options - @see ChannelSplitterOptions
482
+ */
483
+ export const createChannelSplitterGraphNode = (options) => {
484
+ return createBaseAudioNode('splitter', context => {
485
+ const node = createChannelSplitterNode(context, options);
486
+ return [node, node];
487
+ });
488
+ };
489
+ /**
490
+ * Create a ChannelMergerNode
491
+ *
492
+ * @param options - @see ChannelMergerOptions
493
+ */
494
+ export const createChannelMergerGraphNode = (options) => {
495
+ return createBaseAudioNode('merger', context => {
496
+ const node = createChannelMergerNode(context, options);
497
+ return [node, node];
498
+ });
499
+ };
500
+ const createNodeInitDisconnector = () => (srcInit, destInit) => {
501
+ if (!srcInit) {
502
+ return;
503
+ }
504
+ const [source, outputIdx, inputIdx] = Array.isArray(srcInit)
505
+ ? srcInit
506
+ : [srcInit];
507
+ if (!source) {
508
+ return;
509
+ }
510
+ if (isAudioParam(source)) {
511
+ throw new Error('AudioParam cannot be used as the source of disconnect');
512
+ }
513
+ const [destination] = Array.isArray(destInit) ? destInit : [destInit];
514
+ return source.disconnect([destination, outputIdx, inputIdx]);
515
+ };
516
+ const createNodeInitConnector = (context) => (srcInit, destInit) => {
517
+ if (!srcInit || !destInit) {
518
+ return;
519
+ }
520
+ const [source, outputIdx, inputIdx] = Array.isArray(srcInit)
521
+ ? srcInit
522
+ : [srcInit];
523
+ const [destination] = Array.isArray(destInit) ? destInit : [destInit];
524
+ if (!source || !destination) {
525
+ return;
526
+ }
527
+ if (isAudioParam(source)) {
528
+ throw new Error('AudioParam cannot be used as the source of connect');
529
+ }
530
+ const [sourceNode] = source.audioNode
531
+ ? [source.audioNode]
532
+ : source.create(context);
533
+ if (isAudioNodeInit(destination) && !destination.audioNode) {
534
+ destination.create(context, sourceNode);
535
+ }
536
+ return source.connect([destination, outputIdx, inputIdx]);
537
+ };
538
+ /**
539
+ * Accepts AudioNodeInitConnections to build the audio graph within a signal audio context
540
+ *
541
+ * @param initialConnections - A list of AudioNodeInit to build the graph in a linear fashion
542
+ * @param options - @see {@link AudioGraphOptions}
543
+ *
544
+ * @example
545
+ *
546
+ * ```typescript
547
+ * const source = createStreamSourceGraphNode(stream);
548
+ * const analyzer = createAnalyzerGraphNode({fftSize});
549
+ * const audioGraph = createAudioGraph([[source, analyzer]]);
550
+ * ```
551
+ */
552
+ export const createAudioGraph = (initialConnections, options = {}) => {
553
+ const props = {
554
+ closing: false,
555
+ inits: new Set(),
556
+ };
557
+ const audioContext = options.context ?? createAudioContext(options.contextOptions);
558
+ const unsubscribeStateChanged = resumeAudioOnInterruption(audioContext);
559
+ const connectInit = createNodeInitConnector(audioContext);
560
+ const disconnectInit = createNodeInitDisconnector();
561
+ const connect = sequence => {
562
+ sequence.forEach((initParam, idx, initParams) => {
563
+ const prevParam = initParams[idx - 1];
564
+ const [currentInit] = Array.isArray(initParam)
565
+ ? initParam
566
+ : [initParam];
567
+ connectInit(prevParam, currentInit);
568
+ if (isAudioNodeInit(currentInit)) {
569
+ // Keep a reference
570
+ props.inits.add(currentInit);
571
+ }
572
+ });
573
+ };
574
+ const disconnect = sequence => {
575
+ sequence.forEach((initParam, idx, initParams) => {
576
+ const prevParam = initParams[idx - 1];
577
+ const [currentInit] = Array.isArray(initParam)
578
+ ? initParam
579
+ : [initParam];
580
+ disconnectInit(prevParam, currentInit);
581
+ });
582
+ };
583
+ const addWorklet = async (moduleURL, options) => {
584
+ if (!props.workletModule) {
585
+ await audioContext.audioWorklet.addModule(moduleURL, options);
586
+ props.workletModule = { moduleURL, options };
587
+ }
588
+ };
589
+ // Initial connections setup
590
+ initialConnections.forEach(connect);
591
+ const releaseInit = init => {
592
+ if (props.inits.has(init)) {
593
+ init.release();
594
+ props.inits.delete(init);
595
+ }
596
+ };
597
+ return {
598
+ get inits() {
599
+ return Array.from(props.inits);
600
+ },
601
+ get context() {
602
+ return audioContext;
603
+ },
604
+ get state() {
605
+ return props.closing ? 'closing' : audioContext.state;
606
+ },
607
+ connect,
608
+ disconnect,
609
+ addWorklet,
610
+ releaseInit,
611
+ release: () => {
612
+ return new Promise(resolve => {
613
+ if (!props.closing && audioContext.state !== 'closed') {
614
+ props.closing = true;
615
+ props.workletModule = undefined;
616
+ unsubscribeStateChanged();
617
+ void audioContext.close().then(() => {
618
+ props.closing = false;
619
+ props.inits.forEach(releaseInit);
620
+ resolve();
621
+ });
622
+ }
623
+ else {
624
+ resolve();
625
+ }
626
+ });
627
+ },
628
+ };
629
+ };
630
+ export const createAudioGraphProxy = (audioGraph, handlers) => new Proxy(audioGraph, {
631
+ get: (target, p) => {
632
+ switch (p) {
633
+ case 'connect': {
634
+ const r = target[p];
635
+ return (...args) => {
636
+ handlers.connect?.(target, args);
637
+ return r.apply(target, args);
638
+ };
639
+ }
640
+ case 'disconnect': {
641
+ const r = target[p];
642
+ return (...args) => {
643
+ handlers.disconnect?.(target, args);
644
+ return r.apply(target, args);
645
+ };
646
+ }
647
+ default: {
648
+ return target[p];
649
+ }
650
+ }
651
+ },
652
+ set: () => false,
653
+ });
@@ -0,0 +1,12 @@
1
+ import type { Clock } from './types';
2
+ export interface Benchmark {
3
+ begin(): void;
4
+ end(): void;
5
+ calculateFps(): number;
6
+ }
7
+ interface BenchmarkOptions {
8
+ calculationThresholdMS?: number;
9
+ }
10
+ export declare const createBenchmark: (clock?: Clock, { calculationThresholdMS }?: BenchmarkOptions) => Benchmark;
11
+ export declare const calculateFps: (time: number) => number;
12
+ export {};
@@ -0,0 +1,34 @@
1
+ export const createBenchmark = (clock = performance, { calculationThresholdMS = 1000 } = {}) => {
2
+ const props = {
3
+ beginTime: 0,
4
+ endTime: 0,
5
+ sumDelta: 0,
6
+ count: 0,
7
+ lastCalculateTime: 0,
8
+ lastResult: 0,
9
+ };
10
+ return {
11
+ begin: () => {
12
+ props.beginTime = clock.now();
13
+ },
14
+ end: () => {
15
+ props.endTime = clock.now();
16
+ props.sumDelta += props.endTime - props.beginTime;
17
+ ++props.count;
18
+ },
19
+ calculateFps: () => {
20
+ const calculateTime = clock.now();
21
+ if (calculateTime - props.lastCalculateTime >=
22
+ calculationThresholdMS) {
23
+ const result = props.count === 0 ? 0 : props.sumDelta / props.count;
24
+ props.sumDelta = 0;
25
+ props.count = 0;
26
+ props.lastCalculateTime = calculateTime;
27
+ props.lastResult = result;
28
+ return result;
29
+ }
30
+ return props.lastResult;
31
+ },
32
+ };
33
+ };
34
+ export const calculateFps = (time) => time === 0 ? time : 1000.0 / time;
@@ -0,0 +1,4 @@
1
+ export declare const createMediaStreamTrackGenerator: {
2
+ (init: MediaStreamAudioTrackGeneratorInit): MediaStreamAudioTrackGenerator;
3
+ (init: MediaStreamVideoTrackGeneratorInit): MediaStreamVideoTrackGenerator;
4
+ };
@@ -0,0 +1,4 @@
1
+ export const createMediaStreamTrackGenerator = init => {
2
+ const generator = new MediaStreamTrackGenerator(init);
3
+ return generator;
4
+ };