node-web-audio-api 0.18.0 → 0.20.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 (51) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/TODOS.md +134 -12
  3. package/index.mjs +17 -6
  4. package/js/AnalyserNode.js +259 -48
  5. package/js/AudioBuffer.js +243 -0
  6. package/js/AudioBufferSourceNode.js +259 -41
  7. package/js/AudioContext.js +294 -28
  8. package/js/AudioDestinationNode.js +42 -100
  9. package/js/AudioListener.js +219 -0
  10. package/js/AudioNode.js +323 -0
  11. package/js/AudioParam.js +252 -39
  12. package/js/AudioScheduledSourceNode.js +120 -0
  13. package/js/BaseAudioContext.js +434 -0
  14. package/js/BiquadFilterNode.js +218 -29
  15. package/js/ChannelMergerNode.js +93 -22
  16. package/js/ChannelSplitterNode.js +93 -22
  17. package/js/ConstantSourceNode.js +86 -26
  18. package/js/ConvolverNode.js +158 -29
  19. package/js/DelayNode.js +112 -21
  20. package/js/DynamicsCompressorNode.js +195 -27
  21. package/js/Events.js +84 -0
  22. package/js/GainNode.js +104 -21
  23. package/js/IIRFilterNode.js +136 -23
  24. package/js/MediaStreamAudioSourceNode.js +80 -24
  25. package/js/OfflineAudioContext.js +198 -35
  26. package/js/OscillatorNode.js +189 -32
  27. package/js/PannerNode.js +458 -56
  28. package/js/PeriodicWave.js +67 -3
  29. package/js/ScriptProcessorNode.js +179 -0
  30. package/js/StereoPannerNode.js +104 -21
  31. package/js/WaveShaperNode.js +144 -29
  32. package/js/lib/cast.js +19 -0
  33. package/js/lib/errors.js +10 -55
  34. package/js/lib/events.js +10 -0
  35. package/js/lib/symbols.js +20 -0
  36. package/js/lib/utils.js +12 -12
  37. package/js/monkey-patch.js +40 -31
  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 +7 -4
  46. package/run-wpt.md +27 -0
  47. package/run-wpt.sh +5 -0
  48. package/js/AudioNode.mixin.js +0 -132
  49. package/js/AudioScheduledSourceNode.mixin.js +0 -67
  50. package/js/BaseAudioContext.mixin.js +0 -154
  51. package/js/EventTarget.mixin.js +0 -60
@@ -1,60 +1,326 @@
1
- let contextId = 0;
1
+ const conversions = require('webidl-conversions');
2
+
3
+ const {
4
+ throwSanitizedError,
5
+ } = require('./lib/errors.js');
6
+ const {
7
+ isFunction,
8
+ kEnumerableProperty,
9
+ } = require('./lib/utils.js');
10
+ const {
11
+ kNapiObj,
12
+ kOnStateChange,
13
+ kOnSinkChange,
14
+ } = require('./lib/symbols.js');
15
+ const {
16
+ propagateEvent,
17
+ } = require('./lib/events.js');
2
18
 
3
- const kProcessId = Symbol('processId');
4
- const kKeepAwakeId = Symbol('keepAwakeId');
19
+ let contextId = 0;
5
20
 
