webaudiomodules / api

Web Audio Modules (WAMs) API
176 stars 15 forks source link

WebAudioModules API

This repository includes API definition files for WebAudioModules (WAMs) written in TypeScript and abstract classes that conform to the API written in JavaScript. The API is designed for making web-based modular audio plugins and using them in compatible hosts.

The legacy WAM API is available in branch v10.

Installing the API

npm i -D @webaudiomodules/api

API definitions

The WAM API provides a specification which should be implemented in each WAM plugin or host. All the interfaces and types in the specification are documented in TypeScript in src/types.d.ts.

Similar to the VST, AudioUnit or AAX standards supported by desktop DAWs, WAMs are modular audio plugins which include a DSP component and a UI component along with some extra features such as parameter automation, MIDI message processing, and state saving/loading, etc. Plugins and hosts which conform to the standard defined by the API are guaranteed to be compatible, regardless of their underlying implementations.

VSCode IntelliSense will take the types into account by using JSDoc or TypeScript import. For example:

// JavaScript
/** @typedef {import('@webaudiomodules/api').WamEvent} IWamEvent */
// TypeScript
import { WamEvent } from '@webaudiomodules/api';

Features

The API supports these primary features:

API Overview

The interfaces defined are:

WebAudioModule interface

A WAM distribution should include at least a JSON descriptor file and a JavaScript file that exports by default a WebAudioModule constructor. The constructor should provide statically:

After importing the default export from the ESM module, the host can first do a type check using the isWebAudioModuleConstructor getter, then construct the WAM instance using the createInstance method. The following example demonstrates the steps required for a host to create a WAM using the WAM SDK:

/** @typedef {typeof import('@webaudiomodules/api').WebAudioModule} WebAudioModuleConstructor */
(async () => {
    const audioCtx = new AudioContext();
    // Init WamEnv
    const { VERSION: apiVersion } = await import("@webaudiomodules/api");
    const { addFunctionModule, initializeWamEnv, initializeWamGroup } = await import("@webaudiomodules/sdk");
    await addFunctionModule(audioContext.audioWorklet, initializeWamEnv, apiVersion);
    const hostGroupId = 'example-host'; // will be known by host's WAMs
    const hostGroupKey = performance.now().toString(); // should be kept secret from host's WAMs
    await addFunctionModule(audioContext.audioWorklet, initializeWamGroup, hostGroupId, hostGroupKey);

    // Init WAM
    const initialState = {};
    const imported = await import('./path_to_wam/index.js');
    /** @type {WebAudioModuleConstructor} */
    const WAM = imported.default;
    const isWAM = typeof WAM === 'function' && WAM.isWebAudioModuleConstructor;
    if (!isWAM) return;
    const wam = await WAM.createInstance(audioCtx, initialState);
    return wam;
})();

Here,

const wam = await WAM.createInstance(audioCtx, initialState);

is equivalent to

const wam = new WAM(audioCtx);
await wam.initialize(initialState);

The following getters and methods should also be implemented.

For example, a host can get and append to the document the WAM's GUI by doing following:

(async () => {
    const container = document.getElementById('wam-container');
    const wamGui = await wam.createGui();
    container.appendChild(wamGui);
})();

and remove it by:

wamGui.remove();
wam.destroyGui(wamGui);

To connect an initialized WAM to an audio graph:

(async () => {
    const defaultConstraints = {
        audio: {
            echoCancellation: false,
            mozNoiseSuppression: false,
            mozAutoGainControl: false,
        },
    };
    const stream = await navigator.mediaDevices.getUserMedia(defaultConstraints);
    const inputNode = audioCtx.createMediaStreamSource(stream);

    const { audioNode } = wam;
    inputNode.connect(audioNode);
    audioNode.connect(audioCtx.destination);
})();

WamDescriptor interface

The WAM descriptor contains information that can be used by the host to properly categorize, display, and load a WAM. The WamDescriptor interface is an object used in the WAM's descriptor JSON file and in its instance's descriptor property. It has the following fields:

