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,292 @@
1
+ /* eslint-disable no-unused-vars */
2
+ const conversions = require('webidl-conversions');
3
+ const {
4
+ toSanitizedSequence,
5
+ } = require('./lib/cast.js');
6
+ const {
7
+ throwSanitizedError,
8
+ } = require('./lib/errors.js');
9
+ const {
10
+ kNapiObj,
11
+ kProcessorRegistered,
12
+ kGetParameterDescriptors,
13
+ kPrivateConstructor,
14
+ kCreateProcessor,
15
+ } = require('./lib/symbols.js');
16
+ const {
17
+ kEnumerableProperty,
18
+ } = require('./lib/utils.js');
19
+ const {
20
+ propagateEvent,
21
+ } = require('./lib/events.js');
22
+ const {
23
+ ErrorEvent,
24
+ } = require('./Events.js');
25
+
26
+ /* eslint-enable no-unused-vars */
27
+
28
+ const AudioNode = require('./AudioNode.js');
29
+ const AudioParamMap = require('./AudioParamMap.js');
30
+ const IMPLEMENTATION_MAX_NUMBER_OF_CHANNELS = 32;
31
+
32
+ module.exports = (jsExport, nativeBinding) => {
33
+ class AudioWorkletNode extends AudioNode {
34
+ #port = null;
35
+ #parameters = {};
36
+
37
+ constructor(context, name, options) {
38
+ if (arguments.length < 2) {
39
+ throw new TypeError(`Failed to construct 'AudioWorkletNode': 2 arguments required, but only ${arguments.length} present`);
40
+ }
41
+
42
+ if (!(context instanceof jsExport.BaseAudioContext)) {
43
+ throw new TypeError(`Failed to construct 'AudioWorkletNode': argument 1 is not of type BaseAudioContext`);
44
+ }
45
+
46
+ const parsedName = conversions['DOMString'](name, {
47
+ context: `Failed to construct 'AudioWorkletNode': The given 'AudioWorkletProcessor' name`,
48
+ });
49
+
50
+ if (!context.audioWorklet[kProcessorRegistered](parsedName)) {
51
+ throw new DOMException(`Failed to construct 'AudioWorkletNode': processor '${parsedName}' is not registered in 'AudioWorklet'`, 'InvalidStateError');
52
+ }
53
+
54
+ // parsed version of the option to be passed to NAPI
55
+ const parsedOptions = {};
56
+
57
+ if (options && (typeof options !== 'object' || options === null)) {
58
+ throw new TypeError('Failed to construct \'AudioWorkletNode\': argument 3 is not of type \'AudioWorkletNodeOptions\'');
59
+ }
60
+
61
+ if (options && options.numberOfInputs !== undefined) {
62
+ parsedOptions.numberOfInputs = conversions['unsigned long'](options.numberOfInputs, {
63
+ enforceRange: true,
64
+ context: `Failed to construct 'AudioWorkletNode': Failed to read the 'numberOfInputs' property from AudioWorkletNodeOptions: The provided value (${options.numberOfInputs}})`,
65
+ });
66
+ } else {
67
+ parsedOptions.numberOfInputs = 1;
68
+ }
69
+
70
+ if (options && options.numberOfOutputs !== undefined) {
71
+ parsedOptions.numberOfOutputs = conversions['unsigned long'](options.numberOfOutputs, {
72
+ enforceRange: true,
73
+ context: `Failed to construct 'AudioWorkletNode': Failed to read the 'numberOfOutputs' property from AudioWorkletNodeOptions: The provided value (${options.numberOfOutputs}})`,
74
+ });
75
+ } else {
76
+ parsedOptions.numberOfOutputs = 1;
77
+ }
78
+
79
+ // If outputChannelCount exists,
80
+ // - If any value in outputChannelCount is zero or greater than the implementation’s maximum number of channels, throw a NotSupportedError and abort the remaining steps.
81
+ // - If the length of outputChannelCount does not equal numberOfOutputs, throw an IndexSizeError and abort the remaining steps.
82
+ // - If both numberOfInputs and numberOfOutputs are 1, set the channel count of the node output to the one value in outputChannelCount.
83
+ // - Otherwise set the channel count of the kth output of the node to the kth element of outputChannelCount sequence and return.
84
+ if (options && options.outputChannelCount !== undefined) {
85
+ try {
86
+ parsedOptions.outputChannelCount = toSanitizedSequence(options.outputChannelCount, Uint32Array);
87
+ } catch (err) {
88
+ throw new TypeError(`Failed to construct 'AudioWorkletNode': Failed to read the 'outputChannelCount' property from AudioWorkletNodeOptions: The provided value ${err.message}`);
89
+ }
90
+
91
+ parsedOptions.outputChannelCount.forEach((value, index) => {
92
+ if (value <= 0 || value > IMPLEMENTATION_MAX_NUMBER_OF_CHANNELS) {
93
+ throw new DOMException(`Failed to construct 'AudioWorkletNode': Invalid 'outputChannelCount' property from AudioWorkletNodeOptions: Value at index ${index} in outside supported range [1, 32]`, 'NotSupportedError');
94
+ }
95
+ });
96
+
97
+ if (parsedOptions.numberOfOutputs !== parsedOptions.outputChannelCount.length) {
98
+ throw new DOMException(`Failed to construct 'AudioWorkletNode': Invalid 'outputChannelCount' property from AudioWorkletNodeOptions: 'outputChannelCount' length (${parsedOptions.outputChannelCount.length}) does not equal 'numberOfOutputs' (${parsedOptions.numberOfOutputs})`, 'IndexSizeError');
99
+ }
100
+ } else {
101
+ // - If both numberOfInputs and numberOfOutputs are 1, set the initial channel count of the node output to 1 and return.
102
+ // NOTE: For this case, the output chanel count will change to computedNumberOfChannels dynamically based on the input and the channelCountMode at runtime.
103
+ if (parsedOptions.numberOfInputs === 1 && parsedOptions.numberOfOutputs === 1) {
104
+ // rust waits for an empty Vec as the special case value
105
+ parsedOptions.outputChannelCount = new Uint32Array(0);
106
+ } else {
107
+ // - Otherwise set the channel count of each output of the node to 1 and return.
108
+ parsedOptions.outputChannelCount = new Uint32Array(parsedOptions.numberOfOutputs);
109
+ parsedOptions.outputChannelCount.fill(1);
110
+ }
111
+ }
112
+
113
+ // @todo
114
+ // - This should be a "record", let's treat it as a raw object of now
115
+ // - Check if this needs to checked against the declared `parameterDescriptors`
116
+ if (options && options.parameterData !== undefined) {
117
+ if (typeof options.parameterData === 'object' && options.parameterData !== null) {
118
+ parsedOptions.parameterData = {};
119
+
120
+ for (let [key, value] in Object.entries(options.parameterData)) {
121
+ const parsedKey = conversions['DOMString'](key, {
122
+ context: `Failed to construct 'AudioWorkletNode': Invalid 'parameterData' property from AudioWorkletNodeOptions: Invalid key (${key})`,
123
+ });
124
+
125
+ const parsedValue = conversions['double'](value, {
126
+ context: `Failed to construct 'AudioWorkletNode': Invalid 'parameterData' property from AudioWorkletNodeOptions: Invalid value for key ${parsedKey}`,
127
+ });
128
+
129
+ parsedOptions.parameterData[parsedKey] = parsedValue;
130
+ }
131
+ } else {
132
+ throw new TypeError(`Failed to construct 'AudioWorkletNode': Invalid 'parameterData' property from AudioWorkletNodeOptions: 'outputChannelCount' length (${parsedOptions.outputChannelCount.length}) does not equal 'numberOfOutputs' (${parsedOptions.numberOfOutputs})`);
133
+ }
134
+ } else {
135
+ parsedOptions.parameterData = {};
136
+ }
137
+
138
+ // These ones are for the JS processor
139
+ if (options && options.processorOptions !== undefined) {
140
+ if (typeof options.processorOptions === 'object' && options.processorOptions !== null) {
141
+ parsedOptions.processorOptions = Object.assign({}, options.processorOptions);
142
+ } else {
143
+ throw new TypeError(`Failed to construct 'AudioWorkletNode': Invalid 'processorOptions' property from AudioWorkletNodeOptions: 'processorOptions' is not an object`);
144
+ }
145
+ } else {
146
+ parsedOptions.processorOptions = {};
147
+ }
148
+
149
+ // AudioNodeOptions
150
+ if (options && options.channelCount !== undefined) {
151
+ parsedOptions.channelCount = conversions['unsigned long'](options.channelCount, {
152
+ enforceRange: true,
153
+ context: `Failed to construct 'AudioWorkletNode': Failed to read the 'channelCount' property from AudioWorkletNodeOptions: The provided value '${options.channelCount}'`,
154
+ });
155
+
156
+ // if we delegate this check to Rust, this can poison a Mutex
157
+ // (probably the `audio_param_descriptor_channel` one)
158
+ if (parsedOptions.channelCount <= 0 || parsedOptions.channelCount > IMPLEMENTATION_MAX_NUMBER_OF_CHANNELS) {
159
+ throw new DOMException(`Failed to construct 'AudioWorkletNode': Invalid 'channelCount' property: Number of channels: ${parsedOptions.channelCount} is outside range [1, 32]`, 'NotSupportedError')
160
+ }
161
+ }
162
+
163
+ if (options && options.channelCountMode !== undefined) {
164
+ if (!['max', 'clamped-max', 'explicit'].includes(options.channelCountMode)) {
165
+ throw new TypeError(`Failed to construct 'AudioWorkletNode': Failed to read the 'channelCountMode' property from 'AudioNodeOptions': The provided value '${options.channelCountMode}' is not a valid enum value of type ChannelCountMode`);
166
+ }
167
+
168
+ parsedOptions.channelCountMode = conversions['DOMString'](options.channelCountMode, {
169
+ context: `Failed to construct 'AudioWorkletNode': Failed to read the 'channelCount' property from AudioWorkletNodeOptions: The provided value '${options.channelCountMode}'`,
170
+ });
171
+ }
172
+
173
+ if (options && options.channelInterpretation !== undefined) {
174
+ if (!['speakers', 'discrete'].includes(options.channelInterpretation)) {
175
+ throw new TypeError(`Failed to construct 'AudioWorkletNode': Failed to read the 'channelInterpretation' property from 'AudioNodeOptions': The provided value '${options.channelInterpretation}' is not a valid enum value of type ChannelCountMode`);
176
+ }
177
+
178
+ parsedOptions.channelInterpretation = conversions['DOMString'](options.channelInterpretation, {
179
+ context: `Failed to construct 'AudioWorkletNode': Failed to read the 'channelInterpretation' property from AudioWorkletNodeOptions: The provided value '${options.channelInterpretation}'`,
180
+ });
181
+ }
182
+
183
+ // Create NapiAudioWorkletNode
184
+ const parameterDescriptors = context.audioWorklet[kGetParameterDescriptors](parsedName);
185
+ let napiObj;
186
+
187
+ try {
188
+ napiObj = new nativeBinding.AudioWorkletNode(
189
+ context[kNapiObj],
190
+ parsedName,
191
+ parsedOptions,
192
+ parameterDescriptors,
193
+ );
194
+ } catch (err) {
195
+ throwSanitizedError(err);
196
+ }
197
+
198
+ super(context, {
199
+ [kNapiObj]: napiObj,
200
+ });
201
+
202
+ let parameters = new Map();
203
+
204
+ for (let name in this[kNapiObj].parameters) {
205
+ const audioParam = new jsExport.AudioParam({
206
+ [kNapiObj]: this[kNapiObj].parameters[name],
207
+ });
208
+
209
+ parameters.set(name, audioParam);
210
+ }
211
+
212
+ this.#parameters = new AudioParamMap({
213
+ [kPrivateConstructor]: true,
214
+ parameters,
215
+ });
216
+
217
+ // Create JS processor
218
+ this.#port = context.audioWorklet[kCreateProcessor](
219
+ parsedName,
220
+ parsedOptions,
221
+ napiObj.id,
222
+ );
223
+
224
+ this.#port.on('message', msg => {
225
+ // ErrorEvent named processorerror
226
+ switch (msg.cmd) {
227
+ case 'node-web-audio-api:worklet:ctor-error': {
228
+ const message = `Failed to construct '${parsedName}' AudioWorkletProcessor: ${msg.err.message}`;
229
+ const event = new ErrorEvent('processorerror', { message, error: msg.err });
230
+ propagateEvent(this, event);
231
+ break;
232
+ }
233
+ case 'node-web-audio-api:worklet:process-invalid': {
234
+ const message = `Failed to execute 'process' on '${parsedName}' AudioWorkletProcessor: ${msg.err.message}`;
235
+ const error = new TypeError(message);
236
+ error.stack = msg.err.stack.replace(msg.err.message, message);
237
+
238
+ const event = new ErrorEvent('processorerror', { message, error });
239
+ propagateEvent(this, event);
240
+ break;
241
+ }
242
+ case 'node-web-audio-api:worklet:process-error': {
243
+ const message = `Failed to execute 'process' on '${parsedName}' AudioWorkletProcessor: ${msg.err.message}`;
244
+ const event = new ErrorEvent('processorerror', { message, error: msg.err });
245
+ propagateEvent(this, event);
246
+ break;
247
+ }
248
+ }
249
+ });
250
+ }
251
+
252
+ get parameters() {
253
+ if (!(this instanceof AudioWorkletNode)) {
254
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioWorkletNode\'');
255
+ }
256
+
257
+ return this.#parameters;
258
+ }
259
+
260
+ get port() {
261
+ if (!(this instanceof AudioWorkletNode)) {
262
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'AudioWorkletNode\'');
263
+ }
264
+
265
+ return this.#port;
266
+ }
267
+ }
268
+
269
+ Object.defineProperties(AudioWorkletNode, {
270
+ length: {
271
+ __proto__: null,
272
+ writable: false,
273
+ enumerable: false,
274
+ configurable: true,
275
+ value: 2,
276
+ },
277
+ });
278
+
279
+ Object.defineProperties(AudioWorkletNode.prototype, {
280
+ [Symbol.toStringTag]: {
281
+ __proto__: null,
282
+ writable: false,
283
+ enumerable: false,
284
+ configurable: true,
285
+ value: 'AudioWorkletNode',
286
+ },
287
+ parameters: kEnumerableProperty,
288
+ port: kEnumerableProperty,
289
+ });
290
+
291
+ return AudioWorkletNode;
292
+ };
@@ -24,12 +24,16 @@ const {
24
24
  } = require('./lib/utils.js');
