All algorithms are implemented in plain C99 as simple headers and only require:
NULL
, INFINITY
, and (U)INT{8,16,32,64}_{MIN/MAX}
;size_t
and (u)int{8,16,32,64}_t
types;float
type.The user of their APIs is given some flexibility in supplying such definitions (see bw_common).
Each algorithm is implemented in a single module, which is as independent as possible but still may require (i.e., depend on) other modules. Each module consists of an header file having filename module.h
.
Modules are not meant to be built and used as a static or shared library. You should rather just include the needed module files to develop your software.
Both individual modules and module collections are versioned according to the Semantic Versioning Specification 2.0.0. Module dependencies are considered part of the APIs, therefore if a dependency is removed that would lead to MINOR version change, and if one is added that would cause a MAJOR version change.
Unless otherwise stated, C APIs follow the following conventions:
NULL
;Optional C++ wrappers are supplied in most modules. The APIs they expose are a wide subset of the corresponding C APIs targeted at the most common use cases, therefore trading some flexibility for ease of use. C APIs are still accesible from C++.
These wrappers also require:
std::array
obtained by
(unless BW_CXX_NO_ARRAY
is defined);new
and delete
operators (only actually needed by modules that deal with dynamic memory).Unless otherwise stated, the same conventions as C APIs apply. Also:
module_init()
is implemented by class constructors;new
and delete
;Modules are here categorized by API type, so that modules in a given category share similarities in how their APIs are meant to be used.
These modules are required by most, if not all, other modules and just contain a few basic essential definitions. They provide a certain degree of consistency and adaptability to the rest of the code.
Utility modules contain heterogeneous code that is repeatedly used by applications and other modules.
These modules implement signal processing blocks (not necessarily audio-related).
Their APIs are not 100% uniform, but they tend to follow a common pattern described here. Varations to this pattern are indicated in the individual module's documentation.
Each signal processing block conceptually consists of:
Signal inputs and outputs are not explicitly represented by any data structure, but you rather see them as arguments or return values of processing functions. Typically, signal data is PCM-encoded as (buffers of) float
s, whose values usually lie in the [-1.f
, 1.f
] range (these extremes correspond to the 0 dBFS clipping points — belonging to this range is not a strict requirement but values MUST be finite), and uniformly-sampled at a user-supplied sample rate, which is common to all signal inputs and outputs of a block instance. Signals that vary at such sample rate are also known as audio-rate signals.
Parameters, on the other hand, can have different types and ranges and are asynchronously updated at a variable rate, also known as control rate, that is lower than the sample rate.
Less commonly, there can also be control-rate signals. These are analogous to parameters except they are passed as arguments or returned by processing functions.
Coefficients are sets of heterogeneous values specific to a module type that are computed and kept up to date by various functions at control and/or audio rate on the basis of input parameter values. They are stored into a data structure that is opaque to the API user.
If it is supposed to have one, a block instance can be uniquely characterized by a data structure containing its internal state, which is also opaque to the API user and indirectly updated by reset and processing functions.
Data-wise, a block instance can be composed of a set of coefficients and its internal state. A given set of coefficients, however, can be associated with many block instances. In other words, a block instance is in a 1:1 relationship with its internal state (if it has one), while coefficients are in a 1:N relationship with states/instances. When a set of coefficients is shared by more instances, these act like they have all the same input parameter settings (this can be particularly useful for multichannel audio and other parallel structures).
In each module you may find:
struct
containing coefficients, typedef
ed as module_coeffs
;struct
containing the internal state, typedef
ed as module_state
.In C++ wrappers much of this arrangement is hidden from the API user, who is instead presented with class templates implementing a variable number of instances sharing common coefficients.
The APIs of these modules superficially resemble audio plugin APIs, while being based on a common underlying computational and behavioral model influenced by finite-state machines.
API functions make coefficient and internal state data structures transition their states, which are ordered. Each function can be called on coefficients/states that have reached at least a given state.
The rest of this section will describe the API of an archetypical module
.
Before any function call, coefficients and states are in an uninitialized state where their content is undefined and invalid.
You may find a module_init()
function that resets input parameters to their default values, possibly taking initialization-time variables as input arguments. This can be called on a set of coefficients in any state, including the uninitialized one, and transitions it to the initialized state.
You may find a module_set_sample_rate()
function that sets the given sample rate value (which MUST be finite and positive) in the supplied set of coefficients, which MUST be at least in the initialized state and which will transition to a "sample-rate-set" state.
If this function is not present, coefficients in the initialized state automatically reach the "sample-rate-set" state.
Some modules require the API user to provide a chunk of contiguous memory to be used as part of the internal state of an instance. This arrangement avoids the need to directly rely on dynamic memory allocation and puts the burdens and the honours of managing memory in the hands of the API user. The size of the required memory chunk is typically dependent on the sample rate and eventual initialization-time variables.
For this purpose, you may find module_mem_req()
and module_mem_set()
functions which, respectively, retrieve the size of the required memory chunk and associate a memory chunk to a given internal state.
Both need the given set of coefficients to be at least in the "sample-rate-set" state, while the latter does not impose restrictions on the given internal state (it can be uninitialized) and transitions it into the "memory-chunk-set" state.
If these functions are not present, uninitialized internal states automatically reach the "memory-chunk-set" state.
You may find a module_reset_coeffs()
function that "resets" coefficients, which MUST be at least in the "sample-rate-set" state, and transititions them to the "reset" state.
As said, coefficients are computed on the basis of input parameters. As they may be subject to smoothing or other dynamic update patterns, their values may evolve over time even if input parameter values stay constant from a certain moment on. However, they are required to converge towards fixed values given constant input parameter values and sample rate. This function forces coefficients to assume such target values, and thus depends on the current input parameter values.
If this function is not present, coefficients in the "sample-rate-set" state automatically reach the "reset" state.
You may find module_reset()
and module_reset_multi()
functions that "reset" internal states, which MUST be at least in the ""memory-chunk-set" state, and transition them to the "reset" state. Also, the associated coefficients MUST be at least in the "reset" state.
Internal states are reset according to current input parameter values, coefficients, and potentially supplied initial input values.
If these functions are not present, internal states in the "memory-chunk-set" state automatically reach the "reset" state.
You may find:
module_update_coeffs_ctrl()
function that performs control-rate update of coefficients;module_update_coeffs_audio()
functions that performs audio-rate update of coefficients;module_process_ctrl()
function that processes input control-rate signals and updates internal state (control rate);module_process1*()
functions that process one sample of each input and output signal and update internal state (audio rate);module_process()
function that processes a buffer of each input and output signal and updates coefficients and internal state (control and audio rate),module_process_multi()
function, which is a multichannel version of the previous function, and which processes a group of input and output buffers, as well as internal states, updating coefficients and internal states (control and audio rate).In case input parameters are allowed to change, they MUST be continuously updated both at control and audio rate. Unless that is handled already by processing functions, you are responsible for invoking the relevant functions as needed. In this sense, module_process_ctrl()
is analogous to module_update_coeffs_ctrl()
(i.e., it MUST be called at control rate if present and parameters can change and not using module_process()
or module_process_multi()
).
Internal states, instead, are only updated by processing functions.
When using any of these functions, both coefficients and internal states MUST be at least in the "reset" state.
You may find one or more module_set_parameter()
and module_get_parameter()
functions that, respectively, set an input parameter value and get an output parameter value.
When using these functions, coefficients MUST be at least in the initialized state while internal states MUST be at least in the "reset" state.
Definitions in this glossary are not meant to be general but only valid in the context of using these APIs.