import TextConverter from "./TextConverter";
import WASMJSAPI from "./WASMJSAPI";
import AudioEngine from "./AudioEngine";
import AudioGraphProcessor from "./AudioGraphProcessor";
const DEBUG = false;
/**
* Main SwitchboardSDK class.
*/
class SwitchboardSDK {
/**
* Switchboard SDK constructor.
*/
constructor() {
if (DEBUG) console.log("[SB] [SwitchboardSDK] constructor");
this.appID = null;
this.appSecret = null;
this.wasmPath = null;
this.workletProcessorPath = null;
this.wasmBytes = null;
this.extensions = [];
this.classes = null;
this.sdkAPI = null;
}
/**
* Initializes the Switchboard SDK.
* @param {String} appID The app ID value that identifies the app using the SDK.
* @param {String} appSecret The app secret.
* @param {String} wasmPath The path to the SwitchboardSDK.wasm file.
* @param {String} workletProcessorPath The path to the SwitchboardSDKWorkletProcessor.js file.
* @returns {Promise} A promise for the initialization process.
*/
initialize(appID, appSecret, wasmPath, workletProcessorPath) {
if (DEBUG)
console.log(
"[SB] [SwitchboardSDK] initialize (appID=" +
appID +
", wasmPath=" +
wasmPath +
", workletProcessorPath=" +
workletProcessorPath +
")"
);
this.appID = appID;
this.appSecret = appSecret;
this.wasmPath = wasmPath;
this.workletProcessorPath = workletProcessorPath;
return fetch(wasmPath)
.then((response) => response.arrayBuffer())
.then((bytes) => {
this.wasmBytes = bytes;
});
}
/**
* Configures the Switchboard SDK.
* This method can be used inside a custom audio worklet processor to configure the Switchboard SDK.
* @param {Object} sdkConfig The SDK config object received in the worklet node.
* @returns {Promise} A promise for the configuration process.
*/
configure(sdkConfig) {
if (DEBUG) console.log("[SB] [SwitchboardSDK] configure", sdkConfig);
return this._instatiateWasm(sdkConfig.wasmBytes, false)
.then((result) => {
if (DEBUG) console.log("[SB] [SwitchboardSDK] Calling SDK init");
if (DEBUG) console.log(sdkConfig.appID);
if (DEBUG) console.log(sdkConfig.appSecret);
let appIDWASMString = this.sdkAPI.allocString(sdkConfig.appID);
let appSecretWASMString = this.sdkAPI.allocString(sdkConfig.appSecret);
this.classes.SwitchboardSDK.initialize(
appIDWASMString,
appSecretWASMString
);
this.sdkAPI.freeMemory(appIDWASMString);
this.sdkAPI.freeMemory(appSecretWASMString);
})
.then((result) => {
if (sdkConfig.extensions.length == 0) {
return Promise.resolve();
}
let extension = sdkConfig.extensions[0];
return this._instatiateWasm(extension.wasmBytes, true);
});
}
_instatiateWasm(wasmBytes, isExtension) {
if (DEBUG)
console.log(
"[SB] [SwitchboardSDK] _instatiateWasm (isExtension=" +
isExtension +
")"
);
let self = this;
var memory = null;
if (isExtension) {
memory = this.sdkAPI.memory;
} else {
memory = new WebAssembly.Memory({
initial: 10, // 6.4MiB
maximum: 1000, // 640MiB
});
}
const importFunctionCalled = (functionName, args) => {
if (DEBUG)
console.log(
`[SB] [WASM] importFunctionCalled - ${functionName} with arguments`,
args
);
};
const importObject = {
wasi_snapshot_preview1: {
proc_exit: (...args) => importFunctionCalled("proc_exit", args),
fd_seek: (...args) => importFunctionCalled("fd_seek", args),
fd_read: (...args) => importFunctionCalled("fd_read", args),
fd_close: (...args) => importFunctionCalled("fd_close", args),
environ_get: (...args) => importFunctionCalled("environ_get", args),
environ_sizes_get: (...args) =>
importFunctionCalled("environ_sizes_get", args),
clock_time_get: (...args) =>
importFunctionCalled("clock_time_get", args),
fd_write: (...args) => importFunctionCalled("fd_write", args),
},
js: {
mem: memory,
},
env: {
js_log: function (logLevel, strPtr, length) {
const errorLogLevel = 1;
const warnLogLevel = 2;
const jsMessage = TextConverter.toJSString(
self.sdkAPI.memory,
strPtr,
length
);
const logMessage = "[SB] [WASM] " + jsMessage;
if (logLevel === errorLogLevel) {
console.error(logMessage);
} else if (logLevel === warnLogLevel) {
console.warn(logMessage);
} else {
if (DEBUG) console.log(logMessage);
}
},
emscripten_asm_const_int: (...args) =>
importFunctionCalled("emscripten_asm_const_int", args),
__syscall_unlinkat: (...args) =>
importFunctionCalled("__syscall_unlinkat", args),
__syscall_rmdir: (...args) =>
importFunctionCalled("__syscall_rmdir", args),
__syscall_readlinkat: (...args) =>
importFunctionCalled("__syscall_readlinkat", args),
__syscall_getdents64: (...args) =>
importFunctionCalled("__syscall_getdents64", args),
__syscall_getcwd: (...args) =>
importFunctionCalled("__syscall_getcwd", args),
__secs_to_zone: (...args) =>
importFunctionCalled("__secs_to_zone", args),
_tzset_js: (...args) => importFunctionCalled("_tzset_js", args),
IMM_SYM_0497: (...args) => importFunctionCalled("IMM_SYM_0497", args)
},
};
if (isExtension) {
Object.assign(importObject.env, this.sdkAPI.wasm);
}
return WebAssembly.instantiate(wasmBytes, importObject).then((response) => {
let wasmModule = response.module;
let wasmInstance = response.instance;
var helperFunctions = {};
if (isExtension) {
helperFunctions.malloc = this.sdkAPI.wasm.sb_malloc;
helperFunctions.free = this.sdkAPI.wasm.sb_free;
helperFunctions.memset = this.sdkAPI.wasm.sb_memset;
helperFunctions.demangle = this.sdkAPI.wasm.sb_demangle;
} else {
helperFunctions.malloc = wasmInstance.exports.sb_malloc;
helperFunctions.free = wasmInstance.exports.sb_free;
helperFunctions.memset = wasmInstance.exports.sb_memset;
helperFunctions.demangle = wasmInstance.exports.sb_demangle;
}
let wasmJSAPI = new WASMJSAPI(wasmInstance, helperFunctions);
if (DEBUG)
console.log("[SB] [SwitchboardSDK] Created WasmJSAPI", wasmJSAPI);
if (!isExtension) {
this.sdkAPI = wasmJSAPI;
this.classes = wasmJSAPI.classes;
}
});
}
/**
* Loads a Switchboard SDK extension.
* @param {String} name The name of the extension.
* @param {String} wasmPath The path to the extension's WASM file.
* @returns {Promise} A promise for the loading process.
*/
loadExtension(name, wasmPath) {
if (DEBUG)
console.log(
"[SB] [SwitchboardSDK] loadExtension (name=" +
name +
", wasmPath=" +
wasmPath +
")"
);
return fetch(wasmPath)
.then((response) => response.arrayBuffer())
.then((bytes) => {
this.extensions.push({
name: name,
wasmBytes: bytes,
});
});
}
/**
* Creates an AudioWorkletNode instance and sends the SDK configuration object to it.
* @param {AudioContext} audioContext The WebAudio audio context.
* @param {String} module The worklet node's module.
* @param {String} name The worklet node's name.
* @param {Array} inputChannelLayout The input channel layout for the audio graph. E.g. [2] means ones stereo bus, [2, 1] means a stereo and a mono audio buses.
* @param {Array} outputChannelLayout The output channel layout for the audio graph. E.g. [2] means ones stereo bus, [2, 1] means a stereo and a mono audio buses.
* @returns {AudioWorkletNode} The created AudioWorkletNode instance.
*/
async createWorkletNode(
audioContext,
module,
name,
inputChannelLayout,
outputChannelLayout
) {
if (DEBUG)
console.log(
"[SB] [SwitchboardSDK] createWorkletNode (module=" +
module +
", name=" +
name +
", inputChannelLayout=" +
inputChannelLayout +
", outputChannelLayout=" +
outputChannelLayout +
")"
);
let sdkConfig = {
appID: this.appID,
appSecret: this.appSecret,
wasmPath: this.wasmPath,
workletProcessorPath: this.workletProcessorPath,
wasmBytes: this.wasmBytes,
extensions: this.extensions,
};
await audioContext.audioWorklet.addModule(module);
let workletNode = new AudioWorkletNode(audioContext, name, {
numberOfInputs: inputChannelLayout.length,
numberOfOutputs: outputChannelLayout.length,
outputChannelCount: outputChannelLayout,
processorOptions: {
sampleRate: audioContext.sampleRate,
inputChannelLayout: inputChannelLayout,
outputChannelLayout: outputChannelLayout,
},
});
workletNode.port.postMessage({
event: "wasmLoaded",
data: sdkConfig,
});
return workletNode;
}
/**
* Creates an AudioWorkletNode instance with a JSON audio graph config.
* @param {Object} graphConfig A JSON audio graph config.
* @param {AudioContext} audioContext The WebAudio audio context.
* @param {Array} inputChannelLayout The input channel layout for the audio graph. E.g. [2] means ones stereo bus, [2, 1] means a stereo and a mono audio buses.
* @param {Array} outputChannelLayout The output channel layout for the audio graph. E.g. [2] means ones stereo bus, [2, 1] means a stereo and a mono audio buses.
* @returns {AudioWorkletNode} The created AudioWorkletNode instance.
*/
async createJSONGraphWorkletNode(
graphConfig,
audioContext,
inputChannelLayout,
outputChannelLayout
) {
if (DEBUG)
console.log(
"[SB] [SwitchboardSDK] createJSONGraphWorkletNode",
graphConfig
);
let audioWorkletNode = await this.createWorkletNode(
audioContext,
this.workletProcessorPath,
"SwitchboardWorkletProcessor",
inputChannelLayout,
outputChannelLayout
);
audioWorkletNode.port.postMessage({
event: "jsonGraph",
data: graphConfig,
});
return audioWorkletNode;
}
/**
* Creates an audio engine with a single custom AudioWorkletNode.
* @param {String} module The worklet node's module.
* @param {String} name The worklet node's name.
* @param {Array} inputChannelLayout The input channel layout for the audio graph. E.g. [2] means ones stereo bus, [2, 1] means a stereo and a mono audio buses.
* @param {Array} outputChannelLayout The output channel layout for the audio graph. E.g. [2] means ones stereo bus, [2, 1] means a stereo and a mono audio buses.
* @returns {AudioEngine} The created AudioEngine instance.
*/
async createEngine(module, name, inputChannelLayout, outputChannelLayout) {
if (DEBUG)
console.log(
"[SB] [SwitchboardSDK] createEngine (module=" +
module +
", name=" +
name +
", inputChannelLayout=" +
inputChannelLayout +
", outputChannelLayout=" +
outputChannelLayout +
")"
);
let audioEngine = new AudioEngine();
let workletNode = await this.createWorkletNode(
audioEngine.getAudioContext(),
module,
name,
inputChannelLayout,
outputChannelLayout
);
audioEngine.initialize(workletNode);
return audioEngine;
}
/**
* Creates an audio engine with a JSON audio graph config.
* @param {Object} graphConfig The JSON audio graph config.
* @param {Array} inputChannelLayout The input channel layout for the audio graph. E.g. [2] means ones stereo bus, [2, 1] means a stereo and a mono audio buses.
* @param {Array} outputChannelLayout The output channel layout for the audio graph. E.g. [2] means ones stereo bus, [2, 1] means a stereo and a mono audio buses.
* @returns {AudioEngine} The created AudioEngine instance.
*/
async createJSONGraphEngine(
graphConfig,
inputChannelLayout,
outputChannelLayout
) {
if (DEBUG)
console.log("[SB] [SwitchboardSDK] createJSONGraphEngine", graphConfig);
const audioEngine = new AudioEngine();
const workletNode = await this.createJSONGraphWorkletNode(
graphConfig,
audioEngine.getAudioContext(),
inputChannelLayout,
outputChannelLayout
);
audioEngine.initialize(workletNode);
return audioEngine;
}
/**
* Creates an AudioGraphProcessor to simplify running AudioGraph intances in custom worklet nodes.
* @param {Array} inputChannelLayout The input channel layout for the audio graph. E.g. [2] means ones stereo bus, [2, 1] means a stereo and a mono audio buses.
* @param {Array} outputChannelLayout The output channel layout for the audio graph. E.g. [2] means ones stereo bus, [2, 1] means a stereo and a mono audio buses.
* @param {Number} maxNumberOfFrames Max number of frames that the graph will be able to process.
* @param {Number} sampleRate Sample rate.
* @returns {AudioGraphProcessor} The created AudioGraphProcessor instance.
*/
createAudioGraphProcessor(
inputChannelLayout,
outputChannelLayout,
maxNumberOfFrames,
sampleRate
) {
return new AudioGraphProcessor(
this,
inputChannelLayout,
outputChannelLayout,
maxNumberOfFrames,
sampleRate
);
}
createAudioGraph(
inputChannelLayout,
outputChannelLayout,
maxNumberOfFrames,
sampleRate
) {
if (DEBUG)
console.log(
"[SB] [SwitchboardSDK] createAudioGraph (inputChannelLayout=" +
inputChannelLayout +
", outputChannelLayout=" +
outputChannelLayout +
", maxNumberOfFrames=" +
maxNumberOfFrames +
", sampleRate=" +
sampleRate +
")"
);
let switchboard = this;
let AudioGraphJS = class extends switchboard.classes.AudioGraph {
constructor(
inputChannelLayout,
outputChannelLayout,
maxNumberOfFrames,
sampleRate
) {
if (DEBUG)
console.log(
"[SB] [AudioGraphJS] constructor: inputChannelLayout=" +
inputChannelLayout +
" outputChannelLayout=" +
outputChannelLayout +
" maxNumberOfFrames=" +
maxNumberOfFrames +
" sampleRate=" +
sampleRate
);
let maxNumberOfChannels = 2;
super(maxNumberOfChannels, maxNumberOfFrames);
this.graphProcessor = new AudioGraphProcessor(
switchboard,
inputChannelLayout,
outputChannelLayout,
maxNumberOfFrames,
sampleRate
);
}
destruct() {
if (DEBUG) console.log("[SB] [AudioGraphJS] destruct");
this.graphProcessor.destruct();
super.destruct();
}
processGraph(inputs, outputs) {
this.graphProcessor.processGraph(inputs, outputs, this);
return true;
}
};
return new AudioGraphJS(
inputChannelLayout,
outputChannelLayout,
maxNumberOfFrames,
sampleRate
);
}
}
export default SwitchboardSDK;