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
@@ -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\'');
@@ -210,6 +233,20 @@ module.exports = (jsExport, _nativeBinding) => {
210
233
  // --------------------------------------------------------------------
211
234
  // Factory Methods (use the patched AudioNodes)
212
235
  // --------------------------------------------------------------------
236
+ createScriptProcessor(bufferSize = 0, numberOfInputChannels = 2, numberOfOutputChannels = 2) {
237
+ if (!(this instanceof BaseAudioContext)) {
238
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'BaseAudioContext\'');
239
+ }
240
+
241
+ const options = {
242
+ bufferSize,
243
+ numberOfInputChannels,
244
+ numberOfOutputChannels,
245
+ };
246
+
247
+ return new jsExport.ScriptProcessorNode(this, options);
248
+ }
249
+
213
250
  createAnalyser() {
214
251
  if (!(this instanceof BaseAudioContext)) {
215
252
  throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'BaseAudioContext\'');
@@ -389,6 +426,7 @@ module.exports = (jsExport, _nativeBinding) => {
389
426
  configurable: true,
390
427
  value: 'BaseAudioContext',
391
428
  },
429
+ createScriptProcessor: kEnumerableProperty,
392
430
  createAnalyser: kEnumerableProperty,
393
431
  createBufferSource: kEnumerableProperty,
394
432
  createBiquadFilter: kEnumerableProperty,
@@ -33,9 +33,6 @@ const {
33
33
  kNapiObj,
34
34
  kAudioBuffer,
35
35
  } = require('./lib/symbols.js');
36
- const {
37
- bridgeEventTarget,
38
- } = require('./lib/events.js');
39
36
  /* eslint-enable no-unused-vars */
40
37
 
41
38
  const AudioNode = require('./AudioNode.js');
@@ -33,9 +33,6 @@ const {
33
33
  kNapiObj,
34
34
  kAudioBuffer,
35
35
  } = require('./lib/symbols.js');
36
- const {
37
- bridgeEventTarget,
38
- } = require('./lib/events.js');
39
36
  /* eslint-enable no-unused-vars */
40
37
 
41
38
  const AudioNode = require('./AudioNode.js');
@@ -33,9 +33,6 @@ const {
33
33
  kNapiObj,
34
34
  kAudioBuffer,
35
35
  } = require('./lib/symbols.js');
36
- const {
37
- bridgeEventTarget,
38
- } = require('./lib/events.js');
39
36
  /* eslint-enable no-unused-vars */
40
37
 
41
38
  const AudioNode = require('./AudioNode.js');
@@ -33,9 +33,6 @@ const {
33
33
  kNapiObj,
34
34
  kAudioBuffer,
35
35
  } = require('./lib/symbols.js');
36
- const {
37
- bridgeEventTarget,
38
- } = require('./lib/events.js');
39
36
  /* eslint-enable no-unused-vars */
40
37
 
41
38
  const AudioScheduledSourceNode = require('./AudioScheduledSourceNode.js');
@@ -82,9 +79,6 @@ module.exports = (jsExport, nativeBinding) => {
82
79
  [kNapiObj]: napiObj,
83
80
  });
84
81
 
85
- // Bridge Rust native event to Node EventTarget
86
- bridgeEventTarget(this);
87
-
88
82
  this.#offset = new jsExport.AudioParam({
89
83
  [kNapiObj]: this[kNapiObj].offset,
90
84
  });
@@ -33,9 +33,6 @@ const {
33
33
  kNapiObj,
34
34
  kAudioBuffer,
35
35
  } = require('./lib/symbols.js');
36
- const {
37
- bridgeEventTarget,
38
- } = require('./lib/events.js');
39
36
  /* eslint-enable no-unused-vars */
40
37
 
41
38
  const AudioNode = require('./AudioNode.js');
package/js/DelayNode.js CHANGED
@@ -33,9 +33,6 @@ const {
33
33
  kNapiObj,
34
34
  kAudioBuffer,
35
35
  } = require('./lib/symbols.js');
36
- const {
37
- bridgeEventTarget,
38
- } = require('./lib/events.js');
39
36
  /* eslint-enable no-unused-vars */
40
37
 
41
38
  const AudioNode = require('./AudioNode.js');
@@ -33,9 +33,6 @@ const {
33
33
  kNapiObj,
34
34
  kAudioBuffer,
35
35
  } = require('./lib/symbols.js');
36
- const {
37
- bridgeEventTarget,
38
- } = require('./lib/events.js');
39
36
  /* eslint-enable no-unused-vars */
40
37
 
41
38
  const AudioNode = require('./AudioNode.js');
