node-web-audio-api
Version:
Web Audio API implementation for Node.js
202 lines (162 loc) • 6.16 kB
JavaScript
import conversions from 'webidl-conversions';
import nativeBinding from '../load-native.js';
import {
propagateEvent,
} from './lib/events.js';
import {
throwSanitizedError,
} from './lib/errors.js';
import {
isFunction,
kEnumerableProperty,
} from './lib/utils.js';
import {
kNapiObj,
kWorkletRelease,
kCheckProcessorsCreated,
} from './lib/symbols.js';
import { BaseAudioContext } from './BaseAudioContext.js';
import { AudioBuffer } from './AudioBuffer.js';
import { OfflineAudioCompletionEvent } from './Events.js';
export class OfflineAudioContext extends BaseAudioContext {
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.NapiOfflineAudioContext(numberOfChannels, length, sampleRate);
} catch (err) {
throwSanitizedError(err);
}
super({ [kNapiObj]: napiObj });
this[kNapiObj].onstatechange((function(napiEvent) {
const event = new Event(napiEvent.type);
propagateEvent(this, event);
}).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'`);
}
// Make sure all AudioWorkletProcessor are instantiated before rendering
await this.audioWorklet[kCheckProcessorsCreated]();
let napiAudioBuffer;
try {
napiAudioBuffer = await this[kNapiObj].startRendering();
} catch (err) {
throwSanitizedError(err);
}
// Exit AudioWorkletGlobalScope
await this.audioWorklet[kWorkletRelease]();
const renderedBuffer = new AudioBuffer({ [kNapiObj]: napiAudioBuffer });
// Delay "complete" event to next tick to execute after `startRendering` fulfills
const event = new OfflineAudioCompletionEvent('complete', {
renderedBuffer: renderedBuffer,
});
setImmediate(() => propagateEvent(this, event), 0);
return 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,
});