node-web-audio-api
Version:
Web Audio API implementation for Node.js
202 lines (169 loc) • 7.4 kB
JavaScript
import conversions from 'webidl-conversions';
import nativeBinding from '../load-native.js';
import {
isFunction,
kEnumerableProperty,
} from './lib/utils.js';
import {
throwSanitizedError,
} from './lib/errors.js';
import {
kNapiObj,
} from './lib/symbols.js';
import {
propagateEvent,
} from './lib/events.js';
import { AudioNode } from './AudioNode.js';
import { BaseAudioContext } from './BaseAudioContext.js';
import { AudioBuffer } from './AudioBuffer.js';
import { AudioProcessingEvent } from './Events.js';
export class ScriptProcessorNode extends AudioNode {
#onaudioprocess = null;
constructor(context, options) {
if (arguments.length < 1) {
throw new TypeError(`Failed to construct 'ScriptProcessorNode': 1 argument required, but only ${arguments.length} present`);
}
if (!(context instanceof BaseAudioContext)) {
throw new TypeError(`Failed to construct 'ScriptProcessorNode': argument 1 is not of type BaseAudioContext`);
}
const parsedOptions = {};
if (options && typeof options !== 'object') {
throw new TypeError('Failed to construct \'ScriptProcessorNode\': argument 2 is not of type \'ScriptProcessorNodeOptions\'');
}
// IDL defines bufferSize default value as 0
// cf. https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createscriptprocessor
// > If it’s not passed in, or if the value is 0, then the implementation
// > will choose the best buffer size for the given environment, which will
// > be constant power of 2 throughout the lifetime of the node.
if (options && options.bufferSize !== undefined && options.bufferSize !== 0) {
parsedOptions.bufferSize = conversions['unsigned long'](options.bufferSize, {
enforceRange: true,
context: `Failed to construct 'ScriptProcessorNode': Failed to read the 'bufferSize' property from ScriptProcessorNodeOptions: The provided value '${options.bufferSize}'`,
});
} else {
parsedOptions.bufferSize = 256;
}
if (options && options.numberOfInputChannels !== undefined) {
parsedOptions.numberOfInputChannels = conversions['unsigned long'](options.numberOfInputChannels, {
enforceRange: true,
context: `Failed to construct 'ScriptProcessorNode': Failed to read the 'numberOfInputChannels' property from ScriptProcessorNodeOptions: The provided value '${options.numberOfInputChannels}'`,
});
} else {
parsedOptions.numberOfInputChannels = 2;
}
if (options && options.numberOfOutputChannels !== undefined) {
parsedOptions.numberOfOutputChannels = conversions['unsigned long'](options.numberOfOutputChannels, {
enforceRange: true,
context: `Failed to construct 'ScriptProcessorNode': Failed to read the 'numberOfOutputChannels' property from ScriptProcessorNodeOptions: The provided value '${options.numberOfOutputChannels}'`,
});
} else {
parsedOptions.numberOfOutputChannels = 2;
}
if (options && options.channelCount !== undefined) {
parsedOptions.channelCount = conversions['unsigned long'](options.channelCount, {
enforceRange: true,
context: `Failed to construct 'ScriptProcessorNode': Failed to read the 'channelCount' property from ScriptProcessorNodeOptions: The provided value '${options.channelCount}'`,
});
}
if (options && options.channelCountMode !== undefined) {
parsedOptions.channelCountMode = conversions['DOMString'](options.channelCountMode, {
context: `Failed to construct 'ScriptProcessorNode': Failed to read the 'channelCount' property from ScriptProcessorNodeOptions: The provided value '${options.channelCountMode}'`,
});
}
if (options && options.channelInterpretation !== undefined) {
parsedOptions.channelInterpretation = conversions['DOMString'](options.channelInterpretation, {
context: `Failed to construct 'ScriptProcessorNode': Failed to read the 'channelInterpretation' property from ScriptProcessorNodeOptions: The provided value '${options.channelInterpretation}'`,
});
}
let napiObj;
try {
napiObj = new nativeBinding.NapiScriptProcessorNode(context[kNapiObj], parsedOptions);
} catch (err) {
throwSanitizedError(err);
}
super(context, {
[kNapiObj]: napiObj,
});
const numberOfInputChannels = parsedOptions.numberOfInputChannels;
const numberOfOutputChannels = parsedOptions.numberOfOutputChannels;
const bufferSize = parsedOptions.bufferSize;
const inputBuffer = new AudioBuffer({
numberOfChannels: numberOfInputChannels,
length: bufferSize,
sampleRate: context.sampleRate,
});
const outputBuffer = new AudioBuffer({
numberOfChannels: numberOfOutputChannels,
length: bufferSize,
sampleRate: context.sampleRate,
});
this[kNapiObj].onaudioprocess((function(e) {
// retrieve the ArrayBuffer from Buffer;
const inputArrayBuffer = e.buffer;
// first 8 bits are playbackTime
const playbackTime = new Float64Array(inputArrayBuffer, 0, 8)[0];
// unpack channels data
for (let channelNumber = 0; channelNumber < numberOfInputChannels; channelNumber++) {
const offset = (channelNumber * bufferSize * 4) + 8;
const channelData = new Float32Array(inputArrayBuffer, offset, bufferSize);
inputBuffer.copyToChannel(channelData, channelNumber);
}
const audioProcessingEventInit = {
playbackTime,
inputBuffer,
outputBuffer,
};
const event = new AudioProcessingEvent('audioprocess', audioProcessingEventInit);
propagateEvent(this, event);
let channels = new Array(numberOfOutputChannels);
for (let channelNumber = 0; channelNumber < numberOfOutputChannels; channelNumber++) {
const channelBuffer = outputBuffer.getChannelData(channelNumber).buffer;
const bufferView = new Uint8Array(channelBuffer);
channels[channelNumber] = bufferView;
}
// pack output buffer data into raw Buffer
const outputArrayBuffer = Buffer.concat(channels);
return outputArrayBuffer;
}).bind(this));
}
get bufferSize() {
if (!(this instanceof ScriptProcessorNode)) {
throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'ScriptProcessorNode\'');
}
return this[kNapiObj].bufferSize;
}
get onaudioprocess() {
if (!(this instanceof ScriptProcessorNode)) {
throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'ScriptProcessorNode\'');
}
return this.#onaudioprocess;
}
set onaudioprocess(value) {
if (!(this instanceof ScriptProcessorNode)) {
throw new TypeError('Invalid Invocation: Value of \'this\' must be of type \'ScriptProcessorNode\'');
}
if (isFunction(value) || value === null) {
this.#onaudioprocess = value;
}
}
}
Object.defineProperties(ScriptProcessorNode, {
length: {
__proto__: null,
writable: false,
enumerable: false,
configurable: true,
value: 0,
},
});
Object.defineProperties(ScriptProcessorNode.prototype, {
[Symbol.toStringTag]: {
__proto__: null,
writable: false,
enumerable: false,
configurable: true,
value: 'ScriptProcessorNode',
},
bufferSize: kEnumerableProperty,
onaudioprocess: kEnumerableProperty,
});