node-web-audio-api 0.20.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 +4 -0
- package/README.md +1 -3
- package/index.cjs +81 -83
- package/index.mjs +5 -0
- package/js/AudioContext.js +16 -8
- package/js/AudioParamMap.js +88 -0
- package/js/AudioRenderCapacity.js +117 -0
- package/js/AudioWorklet.js +261 -0
- package/js/AudioWorkletGlobalScope.js +303 -0
- package/js/AudioWorkletNode.js +290 -0
- package/js/BaseAudioContext.js +36 -13
- package/js/Events.js +151 -5
- package/js/OfflineAudioContext.js +9 -1
- package/js/lib/symbols.js +8 -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 -143
- package/js/monkey-patch.js +0 -84
- package/run-wpt.md +0 -27
- package/simple-test.cjs +0 -20
- package/simple-test.mjs +0 -20
|
@@ -0,0 +1,261 @@
|
|
|
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:worlet:processor-registered': {
|
|
138
|
+
const { name, parameterDescriptors } = event;
|
|
139
|
+
this.#workletParamDescriptorsMap.set(name, parameterDescriptors);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
case 'node-web-audio-api:worklet:processor-created': {
|
|
143
|
+
const { id } = event;
|
|
144
|
+
this.#pendingCreateProcessors.delete(id);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
get port() {
|
|
152
|
+
return this.#port;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async addModule(moduleUrl) {
|
|
156
|
+
const code = await resolveModule(moduleUrl);
|
|
157
|
+
|
|
158
|
+
// launch Worker if not exists
|
|
159
|
+
if (!this.#port) {
|
|
160
|
+
await new Promise(resolve => {
|
|
161
|
+
const workletPathname = path.join(__dirname, 'AudioWorkletGlobalScope.js');
|
|
162
|
+
this.#port = new Worker(workletPathname, {
|
|
163
|
+
workerData: {
|
|
164
|
+
workletId: this.#workletId,
|
|
165
|
+
sampleRate: this.#sampleRate,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
this.#port.on('online', resolve);
|
|
169
|
+
|
|
170
|
+
this.#bindEvents();
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const promiseId = this.#promiseId++;
|
|
175
|
+
// This promise is resolved when the Worker returns the name and
|
|
176
|
+
// parameterDescriptors from the added module
|
|
177
|
+
await new Promise((resolve, reject) => {
|
|
178
|
+
this.#idPromiseMap.set(promiseId, { resolve, reject });
|
|
179
|
+
|
|
180
|
+
this.#port.postMessage({
|
|
181
|
+
cmd: 'node-web-audio-api:worklet:add-module',
|
|
182
|
+
code,
|
|
183
|
+
promiseId,
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// For OfflineAudioContext only, check that all processors have been properly
|
|
189
|
+
// created before actual `startRendering`
|
|
190
|
+
async [kCheckProcessorsCreated]() {
|
|
191
|
+
// console.log(this.#pendingCreateProcessors);
|
|
192
|
+
return new Promise(async resolve => {
|
|
193
|
+
while (this.#pendingCreateProcessors.size !== 0) {
|
|
194
|
+
// we need a microtask to ensure message can be received
|
|
195
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
resolve();
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
[kProcessorRegistered](name) {
|
|
203
|
+
return Array.from(this.#workletParamDescriptorsMap.keys()).includes(name);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
[kGetParameterDescriptors](name) {
|
|
207
|
+
return this.#workletParamDescriptorsMap.get(name);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
[kCreateProcessor](name, options, id) {
|
|
211
|
+
this.#pendingCreateProcessors.add(id);
|
|
212
|
+
|
|
213
|
+
const { port1, port2 } = new MessageChannel();
|
|
214
|
+
// @todo - check if some processorOptions must be transfered as well
|
|
215
|
+
this.#port.postMessage({
|
|
216
|
+
cmd: 'node-web-audio-api:worklet:create-processor',
|
|
217
|
+
name,
|
|
218
|
+
id,
|
|
219
|
+
options,
|
|
220
|
+
port: port2,
|
|
221
|
+
}, [port2]);
|
|
222
|
+
|
|
223
|
+
return port1;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async [kWorkletRelease]() {
|
|
227
|
+
if (this.#port) {
|
|
228
|
+
await new Promise(resolve => {
|
|
229
|
+
this.#port.on('exit', resolve);
|
|
230
|
+
this.#port.postMessage({
|
|
231
|
+
cmd: 'node-web-audio-api:worklet:exit',
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
Object.defineProperties(AudioWorklet, {
|
|
239
|
+
length: {
|
|
240
|
+
__proto__: null,
|
|
241
|
+
writable: false,
|
|
242
|
+
enumerable: false,
|
|
243
|
+
configurable: true,
|
|
244
|
+
value: 0,
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
Object.defineProperties(AudioWorklet.prototype, {
|
|
249
|
+
[Symbol.toStringTag]: {
|
|
250
|
+
__proto__: null,
|
|
251
|
+
writable: false,
|
|
252
|
+
enumerable: false,
|
|
253
|
+
configurable: true,
|
|
254
|
+
value: 'AudioWorklet',
|
|
255
|
+
},
|
|
256
|
+
addModule: kEnumerableProperty,
|
|
257
|
+
port: kEnumerableProperty,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
module.exports = AudioWorklet;
|
|
261
|
+
|
|
@@ -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
|
+
});
|