25
25
  const {
26
26
  kNapiObj,
27
+ kPrivateConstructor,
27
28
  } = require('./lib/symbols.js');
28
29
 
30
+ const AudioWorklet = require('./AudioWorklet.js');
31
+
29
32
  module.exports = (jsExport, _nativeBinding) => {
30
33
  class BaseAudioContext extends EventTarget {
31
- #listener = null;
34
+ #audioWorklet = null;
32
35
  #destination = null;
36
+ #listener = null;
33
37
 
34
38
  constructor(options) {
35
39
  // Make constructor "private"
@@ -47,24 +51,23 @@ module.exports = (jsExport, _nativeBinding) => {
47
51
  ...kHiddenProperty,
48
52
  });
49
53
 
50
- this.#listener = null; // lazily instanciated
54
+ this.#audioWorklet = new AudioWorklet({
55
+ [kPrivateConstructor]: true,
56
+ workletId: this[kNapiObj].workletId,
57
+ sampleRate: this[kNapiObj].sampleRate,
58
+ });
59
+
51
60
  this.#destination = new jsExport.AudioDestinationNode(this, {
52
61
  [kNapiObj]: this[kNapiObj].destination,
53
62
  });
54
63
  }
55
64
 
56
- get listener() {
65
+ get audioWorklet() {
57
66
  if (!(this instanceof BaseAudioContext)) {
58
67
  throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'BaseAudioContext\'');
59
68
  }
60
69
 
61
- if (this.#listener === null) {
62
- this.#listener = new jsExport.AudioListener({
63
- [kNapiObj]: this[kNapiObj].listener,
64
- });
65
- }
66
-
67
- return this.#listener;
70
+ return this.#audioWorklet;
68
71
  }