6
- module.exports = function(bindings) {
7
- const {
8
- MediaStreamAudioSourceNode,
9
- } = bindings;
21
+ module.exports = function(jsExport, nativeBinding) {
10
22
 
11
- const EventTarget = require('./EventTarget.mixin.js')(bindings.AudioContext, ['statechange', 'sinkchange']);
12
- const BaseAudioContext = require('./BaseAudioContext.mixin.js')(EventTarget, bindings);
23
+ class AudioContext extends jsExport.BaseAudioContext {
24
+ #sinkId = '';
13
25
 
14
- class AudioContext extends BaseAudioContext {
15
- // class AudioContext extends NativeAudioContext {
16
26
  constructor(options = {}) {
17
- super(options);
18
- // EventTargetMixin has been called so EventTargetMixin[kDispatchEvent] is
19
- // bound to this, then we can safely finalize event target initialization
20
- super.__initEventTarget__();
27
+ if (typeof options !== 'object') {
28
+ throw new TypeError(`Failed to construct 'AudioContext': The provided value is not of type 'AudioContextOptions'`);
29
+ }
30
+
31
+ let targetOptions = {};
32
+
33
+ if (options.latencyHint !== undefined) {
34
+ if (['balanced', 'interactive', 'playback'].includes(options.latencyHint)) {
35
+ targetOptions.latencyHint = conversions['DOMString'](options.latencyHint);
36
+ } else {
37
+ targetOptions.latencyHint = conversions['double'](options.latencyHint, {
38
+ context: `Failed to construct 'AudioContext': Failed to read the 'sinkId' property from AudioNodeOptions: The provided value (${options.latencyHint})`,
39
+ });
40
+ }
41
+ } else {
42
+ targetOptions.latencyHint = 'interactive';
43
+ }
44
+
45
+ if (options.sampleRate !== undefined) {
46
+ targetOptions.sampleRate = conversions['float'](options.sampleRate, {
47
+ context: `Failed to construct 'AudioContext': Failed to read the 'sinkId' property from AudioNodeOptions: The provided value (${options.sampleRate})`,
48
+ });
49
+ } else {
50
+ targetOptions.sampleRate = null;
51
+ }
52
+
53
+ if (options.sinkId !== undefined) {
54
+ const sinkId = options.sinkId;
55
+
56
+ if (typeof options.sinkId === 'object') {
57
+ // https://webaudio.github.io/web-audio-api/#enumdef-audiosinktype
58
+ if (!('type' in options.sinkId) || options.sinkId.type !== 'none') {
59
+ throw TypeError(`Failed to construct 'AudioContext': Failed to read the 'sinkId' property from AudioNodeOptions: Failed to read the 'type' property from 'AudioSinkOptions': The provided value (${sinkId.type}) is not a valid enum value of type AudioSinkType.`);
60
+ }
61
+
62
+ targetOptions.sinkId = 'none';
63
+ } else {
64
+ targetOptions.sinkId = conversions['DOMString'](sinkId, {
65
+ context: `Failed to construct 'AudioContext': Failed to read the 'sinkId' property from AudioNodeOptions: Failed to read the 'type' property from 'AudioSinkOptions': The provided value (${sinkId})`,
66
+ });
67
+ }
68
+ } else {
69
+ targetOptions.sinkId = '';
70
+ }
71
+
72
+ let napiObj;
73
+
74
+ try {
75
+ napiObj = new nativeBinding.AudioContext(targetOptions);
76
+ } catch (err) {
77
+ throwSanitizedError(err);
78
+ }
79
+
80
+ super({ [kNapiObj]: napiObj });
81
+
82
+ if (options.sinkId !== undefined) {
83
+ this.#sinkId = options.sinkId;
84
+ }
85
+
86
+ // Add function to Napi object to bridge from Rust events to JS EventTarget
87
+ this[kNapiObj][kOnStateChange] = (err, rawEvent) => {
88
+ if (typeof rawEvent !== 'object' && !('type' in rawEvent)) {
89
+ throw new TypeError('Invalid [kOnStateChange] Invocation: rawEvent should have a type property');
90
+ }
91
+
92
+ const event = new Event(rawEvent.type);
93
+ propagateEvent(this, event);
94
+ };
21
95
 
96
+ this[kNapiObj][kOnSinkChange] = (err, rawEvent) => {
97
+ if (typeof rawEvent !== 'object' && !('type' in rawEvent)) {
98
+ throw new TypeError('Invalid [kOnSinkChange] Invocation: rawEvent should have a type property');
99
+ }
100
+
101
+ const event = new Event(rawEvent.type);
102
+ propagateEvent(this, event);
103
+ };
104
+
105
+ // Workaround to bind the `sinkchange` and `statechange` events to EventTarget.
106
+ // This must be called from JS facade ctor as the JS handler are added to the Napi
107
+ // object after its instantiation, and that we don't have any initial `resume` call.
108
+ this[kNapiObj].listen_to_events();
109
+
110
+ // @todo - check if this is still required
22
111
  // prevent garbage collection and process exit
23
112
  const id = contextId++;
24
113
  // store in process to prevent garbage collection
25
- const processId = Symbol(`__AudioContext_${id}`);
26
- process[processId] = this;
27
- // keep process symbol around to delete later
28
- this[kProcessId] = processId;
114
+ const kAudioContextId = Symbol(`node-web-audio-api:audio-context-${id}`);
115
+ Object.defineProperty(process, kAudioContextId, {
116
+ __proto__: null,
117
+ enumerable: false,
118
+ configurable: true,
119
+ value: this,
120
+ });
29
121
  // keep process awake until context is closed
30
122
  const keepAwakeId = setInterval(() => {}, 10 * 1000);
31
- this[kKeepAwakeId] = keepAwakeId;
32
123
 
33
124
  // clear on close
34
125
  this.addEventListener('statechange', () => {
35
126
  if (this.state === 'closed') {
36
127
  // allow to garbage collect the context and to the close the process
37
- delete process[this[kProcessId]];
38
- clearTimeout(this[kKeepAwakeId]);
128
+ delete process[kAudioContextId];
129
+ clearTimeout(keepAwakeId);
39
130
  }
40
131
  });
41
132
  }
42
133
 
43
- setSinkId(sinkId) {
134
+ get baseLatency() {
135
+ if (!(this instanceof AudioContext)) {
136
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioContext\'');
137
+ }
138
+
139
+ return this[kNapiObj].baseLatency;
140
+ }
141
+
142
+ get outputLatency() {
143
+ if (!(this instanceof AudioContext)) {
144
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioContext\'');
145
+ }
146
+
147
+ return this[kNapiObj].outputLatency;
148
+ }
149
+
150
+ get sinkId() {
151
+ if (!(this instanceof AudioContext)) {
152
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioContext\'');
153
+ }
154
+
155
+ return this.#sinkId;
156
+ }
157
+
158
+ get renderCapacity() {
159
+ if (!(this instanceof AudioContext)) {
160
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioContext\'');
161
+ }
162
+
163
+ throw new Error(`AudioContext::renderCapacity is not yet implemented`);
164
+ }
165
+
166
+ get onsinkchange() {
167
+ if (!(this instanceof AudioContext)) {
168
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioContext\'');
169
+ }
170
+
171
+ return this._sinkchange || null;
172
+ }
173
+
174
+ set onsinkchange(value) {
175
+ if (!(this instanceof AudioContext)) {
176
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioContext\'');
177
+ }
178
+
179
+ if (isFunction(value) || value === null) {
180
+ this._sinkchange = value;
181
+ }
182
+ }
183
+
184
+ getOutputTimestamp() {
185
+ if (!(this instanceof AudioContext)) {
186
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioContext\'');
187
+ }
188
+
189
+ throw new Error(`AudioContext::getOutputTimestamp is not yet implemented`);
190
+ }
191
+
192
+ async resume() {
193
+ if (!(this instanceof AudioContext)) {
194
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioContext\'');
195
+ }
196
+
197
+ await this[kNapiObj].resume();
198
+ }
199
+
200
+ async suspend() {
201
+ if (!(this instanceof AudioContext)) {
202
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioContext\'');
203
+ }
204
+
205
+ await this[kNapiObj].suspend();
206
+ }
207
+
208
+ async close() {
209
+ if (!(this instanceof AudioContext)) {
210
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioContext\'');
211
+ }
212
+
213
+ await this[kNapiObj].close();
214
+ }
215
+
216
+ async setSinkId(sinkId) {
217
+ if (!(this instanceof AudioContext)) {
218
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioContext\'');
219
+ }
220
+
221
+ if (arguments.length < 1) {
222
+ throw new TypeError(`Failed to execute 'setSinkId' on 'AudioContext': 1 argument required, but only ${arguments.length} present`);
223
+ }
224
+
225
+ let targetSinkId = '';
226
+
227
+ if (typeof sinkId === 'object') {
228
+ if (!('type' in sinkId) || sinkId.type !== 'none') {
229
+ throw new TypeError(`Failed to execute 'setSinkId' on 'AudioContext': Failed to read the 'type' property from 'AudioSinkOptions': The provided value '${sinkId.type}' is not a valid enum value of type AudioSinkType.`);
230
+ }
231
+
232
+ targetSinkId = 'none';
233
+ } else {
234
+ targetSinkId = conversions['DOMString'](sinkId, {
235
+ context: `Failed to execute 'setSinkId' on 'AudioContext': Failed to read the 'type' property from 'AudioSinkOptions': The provided value '${sinkId.type}'`,
236
+ });
237
+ }
238
+
239
+ this.#sinkId = sinkId;
240
+
44
241
  try {
45
- super.setSinkId(sinkId);
46
- return Promise.resolve(undefined);
242
+ this[kNapiObj].setSinkId(targetSinkId);
47
243
  } catch (err) {
48
- return Promise.reject(err);
244
+ throwSanitizedError(err);
49
245
  }
