Examples Module Library
This section discusses examples which illustrate advanced module design techniques. The modules are part of the Examples module library introduced in https://dspconcepts.atlassian.net/wiki/spaces/DOCHUB/pages/2748822151. In addition to the scaler_smoothed_example_module.m, this library contains the following:
scaler_example_module.m — Simple scaler. This module demonstrates how to support multiple input and output pins each with, possibly, a different number of channels.
peak_hold_example_module.m — Similar to a level meter in that it measures the peak signal level. This module demonstrates how to allocate arrays and update array sizes within the module's prebuild function. A fixed-point version of this example is also provided: peak_hold_example_fract32_module.m.
fader_example_module.m — Front-back fader which pans a signal between two different outputs. This module demonstrates compiled subsystems, that is, how to build a new module class out of a subsystem. A fixed-point version of this example is also provided: fader_example_fract32_module.m.
downsampler_example_module.m — Downsampler which keeps 1 out of every D samples; no filtering is performed. This module demonstrates how to operate on arbitrary 32-bit data types as well as changing the output block size and sample rate.
lah_limiter_example_module.m — Look ahead limiter delivered as a compiled subsystem. This module demonstrates some advanced features of compiled subsystems including initializing internal module variables and clarifying wire buffer usage.
The example library is found in the <AWE>\AWEModules\Source\Examples directory. Run the main script make_examples.m to generate code for the entire library.
Scaler Module
This example is contained in the file scaler_example_module.m. It is equivalent to scaler_module.m provided in the Deprecated module library. This module implements a straightforward linear scaler without any smoothing. The module scaler_smoothed_example_module.m implements linear scaler with smoothing. The module has single input pin and single output pin with arbitrary number of channels. This module illustrate the basic components of Audio Weaver module.
The input arguments to the MATLAB constructor function are:
function M = scaler_example_module(NAME)
It has only one argument and which is the name of the module (a string).
We next create and name the @awe_module object
M=awe_module('ScalerExample', 'Linear multichannel scaler');
if (nargin == 0)
return;
end
M.name=NAME;
Then set the MATLAB processing function:
M.processFunc=@scaler_example_process;
Input and output pins are added to the module. The module has one input pin and one output pin with arbitrary number of channels.
PT=new_pin_type([], [], [], 'float', []);
add_pin(M, 'input', 'in', 'Input signal', PT);
add_pin(M, 'output', 'out', 'Output signal', PT);
The .gain
variable is added. This gain is applied across all channels:
add_variable(M, 'gain', 'float', 1, 'parameter', 'Linear gain');
The default range information for the variable is set. This can be changed after the module has been instantiated in MATLAB and is used when drawing the inspector. We also set the units to 'linear'. This is just for display purposes and as a reminder in the documentation.
M.gain.range=[-10 10];
M.gain.units='linear';
The C file containing the processing function is specified
followed by documentation for the module:
We also indicate that the module can reuse the same wires for input and output.
M.wireAllocation='across';
Next code is the module browser information used by Audio Weaver Designer. It describes to Audio Weaver Designer where to place the module in the tree, the image of the module under tree, search tag and the basic shape of the module in the Designer.
The processing code is contained in the file InnerScalerExample_Process.c.
The macro
int numSamples = ClassModule_GetNumSamples(pWires[0]);
gets the total number of samples of the input pin, which same as output pin size. We then loop over for numSamples and apply the S->gain for each sample on input and store in output. The input data is located at:
pWires[0]->buffer
and the output data is placed into
pWires[1]->buffer
Peak Hold Module
The peak hold module demonstrates the use of variable size arrays and shows how the constructor function can be specified completely in MATLAB code. The peak hold module has a single multichannel input pin. The module computes the maximum absolute value for each channel on a block-by-block basis. The module records the peak absolute value seen since the module started running and also computes a peak detector with settable attack and decay rates. In many ways, the peak hold module is similar to a meter module working in block-by-block mode. This module is contained within the file peak_hold_example_module.m. A related fixed-point version is found in peak_hold_example_fract32_module.m
The module begins in the usual way. The MATLAB function takes a single argument which is the name of the module:
function M=peak_hold_module(NAME)
We then create the underlying module object, set its name and also set three internal functions.
Next an input pin is added. This pin accepts floating-point data and places no restrictions on the block size or sample rate.
Several top-level parameters are added. These parameters set the attack and decay times (in milliseconds) and also specify a variable which allows the peak values to be reset.
Add two additional hidden variables which are the attack and decay coefficients. These coefficients are computed based on the sampling rate and block size and the calculation occurs within the set function. Both variables are marked as being hidden.
The next part is key to this example. We add two arrays whose size depends upon the number of channels in the input signal. The code is:
At this point, the array is set to the empty matrix; its size is based on the number of input channels which is currently not known. The array size is set below in the module's prebuild function.
Two additional fields are set for each array variable. These variables provide information enabling the C constructor function to be automatically generated. For each array, a call to the framework memory allocation function is made
void *awe_fwMalloc(size_t size, UINT heapIndex, int *retVal);
The first argument is the number of words to allocate (the size) and the second argument specifies which memory heap to allocate the memory from. In the MATLAB code, set:
M.peakHold.arraySizeConstructor='ClassWire_GetChannelCount(pWires[0]) * sizeof(float)';
This string becomes the first argument to awe_fwMalloc()
. The variable pWires
is an argument to the constructor function and points to an array of wire pointers. The code contained in the string determines the number of channels in the first input wire and multiplies by sizeof(float)
. The net result is the amount of memory to allocate for the array.
The second argument to the awe_fwMalloc function is specified by the MATLAB code
M.peakHold.arrayHeap='AWE_HEAP_FAST2SLOW';
This value is #define
'd within Framework.h and specifies that memory should first be taken from the fast internal heap, and if this heap is full, is should then be taken from the slow heap.
Together, the .arraySizeConstructor
and .arrayHeap
settings get turned into the following code by the code generator:
This process repeats for the second array .peakDecay
.
Thus far, the code pieces within MATLAB generate the C constructor function. MATLAB also needs to know the sizes of the arrays for tuning and in order to draw the user interface. The arrays sizes are set in the MATLAB prebuild function. This is also located in the file peak_hold_example_module.m:
For each array, we first set the size to [numChannels 1]
and then set the default value to an array of all zeros of the same size. Note that the order of operations is important. When doing array assignments, Audio Weaver checks that the assigned data is of the appropriate size. If the code were instead
Then the first assignment of zeros()
would fail since the size of the array has not yet been set.
The rest of the function is fairly straightforward. We specify the C processing and set functions using code markers
Documentation is then added (not shown here). A line is added to the generated C file which includes the file FilterDesign.h. This file contains some of the filter design equations used by the C set function.
awe_addcodemarker(M, 'srcFileInclude', '#include "FilterDesign.h"');
There are both MATLAB and C versions of the set function. The functions translate the high-level attackTime and decayTime variables to the low-level attackCoeff and decayCoeff variables. The MATLAB code is
The C code contained in InnerPeakHoldExample_Set.c is:
Why are two versions of the set function required? For this module, the MATLAB function is not required. After a module is allocated on the target in C, the C set function is automatically called and the coefficients computed. The MATLAB code is for convenience and can also be used for regression testing.
The next portion of the peak_hold_example_module.m file contains the inspector information. We only show the .peakHold variable since the others are similar or have already been discussed. The inspector drawn is:
This example has a stereo input. The first two meters shown are .peakDecay
for both channels. The second two meters show .peakHold
. The MATLAB code for drawing the .peakDecay
meters is:
The .peakDecay
variable is defined in MATLAB as a column vector. In this case, since there are two input channels, the vector is of dimension 2x1. When the meter control is drawn, the default behavior is to draw it as 2 high and 1 wide (this matches the MATLAB variable size)
This is not what we want — we want the controls to be side by side. To achieve this, the array of values has to be transposed to form a row vector. The statement
M.peakDecay.guiInfo.transposeArray=1;
achieves this. Next, the range of the control is set, the variable is tied to a meter control, and finally, the control is instructed to convert from linear measurements to dB for display. This completes the MATLAB code for the .peakDecay
inspector control. The .peakHold
inspector specification is almost identical.
The last portion of the file is module browser information as explained in scaler module example above.
The C processing function is straightforward and is not shown here. Refer to the file InnerPeakHoldExample_Process.c.
Fader module
This example teaches how to create a new module class out of a subsystem and shows how to implement a custom bypass function. The fader module has a single mono input pin, a stereo output pin, and 3 internal modules:
The input signal is scaled by scalerF
to yield the front channel and by scalerB
to yield the back channel. The output pin is stereo with the first channel representing the front and the second channel the back. The scalers are set so that a portion of the input signal is fed to each of the front and back channels. The fader implements constant energy panning using a sine/cosine response.
The fader example is found in fader_example_module.m. As usual, create a subsystem and set its .name based on an argument to the function.
Add input and output pins. The input pin is a single channel without any restrictions on the blockSize
or sampleRate
. The output pin is stereo without any restrictions on the blockSize
or sampleRate
.
Then connect them together:
None of the bypass functions provided with Audio Weaver is suitable for this module. The desired bypass behavior is to copy the input channel to each of the two output channels and scale by 0.707. To implement this custom bypass functionality, write an inner bypass function as shown below:
Then set the 'bypassFunction' code marker to insert this file:
awe_addcodemarker(SYS, 'bypassFunction', 'Insert:code\InnerFaderExample_Bypass.c');
The key lesson this example is the MATLAB code
SYS.flattenOnBuild=0;
This identifies the subsystem as one that is not flattened when building. That is, the FaderExample module class exists natively on the target. The Server only sends a single command to the target and the entire subsystem will be instantiated. The automatically generated constructor function is found in ModFaderExample.c and shown below. The constructor function allocates the base module and then allocates each of the 3 internal modules. The module is marked as requiring a total of 4 pins: 1 input, 1 output, and 2 scratch. These pins are allocated outside of the function and then passed in. By looking through the code you can see how the wire information is passed to each of the internal modules' constructor function.
Similarly, since this module is created out of a subsystem, there is no need to provide a processing function; it will be automatically generated by Audio Weaver. (Yes, a custom processing function could be supplied, but the generated one works fine.) The automatically generated processing function is shown in ModFaderExample.c. It simply calls the 3 processing functions for the 3 internal modules:
The set function for the subsystem takes the high-level .smoothingTime
variable and passes it down to each of the internal smoothed scaler modules. The function also takes the .fade variable and computes two separate gains using a sin/cos pan.
The function sets the high-level variables of the smoothed scaler modules and then calls the set function for the module. The set function takes a pointer to the smoothed scaler module and also a bit mask which identifies the specific variables that were modified. In this case, we are setting the smoothingTime
and gain fields of the modules.
The last portion of the file is module browser information as explained in scaler module example above.
There is also a fract32 version of this module called fader_example_fract32_module.m See this module for examples of fixed-point processing.
Downsampler Module
This module downsamples a signal by keeping only one out of every D input samples. This example demonstrates two further techniques used when creating audio modules. First, the module essentially copies 32-bit samples from input to output and doesn't care if the data type is float, fract32, or int. All of these types will work with the same processing function. This is obvious in C but this flexibility must be encoded into the MATLAB scripts. The second technique shown is how to change the block size and sample rate as part of the prebuild function.
This example is contained in the file downsampler_example_module.m and is equivalent to downsampler_module.m found in the Advanced module library.
The MATLAB code defining the input and output pins of the module is:
In this case, the data type is specified as '*32'. This abbreviation is expanded to all possible 32-bit data types in the add_pin function: {'float', 'fract32', 'int'}
. It is possible to specify the data type to the new_pin_type call as the cell array
{'float', 'fract32', 'int'}
Using the '*32' abbreviation gives us flexibility in case the set of data types is ever expanded in the future.
The decimation factor D is the second argument to the MATLAB function and is specified at instantiation time. The variable D appears in the instance data structure and its usage is listed as 'const'. This means that the variable D can no longer be changed.
The module's prebuild function computes the type of the output pin. The block size and sample rate are reduced by the decimation factor D; the number of channels and data type are unchanged. We also verify that the block size of the input is divisible by the decimation factor:
The module's MATLAB implementation of the processing function is found in downsampler_example_process.m. It simply returns 1 out of every D input samples.
The C processing function is similar. Although written for 32-bit floats, the function works equally well with any 32-bit data type.
The last portion of the file is module browser information as explained in scaler module example above.
Note: The shape information is provided as custom svg image. The default shape of the module is rectangle and it can be overwrite with svgImage tag.
Look Ahead Limiter Module
This example demonstrates some advanced code generation techniques. The look ahead limiter is a dynamics processor which applies a time-varying gain to a signal. The module m-file takes two arguments.
SYS=lah_limiter_example_module(NAME, MAXDELAY)
The first is the NAME
of the subsystem and the second, MAXDELAY
, is the maximum amount of delay, in milliseconds. MAXDELAY
is used internally when allocating the delaymsec_module.m
:
The compiled subsystem contains 4 internal modules:
Specifying Internal Module Constructor Arguments
If we follow the normal code generation procedure, then the constructor function for the internal delaymsec_module will be:
If you look at the values of the delayArgs array, you'll see that the maximum delay time of the internal module is hard coded to 5 msec. This is not what we wanted; the delay time should instead be specified at construction time.
To fix this, add a high-level variable to the look ahead limiter system. We'll name it 'maxDelayTime' to be consistent with the delaymsec_module.m.
Next, we'll use the maxDelayTime
field of the look ahead limiter instance structure as an initializer for the maxDelayTime
and currentDelayTime
fields of the internal module. This is done by setting the .constructorCode
field of the delaymsec_module variables as shown below:
In the automatically generated constructor code, S always refers to the base module structure which has already been allocated and initialized by BaseClassModule_Constructor()
. The new constructor code for the delay is:
The argument S->maxDelayTime
initializes the internal module achieving the desired effect.
Disambiguating Wire Allocation
If look at the default subsystem including wiring information, we see:
By default, the input and output pins are mono and the system ends up using 3 scratch wires:
This subsystem is designed to operate on an arbitrary number of channels. However, the wire allocation shown above is only applicable when the input is mono. (To see this, consider the MaxAbs module. This module always has a mono output and is assigned to wire #3. The signal path through DelayMsec can be N-channel and is also assigned to wire #3). If this system is run with stereo inputs, then the wire allocation is incorrect and the audio processing will fail.
To fix this problem, force the system input pin to have 2 channels by default.
The pin still holds an arbitrary number of channels, but its default value is 2. When the subsystem is now routed, the wires will be allocated as:
The system now requires a total of 4 scratch wires, with a new scratch wire, #6 assigned to the output of DelayMsec
. Furthermore, the buffer allocation is now general purpose and will work with any number of input channels.
The last portion of the file is module browser information as explained in scaler module example above.