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.
Files changed (50) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +1 -3
  3. package/index.cjs +81 -83
  4. package/index.mjs +12 -1
  5. package/js/AnalyserNode.js +0 -3
  6. package/js/AudioBuffer.js +10 -11
  7. package/js/AudioBufferSourceNode.js +0 -6
  8. package/js/AudioContext.js +44 -13
  9. package/js/AudioDestinationNode.js +1 -1
  10. package/js/AudioListener.js +2 -2
  11. package/js/AudioParamMap.js +88 -0
  12. package/js/AudioRenderCapacity.js +117 -0
  13. package/js/AudioScheduledSourceNode.js +15 -0
  14. package/js/AudioWorklet.js +261 -0
  15. package/js/AudioWorkletGlobalScope.js +303 -0
  16. package/js/AudioWorkletNode.js +290 -0
  17. package/js/BaseAudioContext.js +51 -13
  18. package/js/BiquadFilterNode.js +0 -3
  19. package/js/ChannelMergerNode.js +0 -3
  20. package/js/ChannelSplitterNode.js +0 -3
  21. package/js/ConstantSourceNode.js +0 -6
  22. package/js/ConvolverNode.js +0 -3
  23. package/js/DelayNode.js +0 -3
  24. package/js/DynamicsCompressorNode.js +0 -3
  25. package/js/Events.js +230 -0
  26. package/js/GainNode.js +0 -3
  27. package/js/IIRFilterNode.js +0 -3
  28. package/js/MediaStreamAudioSourceNode.js +0 -3
  29. package/js/OfflineAudioContext.js +57 -34
  30. package/js/OscillatorNode.js +0 -6
  31. package/js/PannerNode.js +0 -3
  32. package/js/ScriptProcessorNode.js +179 -0
  33. package/js/StereoPannerNode.js +0 -3
  34. package/js/WaveShaperNode.js +0 -3
  35. package/js/lib/events.js +6 -16
  36. package/js/lib/symbols.js +23 -2
  37. package/load-native.cjs +87 -0
  38. package/node-web-audio-api.darwin-arm64.node +0 -0
  39. package/node-web-audio-api.darwin-x64.node +0 -0
  40. package/node-web-audio-api.linux-arm-gnueabihf.node +0 -0
  41. package/node-web-audio-api.linux-arm64-gnu.node +0 -0
  42. package/node-web-audio-api.linux-x64-gnu.node +0 -0
  43. package/node-web-audio-api.win32-arm64-msvc.node +0 -0
  44. package/node-web-audio-api.win32-x64-msvc.node +0 -0
  45. package/package.json +3 -1
  46. package/TODOS.md +0 -149
  47. package/js/monkey-patch.js +0 -77
  48. package/run-wpt.md +0 -27
  49. package/simple-test.cjs +0 -20
  50. 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
+ };