@pexip/media-processor 16.7.1 → 17.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/CHANGELOG.md +16 -0
- package/README.md +1 -7
- package/dist/main/audio.d.ts +123 -0
- package/dist/main/audio.js +653 -0
- package/dist/main/benchUtils.d.ts +12 -0
- package/dist/main/benchUtils.js +34 -0
- package/dist/main/generator.d.ts +4 -0
- package/dist/main/generator.js +4 -0
- package/dist/main/index.d.ts +17 -0
- package/dist/main/index.js +17 -0
- package/dist/main/math.d.ts +41 -0
- package/dist/main/math.js +57 -0
- package/dist/main/path.d.ts +102 -0
- package/dist/main/path.js +103 -0
- package/dist/main/process.d.ts +239 -0
- package/dist/main/process.js +364 -0
- package/dist/main/processor.d.ts +5 -0
- package/dist/main/processor.js +4 -0
- package/dist/main/transformer.d.ts +1 -0
- package/dist/main/transformer.js +4 -0
- package/dist/main/tsconfig.tsbuildinfo +1 -0
- package/dist/main/typeGuards.d.ts +5 -0
- package/dist/main/typeGuards.js +24 -0
- package/dist/main/types.d.ts +342 -0
- package/dist/main/types.js +1 -0
- package/dist/main/utils.d.ts +173 -0
- package/dist/main/utils.js +364 -0
- package/dist/main/video/canvasRenderUtils.d.ts +22 -0
- package/dist/main/video/canvasRenderUtils.js +198 -0
- package/dist/main/video/canvasTransform.d.ts +8 -0
- package/dist/main/video/canvasTransform.js +173 -0
- package/dist/main/video/constants.d.ts +10 -0
- package/dist/main/video/constants.js +12 -0
- package/dist/main/video/index.d.ts +9 -0
- package/dist/main/video/index.js +9 -0
- package/dist/main/video/load.d.ts +17 -0
- package/dist/main/video/load.js +47 -0
- package/dist/main/video/segmenters/index.d.ts +1 -0
- package/dist/main/video/segmenters/index.js +1 -0
- package/dist/main/video/segmenters/mediapipe.d.ts +13 -0
- package/dist/main/video/segmenters/mediapipe.js +85 -0
- package/dist/main/video/transformer.d.ts +10 -0
- package/dist/main/video/transformer.js +56 -0
- package/dist/main/video/typeGuards.d.ts +2 -0
- package/dist/main/video/typeGuards.js +13 -0
- package/dist/main/video/types.d.ts +98 -0
- package/dist/main/video/types.js +20 -0
- package/dist/main/video/utils.d.ts +102 -0
- package/dist/main/video/utils.js +476 -0
- package/dist/main/video/video.d.ts +19 -0
- package/dist/main/video/video.js +48 -0
- package/dist/main/video/videoStreamTrackProcessor.d.ts +14 -0
- package/dist/main/video/videoStreamTrackProcessor.js +82 -0
- package/dist/main/visual.d.ts +80 -0
- package/dist/main/visual.js +135 -0
- package/dist/main/workletNodes.d.ts +2 -0
- package/dist/main/workletNodes.js +3 -0
- package/dist/workers/index.d.ts +0 -0
- package/dist/workers/index.js +1 -0
- package/dist/workers/tsconfig.tsbuildinfo +1 -0
- package/dist/worklets/denoise.worklet.d.ts +1 -0
- package/dist/worklets/denoise.worklet.js +1 -2
- package/dist/worklets/tsconfig.tsbuildinfo +1 -0
- package/dist/worklets/types.d.ts +52 -0
- package/dist/worklets/types.js +0 -0
- package/package.json +10 -8
- package/dist/index.d.ts +0 -1129
- package/dist/index.mjs +0 -2441
- 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;
|