package/js/Events.js ADDED
@@ -0,0 +1,230 @@
1
+ const { kEnumerableProperty } = require('./lib/utils.js');
2
+
3
+ class OfflineAudioCompletionEvent extends Event {
4
+ #renderedBuffer = null;
5
+
6
+ constructor(type, eventInitDict) {
7
+ super(type);
8
+
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`);
15
+ }
16
+
17
+ this.#renderedBuffer = eventInitDict.renderedBuffer;
18
+ }
19
+
20
+ get renderedBuffer() {
21
+ return this.#renderedBuffer;
22
+ }
23
+ }
24
+
25
+ Object.defineProperties(OfflineAudioCompletionEvent.prototype, {
26
+ [Symbol.toStringTag]: {
27
+ __proto__: null,
28
+ writable: false,
29
+ enumerable: false,
30
+ configurable: true,
31
+ value: 'OfflineAudioCompletionEvent',
32
+ },
33
+ renderedBuffer: kEnumerableProperty,
34
+ });
35
+
36
+ class AudioProcessingEvent extends Event {
37
+ #playbackTime = null;
38
+ #inputBuffer = null;
39
+ #outputBuffer = null;
40
+
41
+ constructor(type, eventInitDict) {
42
+ if (
43
+ typeof eventInitDict !== 'object'
44
+ || eventInitDict === null
45
+ || !('playbackTime' in eventInitDict)
46
+ || !('inputBuffer' in eventInitDict)
47
+ || !('outputBuffer' in eventInitDict)
48
+ ) {
49
+ throw TypeError(`Failed to construct 'AudioProcessingEvent': Invalid 'AudioProcessingEventInit' dict given`);
50
+ }
51
+
52
+ super(type);
53
+
54
+ this.#playbackTime = eventInitDict.playbackTime;
55
+ this.#inputBuffer = eventInitDict.inputBuffer;
56
+ this.#outputBuffer = eventInitDict.outputBuffer;
57
+ }
58
+
59
+ get playbackTime() {
60
+ return this.#playbackTime;
61
+ }
62
+
63
+ get inputBuffer() {
64
+ return this.#inputBuffer;
65
+ }
66
+
67
+ get outputBuffer() {
68
+ return this.#outputBuffer;
69
+ }
70
+ }
71
+
72
+ Object.defineProperties(AudioProcessingEvent.prototype, {
73
+ [Symbol.toStringTag]: {
74
+ __proto__: null,
75
+ writable: false,
76
+ enumerable: false,
77
+ configurable: true,
78
+ value: 'AudioProcessingEvent',
79
+ },
80
+ playbackTime: kEnumerableProperty,
81
+ inputBuffer: kEnumerableProperty,
82
+ outputBuffer: kEnumerableProperty,
83
+ });
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
+
227
+ module.exports.OfflineAudioCompletionEvent = OfflineAudioCompletionEvent;
228
+ module.exports.AudioProcessingEvent = AudioProcessingEvent;
229
+ module.exports.AudioRenderCapacityEvent = AudioRenderCapacityEvent;
230
+ module.exports.ErrorEvent = ErrorEvent;
package/js/GainNode.js CHANGED
@@ -33,9 +33,6 @@ const {
33
33
  kNapiObj,
34
34
  kAudioBuffer,
35
35
  } = require('./lib/symbols.js');
36
- const {
37
- bridgeEventTarget,
38
- } = require('./lib/events.js');
39
36
  /* eslint-enable no-unused-vars */
40
37
 
41
38
  const AudioNode = require('./AudioNode.js');
@@ -33,9 +33,6 @@ const {
33
33
  kNapiObj,
34
34
  kAudioBuffer,
35
35
  } = require('./lib/symbols.js');
36
- const {
37
- bridgeEventTarget,
38
- } = require('./lib/events.js');
39
36
  /* eslint-enable no-unused-vars */
40
37
 
41
38
  const AudioNode = require('./AudioNode.js');
@@ -33,9 +33,6 @@ const {
33
33
  kNapiObj,
34
34
  kAudioBuffer,
35
35
  } = require('./lib/symbols.js');
36
- const {
37
- bridgeEventTarget,
38
- } = require('./lib/events.js');
39
36
  /* eslint-enable no-unused-vars */
40
37
 
41
38
  const AudioNode = require('./AudioNode.js');
@@ -1,7 +1,7 @@
1
1
  const conversions = require('webidl-conversions');
2
2
 
3
3
  const {
4
- bridgeEventTarget,
4
+ propagateEvent,
5
5
  } = require('./lib/events.js');
6
6
  const {
7
7
  throwSanitizedError,
@@ -11,19 +11,17 @@ const {
11
11
  kEnumerableProperty,
12
12
  } = require('./lib/utils.js');
13
13
  const {
14
- kNapiObj
14
+ kNapiObj,
15
+ kWorkletRelease,
16
+ kOnStateChange,
17
+ kOnComplete,
18
+ kCheckProcessorsCreated,
15
19
  } = require('./lib/symbols.js');
16
20
 
17
- // constructor(OfflineAudioContextOptions contextOptions);
18
- // constructor(unsigned long numberOfChannels, unsigned long length, float sampleRate);
19
- // Promise<AudioBuffer> startRendering();
20
- // Promise<undefined> resume();
21
- // Promise<undefined> suspend(double suspendTime);
22
- // readonly attribute unsigned long length;
23
- // attribute EventHandler oncomplete;
24
-
25
21
  module.exports = function patchOfflineAudioContext(jsExport, nativeBinding) {
26
22
  class OfflineAudioContext extends jsExport.BaseAudioContext {
23
+ #renderedBuffer = null;
24
+
27
25
  constructor(...args) {
28
26
  if (arguments.length < 1) {
29
27
  throw new TypeError(`Failed to construct 'OfflineAudioContext': 1 argument required, but only ${arguments.length} present`);
@@ -52,7 +50,7 @@ module.exports = function patchOfflineAudioContext(jsExport, nativeBinding) {
52
50
  args = [
53
51
  options.numberOfChannels,
54
52
  options.length,
55
- options.sampleRate
53
+ options.sampleRate,
56
54
  ];
57
55
  }
58
56
 
@@ -60,16 +58,16 @@ module.exports = function patchOfflineAudioContext(jsExport, nativeBinding) {
60
58
 
61
59
  numberOfChannels = conversions['unsigned long'](numberOfChannels, {
62
60
  enforceRange: true,
63
- context: `Failed to construct 'OfflineAudioContext': Failed to read the 'numberOfChannels' property from OfflineContextOptions; The provided value (${numberOfChannels})`
61
+ context: `Failed to construct 'OfflineAudioContext': Failed to read the 'numberOfChannels' property from OfflineContextOptions; The provided value (${numberOfChannels})`,
64
62
  });
65
63
 
66
64
  length = conversions['unsigned long'](length, {
67
65
  enforceRange: true,
68
- context: `Failed to construct 'OfflineAudioContext': Failed to read the 'length' property from OfflineContextOptions; The provided value (${length})`
66
+ context: `Failed to construct 'OfflineAudioContext': Failed to read the 'length' property from OfflineContextOptions; The provided value (${length})`,
69
67
  });
70
68
 
71
69
  sampleRate = conversions['float'](sampleRate, {
72
- context: `Failed to construct 'OfflineAudioContext': Failed to read the 'sampleRate' property from OfflineContextOptions; The provided value (${sampleRate})`
70
+ context: `Failed to construct 'OfflineAudioContext': Failed to read the 'sampleRate' property from OfflineContextOptions; The provided value (${sampleRate})`,
73
71
  });
74
72
 
75
73
  let napiObj;
@@ -81,11 +79,41 @@ module.exports = function patchOfflineAudioContext(jsExport, nativeBinding) {
81
79
  }
82
80
 
83
81
  super({ [kNapiObj]: napiObj });
82
+
83
+ // Add function to Napi object to bridge from Rust events to JS EventTarget
84
+ // They will be effectively registered on rust side when `startRendering` is called
85
+ this[kNapiObj][kOnStateChange] = (err, rawEvent) => {
86
+ if (typeof rawEvent !== 'object' && !('type' in rawEvent)) {
87
+ throw new TypeError('Invalid [kOnStateChange] Invocation: rawEvent should have a type property');
88
+ }
89
+
90
+ const event = new Event(rawEvent.type);
91
+ propagateEvent(this, event);
92
+ };
93
+
94
+ // This event is, per spec, the last trigerred one
95
+ this[kNapiObj][kOnComplete] = (err, rawEvent) => {
96
+ if (typeof rawEvent !== 'object' && !('type' in rawEvent)) {
97
+ throw new TypeError('Invalid [kOnComplete] Invocation: rawEvent should have a type property');
98
+ }
99
+
100
+ // @fixme: workaround the fact that this event seems to be triggered before
101
+ // startRendering fulfills and that we want to return the exact same instance
102
+ if (this.#renderedBuffer === null) {
103
+ this.#renderedBuffer = new jsExport.AudioBuffer({ [kNapiObj]: rawEvent.renderedBuffer });
104
+ }
105
+
106
+ const event = new jsExport.OfflineAudioCompletionEvent(rawEvent.type, {
107
+ renderedBuffer: this.#renderedBuffer,
108
+ });
109
+
110
+ propagateEvent(this, event);
111
+ };
84
112
  }
85
113
 
86
114
  get length() {
87
115
  if (!(this instanceof OfflineAudioContext)) {
88
- throw new TypeError("Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'");
116
+ throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'`);
89
117
  }
