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.
- package/CHANGELOG.md +10 -0
- package/README.md +39 -22
- package/index.cjs +81 -83
- package/index.mjs +5 -0
- package/js/AudioContext.js +25 -21
- package/js/AudioParamMap.js +88 -0
- package/js/AudioRenderCapacity.js +117 -0
- package/js/AudioScheduledSourceNode.js +7 -6
- package/js/AudioWorklet.js +268 -0
- package/js/AudioWorkletGlobalScope.js +371 -0
- package/js/AudioWorkletNode.js +292 -0
- package/js/BaseAudioContext.js +36 -13
- package/js/Events.js +151 -5
- package/js/OfflineAudioContext.js +14 -14
- package/js/ScriptProcessorNode.js +2 -6
- package/js/lib/symbols.js +8 -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 +4 -2
- package/TODOS.md +0 -143
- package/js/monkey-patch.js +0 -84
- package/run-wpt.md +0 -27
- package/simple-test.cjs +0 -20
- package/simple-test.mjs +0 -20
|
@@ -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
|
+
};
|
package/js/BaseAudioContext.js
CHANGED
|
@@ -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
|
-
#
|
|
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.#
|
|
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
|
|
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
|
-
|
|
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 (
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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');
|