@webaudiomodules/sdk-parammgr
Version:
Parameter Manager SDK for WebAudioModules Plugin
144 lines (105 loc) • 8.16 kB
Markdown
# Parameter Manager
This document provides a description of the Parameter Manager used for the `WebAudioModule` [SDK](https://github.com/webaudiomodules/sdk), and a guide to handle parameters in an `WebAudioModule` with the Parameter Manager.
### Motivation
It is conventional for audio plugin users and hosts to schedule plugin parameter changes with an automation timeline. The WebAudio API provides the AudioParam interface, with its `AtTime` methods, to allow developers to schedule sample-accurate `a-rate` or buffer-accurate `k-rate` automations in several ways.
It is important for an `WebAudioModule` to control its parameters sample-accurately. However, the `AudioParam`s exist only inside `AudioNode`s, they are not constructable independently, and they do not exist in the audio thread. This is reason that `WebAudioModule` API provides another interface `WamParameter` for automatable parameters both in the main thread and in the audio thread. The Parameter Manager provides an implementation of the `WamParameter` that uses native but customized `AudioParam` to handle automation scheduling. In fact, Parameter Manager is mainly an `AudioWorkletNode` that creates user defined `AudioParam`s, then transform them to `AudioNode` outputs or funcion calls.
### Plugin Design Patterns
As described in the `WebAudioModule` API, the developer should declare and configure every parameters as `WamParameterInfo` that are controllable and automatable by the host application, and let them accessible via `WamNode`'s methods, such as `getParameterInfo()`. In the Parameter Manager, we consider these parameters are the WAM's *exposed parameters*. (see [`ParametersMappingConfiguratorOptions.paramsConfig`](https://github.com/webaudiomodules/sdk-parammgr/blob/master/src/types.d.ts#L41)).
In a host, by automating or controlling these *exposed parameters*, it will then change the WAM's internal state. The variables to be changed as the internal state, which we call *internal parameters*, can be an `AudioParam` or an event handler that will be called while the values change, under a certain fire rate. (see [`InternalParametersDescriptor`](https://github.com/webaudiomodules/sdk-parammgr/blob/master/src/types.d.ts#L31))
In some use cases, the plugin needs to control multiple *internal parameters* with one single *exposed parameter*, and with different value scalings or mappings. For example, an *exposed parameter* `mix` need to be clipped from 0 to 0.5 and be mapped to 0 and 1 for an *internal parameter* `dry`; at the same time, it need to be clipped from 0.5 to 1 and be mapped to 1 and 0 for an *internal parameter* `wet`. This can be done easily by declaring a `paramsMapping`. (see [`ParametersMapping`](https://github.com/webaudiomodules/sdk-parammgr/blob/master/src/types.d.ts#L38))
By using the `ParamMgrFactory.create` static method, the developer will create an instance of the Parameter Manager that will automatically handle the parameters. It depends on the configuration provided with the `paramsConfig`, `internalParamsConfig` and `paramsMapping` properties of the `optionsIn` argument. There are three main design patterns to declare and link the *exposed parameters* to the *internal parameters* using the Parameter Manager.
0. Direct to `AudioParam`, no need to declare the `paramsConfig` and the `paramsMapping`, declare only the `internalParamsConfig`.

> If the developer leaves the `paramsConfig` and the `paramsMapping` undefined, the SDK will derive the `paramsConfig` from the `internalParamsConfig`, which means they are containing the same parameter names and values. The `paramsMapping` will be filled with peer to peer mappings with no value mapping.
> For example:
```JavaScript
// if audioNode.gain and audioNode.freq are AudioParams
const internalParamsConfig = {
gain: audioNode.gain,
freq: audioNode.freq
};
const paramMgr = await ParamMgrFactory.create(wam, { internalParamsConfig });
```
1. Direct + default event listeners or `AudioParam`s, no need to declare the `paramsConfig` and the `paramsMapping`, declare only the `internalParamsConfig`.
> 
> If the developer declared the `internalParamsConfig` and leaves the `paramsMapping` unset, the SDK will automatically make links between the *exposed parameters* and the *internal parameters*, taking account of the giving `AudioParam`, or the `onChange` callback with the `automationRate`.
> The `paramsMapping` will be filled with peer to peer mappings with no value mapping.
> For example:
```JavaScript
const internalParamsConfig = {
enabled: {
onChange: (value, prevValue) => {
console.log(`Param "enabled" has been changed from ${prevValue} to ${value}`);
}, // callback
automationRate: 10 // 10 times/sec
},
gain: audioNode.gain // AudioParam
};
const paramMgr = await ParamMgrFactory.create(wam, { internalParamsConfig });
```
2. Mapping + default event listeners or `AudioParam`s pattern, need to declare the `paramsConfig`, `internalParamsConfig` and the `paramsMapping`
> 
> This pattern is useful when a different mapping is needed between the *internal parameters* and the *exposed parameters*.
> A value mapping can be set via `sourceRange` and `targetRange` fields. The incoming value of the *exposed parameter* will be firstly clipped using `sourceRange`, then the value in the `sourceRange` will be remapped to the `targetRange`. If these fields remain `undefined`, they will be the same as the `minValue` and the `maxValue` of the *exposed parameter*.
> If one parameter name appears in both `paramsConfig` and `internalParamsConfig`, the mapping will be created automatically if it is not declared explicitly in the `paramsMapping`.
> Dynamically changing the `paramsMapping` is possible using the `setParamsMapping` method.
> For example:
```JavaScript
const paramsConfig = {
mix: {
defaultValue: 0.5,
minValue: 0,
maxValue: 1
}
}
const internalParamsConfig = {
dryGain: dryGainNode.gain,
wetGain: wetGainNode.gain,
};
const paramsMapping = {
mix: {
dryGain: {
sourceRange: [0.5, 1],
targetRange: [1, 0],
},
wetGain: {
sourceRange: [0, 0.5],
targetRange: [0, 1],
},
},
};
const option = {
paramsConfig,
internalParamsConfig,
paramsMapping
};
const paramMgr = await ParamMgrFactory.create(wam, option);
```
### Creating a Composite `AudioNode` using the Parameter Manager
`WebAudioModule` API requires that the module's `audioNode` is connectable as audio I/O, and implements the `WamNode` interface. As a developer, one can use the Parameter Manager to act as the `WamNode` interface, and use another `AudioNode` to act as the audio I/O by creating a `CompositeAudioNode`. We provide a [prototype](https://github.com/webaudiomodules/sdk-parammgr/blob/master/src/CompositeAudioNode.d.ts) of the `CompositeAudioNode` in the Parameter Manager folder.
To get it work with the Parameter Manager, see this example:
```JavaScript
import { WebAudioModule } from '/sdk';
import { ParamMgrFactory, CompositeAudioNode } from '/sdk-parammgr';
class MyCompositeAudioNode extends CompositeAudioNode {
setup(output, paramMgr) {
this.connect(output, 0, 0);
this._wamNode = paramMgr;
this._output = output;
}
}
export default class MyWam extends WebAudioModule {
//... other settings
async createAudioNode(initialState) {
const gainNode = new GainNode(this.audioContext);
const compositeNode = new MyCompositeAudioNode(this.audioContext);
const internalParamsConfig = {
gain: gainNode.gain
};
const paramMgrNode = await ParamMgrFactory.create(this, { internalParamsConfig });
compositeNode.setup(gainNode, paramMgrNode);
if (initialState) compositeNode.setState(initialState);
return compositeNode;
}
}
```