node-web-audio-api 0.19.0 → 0.21.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 +11 -0
- package/README.md +1 -3
- package/index.cjs +81 -83
- package/index.mjs +12 -1
- package/js/AnalyserNode.js +0 -3
- package/js/AudioBuffer.js +10 -11
- package/js/AudioBufferSourceNode.js +0 -6
- package/js/AudioContext.js +44 -13
- package/js/AudioDestinationNode.js +1 -1
- package/js/AudioListener.js +2 -2
- package/js/AudioParamMap.js +88 -0
- package/js/AudioRenderCapacity.js +117 -0
- package/js/AudioScheduledSourceNode.js +15 -0
- package/js/AudioWorklet.js +261 -0
- package/js/AudioWorkletGlobalScope.js +303 -0
- package/js/AudioWorkletNode.js +290 -0
- package/js/BaseAudioContext.js +51 -13
- package/js/BiquadFilterNode.js +0 -3
- package/js/ChannelMergerNode.js +0 -3
- package/js/ChannelSplitterNode.js +0 -3
- package/js/ConstantSourceNode.js +0 -6
- package/js/ConvolverNode.js +0 -3
- package/js/DelayNode.js +0 -3
- package/js/DynamicsCompressorNode.js +0 -3
- package/js/Events.js +230 -0
- package/js/GainNode.js +0 -3
- package/js/IIRFilterNode.js +0 -3
- package/js/MediaStreamAudioSourceNode.js +0 -3
- package/js/OfflineAudioContext.js +57 -34
- package/js/OscillatorNode.js +0 -6
- package/js/PannerNode.js +0 -3
- package/js/ScriptProcessorNode.js +179 -0
- package/js/StereoPannerNode.js +0 -3
- package/js/WaveShaperNode.js +0 -3
- package/js/lib/events.js +6 -16
- package/js/lib/symbols.js +23 -2
- package/load-native.cjs +87 -0
- package/node-web-audio-api.darwin-arm64.node +0 -0
- package/node-web-audio-api.darwin-x64.node +0 -0
- package/node-web-audio-api.linux-arm-gnueabihf.node +0 -0
- package/node-web-audio-api.linux-arm64-gnu.node +0 -0
- package/node-web-audio-api.linux-x64-gnu.node +0 -0
- package/node-web-audio-api.win32-arm64-msvc.node +0 -0
- package/node-web-audio-api.win32-x64-msvc.node +0 -0
- package/package.json +3 -1
- package/TODOS.md +0 -149
- package/js/monkey-patch.js +0 -77
- package/run-wpt.md +0 -27
- package/simple-test.cjs +0 -20
- package/simple-test.mjs +0 -20
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
const {
|
|
2
|
+
parentPort,
|
|
3
|
+
workerData,
|
|
4
|
+
} = require('node:worker_threads');
|
|
5
|
+
|
|
6
|
+
const conversions = require('webidl-conversions');
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
exit_audio_worklet_global_scope,
|
|
10
|
+
run_audio_worklet_global_scope,
|
|
11
|
+
} = require('../load-native.cjs');
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
workletId,
|
|
15
|
+
sampleRate,
|
|
16
|
+
} = workerData;
|
|
17
|
+
|
|
18
|
+
const kWorkletQueueTask = Symbol.for('node-web-audio-api:worklet-queue-task');
|
|
19
|
+
const kWorkletCallableProcess = Symbol.for('node-web-audio-api:worklet-callable-process');
|
|
20
|
+
const kWorkletInputs = Symbol.for('node-web-audio-api:worklet-inputs');
|
|
21
|
+
const kWorkletOutputs = Symbol.for('node-web-audio-api:worklet-outputs');
|
|
22
|
+
const kWorkletParams = Symbol.for('node-web-audio-api:worklet-params');
|
|
23
|
+
const kWorkletParamsCache = Symbol.for('node-web-audio-api:worklet-params-cache');
|
|
24
|
+
// const kWorkletOrderedParamNames = Symbol.for('node-web-audio-api:worklet-ordered-param-names');
|
|
25
|
+
|
|
26
|
+
const nameProcessorCtorMap = new Map();
|
|
27
|
+
const processors = {};
|
|
28
|
+
let pendingProcessorConstructionData = null;
|
|
29
|
+
let loopStarted = false;
|
|
30
|
+
let runLoopImmediateId = null;
|
|
31
|
+
|
|
32
|
+
function isIterable(obj) {
|
|
33
|
+
// checks for null and undefined
|
|
34
|
+
if (obj === null || obj === undefined) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return typeof obj[Symbol.iterator] === 'function';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// cf. https://stackoverflow.com/a/46759625
|
|
41
|
+
function isConstructor(f) {
|
|
42
|
+
try {
|
|
43
|
+
Reflect.construct(String, [], f);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function runLoop() {
|
|
51
|
+
// block until we need to render a quantum
|
|
52
|
+
run_audio_worklet_global_scope(workletId, processors);
|
|
53
|
+
// yield to the event loop, and then repeat
|
|
54
|
+
runLoopImmediateId = setImmediate(runLoop);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// s
|
|
58
|
+
globalThis.currentTime = 0
|
|
59
|
+
globalThis.currentFrame = 0;
|
|
60
|
+
globalThis.sampleRate = sampleRate;
|
|
61
|
+
// @todo - implement in upstream crate
|
|
62
|
+
// globalThis.renderQuantumSize = 128;
|
|
63
|
+
|
|
64
|
+
globalThis.AudioWorkletProcessor = class AudioWorkletProcessor {
|
|
65
|
+
static get parameterDescriptors() {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#port = null;
|
|
70
|
+
|
|
71
|
+
constructor() {
|
|
72
|
+
const {
|
|
73
|
+
port,
|
|
74
|
+
numberOfInputs,
|
|
75
|
+
numberOfOutputs,
|
|
76
|
+
parameterDescriptors,
|
|
77
|
+
} = pendingProcessorConstructionData;
|
|
78
|
+
|
|
79
|
+
// @todo - Mark [[callable process]] as true, set to false in render quantum
|
|
80
|
+
// either "process" doese not exists, either it throws an error
|
|
81
|
+
this[kWorkletCallableProcess] = true;
|
|
82
|
+
// @todo - reuse Float32Arrays between calls + freeze arrays
|
|
83
|
+
this[kWorkletInputs] = new Array(numberOfInputs).fill([]);
|
|
84
|
+
// @todo - use `outputChannelCount`
|
|
85
|
+
this[kWorkletOutputs] = new Array(numberOfOutputs).fill([]);
|
|
86
|
+
// Object to be reused as `process` parameters argument
|
|
87
|
+
this[kWorkletParams] = {};
|
|
88
|
+
// Cache of 2 Float32Array (of length 128 and 1) for each param, to be reused on
|
|
89
|
+
// each process call according to the size the param for the current render quantum
|
|
90
|
+
this[kWorkletParamsCache] = {};
|
|
91
|
+
|
|
92
|
+
parameterDescriptors.forEach(desc => {
|
|
93
|
+
this[kWorkletParamsCache][desc.name] = [
|
|
94
|
+
new Float32Array(128), // should be globalThis.renderQuantumSize
|
|
95
|
+
new Float32Array(1),
|
|
96
|
+
]
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
this.#port = port;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get port() {
|
|
103
|
+
if (!(this instanceof AudioWorkletProcessor)) {
|
|
104
|
+
throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioWorkletProcessor\'');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return this.#port;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
[kWorkletQueueTask](cmd, err) {
|
|
111
|
+
this.#port.postMessage({ cmd, err });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// follow algorithm from:
|
|
116
|
+
// https://webaudio.github.io/web-audio-api/#dom-audioworkletglobalscope-registerprocessor
|
|
117
|
+
globalThis.registerProcessor = function registerProcessor(name, processorCtor) {
|
|
118
|
+
const parsedName = conversions['DOMString'](name, {
|
|
119
|
+
context: `Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': name (${name})`,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (parsedName === '') {
|
|
123
|
+
throw new DOMException(`Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': name is empty`, 'NotSupportedError');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (nameProcessorCtorMap.has(name)) {
|
|
127
|
+
throw new DOMException(`Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': A processor with name '${name}' has already been registered in this scope`, 'NotSupportedError');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!isConstructor(processorCtor)) {
|
|
131
|
+
throw new TypeError(`Cannot execute 'registerProcessor")' in 'AudoWorkletGlobalScope': argument 2 for name '${name}' is not a constructor`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (typeof processorCtor.prototype !== 'object') {
|
|
135
|
+
throw new TypeError(`Cannot execute 'registerProcessor")' in 'AudoWorkletGlobalScope': argument 2 for name '${name}' is not is not a valid AudioWorkletProcessor`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// must support Array, Set or iterators
|
|
139
|
+
let parameterDescriptorsValue = processorCtor.parameterDescriptors;
|
|
140
|
+
|
|
141
|
+
if (!isIterable(parameterDescriptorsValue)) {
|
|
142
|
+
throw new TypeError(`Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}: 'parameterDescriptors' is not iterable'`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const paramDescriptors = Array.from(parameterDescriptorsValue);
|
|
146
|
+
const parsedParamDescriptors = [];
|
|
147
|
+
|
|
148
|
+
// Parse AudioParamDescriptor sequence
|
|
149
|
+
// cf. https://webaudio.github.io/web-audio-api/#AudioParamDescriptor
|
|
150
|
+
for (let i = 0; i < paramDescriptors.length; i++) {
|
|
151
|
+
const descriptor = paramDescriptors[i];
|
|
152
|
+
const parsedDescriptor = {};
|
|
153
|
+
|
|
154
|
+
if (typeof descriptor !== 'object' || descriptor === null) {
|
|
155
|
+
throw new TypeError(`Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}: Element at index ${i} is not an instance of 'AudioParamDescriptor'`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (descriptor.name === undefined) {
|
|
159
|
+
throw new TypeError(`Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}: Element at index ${i} is not an instance of 'AudioParamDescriptor'`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
parsedDescriptor.name = conversions['DOMString'](descriptor.name, {
|
|
163
|
+
context: `Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}: Invalid 'name' for 'AudioParamDescriptor' at index ${i}`,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (descriptor.defaultValue !== undefined) {
|
|
167
|
+
parsedDescriptor.defaultValue = conversions['float'](descriptor.defaultValue, {
|
|
168
|
+
context: `Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}: Invalid 'defaultValue' for 'AudioParamDescriptor' at index ${i}`,
|
|
169
|
+
});
|
|
170
|
+
} else {
|
|
171
|
+
parsedDescriptor.defaultValue = 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (descriptor.maxValue !== undefined) {
|
|
175
|
+
parsedDescriptor.maxValue = conversions['float'](descriptor.maxValue, {
|
|
176
|
+
context: `Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}: Invalid 'maxValue' for 'AudioParamDescriptor' at index ${i}`,
|
|
177
|
+
});
|
|
178
|
+
} else {
|
|
179
|
+
parsedDescriptor.maxValue = 3.4028235e38;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (descriptor.minValue !== undefined) {
|
|
183
|
+
parsedDescriptor.minValue = conversions['float'](descriptor.minValue, {
|
|
184
|
+
context: `Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}: Invalid 'minValue' for 'AudioParamDescriptor' at index ${i}`,
|
|
185
|
+
});
|
|
186
|
+
} else {
|
|
187
|
+
parsedDescriptor.minValue = -3.4028235e38;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (descriptor.automationRate !== undefined) {
|
|
191
|
+
if (!['a-rate', 'k-rate'].includes(descriptor.automationRate)) {
|
|
192
|
+
throw new TypeError(`Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}: The provided value '${descriptor.automationRate}' is not a valid enum value of type AutomationRate for 'AudioParamDescriptor' at index ${i}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
parsedDescriptor.automationRate = conversions['DOMString'](descriptor.automationRate, {
|
|
196
|
+
context: `Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}: The provided value '${descriptor.automationRate}'`,
|
|
197
|
+
});
|
|
198
|
+
} else {
|
|
199
|
+
parsedDescriptor.automationRate = 'a-rate';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
parsedParamDescriptors.push(parsedDescriptor);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// check for duplicate parame names and consistency of min, max and default values
|
|
206
|
+
const paramNames = [];
|
|
207
|
+
|
|
208
|
+
for (let i = 0; i < parsedParamDescriptors.length; i++) {
|
|
209
|
+
const { name, defaultValue, minValue, maxValue } = parsedParamDescriptors[i];
|
|
210
|
+
|
|
211
|
+
if (paramNames.includes(name)) {
|
|
212
|
+
throw new DOMException(`Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}': 'AudioParamDescriptor' with name '${name}' already declared`, 'NotSupportedError');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
paramNames.push(name);
|
|
216
|
+
|
|
217
|
+
if (!(minValue <= defaultValue && defaultValue <= maxValue)) {
|
|
218
|
+
throw new DOMException(`Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}': The constraint minValue <= defaultValue <= maxValue is not met`, 'InvalidStateError');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// store constructor
|
|
223
|
+
nameProcessorCtorMap.set(parsedName, processorCtor);
|
|
224
|
+
// send param descriptors back to main thread
|
|
225
|
+
parentPort.postMessage({
|
|
226
|
+
cmd: 'node-web-audio-api:worlet:processor-registered',
|
|
227
|
+
name: parsedName,
|
|
228
|
+
parameterDescriptors: parsedParamDescriptors,
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
// @todo - recheck this, not sure this is relevant in our case
|
|
234
|
+
// NOTE: Authors that register an event listener on the "message" event of this
|
|
235
|
+
// port should call close on either end of the MessageChannel (either in the
|
|
236
|
+
// AudioWorklet or the AudioWorkletGlobalScope side) to allow for resources to be collected.
|
|
237
|
+
parentPort.on('exit', () => {
|
|
238
|
+
process.stdout.write('closing worklet');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
parentPort.on('message', event => {
|
|
242
|
+
console.log(event.cmd + '\n');
|
|
243
|
+
|
|
244
|
+
switch (event.cmd) {
|
|
245
|
+
case 'node-web-audio-api:worklet:init': {
|
|
246
|
+
const { workletId, processors, promiseId } = event;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
case 'node-web-audio-api:worklet:exit': {
|
|
250
|
+
clearImmediate(runLoopImmediateId);
|
|
251
|
+
// properly exit audio worklet on rust side
|
|
252
|
+
exit_audio_worklet_global_scope(workletId, processors);
|
|
253
|
+
// exit process
|
|
254
|
+
process.exit(0);
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
case 'node-web-audio-api:worklet:add-module': {
|
|
258
|
+
const { code, promiseId } = event;
|
|
259
|
+
const func = new Function('AudioWorkletProcessor', 'registerProcessor', code);
|
|
260
|
+
func(AudioWorkletProcessor, registerProcessor);
|
|
261
|
+
|
|
262
|
+
// send registered param descriptors on main thread and resolve Promise
|
|
263
|
+
parentPort.postMessage({
|
|
264
|
+
cmd: 'node-web-audio-api:worklet:module-added',
|
|
265
|
+
promiseId,
|
|
266
|
+
});
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
case 'node-web-audio-api:worklet:create-processor': {
|
|
270
|
+
const { name, id, options, port } = event;
|
|
271
|
+
const ctor = nameProcessorCtorMap.get(name);
|
|
272
|
+
|
|
273
|
+
// rewrap options of interest for the AudioWorkletNodeBaseClass
|
|
274
|
+
pendingProcessorConstructionData = {
|
|
275
|
+
port,
|
|
276
|
+
numberOfInputs: options.numberOfInputs,
|
|
277
|
+
numberOfOutputs: options.numberOfOutputs,
|
|
278
|
+
parameterDescriptors: ctor.parameterDescriptors,
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
let instance;
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
instance = new ctor(options);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
port.postMessage({ cmd: 'node-web-audio-api:worklet:ctor-error', err });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
pendingProcessorConstructionData = null;
|
|
290
|
+
// store in global so that Rust can match the JS processor
|
|
291
|
+
// with its corresponding NapiAudioWorkletProcessor
|
|
292
|
+
processors[`${id}`] = instance;
|
|
293
|
+
// notify audio worklet back that processor has finished instanciation
|
|
294
|
+
parentPort.postMessage({ cmd: 'node-web-audio-api:worklet:processor-created', id });
|
|
295
|
+
|
|
296
|
+
if (!loopStarted) {
|
|
297
|
+
loopStarted = true;
|
|
298
|
+
setImmediate(runLoop);
|
|
299
|
+
}
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
});
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
const conversions = require('webidl-conversions');
|
|
3
|
+
const {
|
|
4
|
+
toSanitizedSequence,
|
|
5
|
+
} = require('./lib/cast.js');
|
|
6
|
+
const {
|
|
7
|
+
throwSanitizedError,
|
|
8
|
+
} = require('./lib/errors.js');
|
|
9
|
+
const {
|
|
10
|
+
kNapiObj,
|
|
11
|
+
kProcessorRegistered,
|
|
12
|
+
kGetParameterDescriptors,
|
|
13
|
+
kPrivateConstructor,
|
|
14
|
+
kCreateProcessor,
|
|
15
|
+
} = require('./lib/symbols.js');
|
|
16
|
+
const {
|
|
17
|
+
kEnumerableProperty,
|
|
18
|
+
} = require('./lib/utils.js');
|
|
19
|
+
const {
|
|
20
|
+
propagateEvent,
|
|
21
|
+
} = require('./lib/events.js');
|
|
22
|
+
const {
|
|
23
|
+
ErrorEvent,
|
|
24
|
+
} = require('./Events.js');
|
|
25
|
+
|
|
26
|
+
/* eslint-enable no-unused-vars */
|
|
27
|
+
|
|
28
|
+
const AudioNode = require('./AudioNode.js');
|
|
29
|
+
const AudioParamMap = require('./AudioParamMap.js');
|
|
30
|
+
const IMPLEMENTATION_MAX_NUMBER_OF_CHANNELS = 32;
|
|
31
|
+
|
|
32
|
+
module.exports = (jsExport, nativeBinding) => {
|
|
33
|
+
class AudioWorkletNode extends AudioNode {
|
|
34
|
+
#port = null;
|
|
35
|
+
#parameters = {};
|
|
36
|
+
|
|
37
|
+
constructor(context, name, options) {
|
|
38
|
+
if (arguments.length < 2) {
|
|
39
|
+
throw new TypeError(`Failed to construct 'AudioWorkletNode': 2 arguments required, but only ${arguments.length} present`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!(context instanceof jsExport.BaseAudioContext)) {
|
|
43
|
+
throw new TypeError(`Failed to construct 'AudioWorkletNode': argument 1 is not of type BaseAudioContext`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const parsedName = conversions['DOMString'](name, {
|
|
47
|
+
context: `Failed to construct 'AudioWorkletNode': The given 'AudioWorkletProcessor' name`,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (!context.audioWorklet[kProcessorRegistered](parsedName)) {
|
|
51
|
+
throw new DOMException(`Failed to construct 'AudioWorkletNode': processor '${parsedName}' is not registered in 'AudioWorklet'`, 'InvalidStateError');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// parsed version of the option to be passed to NAPI
|
|
55
|
+
const parsedOptions = {};
|
|
56
|
+
|
|
57
|
+
if (options && (typeof options !== 'object' || options === null)) {
|
|
58
|
+
throw new TypeError('Failed to construct \'AudioWorkletNode\': argument 3 is not of type \'AudioWorkletNodeOptions\'');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (options && options.numberOfInputs !== undefined) {
|
|
62
|
+
parsedOptions.numberOfInputs = conversions['unsigned long'](options.numberOfInputs, {
|
|
63
|
+
enforceRange: true,
|
|
64
|
+
context: `Failed to construct 'AudioWorkletNode': Failed to read the 'numberOfInputs' property from AudioWorkletNodeOptions: The provided value (${options.numberOfInputs}})`,
|
|
65
|
+
});
|
|
66
|
+
} else {
|
|
67
|
+
parsedOptions.numberOfInputs = 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (options && options.numberOfOutputs !== undefined) {
|
|
71
|
+
parsedOptions.numberOfOutputs = conversions['unsigned long'](options.numberOfOutputs, {
|
|
72
|
+
enforceRange: true,
|
|
73
|
+
context: `Failed to construct 'AudioWorkletNode': Failed to read the 'numberOfOutputs' property from AudioWorkletNodeOptions: The provided value (${options.numberOfOutputs}})`,
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
parsedOptions.numberOfOutputs = 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// If outputChannelCount exists,
|
|
80
|
+
// - If any value in outputChannelCount is zero or greater than the implementation’s maximum number of channels, throw a NotSupportedError and abort the remaining steps.
|
|
81
|
+
// - If the length of outputChannelCount does not equal numberOfOutputs, throw an IndexSizeError and abort the remaining steps.
|
|
82
|
+
// - If both numberOfInputs and numberOfOutputs are 1, set the channel count of the node output to the one value in outputChannelCount.
|
|
83
|
+
// - Otherwise set the channel count of the kth output of the node to the kth element of outputChannelCount sequence and return.
|
|
84
|
+
if (options && options.outputChannelCount !== undefined) {
|
|
85
|
+
try {
|
|
86
|
+
parsedOptions.outputChannelCount = toSanitizedSequence(options.outputChannelCount, Uint32Array);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
throw new TypeError(`Failed to construct 'AudioWorkletNode': Failed to read the 'outputChannelCount' property from AudioWorkletNodeOptions: The provided value ${err.message}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
parsedOptions.outputChannelCount.forEach((value, index) => {
|
|
92
|
+
if (value <= 0 || value > IMPLEMENTATION_MAX_NUMBER_OF_CHANNELS) {
|
|
93
|
+
throw new DOMException(`Failed to construct 'AudioWorkletNode': Invalid 'outputChannelCount' property from AudioWorkletNodeOptions: Value at index ${index} in outside supported range [1, 32]`, 'NotSupportedError');
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (parsedOptions.numberOfOutputs !== parsedOptions.outputChannelCount.length) {
|
|
98
|
+
throw new DOMException(`Failed to construct 'AudioWorkletNode': Invalid 'outputChannelCount' property from AudioWorkletNodeOptions: 'outputChannelCount' length (${parsedOptions.outputChannelCount.length}) does not equal 'numberOfOutputs' (${parsedOptions.numberOfOutputs})`, 'IndexSizeError');
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
// If outputChannelCount does not exists,
|
|
102
|
+
// - If both numberOfInputs and numberOfOutputs are 1, set the initial channel count of the node output to 1 and return.
|
|
103
|
+
// NOTE: For this case, the output chanel count will change to computedNumberOfChannels dynamically based on the input and the channelCountMode at runtime.
|
|
104
|
+
// - Otherwise set the channel count of each output of the node to 1 and return.
|
|
105
|
+
|
|
106
|
+
// @note - not sure what this means, let's go simple
|
|
107
|
+
parsedOptions.outputChannelCount = new Uint32Array(parsedOptions.numberOfOutputs);
|
|
108
|
+
parsedOptions.outputChannelCount.fill(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// @todo
|
|
112
|
+
// - This should be a "record", let's treat it as a raw object of now
|
|
113
|
+
// - Check if this needs to checked against the declared `parameterDescriptors`
|
|
114
|
+
if (options && options.parameterData !== undefined) {
|
|
115
|
+
if (typeof options.parameterData === 'object' && options.parameterData !== null) {
|
|
116
|
+
parsedOptions.parameterData = {};
|
|
117
|
+
|
|
118
|
+
for (let [key, value] in Object.entries(options.parameterData)) {
|
|
119
|
+
const parsedKey = conversions['DOMString'](key, {
|
|
120
|
+
context: `Failed to construct 'AudioWorkletNode': Invalid 'parameterData' property from AudioWorkletNodeOptions: Invalid key (${key})`,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const parsedValue = conversions['double'](value, {
|
|
124
|
+
context: `Failed to construct 'AudioWorkletNode': Invalid 'parameterData' property from AudioWorkletNodeOptions: Invalid value for key ${parsedKey}`,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
parsedOptions.parameterData[parsedKey] = parsedValue;
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
throw new TypeError(`Failed to construct 'AudioWorkletNode': Invalid 'parameterData' property from AudioWorkletNodeOptions: 'outputChannelCount' length (${parsedOptions.outputChannelCount.length}) does not equal 'numberOfOutputs' (${parsedOptions.numberOfOutputs})`);
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
parsedOptions.parameterData = {};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// These ones are for the JS processor
|
|
137
|
+
if (options && options.processorOptions !== undefined) {
|
|
138
|
+
if (typeof options.processorOptions === 'object' && options.processorOptions !== null) {
|
|
139
|
+
parsedOptions.processorOptions = Object.assign({}, options.processorOptions);
|
|
140
|
+
} else {
|
|
141
|
+
throw new TypeError(`Failed to construct 'AudioWorkletNode': Invalid 'processorOptions' property from AudioWorkletNodeOptions: 'processorOptions' is not an object`);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
parsedOptions.processorOptions = {};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// AudioNodeOptions
|
|
148
|
+
if (options && options.channelCount !== undefined) {
|
|
149
|
+
parsedOptions.channelCount = conversions['unsigned long'](options.channelCount, {
|
|
150
|
+
enforceRange: true,
|
|
151
|
+
context: `Failed to construct 'AudioWorkletNode': Failed to read the 'channelCount' property from AudioWorkletNodeOptions: The provided value '${options.channelCount}'`,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// if we delegate this check to Rust, this can poison a Mutex
|
|
155
|
+
// (probably the `audio_param_descriptor_channel` one)
|
|
156
|
+
if (parsedOptions.channelCount <= 0 || parsedOptions.channelCount > IMPLEMENTATION_MAX_NUMBER_OF_CHANNELS) {
|
|
157
|
+
throw new DOMException(`Failed to construct 'AudioWorkletNode': Invalid 'channelCount' property: Number of channels: ${parsedOptions.channelCount} is outside range [1, 32]`, 'NotSupportedError')
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (options && options.channelCountMode !== undefined) {
|
|
162
|
+
if (!['max', 'clamped-max', 'explicit'].includes(options.channelCountMode)) {
|
|
163
|
+
throw new TypeError(`Failed to construct 'AudioWorkletNode': Failed to read the 'channelCountMode' property from 'AudioNodeOptions': The provided value '${options.channelCountMode}' is not a valid enum value of type ChannelCountMode`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
parsedOptions.channelCountMode = conversions['DOMString'](options.channelCountMode, {
|
|
167
|
+
context: `Failed to construct 'AudioWorkletNode': Failed to read the 'channelCount' property from AudioWorkletNodeOptions: The provided value '${options.channelCountMode}'`,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (options && options.channelInterpretation !== undefined) {
|
|
172
|
+
if (!['speakers', 'discrete'].includes(options.channelInterpretation)) {
|
|
173
|
+
throw new TypeError(`Failed to construct 'AudioWorkletNode': Failed to read the 'channelInterpretation' property from 'AudioNodeOptions': The provided value '${options.channelInterpretation}' is not a valid enum value of type ChannelCountMode`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
parsedOptions.channelInterpretation = conversions['DOMString'](options.channelInterpretation, {
|
|
177
|
+
context: `Failed to construct 'AudioWorkletNode': Failed to read the 'channelInterpretation' property from AudioWorkletNodeOptions: The provided value '${options.channelInterpretation}'`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Create NapiAudioWorkletNode
|
|
182
|
+
const parameterDescriptors = context.audioWorklet[kGetParameterDescriptors](parsedName);
|
|
183
|
+
let napiObj;
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
napiObj = new nativeBinding.AudioWorkletNode(
|
|
187
|
+
context[kNapiObj],
|
|
188
|
+
parsedName,
|
|
189
|
+
parsedOptions,
|
|
190
|
+
parameterDescriptors,
|
|
191
|
+
);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
throwSanitizedError(err);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
super(context, {
|
|
197
|
+
[kNapiObj]: napiObj,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
let parameters = new Map();
|
|
201
|
+
|
|
202
|
+
for (let name in this[kNapiObj].parameters) {
|
|
203
|
+
const audioParam = new jsExport.AudioParam({
|
|
204
|
+
[kNapiObj]: this[kNapiObj].parameters[name],
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
parameters.set(name, audioParam);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
this.#parameters = new AudioParamMap({
|
|
211
|
+
[kPrivateConstructor]: true,
|
|
212
|
+
parameters,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Create JS processor
|
|
216
|
+
this.#port = context.audioWorklet[kCreateProcessor](
|
|
217
|
+
parsedName,
|
|
218
|
+
parsedOptions,
|
|
219
|
+
napiObj.id,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
this.#port.on('message', msg => {
|
|
223
|
+
// ErrorEvent named processorerror
|
|
224
|
+
switch (msg.cmd) {
|
|
225
|
+
case 'node-web-audio-api:worklet:ctor-error': {
|
|
226
|
+
const message = `Failed to construct '${parsedName}' AudioWorkletProcessor: ${msg.err.message}`;
|
|
227
|
+
const event = new ErrorEvent('processorerror', { message, error: msg.err });
|
|
228
|
+
propagateEvent(this, event);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
case 'node-web-audio-api:worklet:process-invalid': {
|
|
232
|
+
const message = `Failed to execute 'process' on '${parsedName}' AudioWorkletProcessor: ${msg.err.message}`;
|
|
233
|
+
const error = new TypeError(message);
|
|
234
|
+
error.stack = msg.err.stack.replace(msg.err.message, message);
|
|
235
|
+
|
|
236
|
+
const event = new ErrorEvent('processorerror', { message, error });
|
|
237
|
+
propagateEvent(this, event);
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case 'node-web-audio-api:worklet:process-error': {
|
|
241
|
+
const message = `Failed to execute 'process' on '${parsedName}' AudioWorkletProcessor: ${msg.err.message}`;
|
|
242
|
+
const event = new ErrorEvent('processorerror', { message, error: msg.err });
|
|
243
|
+
propagateEvent(this, event);
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
get parameters() {
|
|
251
|
+
if (!(this instanceof AudioWorkletNode)) {
|
|
252
|
+
throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioWorkletNode\'');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return this.#parameters;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
get port() {
|
|
259
|
+
if (!(this instanceof AudioWorkletNode)) {
|
|
260
|
+
throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioWorkletNode\'');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return this.#port;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
Object.defineProperties(AudioWorkletNode, {
|
|
268
|
+
length: {
|
|
269
|
+
__proto__: null,
|
|
270
|
+
writable: false,
|
|
271
|
+
enumerable: false,
|
|
272
|
+
configurable: true,
|
|
273
|
+
value: 2,
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
Object.defineProperties(AudioWorkletNode.prototype, {
|
|
278
|
+
[Symbol.toStringTag]: {
|
|
279
|
+
__proto__: null,
|
|
280
|
+
writable: false,
|
|
281
|
+
enumerable: false,
|
|
282
|
+
configurable: true,
|
|
283
|
+
value: 'AudioWorkletNode',
|
|
284
|
+
},
|
|
285
|
+
parameters: kEnumerableProperty,
|
|
286
|
+
port: kEnumerableProperty,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return AudioWorkletNode;
|
|
290
|
+
};
|