Source: SwitchboardSDK.js

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;