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,57 +1,220 @@
1
- const { nameCodeMap, DOMException } = require('./lib/errors.js');
2
- const { isPlainObject, isPositiveInt, isPositiveNumber } = require('./lib/utils.js');
1
+ const conversions = require('webidl-conversions');
3
2
 
4
- module.exports = function patchOfflineAudioContext(bindings) {
5
- // @todo - EventTarget
6
- // - https://github.com/orottier/web-audio-api-rs/issues/411
7
- // - https://github.com/orottier/web-audio-api-rs/issues/416
3
+ const {
4
+ propagateEvent,
5
+ } = require('./lib/events.js');
6
+ const {
7
+ throwSanitizedError,
8
+ } = require('./lib/errors.js');
9
+ const {
10
+ isFunction,
11
+ kEnumerableProperty,
12
+ } = require('./lib/utils.js');
13
+ const {
14
+ kNapiObj,
15
+ kOnStateChange,
16
+ kOnComplete,
17
+ } = require('./lib/symbols.js');
8
18
 
9
- const EventTarget = require('./EventTarget.mixin.js')(bindings.OfflineAudioContext, ['statechange']);
10
- const BaseAudioContext = require('./BaseAudioContext.mixin.js')(EventTarget, bindings);
19
+ module.exports = function patchOfflineAudioContext(jsExport, nativeBinding) {
20
+ class OfflineAudioContext extends jsExport.BaseAudioContext {
21
+ #renderedBuffer = null;
11
22
 
12
- class OfflineAudioContext extends BaseAudioContext {
13
23
  constructor(...args) {
14
- // handle initialisation with either an options object or a sequence of parameters
24
+ if (arguments.length < 1) {
25
+ throw new TypeError(`Failed to construct 'OfflineAudioContext': 1 argument required, but only ${arguments.length} present`);
26
+ }
27
+
15
28
  // https://webaudio.github.io/web-audio-api/#dom-offlineaudiocontext-constructor-contextoptions-contextoptions
16
- if (isPlainObject(args[0]) && 'length' in args[0] && 'sampleRate' in args[0]
17
- ) {
18
- let { numberOfChannels, length, sampleRate } = args[0];
19
- if (numberOfChannels === undefined) {
20
- numberOfChannels = 1;
29
+ if (arguments.length === 1) {
30
+ const options = args[0];
31
+
32
+ if (typeof options !== 'object') {
33
+ throw new TypeError(`Failed to construct 'OfflineAudioContext': argument 1 is not of type 'OfflineAudioContextOptions'`);
34
+ }
35
+
36
+ if (options.length === undefined) {
37
+ throw new TypeError(`Failed to construct 'OfflineAudioContext': Failed to read the 'length' property from 'OfflineAudioContextOptions': Required member is undefined.`);
38
+ }
39
+
40
+ if (options.sampleRate === undefined) {
41
+ throw new TypeError(`Failed to construct 'OfflineAudioContext': Failed to read the 'sampleRate' property from 'OfflineAudioContextOptions': Required member is undefined.`);
42
+ }
43
+
44
+ if (options.numberOfChannels === undefined) {
45
+ options.numberOfChannels = 1;
46
+ }
47
+
48
+ args = [
49
+ options.numberOfChannels,
50
+ options.length,
51
+ options.sampleRate,
52
+ ];
53
+ }
54
+
55
+ let [numberOfChannels, length, sampleRate] = args;
56
+
57
+ numberOfChannels = conversions['unsigned long'](numberOfChannels, {
58
+ enforceRange: true,
59
+ context: `Failed to construct 'OfflineAudioContext': Failed to read the 'numberOfChannels' property from OfflineContextOptions; The provided value (${numberOfChannels})`,
60
+ });
61
+
62
+ length = conversions['unsigned long'](length, {
63
+ enforceRange: true,
64
+ context: `Failed to construct 'OfflineAudioContext': Failed to read the 'length' property from OfflineContextOptions; The provided value (${length})`,
65
+ });
66
+
67
+ sampleRate = conversions['float'](sampleRate, {
68
+ context: `Failed to construct 'OfflineAudioContext': Failed to read the 'sampleRate' property from OfflineContextOptions; The provided value (${sampleRate})`,
69
+ });
70
+
71
+ let napiObj;
72
+
73
+ try {
74
+ napiObj = new nativeBinding.OfflineAudioContext(numberOfChannels, length, sampleRate);
75
+ } catch (err) {
76
+ throwSanitizedError(err);
77
+ }
78
+
79
+ super({ [kNapiObj]: napiObj });
80
+
81
+ // Add function to Napi object to bridge from Rust events to JS EventTarget
82
+ // 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');
21
86
  }
22
- args = [numberOfChannels, length, sampleRate];
87
+
88
+ const event = new Event(rawEvent.type);
89
+ propagateEvent(this, event);
90
+ };
91
+
92
+ // 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
99
+ // startRendering fulfills and that we want to return the exact same instance
100
+ if (this.#renderedBuffer === null) {
101
+ this.#renderedBuffer = new jsExport.AudioBuffer({ [kNapiObj]: rawEvent.renderedBuffer });
102
+ }
103
+
104
+ const event = new jsExport.OfflineAudioCompletionEvent(rawEvent.type, {
105
+ renderedBuffer: this.#renderedBuffer,
106
+ });
107
+
108
+ propagateEvent(this, event);
109
+ };
110
+ }
111
+
112
+ get length() {
113
+ if (!(this instanceof OfflineAudioContext)) {
114
+ throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'`);
23
115
  }
24
116
 
25
- const [numberOfChannels, length, sampleRate] = args;
117
+ return this[kNapiObj].length;
118
+ }
26
119
 
27
- if (!isPositiveInt(numberOfChannels)) {
28
- throw new TypeError(`Invalid value for numberOfChannels: ${numberOfChannels}`);
29
- } else if (!isPositiveInt(length)) {
30
- throw new TypeError(`Invalid value for length: ${length}`);
31
- } else if (!isPositiveNumber(sampleRate)) {
32
- throw new TypeError(`Invalid value for sampleRate: ${sampleRate}`);
120
+ get oncomplete() {
121
+ if (!(this instanceof OfflineAudioContext)) {
122
+ throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'`);
33
123
  }
34
124
 
35
- super(numberOfChannels, length, sampleRate);
125
+ return this._complete || null;
126
+ }
36
127
 
37
- // EventTargetMixin has been called so EventTargetMixin[kDispatchEvent] is
38
- // bound to this, then we can safely finalize event target initialization
39
- super.__initEventTarget__();
128
+ set oncomplete(value) {
129
+ if (!(this instanceof OfflineAudioContext)) {
130
+ throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'`);
131
+ }
132
+
133
+ if (isFunction(value) || value === null) {
134
+ this._complete = value;
135
+ }
40
136
  }
41
137
 
42
138
  async startRendering() {
43
- const renderedBuffer = await super.startRendering();
139
+ if (!(this instanceof OfflineAudioContext)) {
140
+ throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'`);
141
+ }
142
+
143
+ let nativeAudioBuffer;
144
+
145
+ try {
146
+ nativeAudioBuffer = await this[kNapiObj].startRendering();
147
+ } catch (err) {
148
+ throwSanitizedError(err);
149
+ }
150
+
151
+ // @fixme: workaround the fact that this event seems to be triggered before
152
+ // startRendering fulfills and that we want to return the exact same instance
153
+ if (this.#renderedBuffer === null) {
154
+ this.#renderedBuffer = new jsExport.AudioBuffer({ [kNapiObj]: nativeAudioBuffer });
155
+ }
156
+
157
+ return this.#renderedBuffer;
158
+ }
159
+
160
+ async resume() {
161
+ if (!(this instanceof OfflineAudioContext)) {
162
+ throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'`);
163
+ }
164
+
165
+ try {
166
+ await this[kNapiObj].resume();
167
+ } catch (err) {
168
+ throwSanitizedError(err);
169
+ }
170
+ }
171
+
172
+ async suspend(suspendTime) {
173
+ if (!(this instanceof OfflineAudioContext)) {
174
+ throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'`);
175
+ }
176
+
177
+ if (arguments.length < 1) {
178
+ throw new TypeError(`Failed to execute 'suspend' on 'OfflineAudioContext': 1 argument required, but only ${arguments.length} present`);
179
+ }
44
180
 
45
- // We do this here, so that we can just share the same audioBuffer instance.
46
- // This also simplifies code on the rust side as we don't need to deal
47
- // with the OfflineAudioCompletionEvent.
48
- const event = new Event('complete');
49
- event.renderedBuffer = renderedBuffer;
50
- this.dispatchEvent(event)
181
+ suspendTime = conversions['double'](suspendTime, {
182
+ context: `Failed to execute 'suspend' on 'OfflineAudioContext': argument 1`,
183
+ });
51
184
 
52
- return renderedBuffer;
185
+ try {
186
+ await this[kNapiObj].suspend(suspendTime);
187
+ } catch (err) {
188
+ throwSanitizedError(err);
189
+ }
53
190
  }
54
191
  }
55
192
 
193
+ Object.defineProperties(OfflineAudioContext, {
194
+ length: {
195
+ __proto__: null,
196
+ writable: false,
197
+ enumerable: false,
198
+ configurable: true,
199
+ value: 1,
200
+ },
201
+ });
202
+
203
+ Object.defineProperties(OfflineAudioContext.prototype, {
204
+ [Symbol.toStringTag]: {
205
+ __proto__: null,
206
+ writable: false,
207
+ enumerable: false,
208
+ configurable: true,
209
+ value: 'OfflineAudioContext',
210
+ },
211
+
212
+ length: kEnumerableProperty,
213
+ oncomplete: kEnumerableProperty,
214
+ startRendering: kEnumerableProperty,
215
+ resume: kEnumerableProperty,
216
+ suspend: kEnumerableProperty,
217
+ });
218
+
56
219
  return OfflineAudioContext;
57
220
  };
@@ -17,56 +17,192 @@
17
17
  // -------------------------------------------------------------------------- //
18
18
  // -------------------------------------------------------------------------- //
19
19
 
20
- // eslint-disable-next-line no-unused-vars
21
- const { throwSanitizedError } = require('./lib/errors.js');
22
- // eslint-disable-next-line no-unused-vars
23
- const { AudioParam } = require('./AudioParam.js');
24
- const EventTargetMixin = require('./EventTarget.mixin.js');
25
- const AudioNodeMixin = require('./AudioNode.mixin.js');
26
- const AudioScheduledSourceNodeMixin = require('./AudioScheduledSourceNode.mixin.js');
20
+ /* eslint-disable no-unused-vars */
21
+ const conversions = require('webidl-conversions');
22
+ const {
23
+ toSanitizedSequence,
24
+ } = require('./lib/cast.js');
25
+ const {
26
+ isFunction,
27
+ kEnumerableProperty,
28
+ } = require('./lib/utils.js');
29
+ const {
30
+ throwSanitizedError,
31
+ } = require('./lib/errors.js');
32
+ const {
33
+ kNapiObj,
34
+ kAudioBuffer,
35
+ } = require('./lib/symbols.js');
36
+ /* eslint-enable no-unused-vars */
27
37
 
28
- module.exports = (NativeOscillatorNode) => {
29
-
30
- const EventTarget = EventTargetMixin(NativeOscillatorNode, ['ended']);
31
- const AudioNode = AudioNodeMixin(EventTarget);
32
- const AudioScheduledSourceNode = AudioScheduledSourceNodeMixin(AudioNode);
38
+ const AudioScheduledSourceNode = require('./AudioScheduledSourceNode.js');
33
39
 
40
+ module.exports = (jsExport, nativeBinding) => {
34
41
  class OscillatorNode extends AudioScheduledSourceNode {
42
+
43
+ #frequency = null;
44
+ #detune = null;
45
+
35
46
  constructor(context, options) {
36
- if (options !== undefined && typeof options !== 'object') {
37
- throw new TypeError("Failed to construct 'OscillatorNode': argument 2 is not of type 'OscillatorOptions'")
47
+
48
+ if (arguments.length < 1) {
49
+ throw new TypeError(`Failed to construct 'OscillatorNode': 1 argument required, but only ${arguments.length} present`);
50
+ }
51
+
52
+ if (!(context instanceof jsExport.BaseAudioContext)) {
53
+ throw new TypeError(`Failed to construct 'OscillatorNode': argument 1 is not of type BaseAudioContext`);
54
+ }
55
+
56
+ // parsed version of the option to be passed to NAPI
57
+ const parsedOptions = {};
58
+
59
+ if (options && typeof options !== 'object') {
60
+ throw new TypeError('Failed to construct \'OscillatorNode\': argument 2 is not of type \'OscillatorOptions\'');
61
+ }
62
+
63
+ if (options && options.type !== undefined) {
64
+ if (!['sine', 'square', 'sawtooth', 'triangle', 'custom'].includes(options.type)) {
65
+ throw new TypeError(`Failed to construct 'OscillatorNode': Failed to read the 'type' property from OscillatorOptions: The provided value '${options.type}' is not a valid enum value of type OscillatorType`);
66
+ }
67
+
68
+ parsedOptions.type = conversions['DOMString'](options.type, {
69
+ context: `Failed to construct 'OscillatorNode': Failed to read the 'type' property from OscillatorOptions: The provided value '${options.type}'`,
70
+ });
71
+ } else {
72
+ parsedOptions.type = 'sine';
73
+ }
74
+
75
+ if (options && options.frequency !== undefined) {
76
+ parsedOptions.frequency = conversions['float'](options.frequency, {
77
+ context: `Failed to construct 'OscillatorNode': Failed to read the 'frequency' property from OscillatorOptions: The provided value (${options.frequency}})`,
78
+ });
79
+ } else {
80
+ parsedOptions.frequency = 440;
81
+ }
82
+
83
+ if (options && options.detune !== undefined) {
84
+ parsedOptions.detune = conversions['float'](options.detune, {
85
+ context: `Failed to construct 'OscillatorNode': Failed to read the 'detune' property from OscillatorOptions: The provided value (${options.detune}})`,
86
+ });
87
+ } else {
88
+ parsedOptions.detune = 0;
89
+ }
90
+
91
+ if (options && options.periodicWave !== undefined) {
92
+ if (!(options.periodicWave instanceof jsExport.PeriodicWave)) {
93
+ throw new TypeError(`Failed to construct 'OscillatorNode': Failed to read the 'periodicWave' property from OscillatorOptions: The provided value '${options.periodicWave}' is not an instance of PeriodicWave`);
94
+ }
95
+
96
+ parsedOptions.periodicWave = options.periodicWave[kNapiObj];
97
+ } else {
98
+ parsedOptions.periodicWave = null;
99
+ }
100
+
101
+ if (parsedOptions.type === 'custom' && parsedOptions.periodicWave === null) {
102
+ throw new DOMException('Failed to construct \'OscillatorNode\': A PeriodicWave must be specified if the type is set to \'custom\'', 'InvalidStateError');
103
+ }
104
+
105
+ if (parsedOptions.periodicWave !== null) {
106
+ parsedOptions.type = 'custom';
107
+ }
108
+
109
+ if (options && options.channelCount !== undefined) {
110
+ parsedOptions.channelCount = conversions['unsigned long'](options.channelCount, {
111
+ enforceRange: true,
112
+ context: `Failed to construct 'OscillatorNode': Failed to read the 'channelCount' property from OscillatorOptions: The provided value '${options.channelCount}'`,
113
+ });
114
+ }
115
+
116
+ if (options && options.channelCountMode !== undefined) {
117
+ parsedOptions.channelCountMode = conversions['DOMString'](options.channelCountMode, {
118
+ context: `Failed to construct 'OscillatorNode': Failed to read the 'channelCount' property from OscillatorOptions: The provided value '${options.channelCountMode}'`,
119
+ });
120
+ }
121
+
122
+ if (options && options.channelInterpretation !== undefined) {
123
+ parsedOptions.channelInterpretation = conversions['DOMString'](options.channelInterpretation, {
124
+ context: `Failed to construct 'OscillatorNode': Failed to read the 'channelInterpretation' property from OscillatorOptions: The provided value '${options.channelInterpretation}'`,
125
+ });
126
+ }
127
+
128
+ let napiObj;
129
+
130
+ try {
131
+ napiObj = new nativeBinding.OscillatorNode(context[kNapiObj], parsedOptions);
132
+ } catch (err) {
133
+ throwSanitizedError(err);
38
134
  }
39
135
 
40
- super(context, options);
41
- // EventTargetMixin has been called so EventTargetMixin[kDispatchEvent] is
42
- // bound to this, then we can safely finalize event target initialization
43
- super.__initEventTarget__();
136
+ super(context, {
137
+ [kNapiObj]: napiObj,
138
+ });
44
139
 
45
- this.frequency = new AudioParam(this.frequency);
46
- this.detune = new AudioParam(this.detune);
140
+ this.#frequency = new jsExport.AudioParam({
141
+ [kNapiObj]: this[kNapiObj].frequency,
142
+ });
143
+ this.#detune = new jsExport.AudioParam({
144
+ [kNapiObj]: this[kNapiObj].detune,
145
+ });
47
146
  }
48
147
 
49
- // getters
148
+ get frequency() {
149
+ if (!(this instanceof OscillatorNode)) {
150
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'OscillatorNode\'');
151
+ }
50
152
 
51
- get type() {
52
- return super.type;
153
+ return this.#frequency;
53
154
  }
54
155
 
55
- // setters
156
+ get detune() {
157
+ if (!(this instanceof OscillatorNode)) {
158
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'OscillatorNode\'');
159
+ }
160
+
161
+ return this.#detune;
162
+ }
163
+
164
+ get type() {
165
+ if (!(this instanceof OscillatorNode)) {
166
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'OscillatorNode\'');
167
+ }
168
+
169
+ return this[kNapiObj].type;
170
+ }
56
171
 
