node-web-audio-api
Version:
Web Audio API implementation for Node.js
218 lines (175 loc) • 7.11 kB
JavaScript
const conversions = require('webidl-conversions');
const {
propagateEvent,
} = require('./lib/events.js');
const {
throwSanitizedError,
} = require('./lib/errors.js');
const {
isFunction,
kEnumerableProperty,
} = require('./lib/utils.js');
const {
kNapiObj,
kWorkletRelease,
kOnStateChange,
kOnComplete,
kCheckProcessorsCreated,
} = require('./lib/symbols.js');
module.exports = function patchOfflineAudioContext(jsExport, nativeBinding) {
class OfflineAudioContext extends jsExport.BaseAudioContext {
#renderedBuffer = null;
constructor(...args) {
if (arguments.length < 1) {
throw new TypeError(`Failed to construct 'OfflineAudioContext': 1 argument required, but only ${arguments.length} present`);
}
// https://webaudio.github.io/web-audio-api/#dom-offlineaudiocontext-constructor-contextoptions-contextoptions
if (arguments.length === 1) {
const options = args[0];
if (typeof options !== 'object') {
throw new TypeError(`Failed to construct 'OfflineAudioContext': argument 1 is not of type 'OfflineAudioContextOptions'`);
}
if (options.length === undefined) {
throw new TypeError(`Failed to construct 'OfflineAudioContext': Failed to read the 'length' property from 'OfflineAudioContextOptions': Required member is undefined.`);
}
if (options.sampleRate === undefined) {
throw new TypeError(`Failed to construct 'OfflineAudioContext': Failed to read the 'sampleRate' property from 'OfflineAudioContextOptions': Required member is undefined.`);
}
if (options.numberOfChannels === undefined) {
options.numberOfChannels = 1;
}
args = [
options.numberOfChannels,
options.length,
options.sampleRate,
];
}
let [numberOfChannels, length, sampleRate] = args;
numberOfChannels = conversions['unsigned long'](numberOfChannels, {
enforceRange: true,
context: `Failed to construct 'OfflineAudioContext': Failed to read the 'numberOfChannels' property from OfflineContextOptions; The provided value (${numberOfChannels})`,
});
length = conversions['unsigned long'](length, {
enforceRange: true,
context: `Failed to construct 'OfflineAudioContext': Failed to read the 'length' property from OfflineContextOptions; The provided value (${length})`,
});
sampleRate = conversions['float'](sampleRate, {
context: `Failed to construct 'OfflineAudioContext': Failed to read the 'sampleRate' property from OfflineContextOptions; The provided value (${sampleRate})`,
});
let napiObj;
try {
napiObj = new nativeBinding.OfflineAudioContext(numberOfChannels, length, sampleRate);
} catch (err) {
throwSanitizedError(err);
}
super({ [kNapiObj]: napiObj });
// Add function to Napi object to bridge from Rust events to JS EventTarget
// They will be effectively registered on rust side when `startRendering` is called
this[kNapiObj][kOnStateChange] = (function(_err, rawEvent) {
const event = new Event(rawEvent.type);
propagateEvent(this, event);
}).bind(this);
// This event is, per spec, the last trigerred one
this[kNapiObj][kOnComplete] = (function(err, rawEvent) {
// workaround the fact that the oncomplete event is triggered before
// startRendering fulfills and that we want to return the exact same instance
this.#renderedBuffer = new jsExport.AudioBuffer({ [kNapiObj]: rawEvent.renderedBuffer });
const event = new jsExport.OfflineAudioCompletionEvent(rawEvent.type, {
renderedBuffer: this.#renderedBuffer,
});
// delay event propagation to next tick that it is executed after startRendering fulfills
setImmediate(() => {
propagateEvent(this, event);
}, 0);
}).bind(this);
}
get length() {
if (!(this instanceof OfflineAudioContext)) {
throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'`);
}
return this[kNapiObj].length;
}
get oncomplete() {
if (!(this instanceof OfflineAudioContext)) {
throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'`);
}
return this._complete || null;
}
set oncomplete(value) {
if (!(this instanceof OfflineAudioContext)) {
throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'`);
}
if (isFunction(value) || value === null) {
this._complete = value;
}
}
async startRendering() {
if (!(this instanceof OfflineAudioContext)) {
throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'`);
}
// ensure all AudioWorkletProcessor have finished their instanciation
await this.audioWorklet[kCheckProcessorsCreated]();
// keep this to highlight the workaround w/ the oncomplete event
let _nativeAudioBuffer;
try {
// eslint-disable-next-line no-unused-vars
_nativeAudioBuffer = await this[kNapiObj].startRendering();
} catch (err) {
throwSanitizedError(err);
}
// release audio worklets
await this.audioWorklet[kWorkletRelease]();
return this.#renderedBuffer;
}
async resume() {
if (!(this instanceof OfflineAudioContext)) {
throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'`);
}
try {
await this[kNapiObj].resume();
} catch (err) {
throwSanitizedError(err);
}
}
async suspend(suspendTime) {
if (!(this instanceof OfflineAudioContext)) {
throw new TypeError(`Invalid Invocation: Value of 'this' must be of type 'OfflineAudioContext'`);
}
if (arguments.length < 1) {
throw new TypeError(`Failed to execute 'suspend' on 'OfflineAudioContext': 1 argument required, but only ${arguments.length} present`);
}
suspendTime = conversions['double'](suspendTime, {
context: `Failed to execute 'suspend' on 'OfflineAudioContext': argument 1`,
});
try {
await this[kNapiObj].suspend(suspendTime);
} catch (err) {
throwSanitizedError(err);
}
}
}
Object.defineProperties(OfflineAudioContext, {
length: {
__proto__: null,
writable: false,
enumerable: false,
configurable: true,
value: 1,
},
});
Object.defineProperties(OfflineAudioContext.prototype, {
[Symbol.toStringTag]: {
__proto__: null,
writable: false,
enumerable: false,
configurable: true,
value: 'OfflineAudioContext',
},
length: kEnumerableProperty,
oncomplete: kEnumerableProperty,
startRendering: kEnumerableProperty,
resume: kEnumerableProperty,
suspend: kEnumerableProperty,
});
return OfflineAudioContext;
};