50
246
  }
51
247
 
52
248
  // online context only AudioNodes
53
249
  createMediaStreamSource(mediaStream) {
54
- const options = { mediaStream };
55
- return new MediaStreamAudioSourceNode(this, options);
250
+ if (!(this instanceof AudioContext)) {
251
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioContext\'');
252
+ }
253
+
254
+ if (arguments.length < 1) {
255
+ throw new TypeError(`Failed to execute 'createMediaStreamSource' on 'AudioContext': 1 argument required, but only ${arguments.length} present`);
256
+ }
257
+
258
+ const options = {
259
+ mediaStream,
260
+ };
261
+
262
+ return new jsExport.MediaStreamAudioSourceNode(this, options);
263
+ }
264
+
265
+ createMediaElementSource() {
266
+ if (!(this instanceof AudioContext)) {
267
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioContext\'');
268
+ }
269
+
270
+ throw new Error(`AudioContext::createMediaElementSource() is not yet implemented, cf. https://github.com/ircam-ismm/node-web-audio-api/issues/91 for more information`);
271
+ }
272
+
273
+ createMediaStreamTrackSource() {
274
+ if (!(this instanceof AudioContext)) {
275
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioContext\'');
276
+ }
277
+
278
+ throw new Error(`AudioContext::createMediaStreamTrackSource() is not yet implemented, cf. https://github.com/ircam-ismm/node-web-audio-api/issues/91 for more information`);
279
+ }
280
+
281
+ createMediaStreamDestination() {
282
+ if (!(this instanceof AudioContext)) {
283
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioContext\'');
284
+ }
285
+
286
+ throw new Error(`AudioContext::createMediaStreamDestination() is not yet implemented, cf. https://github.com/ircam-ismm/node-web-audio-api/issues/91 for more information`);
56
287
  }
