node-web-audio-api 0.20.0 → 0.21.1

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.
@@ -0,0 +1,268 @@
1
+ const {
2
+ resolveObjectURL
3
+ } = require('node:buffer');
4
+ const fs = require('node:fs').promises;
5
+ const { existsSync } = require('node:fs');
6
+ const path = require('node:path');
7
+ const {
8
+ Worker,
9
+ MessageChannel,
10
+ } = require('node:worker_threads');
11
+
12
+ const {
13
+ kProcessorRegistered,
14
+ kGetParameterDescriptors,
15
+ kCreateProcessor,
16
+ kPrivateConstructor,
17
+ kWorkletRelease,
18
+ kCheckProcessorsCreated,
19
+ } = require('./lib/symbols.js');
20
+ const {
21
+ kEnumerableProperty,
22
+ } = require('./lib/utils.js');
23
+
24
+ const caller = require('caller');
25
+ // cf. https://www.npmjs.com/package/node-fetch#commonjs
26
+ const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
27
+
28
+ /**
29
+ * Retrieve code with different module resolution strategies
30
+ * - file - absolute or relative to cwd path
31
+ * - URL
32
+ * - Blob
33
+ * - fallback: relative to caller site
34
+ * + in fs
35
+ * + caller site is url - required for wpt, probably no other use case
36
+ */
37
+ const resolveModule = async (moduleUrl) => {
38
+ let code;
39
+
40
+ if (existsSync(moduleUrl)) {
41
+ const pathname = moduleUrl;
42
+
43
+ try {
44
+ const buffer = await fs.readFile(pathname);
45
+ code = buffer.toString();
46
+ } catch (err) {
47
+ throw new Error(`Failed to execute 'addModule' on 'AudioWorklet': ${err.message}`);
48
+ }
49
+ } else if (moduleUrl.startsWith('http')) {
50
+ try {
51
+ const res = await fetch(moduleUrl);
52
+ code = await res.text();
53
+ } catch (err) {
54
+ throw new Error(`Failed to execute 'addModule' on 'AudioWorklet': ${err.message}`);
55
+ }
56
+ } else if (moduleUrl.startsWith('blob:')) {
57
+ try {
58
+ const blob = resolveObjectURL(moduleUrl);
59
+ code = await blob.text();
60
+ } catch (err) {
61
+ throw new Error(`Failed to execute 'addModule' on 'AudioWorklet': ${err.message}`);
62
+ }
63
+ } else {
64
+ // get caller site from error stack trace
65
+ const callerSite = caller(2);
66
+
67
+ if (callerSite.startsWith('http')) {
68
+ let url;
69
+ // handle origin relative and caller path relative URLs
70
+ if (moduleUrl.startsWith('/')) {
71
+ const origin = new URL(baseUrl).origin;
72
+ url = origin + moduleUrl;
73
+ } else {
74
+ // we know separators are '/'
75
+ const baseUrl = callerSite.substr(0, callerSite.lastIndexOf('/'));
76
+ url = baseUrl + '/' + moduleUrl;
77
+ }
78
+
79
+ try {
80
+ const res = await fetch(url);
81
+ code = await res.text();
82
+ } catch (err) {
83
+ throw new Error(`Failed to execute 'addModule' on 'AudioWorklet': ${err.message}`);
84
+ }
85
+ } else {
86
+ const dirname = callerSite.substr(0, callerSite.lastIndexOf(path.sep));
87
+ const absDirname = dirname.replace('file://', '');
88
+ const pathname = path.join(absDirname, moduleUrl);
89
+
90
+ if (existsSync(pathname)) {
91
+ try {
92
+ const buffer = await fs.readFile(pathname);
93
+ code = buffer.toString();
94
+ } catch (err) {
95
+ throw new Error(`Failed to execute 'addModule' on 'AudioWorklet': ${err.message}`);
96
+ }
97
+ } else {
98
+ throw new Error(`Failed to execute 'addModule' on 'AudioWorklet': Cannot resolve module ${moduleUrl}`);
99
+ }
100
+ }
101
+ }
102
+
103
+ return code;
104
+ }
105
+
106
+ class AudioWorklet {
107
+ #workletId = null;
108
+ #sampleRate = null;
109
+ #port = null;
110
+ #idPromiseMap = new Map();
111
+ #promiseId = 0;
112
+ #workletParamDescriptorsMap = new Map();
113
+ #pendingCreateProcessors = new Set();
114
+
115
+ constructor(options) {
116
+ if (
117
+ (typeof options !== 'object') ||
118
+ options[kPrivateConstructor] !== true
119
+ ) {
120
+ throw new TypeError('Illegal constructor');
121
+ }
122
+
123
+ this.#workletId = options.workletId;
124
+ this.#sampleRate = options.sampleRate;
125
+ }
126
+
127
+ #bindEvents() {
128
+ this.#port.on('message', event => {
129
+ switch (event.cmd) {
130
+ case 'node-web-audio-api:worklet:module-added': {
131
+ const { promiseId } = event;
132
+ const { resolve } = this.#idPromiseMap.get(promiseId);
133
+ this.#idPromiseMap.delete(promiseId);
134
+ resolve();
135
+ break;
136
+ }
137
+ case 'node-web-audio-api:worklet:add-module-failed': {
138
+ const { promiseId, ctor, name, message } = event;
139
+ const { reject } = this.#idPromiseMap.get(promiseId);
140
+ this.#idPromiseMap.delete(promiseId);
141
+ const err = new globalThis[ctor](message, name);
142
+ reject(err);
143
+ break;
144
+ }
145
+ case 'node-web-audio-api:worlet:processor-registered': {
146
+ const { name, parameterDescriptors } = event;
147
+ this.#workletParamDescriptorsMap.set(name, parameterDescriptors);
148
+ break;
149
+ }
150
+ case 'node-web-audio-api:worklet:processor-created': {
151
+ const { id } = event;
152
+ this.#pendingCreateProcessors.delete(id);
153
+ break;
154
+ }
155
+ }
156
+ });
157
+ }
158
+
159
+ get port() {
160
+ return this.#port;
161
+ }
162
+
163
+ async addModule(moduleUrl) {
164
+ const code = await resolveModule(moduleUrl);
165
+
166
+ // launch Worker if not exists
167
+ if (!this.#port) {
168
+ await new Promise(resolve => {
169
+ const workletPathname = path.join(__dirname, 'AudioWorkletGlobalScope.js');
170
+ this.#port = new Worker(workletPathname, {
171
+ workerData: {
172
+ workletId: this.#workletId,
173
+ sampleRate: this.#sampleRate,
174
+ },
175
+ });
176
+ this.#port.on('online', resolve);
177
+
178
+ this.#bindEvents();
179
+ });
180
+ }
181
+
182
+ const promiseId = this.#promiseId++;
183
+ // This promise is resolved when the Worker returns the name and
184
+ // parameterDescriptors from the added module
185
+ await new Promise((resolve, reject) => {
186
+ this.#idPromiseMap.set(promiseId, { resolve, reject });
187
+
188
+ this.#port.postMessage({
189
+ cmd: 'node-web-audio-api:worklet:add-module',
190
+ code,
191
+ promiseId,
192
+ });
193
+ });
194
+ }
195
+
196
+ // For OfflineAudioContext only, check that all processors have been properly
197
+ // created before actual `startRendering`
198
+ async [kCheckProcessorsCreated]() {
199
+ return new Promise(async resolve => {
200
+ while (this.#pendingCreateProcessors.size !== 0) {
201
+ // we need a microtask to ensure message can be received
202
+ await new Promise(resolve => setTimeout(resolve, 0));
203
+ }
204
+
205
+ resolve();
206
+ });
207
+ }
208
+
209
+ [kProcessorRegistered](name) {
210
+ return Array.from(this.#workletParamDescriptorsMap.keys()).includes(name);
211
+ }
212
+
213
+ [kGetParameterDescriptors](name) {
214
+ return this.#workletParamDescriptorsMap.get(name);
215
+ }
216
+
217
+ [kCreateProcessor](name, options, id) {
218
+ this.#pendingCreateProcessors.add(id);
219
+
220
+ const { port1, port2 } = new MessageChannel();
221
+ // @todo - check if some processorOptions must be transfered as well
222
+ this.#port.postMessage({
223
+ cmd: 'node-web-audio-api:worklet:create-processor',
224
+ name,
225
+ id,
226
+ options,
227
+ port: port2,
228
+ }, [port2]);
229
+
230
+ return port1;
231
+ }
232
+
233
+ async [kWorkletRelease]() {
234
+ if (this.#port) {
235
+ await new Promise(resolve => {
236
+ this.#port.on('exit', resolve);
237
+ this.#port.postMessage({
238
+ cmd: 'node-web-audio-api:worklet:exit',
239
+ });
240
+ });
241
+ }
242
+ }
243
+ }
244
+
245
+ Object.defineProperties(AudioWorklet, {
246
+ length: {
247
+ __proto__: null,
248
+ writable: false,
249
+ enumerable: false,
250
+ configurable: true,
251
+ value: 0,
252
+ },
253
+ });
254
+
255
+ Object.defineProperties(AudioWorklet.prototype, {
256
+ [Symbol.toStringTag]: {
257
+ __proto__: null,
258
+ writable: false,
259
+ enumerable: false,
260
+ configurable: true,
261
+ value: 'AudioWorklet',
262
+ },
263
+ addModule: kEnumerableProperty,
264
+ port: kEnumerableProperty,
265
+ });
266
+
267
+ module.exports = AudioWorklet;
268
+
@@ -0,0 +1,371 @@
1
+ const {
2
+ parentPort,
3
+ workerData,
4
+ markAsUntransferable,
5
+ } = require('node:worker_threads');
6
+
7
+ const conversions = require('webidl-conversions');
8
+
9
+ // these are defined in rust side
10
+ const {
11
+ exit_audio_worklet_global_scope,
12
+ run_audio_worklet_global_scope,
13
+ } = require('../load-native.cjs');
14
+
15
+ const {
16
+ workletId,
17
+ sampleRate,
18
+ } = workerData;
19
+
20
+ const kWorkletQueueTask = Symbol.for('node-web-audio-api:worklet-queue-task');
21
+ const kWorkletCallableProcess = Symbol.for('node-web-audio-api:worklet-callable-process');
22
+ const kWorkletInputs = Symbol.for('node-web-audio-api:worklet-inputs');
23
+ const kWorkletOutputs = Symbol.for('node-web-audio-api:worklet-outputs');
24
+ const kWorkletParams = Symbol.for('node-web-audio-api:worklet-params');
25
+ const kWorkletParamsCache = Symbol.for('node-web-audio-api:worklet-params-cache');
26
+ const kWorkletGetBuffer = Symbol.for('node-web-audio-api:worklet-get-buffer');
27
+ const kWorkletRecycleBuffer = Symbol.for('node-web-audio-api:worklet-recycle-buffer');
28
+ const kWorkletRecycleBuffer1 = Symbol.for('node-web-audio-api:worklet-recycle-buffer-1');
29
+ const kWorkletMarkAsUntransferable = Symbol.for('node-web-audio-api:worklet-mark-as-untransferable');
30
+ // const kWorkletOrderedParamNames = Symbol.for('node-web-audio-api:worklet-ordered-param-names');
31
+
32
+
33
+ const nameProcessorCtorMap = new Map();
34
+ const processors = {};
35
+ let pendingProcessorConstructionData = null;
36
+ let loopStarted = false;
37
+ let runLoopImmediateId = null;
38
+
39
+ class BufferPool {
40
+ #bufferSize;
41
+ #pool;
42
+
43
+ constructor(bufferSize, initialPoolSize) {
44
+ this.#bufferSize = bufferSize;
45
+ this.#pool = new Array(initialPoolSize);
46
+
47
+ for (let i = 0; i < this.#pool.length; i++) {
48
+ this.#pool[i] = this.#allocate();
49
+ }
50
+ }
51
+
52
+ #allocate() {
53
+ const float32 = new Float32Array(this.#bufferSize);
54
+ markAsUntransferable(float32);
55
+ // Mark underlying buffer as untransfrable too, this will fail one of
56
+ // the task in `audioworkletprocessor-process-frozen-array.https.html`
57
+ // but prevent segmentation fault
58
+ markAsUntransferable(float32.buffer);
59
+
60
+ return float32;
61
+ }
62
+
63
+ get() {
64
+ if (this.#pool.length === 0) {
65
+ return this.#allocate();
66
+ }
67
+
68
+ return this.#pool.pop();
69
+ }
70
+
71
+ recycle(buffer) {
72
+ // make sure we cannot polute our pool
73
+ if (buffer.length === this.#bufferSize) {
74
+ this.#pool.push(buffer);
75
+ }
76
+ }
77
+ }
78
+
79
+ const renderQuantumSize = 128;
80
+
81
+ const pool128 = new BufferPool(renderQuantumSize, 256);
82
+ const pool1 = new BufferPool(1, 64);
83
+ // allow rust to access some methods required when io layout change
84
+ globalThis[kWorkletGetBuffer] = () => pool128.get();
85
+ globalThis[kWorkletRecycleBuffer] = buffer => pool128.recycle(buffer);
86
+ globalThis[kWorkletRecycleBuffer1] = buffer => pool1.recycle(buffer);
87
+ globalThis[kWorkletMarkAsUntransferable] = obj => {
88
+ markAsUntransferable(obj);
89
+ return obj;
90
+ }
91
+
92
+ function isIterable(obj) {
93
+ // checks for null and undefined
94
+ if (obj === null || obj === undefined) {
95
+ return false;
96
+ }
97
+ return typeof obj[Symbol.iterator] === 'function';
98
+ }
99
+
100
+ // cf. https://stackoverflow.com/a/46759625
101
+ function isConstructor(f) {
102
+ try {
103
+ Reflect.construct(String, [], f);
104
+ } catch (e) {
105
+ return false;
106
+ }
107
+ return true;
108
+ }
109
+
110
+ function runLoop() {
111
+ // block until we need to render a quantum
112
+ run_audio_worklet_global_scope(workletId, processors);
113
+ // yield to the event loop, and then repeat
114
+ runLoopImmediateId = setImmediate(runLoop);
115
+ }
116
+
117
+ globalThis.currentTime = 0
118
+ globalThis.currentFrame = 0;
119
+ globalThis.sampleRate = sampleRate;
120
+ // @todo - implement in upstream crate
121
+ globalThis.renderQuantumSize = renderQuantumSize;
122
+
123
+ globalThis.AudioWorkletProcessor = class AudioWorkletProcessor {
124
+ static get parameterDescriptors() {
125
+ return [];
126
+ }
127
+
128
+ #port = null;
129
+
130
+ constructor() {
131
+ const {
132
+ port,
133
+ numberOfInputs,
134
+ numberOfOutputs,
135
+ parameterDescriptors,
136
+ } = pendingProcessorConstructionData;
137
+
138
+ // Mark [[callable process]] as true, set to false in render quantum
139
+ // either "process" doese not exists, either it throws an error
140
+ this[kWorkletCallableProcess] = true;
141
+
142
+ // Populate with dummy values which will be replaced in first render call
143
+ this[kWorkletInputs] = new Array(numberOfInputs).fill([]);
144
+ this[kWorkletOutputs] = new Array(numberOfOutputs).fill([]);
145
+
146
+ // Object to be reused as `process` parameters argument
147
+ this[kWorkletParams] = {};
148
+ // Cache of 2 Float32Array (of length 128 and 1) for each param, to be reused on
149
+ // each process call according to the size the param for the current render quantum
150
+ this[kWorkletParamsCache] = {};
151
+
152
+ parameterDescriptors.forEach(desc => {
153
+ this[kWorkletParamsCache][desc.name] = [
154
+ pool128.get(), // should be globalThis.renderQuantumSize
155
+ pool1.get(),
156
+ ]
157
+ });
158
+
159
+ this.#port = port;
160
+ }
161
+
162
+ get port() {
163
+ if (!(this instanceof AudioWorkletProcessor)) {
164
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioWorkletProcessor\'');
165
+ }
166
+
167
+ return this.#port;
168
+ }
169
+
170
+ [kWorkletQueueTask](cmd, err) {
171
+ this.#port.postMessage({ cmd, err });
172
+ }
173
+ }
174
+
175
+ // follow algorithm from:
176
+ // https://webaudio.github.io/web-audio-api/#dom-audioworkletglobalscope-registerprocessor
177
+ globalThis.registerProcessor = function registerProcessor(name, processorCtor) {
178
+ const parsedName = conversions['DOMString'](name, {
179
+ context: `Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': name (${name})`,
180
+ });
181
+
182
+ if (parsedName === '') {
183
+ throw new DOMException(`Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': name is empty`, 'NotSupportedError');
184
+ }
185
+
186
+ if (nameProcessorCtorMap.has(name)) {
187
+ throw new DOMException(`Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': A processor with name '${name}' has already been registered in this scope`, 'NotSupportedError');
188
+ }
189
+
190
+ if (!isConstructor(processorCtor)) {
191
+ throw new TypeError(`Cannot execute 'registerProcessor")' in 'AudoWorkletGlobalScope': argument 2 for name '${name}' is not a constructor`);
192
+ }
193
+
194
+ if (typeof processorCtor.prototype !== 'object') {
195
+ throw new TypeError(`Cannot execute 'registerProcessor")' in 'AudoWorkletGlobalScope': argument 2 for name '${name}' is not is not a valid AudioWorkletProcessor`);
196
+ }
197
+
198
+ // must support Array, Set or iterators
199
+ let parameterDescriptorsValue = processorCtor.parameterDescriptors;
200
+
201
+ if (!isIterable(parameterDescriptorsValue)) {
202
+ throw new TypeError(`Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}: 'parameterDescriptors' is not iterable'`);
203
+ }
204
+
205
+ const paramDescriptors = Array.from(parameterDescriptorsValue);
206
+ const parsedParamDescriptors = [];
207
+
208
+ // Parse AudioParamDescriptor sequence
209
+ // cf. https://webaudio.github.io/web-audio-api/#AudioParamDescriptor
210
+ for (let i = 0; i < paramDescriptors.length; i++) {
211
+ const descriptor = paramDescriptors[i];
212
+ const parsedDescriptor = {};
213
+
214
+ if (typeof descriptor !== 'object' || descriptor === null) {
215
+ throw new TypeError(`Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}: Element at index ${i} is not an instance of 'AudioParamDescriptor'`);
216
+ }
217
+
218
+ if (descriptor.name === undefined) {
219
+ throw new TypeError(`Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}: Element at index ${i} is not an instance of 'AudioParamDescriptor'`);
220
+ }
221
+
222
+ parsedDescriptor.name = conversions['DOMString'](descriptor.name, {
223
+ context: `Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}: Invalid 'name' for 'AudioParamDescriptor' at index ${i}`,
224
+ });
225
+
226
+ if (descriptor.defaultValue !== undefined) {
227
+ parsedDescriptor.defaultValue = conversions['float'](descriptor.defaultValue, {
228
+ context: `Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}: Invalid 'defaultValue' for 'AudioParamDescriptor' at index ${i}`,
229
+ });
230
+ } else {
231
+ parsedDescriptor.defaultValue = 0;
232
+ }
233
+
234
+ if (descriptor.maxValue !== undefined) {
235
+ parsedDescriptor.maxValue = conversions['float'](descriptor.maxValue, {
236
+ context: `Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}: Invalid 'maxValue' for 'AudioParamDescriptor' at index ${i}`,
237
+ });
238
+ } else {
239
+ parsedDescriptor.maxValue = 3.4028235e38;
240
+ }
241
+
242
+ if (descriptor.minValue !== undefined) {
243
+ parsedDescriptor.minValue = conversions['float'](descriptor.minValue, {
244
+ context: `Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}: Invalid 'minValue' for 'AudioParamDescriptor' at index ${i}`,
245
+ });
246
+ } else {
247
+ parsedDescriptor.minValue = -3.4028235e38;
248
+ }
249
+
250
+ if (descriptor.automationRate !== undefined) {
251
+ if (!['a-rate', 'k-rate'].includes(descriptor.automationRate)) {
252
+ 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}`);
253
+ }
254
+
255
+ parsedDescriptor.automationRate = conversions['DOMString'](descriptor.automationRate, {
256
+ context: `Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}: The provided value '${descriptor.automationRate}'`,
257
+ });
258
+ } else {
259
+ parsedDescriptor.automationRate = 'a-rate';
260
+ }
261
+
262
+ parsedParamDescriptors.push(parsedDescriptor);
263
+ }
264
+
265
+ // check for duplicate parame names and consistency of min, max and default values
266
+ const paramNames = [];
267
+
268
+ for (let i = 0; i < parsedParamDescriptors.length; i++) {
269
+ const { name, defaultValue, minValue, maxValue } = parsedParamDescriptors[i];
270
+
271
+ if (paramNames.includes(name)) {
272
+ throw new DOMException(`Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}': 'AudioParamDescriptor' with name '${name}' already declared`, 'NotSupportedError');
273
+ }
274
+
275
+ paramNames.push(name);
276
+
277
+ if (!(minValue <= defaultValue && defaultValue <= maxValue)) {
278
+ throw new DOMException(`Cannot execute 'registerProcessor' in 'AudoWorkletGlobalScope': Invalid 'parameterDescriptors' for processor '${name}': The constraint minValue <= defaultValue <= maxValue is not met`, 'InvalidStateError');
279
+ }
280
+ }
281
+
282
+ // store constructor
283
+ nameProcessorCtorMap.set(parsedName, processorCtor);
284
+ // send param descriptors back to main thread
285
+ parentPort.postMessage({
286
+ cmd: 'node-web-audio-api:worlet:processor-registered',
287
+ name: parsedName,
288
+ parameterDescriptors: parsedParamDescriptors,
289
+ });
290
+ };
291
+
292
+
293
+ // @todo - recheck this, not sure this is relevant in our case
294
+ // NOTE: Authors that register an event listener on the "message" event of this
295
+ // port should call close on either end of the MessageChannel (either in the
296
+ // AudioWorklet or the AudioWorkletGlobalScope side) to allow for resources to be collected.
297
+ // parentPort.on('exit', () => {
298
+ // process.stdout.write('closing worklet');
299
+ // });
300
+
301
+ parentPort.on('message', event => {
302
+ switch (event.cmd) {
303
+ case 'node-web-audio-api:worklet:init': {
304
+ const { workletId, processors, promiseId } = event;
305
+ break;
306
+ }
307
+ case 'node-web-audio-api:worklet:exit': {
308
+ clearImmediate(runLoopImmediateId);
309
+ // properly exit audio worklet on rust side
310
+ exit_audio_worklet_global_scope(workletId, processors);
311
+ // exit process
312
+ process.exit(0);
313
+ break;
314
+ }
315
+ case 'node-web-audio-api:worklet:add-module': {
316
+ const { code, promiseId } = event;
317
+ const func = new Function('AudioWorkletProcessor', 'registerProcessor', code);
318
+
319
+ try {
320
+ func(AudioWorkletProcessor, registerProcessor);
321
+ // send registered param descriptors on main thread and resolve Promise
322
+ parentPort.postMessage({
323
+ cmd: 'node-web-audio-api:worklet:module-added',
324
+ promiseId,
325
+ });
326
+ } catch (err) {
327
+ parentPort.postMessage({
328
+ cmd: 'node-web-audio-api:worklet:add-module-failed',
329
+ promiseId,
330
+ ctor: err.constructor.name,
331
+ name: err.name,
332
+ message: err.message,
333
+ });
334
+ }
335
+ break;
336
+ }
337
+ case 'node-web-audio-api:worklet:create-processor': {
338
+ const { name, id, options, port } = event;
339
+ const ctor = nameProcessorCtorMap.get(name);
340
+
341
+ // rewrap options of interest for the AudioWorkletNodeBaseClass
342
+ pendingProcessorConstructionData = {
343
+ port,
344
+ numberOfInputs: options.numberOfInputs,
345
+ numberOfOutputs: options.numberOfOutputs,
346
+ parameterDescriptors: ctor.parameterDescriptors,
347
+ };
348
+
349
+ let instance;
350
+
351
+ try {
352
+ instance = new ctor(options);
353
+ } catch (err) {
354
+ port.postMessage({ cmd: 'node-web-audio-api:worklet:ctor-error', err });
355
+ }
356
+
357
+ pendingProcessorConstructionData = null;
358
+ // store in global so that Rust can match the JS processor
359
+ // with its corresponding NapiAudioWorkletProcessor
360
+ processors[`${id}`] = instance;
361
+ // notify audio worklet back that processor has finished instanciation
362
+ parentPort.postMessage({ cmd: 'node-web-audio-api:worklet:processor-created', id });
363
+
364
+ if (!loopStarted) {
365
+ loopStarted = true;
366
+ setImmediate(runLoop);
367
+ }
368
+ break;
369
+ }
370
+ }
371
+ });