UNPKG

@webaudiomodules/sdk-parammgr

Version:

Parameter Manager SDK for WebAudioModules Plugin

144 lines (105 loc) 8.16 kB
# 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`. ![Direct to AudioParam](media://paramMgr_0.png) > 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`. > ![Direct + default event listeners or `AudioParam`s](media://paramMgr_1.png) > 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` > ![Mapping + default event listeners or `AudioParam`s pattern](media://paramMgr_2.png) > 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 '@webaudiomodules/sdk'; import { ParamMgrFactory, CompositeAudioNode } from '@webaudiomodules/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; } } ```