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,117 @@
|
|
|
1
|
+
const conversions = require('webidl-conversions');
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
kNapiObj,
|
|
5
|
+
kOnUpdate,
|
|
6
|
+
} = require('./lib/symbols.js');
|
|
7
|
+
const {
|
|
8
|
+
kEnumerableProperty,
|
|
9
|
+
} = require('./lib/utils.js');
|
|
10
|
+
const {
|
|
11
|
+
propagateEvent,
|
|
12
|
+
} = require('./lib/events.js');
|
|
13
|
+
const {
|
|
14
|
+
AudioRenderCapacityEvent,
|
|
15
|
+
} = require('./Events.js');
|
|
16
|
+
|
|
17
|
+
class AudioRenderCapacity extends EventTarget {
|
|
18
|
+
#onupdate = null;
|
|
19
|
+
|
|
20
|
+
constructor(options) {
|
|
21
|
+
// Make constructor "private"
|
|
22
|
+
if (
|
|
23
|
+
(typeof options !== 'object')
|
|
24
|
+
|| !(kNapiObj in options)
|
|
25
|
+
|| options[kNapiObj]['Symbol.toStringTag'] !== 'AudioRenderCapacity'
|
|
26
|
+
) {
|
|
27
|
+
throw new TypeError('Illegal constructor');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
super();
|
|
31
|
+
|
|
32
|
+
this[kNapiObj] = options[kNapiObj];
|
|
33
|
+
|
|
34
|
+
this[kNapiObj][kOnUpdate] = (err, rawEvent) => {
|
|
35
|
+
const event = new AudioRenderCapacityEvent('update', rawEvent);
|
|
36
|
+
propagateEvent(this, event);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
this[kNapiObj].listen_to_events();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get onupdate() {
|
|
43
|
+
if (!(this instanceof AudioRenderCapacity)) {
|
|
44
|
+
throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioRenderCapacity\'');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return this.#onupdate;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
set onupdate(value) {
|
|
51
|
+
if (!(this instanceof AudioRenderCapacity)) {
|
|
52
|
+
throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioRenderCapacity\'');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (isFunction(value) || value === null) {
|
|
56
|
+
this.#onupdate = value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
start(options = null) {
|
|
61
|
+
if (!(this instanceof AudioRenderCapacity)) {
|
|
62
|
+
throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'AudioRenderCapacity'`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let targetOptions = {};
|
|
66
|
+
|
|
67
|
+
if (typeof options === 'object' && options !== null) {
|
|
68
|
+
if (!('updateInterval' in options)) {
|
|
69
|
+
throw new TypeError(`Failed to execute 'start' on 'AudioRenderCapacity': Failed to read the 'updateInterval' property on 'AudioRenderCapacityOptions'`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
targetOptions.updateInterval = conversions['double'](options.updateInterval, {
|
|
73
|
+
context: `Failed to execute 'start' on 'AudioRenderCapacity': Failed to read the 'updateInterval' property on 'AudioRenderCapacityOptions': The provided value ()`
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
targetOptions.updateInterval = 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return this[kNapiObj].start(targetOptions);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
stop() {
|
|
83
|
+
if (!(this instanceof AudioRenderCapacity)) {
|
|
84
|
+
throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'AudioRenderCapacity'`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return this[kNapiObj].start();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
Object.defineProperties(AudioRenderCapacity, {
|
|
92
|
+
length: {
|
|
93
|
+
__proto__: null,
|
|
94
|
+
writable: false,
|
|
95
|
+
enumerable: false,
|
|
96
|
+
configurable: true,
|
|
97
|
+
value: 0,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
Object.defineProperties(AudioRenderCapacity.prototype, {
|
|
102
|
+
[Symbol.toStringTag]: {
|
|
103
|
+
__proto__: null,
|
|
104
|
+
writable: false,
|
|
105
|
+
enumerable: false,
|
|
106
|
+
configurable: true,
|
|
107
|
+
value: 'AudioRenderCapacity',
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
onupdate: kEnumerableProperty,
|
|
111
|
+
stop: kEnumerableProperty,
|
|
112
|
+
stop: kEnumerableProperty,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
module.exports = AudioRenderCapacity;
|
|
116
|
+
|
|
117
|
+
|
|
@@ -3,12 +3,16 @@ const conversions = require('webidl-conversions');
|
|
|
3
3
|
const {
|
|
4
4
|
throwSanitizedError,
|
|
5
5
|
} = require('./lib/errors.js');
|
|
6
|
+
const {
|
|
7
|
+
propagateEvent,
|
|
8
|
+
} = require('./lib/events.js');
|
|
6
9
|
const {
|
|
7
10
|
isFunction,
|
|
8
11
|
kEnumerableProperty,
|
|
9
12
|
} = require('./lib/utils.js');
|
|
10
13
|
const {
|
|
11
14
|
kNapiObj,
|
|
15
|
+
kOnEnded,
|
|
12
16
|
} = require('./lib/symbols.js');
|
|
13
17
|
|
|
14
18
|
const AudioNode = require('./AudioNode.js');
|
|
@@ -26,6 +30,17 @@ class AudioScheduledSourceNode extends AudioNode {
|
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
super(context, options);
|
|
33
|
+
|
|
34
|
+
// Add function to Napi object to bridge from Rust events to JS EventTarget
|
|
35
|
+
// It will be effectively registered on rust side when `start` is called
|
|
36
|
+
this[kNapiObj][kOnEnded] = (err, rawEvent) => {
|
|
37
|
+
if (typeof rawEvent !== 'object' && !('type' in rawEvent)) {
|
|
38
|
+
throw new TypeError('Invalid [kOnEnded] Invocation: rawEvent should have a type property');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const event = new Event(rawEvent.type);
|
|
42
|
+
propagateEvent(this, event);
|
|
43
|
+
};
|
|
29
44
|
}
|
|
30
45
|
|
|
31
46
|
get onended() {
|
|
@@ -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
|
+
|