57
288
  }
58
289
 
290
+ Object.defineProperties(AudioContext, {
291
+ length: {
292
+ __proto__: null,
293
+ writable: false,
294
+ enumerable: false,
295
+ configurable: true,
296
+ value: 0,
297
+ },
298
+ });
299
+
300
+ Object.defineProperties(AudioContext.prototype, {
301
+ [Symbol.toStringTag]: {
302
+ __proto__: null,
303
+ writable: false,
304
+ enumerable: false,
305
+ configurable: true,
306
+ value: 'AudioContext',
307
+ },
308
+
309
+ baseLatency: kEnumerableProperty,
310
+ outputLatency: kEnumerableProperty,
311
+ sinkId: kEnumerableProperty,
312
+ renderCapacity: kEnumerableProperty,
313
+ onsinkchange: kEnumerableProperty,
314
+ getOutputTimestamp: kEnumerableProperty,
315
+ resume: kEnumerableProperty,
316
+ suspend: kEnumerableProperty,
317
+ close: kEnumerableProperty,
318
+ setSinkId: kEnumerableProperty,
319
+ createMediaStreamSource: kEnumerableProperty,
320
+ createMediaElementSource: kEnumerableProperty,
321
+ createMediaStreamTrackSource: kEnumerableProperty,
322
+ createMediaStreamDestination: kEnumerableProperty,
323
+ });
324
+
59
325
  return AudioContext;
60
326
  };
@@ -1,111 +1,53 @@
1
- // @note - This should be reviewed (but how...?)
2
- //
3
- // We can't really use the AudioNode mixin because we need to wrap
4
- // the native destination instance.
1
+ const { kNapiObj } = require('./lib/symbols.js');
2
+ const { kEnumerableProperty } = require('./lib/utils.js');
3
+ const AudioNode = require('./AudioNode.js');
5
4
 
