Created
March 23, 2024 16:36
-
-
Save jampekka/5ccca4810e665bc606e2776d401e297a to your computer and use it in GitHub Desktop.
node-web-audio-api facade binding
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Wraps GainNode and OscillatorNode using JS-only class hierarchy | |
// Requiring needed internal things. Done from the "outside", so very ugly. | |
const napiBinding = require('./node_modules/node-web-audio-api/node-web-audio-api.linux-x64-gnu.node') | |
const { | |
throwSanitizedError, | |
} = require('./node_modules/node-web-audio-api/js/lib/errors.js'); | |
const { | |
AudioParam, | |
kNativeAudioParam, | |
} = require('./node_modules/node-web-audio-api/js/AudioParam.js'); | |
const { | |
AudioDestinationNode, | |
kNativeAudioDestinationNode, | |
} = require('./node_modules/node-web-audio-api/js/AudioDestinationNode.js'); | |
const kEventListeners = Symbol('node-web-audio-api:event-listeners'); | |
const kDispatchEvent = Symbol.for('node-web-audio-api:napi-dispatch-event'); | |
// EventTarget is now just a normal class that takes the eventTypes as constructor parameters | |
class EventTarget { | |
[kEventListeners] = new Map(); | |
constructor(eventTypes) { | |
eventTypes.forEach((eventType) => { | |
this[`on${eventType}`] = null; | |
}); | |
// we need to bind because calling [kDispatchEvent] from rust loose `this` | |
this[kDispatchEvent] = this[kDispatchEvent].bind(this); | |
} | |
// instance might | |
addEventListener(eventType, callback) { | |
if (!this[kEventListeners].has(eventType)) { | |
this[kEventListeners].set(eventType, new Set()); | |
} | |
const callbacks = this[kEventListeners].get(eventType); | |
callbacks.add(callback); | |
} | |
removeEventListener(eventType, callback) { | |
// this is valid event eventType, otherwaise just ignore | |
if (this[kEventListeners].has(eventType)) { | |
const callbacks = this[kEventListeners].get(eventType); | |
callbacks.delete(callback); | |
} | |
} | |
dispatchEvent(event) { | |
if (isFunction(this[`on${event.type}`])) { | |
this[`on${event.type}`](event); | |
} | |
if (this[kEventListeners].has(event.type)) { | |
const callbacks = this[kEventListeners].get(event.type); | |
callbacks.forEach(callback => callback(event)); | |
} | |
} | |
// called from rust | |
[kDispatchEvent](err, eventType) { | |
const event = new Event(eventType); | |
// cannot override, this would need to derive EventTarget | |
// cf. https://www.nearform.com/blog/node-js-and-the-struggles-of-being-an-eventtarget/ | |
// event.target = this; | |
// event.currentTarget = this; | |
// event.srcElement = this; | |
this.dispatchEvent(event); | |
} | |
}; | |
// AudioNode now acts as a facade to the native node implementation | |
// and inherits from EventTarget normally | |
class AudioNode extends EventTarget { | |
/* eslint-disable constructor-super */ | |
constructor(impl, eventTypes) { | |
try { | |
super(eventTypes); | |
this._impl = impl; | |
} catch (err) { | |
throwSanitizedError(err); | |
} | |
} | |
/* eslint-enable constructor-super */ | |
// getters | |
get context() { | |
return this._impl.context; | |
} | |
get numberOfInputs() { | |
return this._impl.numberOfInputs; | |
} | |
get numberOfOutputs() { | |
return this._impl.numberOfOutputs; | |
} | |
get channelCount() { | |
return this._impl.channelCount; | |
} | |
get channelCountMode() { | |
return this._impl.channelCountMode; | |
} | |
get channelInterpretation() { | |
return this._impl.channelInterpretation; | |
} | |
// setters | |
set channelCount(value) { | |
try { | |
this._impl.channelCount = value; | |
} catch (err) { | |
throwSanitizedError(err); | |
} | |
} | |
set channelCountMode(value) { | |
try { | |
this._impl.channelCountMode = value; | |
} catch (err) { | |
throwSanitizedError(err); | |
} | |
} | |
set channelInterpretation(value) { | |
try { | |
this._impl.channelInterpretation = value; | |
} catch (err) { | |
throwSanitizedError(err); | |
} | |
} | |
// methods - connect / disconnect | |
connect(...args) { | |
// unwrap raw audio params from facade | |
if (args[0] instanceof AudioParam) { | |
args[0] = args[0][kNativeAudioParam]; | |
} | |
// unwrap raw audio destination from facade | |
if (args[0] instanceof AudioDestinationNode) { | |
args[0] = args[0][kNativeAudioDestinationNode]; | |
} | |
if (args[0] instanceof AudioNode) { | |
args[0] = args[0]._impl; | |
} | |
try { | |
return this._impl.connect(...args); | |
} catch (err) { | |
throwSanitizedError(err); | |
} | |
} | |
disconnect(...args) { | |
// unwrap raw audio params from facade | |
if (args[0] instanceof AudioParam) { | |
args[0] = args[0][kNativeAudioParam]; | |
} | |
// unwrap raw audio destination from facade | |
if (args[0] instanceof AudioDestinationNode) { | |
args[0] = args[0][kNativeAudioDestinationNode]; | |
} | |
try { | |
return this._impl.disconnect(...args); | |
} catch (err) { | |
throwSanitizedError(err); | |
} | |
} | |
} | |
class AudioScheduledSourceNode extends AudioNode { | |
constructor(...args) { | |
super(...args); | |
} | |
// getters | |
get onended() { | |
return this._impl.onended; | |
} | |
// setters | |
set onended(value) { | |
try { | |
this._impl.onended = value; | |
} catch (err) { | |
throwSanitizedError(err); | |
} | |
} | |
// methods - start / stop | |
start(...args) { | |
try { | |
return this._impl.start(...args); | |
} catch (err) { | |
throwSanitizedError(err); | |
} | |
} | |
stop(...args) { | |
try { | |
return this._impl.stop(...args); | |
} catch (err) { | |
throwSanitizedError(err); | |
} | |
} | |
} | |
class GainNode extends AudioNode { | |
constructor(context, options) { | |
// keep a handle to the original object, if we need to manipulate the | |
// options before passing them to NAPI | |
const parsedOptions = Object.assign({}, options); | |
if (options !== undefined) { | |
if (typeof options !== 'object') { | |
throw new TypeError('Failed to construct \'GainNode\': argument 2 is not of type \'GainOptions\''); | |
} | |
} | |
const impl = new napiBinding.GainNode(context, options); | |
super(impl, ["stop"]); | |
this.gain = new AudioParam(impl.gain); | |
} | |
} | |
class OscillatorNode extends AudioScheduledSourceNode { | |
constructor(context, options) { | |
// keep a handle to the original object, if we need to manipulate the | |
// options before passing them to NAPI | |
const parsedOptions = Object.assign({}, options); | |
if (options !== undefined) { | |
if (typeof options !== 'object') { | |
throw new TypeError('Failed to construct \'OscillatorNode\': argument 2 is not of type \'OscillatorOptions\''); | |
} | |
} | |
const impl = new napiBinding.OscillatorNode(context, options); | |
super(impl, ["stop"]); | |
this.impl_ = impl; | |
// NOTE: I don't understand the purpose of this, but seems to go thorugh like this | |
// EventTargetMixin constructor has been called so EventTargetMixin[kDispatchEvent] | |
// is bound to this, then we can safely finalize event target initialization | |
impl.__initEventTarget__(); | |
this.frequency = new AudioParam(impl.frequency); | |
this.detune = new AudioParam(impl.detune); | |
} | |
get type() { | |
return this._impl.type; | |
} | |
set type(value) { | |
try { | |
this._impl.type = value; | |
} catch (err) { | |
throwSanitizedError(err); | |
} | |
} | |
setPeriodicWave(...args) { | |
try { | |
return this._impl.setPeriodicWave(...args); | |
} catch (err) { | |
throwSanitizedError(err); | |
} | |
} | |
} | |
// The README example using the wrapped nodes | |
const {AudioContext} = require('node-web-audio-api'); | |
const audioContext = new AudioContext(); | |
setInterval(() => { | |
const now = audioContext.currentTime; | |
const frequency = 200 + Math.random() * 2800; | |
const env = new GainNode(audioContext, { gain: 0 }); | |
env.connect(audioContext.destination); | |
env.gain | |
.setValueAtTime(0, now) | |
.linearRampToValueAtTime(0.2, now + 0.02) | |
.exponentialRampToValueAtTime(0.0001, now + 1); | |
const osc = new OscillatorNode(audioContext, { frequency }); | |
osc.connect(env); | |
osc.start(now); | |
osc.stop(now + 1); | |
}, 80); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment