Skip to content

Instantly share code, notes, and snippets.

@jampekka
Created March 23, 2024 16:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jampekka/5ccca4810e665bc606e2776d401e297a to your computer and use it in GitHub Desktop.
Save jampekka/5ccca4810e665bc606e2776d401e297a to your computer and use it in GitHub Desktop.
node-web-audio-api facade binding
// 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