6
- const { throwSanitizedError } = require('./lib/errors.js');
7
- const { AudioParam, kNativeAudioParam } = require('./AudioParam.js');
8
- const kNativeAudioDestinationNode = Symbol('node-web-audio-api:audio-destination-node');
9
-
10
- class AudioDestinationNode {
11
- constructor(nativeAudioDestinationNode) {
12
- this[kNativeAudioDestinationNode] = nativeAudioDestinationNode;
13
- }
14
-
15
- // AudioNode interface
16
- get context() {
17
- return this[kNativeAudioDestinationNode].context;
18
- }
19
-
20
- get numberOfInputs() {
21
- return this[kNativeAudioDestinationNode].numberOfInputs;
22
- }
23
-
24
- get numberOfOutputs() {
25
- return this[kNativeAudioDestinationNode].numberOfOutputs;
26
- }
27
-
28
- get channelCount() {
29
- return this[kNativeAudioDestinationNode].channelCount;
30
- }
31
-
32
- get channelCountMode() {
33
- return this[kNativeAudioDestinationNode].channelCountMode;
34
- }
35
-
36
- get channelInterpretation() {
37
- return this[kNativeAudioDestinationNode].channelInterpretation;
38
- }
39
-
40
- // setters
41
-
42
- set channelCount(value) {
43
- try {
44
- this[kNativeAudioDestinationNode].channelCount = value;
45
- } catch (err) {
46
- throwSanitizedError(err);
47
- }
48
- }
49
-
50
- set channelCountMode(value) {
51
- try {
52
- this[kNativeAudioDestinationNode].channelCountMode = value;
53
- } catch (err) {
54
- throwSanitizedError(err);
55
- }
56
- }
57
-
58
- set channelInterpretation(value) {
59
- try {
60
- this[kNativeAudioDestinationNode].channelInterpretation = value;
61
- } catch (err) {
62
- throwSanitizedError(err);
63
- }
64
- }
65
-
66
- // methods - connect / disconnect
67
-
68
- connect(...args) {
69
- // unwrap raw audio params from facade
70
- if (args[0] instanceof AudioParam) {
71
- args[0] = args[0][kNativeAudioParam];
72
- }
73
-
74
- // unwrap raw audio destination from facade
75
- if (args[0] instanceof AudioDestinationNode) {
76
- args[0] = args[0][kNativeAudioDestinationNode];
5
+ class AudioDestinationNode extends AudioNode {
6
+ constructor(context, options) {
7
+ // Make constructor "private"
8
+ if (
9
+ (typeof options !== 'object')
10
+ || !(kNapiObj in options)
11
+ || options[kNapiObj]['Symbol.toStringTag'] !== 'AudioDestinationNode'
12
+ ) {
13
+ throw new TypeError('Illegal constructor');
77
14
  }
78
15
 
79
- try {
80
- return this[kNativeAudioDestinationNode].connect(...args);
81
- } catch (err) {
82
- throwSanitizedError(err);
83
- }
16
+ super(context, {
17
+ [kNapiObj]: options[kNapiObj],
18
+ });
84
19
  }
85
20
 
86
- disconnect(...args) {
87
- // unwrap raw audio params from facade
88
- if (args[0] instanceof AudioParam) {
89
- args[0] = args[0][kNativeAudioParam];
90
- }
91
-
92
- // unwrap raw audio destination from facade
93
- if (args[0] instanceof AudioDestinationNode) {
94
- args[0] = args[0][kNativeAudioDestinationNode];
95
- }
96
-
97
- try {
98
- return this[kNativeAudioDestinationNode].disconnect(...args);
99
- } catch (err) {
100
- throwSanitizedError(err);
21
+ get maxChannelCount() {
22
+ if (!(this instanceof AudioDestinationNode)) {
23
+ throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'AudioDestinationNode'`);
101
24
  }
102
- }
103
25
 
104
- get maxChannelCount() {
105
- return this[kNativeAudioDestinationNode].maxChannelCount;
26
+ return this[kNapiObj].maxChannelCount;
106
27
  }
107
28
  }
108
29
 
109
- module.exports.kNativeAudioDestinationNode = kNativeAudioDestinationNode;
110
- module.exports.AudioDestinationNode = AudioDestinationNode;
30
+ Object.defineProperties(AudioDestinationNode, {
31
+ length: {
32
+ __proto__: null,
33
+ writable: false,
34
+ enumerable: false,
35
+ configurable: true,
36
+ value: 0,
37
+ },
38
+ });
39
+
40
+ Object.defineProperties(AudioDestinationNode.prototype, {
41
+ [Symbol.toStringTag]: {
42
+ __proto__: null,
43
+ writable: false,
44
+ enumerable: false,
45
+ configurable: true,
46
+ value: 'AudioDestinationNode',
47
+ },
48
+
49
+ maxChannelCount: kEnumerableProperty,
50
+ });
51
+
52
+ module.exports = AudioDestinationNode;
111
53