UNPKG

node-web-audio-api

Version:
218 lines (175 loc) 7.11 kB
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; };