57
172
  set type(value) {
173
+ if (!(this instanceof OscillatorNode)) {
174
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'OscillatorNode\'');
175
+ }
176
+
177
+ if (!['sine', 'square', 'sawtooth', 'triangle', 'custom'].includes(value)) {
178
+ console.warn(`Failed to set the 'type' property on 'OscillatorNode': Value '${value}' is not a valid 'OscillatorType' enum value`);
179
+ return;
180
+ }
181
+
58
182
  try {
59
- super.type = value;
183
+ this[kNapiObj].type = value;
60
184
  } catch (err) {
61
185
  throwSanitizedError(err);
62
186
  }
63
187
  }
64
188
 
65
- // methods
66
-
67
- setPeriodicWave(...args) {
189
+ setPeriodicWave(periodicWave) {
190
+ if (!(this instanceof OscillatorNode)) {
191
+ throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'OscillatorNode\'');
192
+ }
193
+
194
+ if (arguments.length < 1) {
195
+ throw new TypeError(`Failed to execute 'setPeriodicWave' on 'OscillatorNode': 1 argument required, but only ${arguments.length} present`);
196
+ }
197
+
198
+ if (!(periodicWave instanceof jsExport.PeriodicWave)) {
199
+ throw new TypeError(`Failed to execute 'setPeriodicWave' on 'OscillatorNode': Parameter 1 is not of type 'PeriodicWave'`);
200
+ }
201
+
202
+ periodicWave = periodicWave[kNapiObj];
203
+
68
204
  try {
69
- return super.setPeriodicWave(...args);
205
+ return this[kNapiObj].setPeriodicWave(periodicWave);
70
206
  } catch (err) {
71
207
  throwSanitizedError(err);
72
208
  }
@@ -74,8 +210,29 @@ module.exports = (NativeOscillatorNode) => {
74
210
 
75
211
  }
76
212
 
77
- return OscillatorNode;
78
- };
213
+ Object.defineProperties(OscillatorNode, {
214
+ length: {
215
+ __proto__: null,
216
+ writable: false,
217
+ enumerable: false,
218
+ configurable: true,
219
+ value: 1,
220
+ },
221
+ });
79
222
 
223
+ Object.defineProperties(OscillatorNode.prototype, {
224
+ [Symbol.toStringTag]: {
225
+ __proto__: null,
226
+ writable: false,
227
+ enumerable: false,
228
+ configurable: true,
229
+ value: 'OscillatorNode',
230
+ },
231
+ frequency: kEnumerableProperty,
232
+ detune: kEnumerableProperty,
233
+ type: kEnumerableProperty,
234
+ setPeriodicWave: kEnumerableProperty,
235
+ });
80
236
 
81
-
237
+ return OscillatorNode;
238
+ };