90
118
 
91
119
  return this[kNapiObj].length;
@@ -93,7 +121,7 @@ module.exports = function patchOfflineAudioContext(jsExport, nativeBinding) {
93
121
 
94
122
  get oncomplete() {
95
123
  if (!(this instanceof OfflineAudioContext)) {
96
- throw new TypeError("Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'");
124
+ throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'`);
97
125
  }
98
126
 
99
127
  return this._complete || null;
@@ -101,7 +129,7 @@ module.exports = function patchOfflineAudioContext(jsExport, nativeBinding) {
101
129
 
102
130
  set oncomplete(value) {
103
131
  if (!(this instanceof OfflineAudioContext)) {
104
- throw new TypeError("Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'");
132
+ throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'`);
105
133
  }
106
134
 
107
135
  if (isFunction(value) || value === null) {
@@ -111,11 +139,11 @@ module.exports = function patchOfflineAudioContext(jsExport, nativeBinding) {
111
139
 
112
140
  async startRendering() {
113
141
  if (!(this instanceof OfflineAudioContext)) {
114
- throw new TypeError("Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'");
142
+ throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'`);
115
143
  }
116
144
 
117
- // Lazily register event callback on rust side
118
- bridgeEventTarget(this);
145
+ // ensure all AudioWorkletProcessor have finished their instanciation
146
+ await this.audioWorklet[kCheckProcessorsCreated]();
119
147
 
120
148
  let nativeAudioBuffer;
121
149
 
@@ -125,26 +153,21 @@ module.exports = function patchOfflineAudioContext(jsExport, nativeBinding) {
125
153
  throwSanitizedError(err);
126
154
  }
127
155
 
128
- const audioBuffer = new jsExport.AudioBuffer({ [kNapiObj]: nativeAudioBuffer });
129
-
130
- // We dispatch the complete event manually to simplify the sharing of the
131
- // `AudioBuffer` instance. This also simplifies code on the rust side as
132
- // we don't need to deal with the `OfflineAudioCompletionEvent` type.
133
- const event = new Event('complete');
134
- event.renderedBuffer = audioBuffer;
156
+ // release audio worklet, if any
157
+ await this.audioWorklet[kWorkletRelease]();
135
158
 
136
- if (isFunction(this[`oncomplete`])) {
137
- this[`oncomplete`](event);
159
+ // workaround the fact that this event seems to be triggered before
160
+ // startRendering fulfills and that we want to return the exact same instance
161
+ if (this.#renderedBuffer === null) {
162
+ this.#renderedBuffer = new jsExport.AudioBuffer({ [kNapiObj]: nativeAudioBuffer });
138
163
  }
139
164
 
140
- this.dispatchEvent(event);
141
-
142
- return audioBuffer;
165
+ return this.#renderedBuffer;
143
166
  }
144
167
 
145
168
  async resume() {
146
169
  if (!(this instanceof OfflineAudioContext)) {
147
- throw new TypeError("Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'");
170
+ throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'`);
148
171
  }
149
172
 
150
173
  try {
@@ -156,7 +179,7 @@ module.exports = function patchOfflineAudioContext(jsExport, nativeBinding) {
156
179
 
157
180
  async suspend(suspendTime) {
158
181
  if (!(this instanceof OfflineAudioContext)) {
159
- throw new TypeError("Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'");
182
+ throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'`);
160
183
  }
161
184
 
162
185
  if (arguments.length < 1) {
@@ -33,9 +33,6 @@ const {
33
33
  kNapiObj,
34
34
  kAudioBuffer,
35
35
  } = require('./lib/symbols.js');
36
- const {
37
- bridgeEventTarget,
38
- } = require('./lib/events.js');
39
36
  /* eslint-enable no-unused-vars */
40
37
 
41
38
  const AudioScheduledSourceNode = require('./AudioScheduledSourceNode.js');
@@ -140,9 +137,6 @@ module.exports = (jsExport, nativeBinding) => {
140
137
  [kNapiObj]: napiObj,
141
138
  });
142
139
 
143
- // Bridge Rust native event to Node EventTarget
144
- bridgeEventTarget(this);
145
-
146
140
  this.#frequency = new jsExport.AudioParam({
147
141
  [kNapiObj]: this[kNapiObj].frequency,
148
142
  });
package/js/PannerNode.js CHANGED
@@ -33,9 +33,6 @@ const {
33
33
  kNapiObj,
34
34
  kAudioBuffer,
35
35
  } = require('./lib/symbols.js');
36
- const {
37
- bridgeEventTarget,
38
- } = require('./lib/events.js');
39
36
  /* eslint-enable no-unused-vars */
40
37
 
41
38
  const AudioNode = require('./AudioNode.js');