69
72
 
70
73
  get destination() {
@@ -75,6 +78,20 @@ module.exports = (jsExport, _nativeBinding) => {
75
78
  return this.#destination;
76
79
  }
77
80
 
81
+ get listener() {
82
+ if (!(this instanceof BaseAudioContext)) {
83
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'BaseAudioContext\'');
84
+ }
85
+
86
+ if (this.#listener === null) {
87
+ this.#listener = new jsExport.AudioListener({
88
+ [kNapiObj]: this[kNapiObj].listener,
89
+ });
90
+ }
91
+
92
+ return this.#listener;
93
+ }
94
+
78
95
  get sampleRate() {
79
96
  if (!(this instanceof BaseAudioContext)) {
80
97
  throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'BaseAudioContext\'');
@@ -91,6 +108,15 @@ module.exports = (jsExport, _nativeBinding) => {
91
108
  return this[kNapiObj].currentTime;
92
109
  }
93
110
 
111
+ // @todo - implement in upstream crate + pass to AudioWorkletGlobalScope
112
+ // get renderQuantumSize() {
113
+ // if (!(this instanceof BaseAudioContext)) {
114
+ // throw new TypeError("Invalid Invocation: Value of 'this' must be of type 'BaseAudioContext'");
115
+ // }
116
+
117
+ // return this[kNapiObj].renderQuantumSize;
118
+ // }
119
+
94
120
  get state() {
95
121
  if (!(this instanceof BaseAudioContext)) {
96
122
  throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'BaseAudioContext\'');
@@ -99,9 +125,6 @@ module.exports = (jsExport, _nativeBinding) => {
99
125
  return this[kNapiObj].state;
100
126
  }
101
127
 
102
- // renderQuantumSize
103
- // audioWorklet
104
-
105
128
  get onstatechange() {
106
129
  if (!(this instanceof BaseAudioContext)) {
107
130
  throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'BaseAudioContext\'');
package/js/Events.js CHANGED
@@ -6,8 +6,12 @@ class OfflineAudioCompletionEvent extends Event {
6
6
  constructor(type, eventInitDict) {
7
7
  super(type);
8
8
 
9
- if (typeof eventInitDict !== 'object' || eventInitDict === null || !('renderedBuffer' in eventInitDict)) {
10
- throw TypeError(`Failed to construct 'OfflineAudioCompletionEvent': Failed to read the 'renderedBuffer' property from 'OfflineAudioCompletionEvent': Required member is undefined.`);
9
+ if (
10
+ typeof eventInitDict !== 'object'
11
+ || eventInitDict === null
12
+ || !('renderedBuffer' in eventInitDict)
13
+ ) {
14
+ throw TypeError(`Failed to construct 'OfflineAudioCompletionEvent': Invalid 'OfflineAudioCompletionEventInit' dict given`);
11
15
  }
12
16
 
13
17
  this.#renderedBuffer = eventInitDict.renderedBuffer;
@@ -26,7 +30,6 @@ Object.defineProperties(OfflineAudioCompletionEvent.prototype, {
26
30
  configurable: true,
27
31
  value: 'OfflineAudioCompletionEvent',
28
32
  },
29
-
30
33
  renderedBuffer: kEnumerableProperty,
31
34
  });
32
35
 
@@ -43,7 +46,7 @@ class AudioProcessingEvent extends Event {
43
46
  || !('inputBuffer' in eventInitDict)
44
47
  || !('outputBuffer' in eventInitDict)
45
48
  ) {
46
- throw TypeError(`Failed to construct 'AudioProcessingEvent': Invalid 'AudioProcessingEventInit' given`);
49
+ throw TypeError(`Failed to construct 'AudioProcessingEvent': Invalid 'AudioProcessingEventInit' dict given`);
47
50
  }
48
51
 
49
52
  super(type);
@@ -74,11 +77,154 @@ Object.defineProperties(AudioProcessingEvent.prototype, {
74
77
  configurable: true,
75
78
  value: 'AudioProcessingEvent',
76
79
  },
77
-
78
80
  playbackTime: kEnumerableProperty,
79
81
  inputBuffer: kEnumerableProperty,
80
82
  outputBuffer: kEnumerableProperty,
81
83
  });
82
84
 
85
+ class AudioRenderCapacityEvent extends Event {
86
+ #timestamp = 0;
87
+ #averageLoad = 0;
88
+ #peakLoad = 0;
89
+ #underrunRatio = 0;
90
+
91
+ constructor(type, eventInitDict) {
92
+ if (
93
+ typeof eventInitDict !== 'object'
94
+ || eventInitDict === null
95
+ || !('timestamp' in eventInitDict)
96
+ || !('averageLoad' in eventInitDict)
97
+ || !('peakLoad' in eventInitDict)
98
+ || !('underrunRatio' in eventInitDict)
99
+ ) {
100
+ throw TypeError(`Failed to construct 'AudioRenderCapacityEvent': Invalid 'AudioRenderCapacityEventInit' dict given`);
101
+ }
102
+
103
+ super(type);
104
+
105
+ this.#timestamp = eventInitDict.timestamp;
106
+ this.#averageLoad = eventInitDict.averageLoad;
107
+ this.#peakLoad = eventInitDict.peakLoad;
108
+ this.#underrunRatio = eventInitDict.underrunRatio;
109
+ }
110
+
111
+ get timestamp() {
112
+ return this.#timestamp;
113
+ }
114
+
115
+ get averageLoad() {
116
+ return this.#averageLoad;
117
+ }
118
+
119
+ get peakLoad() {
120
+ return this.#peakLoad;
121
+ }
122
+
123
+ get underrunRatio() {
124
+ return this.#underrunRatio;
125
+ }
126
+ }
127
+
128
+ Object.defineProperties(AudioRenderCapacityEvent.prototype, {
129
+ [Symbol.toStringTag]: {
130
+ __proto__: null,
131
+ writable: false,
132
+ enumerable: false,
133
+ configurable: true,
134
+ value: 'AudioRenderCapacityEvent',
135
+ },
136
+ timestamp: kEnumerableProperty,
137
+ averageLoad: kEnumerableProperty,
138
+ peakLoad: kEnumerableProperty,
139
+ underrunRatio: kEnumerableProperty,
140
+ });
141
+
142
+ // https://html.spec.whatwg.org/multipage/webappapis.html#errorevent
143
+ // interface ErrorEvent : Event {
144
+ // constructor(DOMString type, optional ErrorEventInit eventInitDict = {});
145
+
146
+ // readonly attribute DOMString message;
147
+ // readonly attribute USVString filename;
148
+ // readonly attribute unsigned long lineno;
149
+ // readonly attribute unsigned long colno;
150
+ // readonly attribute any error;
151
+ // };
152
+
153
+ // dictionary ErrorEventInit : EventInit {
154
+ // DOMString message = "";
155
+ // USVString filename = "";
156
+ // unsigned long lineno = 0;
157
+ // unsigned long colno = 0;
158
+ // any error;
159
+ // };
160
+ class ErrorEvent extends Event {
161
+ #message = '';
162
+ #filename = '';
163
+ #lineno = 0;
164
+ #colno = 0;
165
+ #error = undefined;
166
+
167
+ constructor(type, eventInitDict = {}) {
168
+ super(type);
169
+
170
+ if (eventInitDict && typeof eventInitDict.message === 'string') {
171
+ this.#message = eventInitDict.message;
172
+ }
173
+
174
+ if (eventInitDict && typeof eventInitDict.filename === 'string') {
175
+ this.#filename = eventInitDict.filename;
176
+ }
177
+
178
+ if (eventInitDict && Number.isFinite(eventInitDict.lineno)) {
179
+ this.#lineno = eventInitDict.lineno;
180
+ }
181
+
182
+ if (eventInitDict && Number.isFinite(eventInitDict.colno)) {
183
+ this.#colno = eventInitDict.colno;
184
+ }
185
+
186
+ if (eventInitDict && eventInitDict.error instanceof Error) {
187
+ this.#error = eventInitDict.error;
188
+ }
189
+ }
190
+
191
+ get message() {
192
+ return this.#message;
193
+ }
194
+
195
+ get filename() {
196
+ return this.#filename;
197
+ }
198
+
199
+ get lineno() {
200
+ return this.#lineno;
201
+ }
202
+
203
+ get colno() {
204
+ return this.#colno;
205
+ }
206
+
207
+ get error() {
208
+ return this.#error;
209
+ }
210
+ }
211
+
212
+ Object.defineProperties(ErrorEvent.prototype, {
213
+ [Symbol.toStringTag]: {
214
+ __proto__: null,
215
+ writable: false,
216
+ enumerable: false,
217
+ configurable: true,
218
+ value: 'ErrorEvent',
219
+ },
220
+ message: kEnumerableProperty,
221
+ filename: kEnumerableProperty,
222
+ lineno: kEnumerableProperty,
223
+ colno: kEnumerableProperty,
224
+ error: kEnumerableProperty,
225
+ });
226
+
83
227
  module.exports.OfflineAudioCompletionEvent = OfflineAudioCompletionEvent;
84
228
  module.exports.AudioProcessingEvent = AudioProcessingEvent;
229
+ module.exports.AudioRenderCapacityEvent = AudioRenderCapacityEvent;
230
+ module.exports.ErrorEvent = ErrorEvent;
@@ -12,8 +12,10 @@ const {
12
12
  } = require('./lib/utils.js');
13
13
  const {
14
14
  kNapiObj,
15
+ kWorkletRelease,
15
16
  kOnStateChange,
16
17
  kOnComplete,
18
+ kCheckProcessorsCreated,
17
19
  } = require('./lib/symbols.js');
18
20
 
19
21
  module.exports = function patchOfflineAudioContext(jsExport, nativeBinding) {
@@ -80,22 +82,14 @@ module.exports = function patchOfflineAudioContext(jsExport, nativeBinding) {
80
82
 
81
83
  // Add function to Napi object to bridge from Rust events to JS EventTarget
82
84
  // They will be effectively registered on rust side when `startRendering` is called
83
- this[kNapiObj][kOnStateChange] = (err, rawEvent) => {
84
- if (typeof rawEvent !== 'object' && !('type' in rawEvent)) {
85
- throw new TypeError('Invalid [kOnStateChange] Invocation: rawEvent should have a type property');
86
- }
87
-
85
+ this[kNapiObj][kOnStateChange] = (function(err, rawEvent) {
88
86
  const event = new Event(rawEvent.type);
89
87
  propagateEvent(this, event);
90
- };
88
+ }).bind(this);
91
89
 
92
90
  // This event is, per spec, the last trigerred one
93
- this[kNapiObj][kOnComplete] = (err, rawEvent) => {
94
- if (typeof rawEvent !== 'object' && !('type' in rawEvent)) {
95
- throw new TypeError('Invalid [kOnComplete] Invocation: rawEvent should have a type property');
96
- }
97
-
98
- // @fixme: workaround the fact that this event seems to be triggered before
91
+ this[kNapiObj][kOnComplete] = (function(err, rawEvent) {
92
+ // workaround the fact that this event seems to be triggered before
99
93
  // startRendering fulfills and that we want to return the exact same instance
100
94
  if (this.#renderedBuffer === null) {
101
95
  this.#renderedBuffer = new jsExport.AudioBuffer({ [kNapiObj]: rawEvent.renderedBuffer });
@@ -106,7 +100,7 @@ module.exports = function patchOfflineAudioContext(jsExport, nativeBinding) {
106
100
  });
107
101
 
108
102
  propagateEvent(this, event);
109
- };
103
+ }).bind(this);
110
104
  }
111
105
 
112
106
  get length() {
@@ -140,6 +134,9 @@ module.exports = function patchOfflineAudioContext(jsExport, nativeBinding) {
140
134
  throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'`);
141
135
  }
142
136
 
137
+ // ensure all AudioWorkletProcessor have finished their instanciation
138
+ await this.audioWorklet[kCheckProcessorsCreated]();
139
+
143
140
  let nativeAudioBuffer;
144
141
 
145
142
  try {
@@ -148,7 +145,10 @@ module.exports = function patchOfflineAudioContext(jsExport, nativeBinding) {
148
145
  throwSanitizedError(err);
149
146
  }
150
147
 
151
- // @fixme: workaround the fact that this event seems to be triggered before
148
+ // release audio worklet, if any
149
+ await this.audioWorklet[kWorkletRelease]();
150
+
151
+ // workaround the fact that this event seems to be triggered before
152
152
  // startRendering fulfills and that we want to return the exact same instance
153
153
  if (this.#renderedBuffer === null) {
154
154
  this.#renderedBuffer = new jsExport.AudioBuffer({ [kNapiObj]: nativeAudioBuffer });
@@ -107,11 +107,7 @@ module.exports = (jsExport, nativeBinding) => {
107
107
  [kNapiObj]: napiObj,
108
108
  });
109
109
 
110
- this[kNapiObj][kOnAudioProcess] = (err, rawEvent) => {
111
- if (typeof rawEvent !== 'object' && !('type' in rawEvent)) {
112
- throw new TypeError('Invalid [kOnStateChange] Invocation: rawEvent should have a type property');
113
- }
114
-
110
+ this[kNapiObj][kOnAudioProcess] = (function(err, rawEvent) {
115
111
  const audioProcessingEventInit = {
116
112
  playbackTime: rawEvent.playbackTime,
117
113
  inputBuffer: new jsExport.AudioBuffer({ [kNapiObj]: rawEvent.inputBuffer }),
@@ -120,7 +116,7 @@ module.exports = (jsExport, nativeBinding) => {
120
116
 
121
117
  const event = new jsExport.AudioProcessingEvent('audioprocess', audioProcessingEventInit);
122
118
  propagateEvent(this, event);
123
- };
119
+ }).bind(this);
124
120
 
125
121
  this[kNapiObj].listen_to_events();
126
122
  }
package/js/lib/symbols.js CHANGED
@@ -1,6 +1,11 @@
1
1
  module.exports.kNapiObj = Symbol('node-web-audio-api:napi-obj');
2
2
  module.exports.kAudioBuffer = Symbol('node-web-audio-api:audio-buffer');
3
-
3
+ module.exports.kPrivateConstructor = Symbol('node-web-audio-api:private-constructor');
4
+ module.exports.kCreateProcessor = Symbol('node-web-audio-api:create-processor');
5
+ module.exports.kProcessorRegistered = Symbol('node-web-audio-api:processor-registered');
6
+ module.exports.kGetParameterDescriptors = Symbol('node-web-audio-api:get-parameter-descriptors');
7
+ module.exports.kWorkletRelease = Symbol('node-web-audio-api:worklet-release');
8
+ module.exports.kCheckProcessorsCreated = Symbol('node-web-audio-api:check-processor-created');
4
9
 
5
10
  // semi-private keys for events listeners
6
11
 
@@ -17,4 +22,5 @@ module.exports.kOnComplete = Symbol.for('node-web-audio-api:oncomplete');
17
22
  module.exports.kOnEnded = Symbol.for('node-web-audio-api:onended');
18
23
  // # ScriptProcessorNode
19
24
  module.exports.kOnAudioProcess = Symbol.for('node-web-audio-api:onaudioprocess');
20
-
25
+ // # AudioRenderCapacity
26
+ module.exports.kOnUpdate = Symbol.for('node-web-audio-api:onupdate');