The WamDescriptor also contains a set of boolean properties indicating the WAM's IO support. They are optional in the descriptor JSON, but mandatory in the descriptor getter under the WebAudioModule interface. These properties will affect the WAM's behavior in the host when it receives audio or events from upstream WAMs.

WamNode interface

WamNode extends WebAudio's AudioNode. Instances are accessed via the audioNode getter under the WebAudioModule interface.

A WAM host will use its native (or overridden) connect and disconnect methods to run its underlying DSP in an audio graph. The WamNode can also be the destination node of any AudioNode connection.

It has following getters and methods:

Lifecycle related:

State related:

A state object can be any serializable type and should contain all information required to fully save or restore a WAM.

(async () => {
    const currentState = await wamNode.getState();
    await wamNode.setState(currentState);
})();

Parameter related:

Most WAMs have one or more parameters that allow the user to alter the behavior of the plugin. Note that a WAM parameter is different from WebAudio AudioParams, which are ignored in the WAM API. To schedule WAM parameter automation the host should instead use scheduleEvents.

The methods above are available on the main thread and should not be used in time-critical situations.

Event related:

WAM events can contain parameter changes, MIDI events, etc. To allow a WAM to send events to other WAMs the host can call following methods:

Though initiated on the main thread, ultimately the connections must be handled in the audio thread by calling webAudioModules.connectEvents or webAudioModules.disconnectEvents. Events without timestamps will be processed 'ASAP' while those with timestamps will be processed at the specified time in the future. Interested parties such as hosts or WAM GUIs can be notified when an event is processed via addEventListener.

Processing related:

WamProcessor interface

Each WAM plugin should implement the WamProcessor interface on the audio thread. The interface extends WebAudio's AudioWorkletProcessor and is instantiated by a WamNode. On the audio thread, the processor can access the WamEnv interface under globalThis.webAudioModules. When the processor is created, it should call webAudioModules.addWam to register itself with the WamEnv.

WamProcessor has the following getters and methods:

The following getters and methods mirror the WamNode interface, providing the same functionality on the audio thread:

Event related:

WamGroup interface

Hosts and WAMs which act as sub-hosts (such as 'pedalboard' type plugins) must register a WamGroup with the WamEnv in order to manage WamProcessors and facilitate WamEvent connections on the audio thread.

After initializing the WamEnv, hosts must also initialize a WamGroup before creating any WAM instances. Registering a WamGroup requires both a groupId and a groupKey. The former will be shared with all plugins the host (or sub-host) creates via the WebAudioModule constructor or createInstance method, thus facilitating those WAMs' interactions with the WamEnv. The latter should be kept 'secret' to prevent any entity other than the host/sub-host from gaining access to a reference to the WamGroup instance via WamEnv's getGroup method.

WamGroups make it possible for there to be multiple hosts sharing the same AudioContext. WAMs will not interact directly with their WamGroups -- these interactions are instead mediated by the WamEnv. This is meant to ensure that WAMs are 'sandboxed' within a WamGroup, thus facilitating the creation of sub-host WAMs which can manage and have privileged access to a sub-graph composed of its child WAMs while preventing them from accessing other WAMs belonging to the primary host. Therefore a sub-host WAM's groupId will be that of the primary host, while the groupId of any plugins the sub-host creates will be a different ID corresponding to the sub-host's own WamGroup.

WamGroup has the following getters and methods:

WamEnv interface

The host application must initialize the WamEnv and then initialize a WamGroup before any WAMs can be instantiated. The WamEnv is a global singleton which facilitates WAM functionality on the audio thread. WamEnv manages WamGroup instances and acts as an intermediary between WamProcessors and their corresponding WamGroups when adding/removing WamProcessors or connecting/disconnecting/emitting WamEvents. It also allows WAMs to access code on the audio thread as an alternative to import statements, which should not be used in audio thread code.

WamEnv has the following getters and methods:

Since parent WamGroups are not directly accessible by WamProcessors, the following proxy methods are provided with an additional groupId argument: