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,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
+