Document toolboxDocument toolbox

(8.D.2.6) C Run-Time Environment

This section describes the portion of the C run-time environment used by audio modules.  This includes the module processing and constructor functions, memory allocation, and how the modules interface to the Server running on the target.

Each audio module has an associated header file ModClassName.h containing a class definition, and a source file ModClassName.c containing the associated class structure and functions.  The header and source files are generated by MATLAB, but it is important to understand their contents and how all of the pieces fit together.

Data types and Structure Definitions

Let's begin by looking at the class definition for the smoothly varying scaler found in ModScalerSmoothedExample.h:

typedef struct _awe_modScalerSmoothedExampleInstance { ModuleInstanceDescriptor instance; FLOAT32 gain; // Target gain FLOAT32 smoothingTime; // Time constant of the smoothing process FLOAT32 currentGain; // Instantaneous gain applied by the module. This is also the starting gain of the module. FLOAT32 smoothingCoeff; // Smoothing coefficient } awe_modScalerSmoothedExampleInstance;

The first field, "instance", is the common header used by all audio modules and the type ModuleInstanceDescriptor is defined in

<AWE>\AWEModules\Source\Examples\Include\Framework.h. 

Framework.h contains the primary data types and definitions used by Audio Weaver.  Most of the definitions are used internally by the Server, but a few apply to audio modules.

Module Instance Structure

Following the instance header are all of the module specific variables.  Only 8 types of module variables are currently supported:

  • int — 32-bit signed integer

  • uint — 32-bit unsigned integer

  • float — 32-bit floating-point value

  • fract32 — 32-bit fractional in 1.31 format

  • int * — pointer to an array of signed integers

  • uint * — pointer to an array of unsigned integers

  • float * — pointer to an array of floating-point values 

  • fract32 * — pointer to an array of fractional values

You'll note that only 32-bit data types are currently supported.  This is for simplicity, but may be expanded in the future.

All scalar module variables must precede arrays in the instance structure.  This is due to the manner in which audio modules are allocated and initialized by the Server.  The MATLAB module generation scripts automatically reorder variables (using the function awe_reorder_variables.m).  Detailed description of awe_reorder_variables.m is found in Reordering of Render Variables.

The module instance descriptor is also defined within Framework.h:

 

/** A module instance. The instance is variable length on the heap, the size being determined by the constructor arguments. */ /*private*/ typedef struct _ModuleInstanceDescriptor { /** The basic instance data. */ ModInstanceDescriptor instanceDescriptor; /* 0 */ /** Pointer to owning layout instance (type is a forward reference). */ struct _LayoutInstance *pOwner; /* 12 */ /** Array of input and output wires. There are nInwires + nOutWires + nScratchWires elements. */ WireInstance **pWires; /* 16 */ /** Pump function for the module. May be empty, pProcessFunc, or, pBypassFunc. */ void (*pProcessFunc)(void *pInstance); /* 20 */ /** Control flags. */ UINT32 packedFlags; /* 24 */ /** Module specific profiling time. This is number of cycles times 256. */ UINT32 profileTime; /* 28 */ } ModuleInstanceDescriptor; /* 32 bytes */

The module instance header is 8 words in length and contains the fields:

  • instanceDescriptor — 3 word data structure that allows the Server to keep track of the module once it has been allocated on the target.

  • pOwner — points to the overall layout, or collection of audio modules.

  • pWires — pointer to an array of wires used by the module.  Wires are ordered as inputs, outputs, and scratch wires.

  • pProcessFunc — pointer to the module's current run-time function.  This points to the processing function if the module is ACTIVE.

  • packedFlag — a packed integer containing the number of input, output, and scratch pins, as well as the current modules status.

  • profile_time — CPU profiling information.  This is measured on a block-by-block basis and smoothed by a first order filter.  Time is measured in units of clock ticks, where the resolution is platform dependent.  Some platforms, such as the SHARC and Blackfin, are cycle accurate.

Access to the module instance fields is via macros defined in Framework.h.  You should use these macros since it enables us to change the underlying instance structure without breaking existing modules.  Let S be a pointer to an audio module.  The following macros are provided:

  • ClassModule_GetWires(S) — returns a pointer to the array of wires associated with the module.  The wires are ordered as input wires, output wires, and then scratch wires.

  • ClassModule_GetNInWires(S) — returns the number of input wires.

  • ClassModule_GetNOutWires(S) — returns the number of output wires

  • ClassModule_GetNScratchWires(S) — returns the number of scratch wires

  • ClassModule_GetModuleState(S) — returns the module's run-time state.  ACTIVE=0, BYPASSED=1, MUTED=2, and INACTIVE=3.

Pins and Wires

A wire in Audio Weaver contains more than just audio samples.  A wire is defined as:

typedef struct _WireInstance { /** The basic instance data. */ InstanceDescriptor instanceDescriptor; /** The wire buffer. */ Sample *buffer; /** Wire sample rate. */ float sampleRate; // Number of channels 10 bits - 0x3ff // Max block size 17 bits - 0x7fffC00 // isComplex 1 bit - 0x8000000 // Sample size in bytes. 4 bits - 0xf0000000 UINT32 wireInfo1; // Block size 17 bits - 0x1ffff // Data Type 6 bits - 0x7e0000 UINT32 wireInfo2; // Rows 10 bits - 0x3ff // Columns 10 bits - 0xffc00 UINT32 wireInfo3; /** Bind chain pointer default NULL. */ struct _WireInstance *m_pNextBind; /** What its bound to default NULL. */ IOPinDescriptor *m_pBoundPin; #ifndef USE_TEST_PADDING #if defined(BUILD64) || defined(AWE_STORAGE_ALIGN4) UINT32 padding2; #endif UINT32 padding; #endif } WireInstance;

The data type WireInstance describes a wire.  To get a pointer to the data portion of the first wire, use the macro:

Sample *buffer = (pWires[0]->buffer);

*buffer points to the buffer of audio samples.  The data type Sample is a union of integer and floating point values:

Since Sample is a union, you must cast it to one of the possible data types:

(float *) W->buffer;

(INT32 *) W->buffer;

(fract32 *) W->buffer;

(UINT32 *) W->buffer;

The fract32 data type is actually defined as int32.  Providing the fract32 data type is for convenience allowing the programmer to distinguish between integer and fractional values.

A wire can have an arbitrary number of interleaved channels and an arbitrary block size (number of samples per channel).  A wire with N channels and a block size B has a total of NB audio samples.  The samples are ordered as:

The items in the wire's pin descriptor are accessed via macros:

  • ClassWire_GetChannelCount(W) — returns the number of channels in the wire.

  • ClassWire_GetBlockSize(W) — returns the block size of the wire.

  • ClassWire_GetNumSamples(W) — returns the total number of samples in the wire.  This equals the number of channels times the block size.

  • ClassWire_GetSampleRate(W) — returns the sample rate of the data in the wire.  This is used by design functions, such as filter coefficient generation, that depend upon the sample rate of the data.  The sample rate is in Hz.

A pin descriptor structure can be shared by multiple wires as long as the number of channels, block size, and sample rate are the same.  Allocating and assigning pin descriptors is handled by MATLAB when a system is being built.

You'll note that the pin descriptor does not specify the type of data (float, int, or fract32) contained in the wire.  This allows a wire containing floating-point data to be reused for integer data as long as the number of channels, block size, and sample rate are identical.

Examples

The absolute value module has a single input and output pin, and can handle an arbitrary number of channels, block size, and sample rate.  S is a pointer to the module instance.  The processing code is:

Where:

(float *) pWires[0]->buffer points to the buffer of input samples.

(float *) pWires[1]->buffer points to the buffer of output samples

ClassWire_GetNumSampels(pWires[0]) is the total number of samples in the input wire.

Vec_Abs() is an optimized vector function for computing the absolute value.

A second example is the processing function found ModScalerN.c.  This module has a single input pin with multiple channels.  Each channel is scaled by a separate gain. The processing function is:

This module has a multichannel input and output pin.  At the start of the processing function, the number of channels and block size are determined as well as pointers to the input and output buffers.  The module then loops over each channel scaling it by the appropriate scale factor from the gain[] array.  The arguments to the Vec_Scale function are:

  • src+channel — pointer to the buffer of input data.

  • numChannels — stride for the input data.

  • dst+channel — pointer to the buffer of the output data

  • numChannels — stride for the output data

  • S->gain[channel] — scale factor for a particular channel.

  • blockSize — number of samples to scale.

You can clearly see that the data is interleaved based on the offset to the start of the src and dst data as well as the strides. 

Both of the module examples use a set of "vector" functions to perform the actual computation.  It is possible to perform the computation within the module's processing function – many modules do this.  For some modules, however, we have chosen to use vector functions since smaller functions may be more easily optimized in assembly and a single function may be reused by multiple modules.  For example, Vec_Scale() is used by the scaler_module.m, scalern_module.m, and scaler_db_module.m.  Thus, by optimizing a single function vector for a target processor we obtain 3 optimized modules.

Class Object and Constructor Functions

The module constructor function is called in response to a create_module command sent from the Server to the target.  A module's constructor function is called once per module instance and is responsible for allocating memory for the module and copying arguments from the create_module command into the instance structure. 

The operation of the constructor function hinges on information within the module's class structure.  All audio modules of the same class share a single module class structure.  Thus, if a target contains 100 different classes of audio modules, there will be 100 class structures defined.  The class structure is defined in Framework.h as:

The first item of type ModClassDescriptor is defined as:

Let's look at each of these fields more closely.

  • classID — is a unique integer that identifies the module on the target.
    When the target is asked to create a scaler_module.m, it is asked to create a module with a specific integer class ID.  In order to ensure that all classID's are unique, they are stored as offsets in the file classids.csv.  When asked to construct a module, the target searches the table of module class pointers looking for the specified classID.  If there is a match, then the Constructor function, also contained within ModClassDescriptor is called. 

  • Constructor() —  pointer to the module's constructor function. The constructor function takes a specific set of arguments:

Now returning to the other fields in ModClassModule.  The modClassDescriptor is followed by:

UINT nPackedParameters;

This integer specifies the number of constructor arguments required by the module.  Two separate values are packed into this 32-bit integer and these are accessed via separate macros:

  • ClassMod_GetNPublicArgs(packed) — returns the number of public arguments that the module requires.  The number of public arguments equals the number of scalar variables that must be sent from the Server to the Constructor() function.

  • ClassMod_GetNPrivateArgs(packed) — returns the number of private arguments required by the module.  The private arguments are not sent by the Server but must be separately managed by the Constructor function.  Private arguments typically hold pointers to array variables.

The total size of a module's instance structure equals sizeof(ModClassModule) + # of public words + # of private words.

The remaining items in the module class structure are pointers to module functions.

  • pProcessFunc — pointer to the module's processing function.  Called when the module is in ACTIVE mode.

  • pBypassFunc — pointer to the module's bypass function.  Called when the module is in BYPASSED mode.

  • pSet — pointer to the module's set function.  This function is called after the Server updates one of the module's variables.  This function is used to implement custom design functions on the target.

  • pGet — pointer to the module's get function.  This function is called prior to reading one of the module's instance variables by the Server.

  • pSetState — pointer to the module’s set module state function. This function is called prior to change the state of the module. Most of the module’s will have this field as NULL.

A module must provide processing and bypass functions; all other functions are optional.

Let's examine the class structure for the biquad_module.m.  This module implements a 5 multiply second order IIR filter.  The module's class structure defined in ModBiquad.c is:

The CLASSID_ModuleBiquad equals a base value plus an offset:

CLASS_ID_MODBASE + 13

The offset is defined in the file classids.csv:

Biquad,13

The module has a custom constructor function, ModuleBiquadConstructor(), and a total of 5 public instance variables and 1 private instance variable.  Looking at the module's instance structure, we see:

The 5 public variables are the filter coefficients: b0, b1, b2, a1, and a2.  The private variable is the pointer to the array of state variables.  The Biquad module operates on multichannel signals and allocates two state variables per channel.  The Biquad module utilizes the processing function awe_modBiquad_Process(), the bypass function IOMatchUpModule_Bypass(), and does not define a Get or Set function.

The module constructor function first allocates memory for the instance structure, then sets public variables using supplied values, and finally, allocates memory for any arrays.  Constructor functions all have a similar structure exemplified by the constructor for the Biquad module.  Starting with the function arguments:

The function arguments are:

  • retVal — pointer to the function's error return value.  If successful, the function sets *retVal = E_SUCCESS.

  • nIO — a packed integer specifying the number of input, output, and scratch wires used by the module.

  • pWires — pointer to the array of wires used by the module.

  • argCount — total number of arguments supplied to the constructor.  Typically, the constructor arguments are computed by MATLAB and supplied to the create_module Server function.

  • args — pointer to the list of function arguments.  The function arguments can be either integer or floating-point values and are specified as a union.  The constructor function must know whether each argument is an integer or floating-point value.  arg[0].iVal is the first argument as an integer; arg[0].fVal is the first argument as a floating-point value; arg[0].uiVal is the first argument as an unsigned integer.

The first responsibility of the Constructor function is to call BaseClassModule_Constructor().  This function does several things:

  1. Allocates memory for the module's instance structure.

  2. Verifies that argCount equals the number of public instance variables specified in the module's class structure.

  3. Copies the initial values of the public variables from args[] to the variables at the start of the instance structure.  (This method of initializing module variables requires that the public module variables be located contiguously at the start of the instance structure.)

  4. Sets the pWires and packedFlags fields of the ModuleInstanceDescriptor.

  5. Configures the module for ACTIVE mode.

The Constructor function next allocates memory for the state array;

Note: The size of the array is computed based on the number of channels in the first input wire.  The function awe_fwMalloc() allocates memory from one of Audio Weaver's memory heaps.  The heap is specified by the second argument: AE_HEAP_FAST2SLOW and user can find all the macros defined in Framework.h.

Lastly, the Constructor function sets the return value and returns a pointer to the allocated and initialized object:

The instance pointer is cast to a common ModInstanceDescriptor return type.

The Biquad module was used as an example to illustrate the constructor function because it requires an array to be allocated.  If the Constructor function pointer in the module's class structure is set to NULL, then the generic module constructor is called.  The generic constructor simply calls BaseClassModule_Constructor() and is sufficient for modules that do not require additional memory allocation outside of the instance structure or other custom initialization.  The generic constructor is sufficient for a large number of modules and reduces the code size of the target executable.

Memory Allocation using awe_fwMalloc()

Dynamic memory allocation is accomplished in Audio Weaver by the function awe_fwMalloc().  To avoid memory fragmentation, memory can only be allocated but never individually freed.  You can only free all memory completely using the destroy command and then start again.

The memory allocation function is declared as:

void *awe_fwMalloc(UINT32 size, UINT32 heapIndex, INT32 *retVal);

All memory allocation in Audio Weaver occurs in units of 32-bit words.  The first argument to awe_fwMalloc() specifies the number of 32-bit words to allocate.

The second argument, heapIndex, specifies which memory heap should be used for allocation.  Audio Weaver provides 3 heaps and the heaps match the memory architecture of the SHARC processor.  There are two separate internal heaps – traditionally referred to as "DM" and "PM" on the SHARC – which are placed in different memory blocks; and a large heap in external memory.  Having separate internal memory heaps is required to obtain optimal performance from the SHARC.  For example, an FIR filter implemented on the SHARC processor requires that the state and coefficients be in separate memory blocks.  On the Blackfin with cache enabled, the heaps specifiers are irrelevant.

Audio Weaver refers to the heaps as "FAST", "FASTB", and "SLOW".  The heapIndex argument is set using #define's in Framework.h.  In the simplest case, you specify one of the following:

  • AWE_HEAP_FAST — fast internal memory.  Defined as 1.

  • AWE_HEAP_FASTB — a second internal memory heap located in a different block of memory than AWE_HEAP_FAST.  Defined as 2.

  • AWE_HEAP_SLOW — slower memory; usually external.  Defined as 3.

The heapIndex argument can encode several different heaps one nibble (4-bits) at a time.  Set the low 4 bits of heapIndex to the heap you want to allocate memory from first; then set bits 4 to 7 to the next heap; and so forth.  For example, if you want to allocate memory from AWE_HEAP_FAST and then overflow into AWE_HEAP_SLOW, set the heapIndex argument to:

AWE_HEAP_FAST | (AWE_HEAP_SLOW << 4)

Several variations are already defined for you. 

#define AWE_HEAP_FAST2SLOW (AWE_HEAP_FAST | (AWE_HEAP_FASTB << 4) | (AWE_HEAP_SLOW << 8))

Allocate in the FAST heap first.  If this fails, then allocate in FASTB.  Finally, if this fails, then allocate SLOW external heap.

#define AWE_HEAP_FASTB2SLOW (AWE_HEAP_FASTB | (AWE_HEAP_SLOW << 4))

Allocate in FASTB.  If this fails, then allocate from the SLOW heap.

Most modules supplied with Audio Weaver take advantage of this overflow behavior to smoothly transition from internal to external memory once the heaps have filled up.

Module Function Details

Each audio module has a set of 5 functions associated with it.  This section describes the arguments to each of the functions.

Constructor Function

This function is usually generated by MATLAB but at times it needs to be hand written – especially if the module does more than just allocate memory.  Audio Weaver names the constructor function using the convention

awe_modCLASSNAMEConstructor()

where CLASSNAME is the module class.  The function allocates memory for the module from the heaps and then returns a pointer to the newly created object.  The constructor function is declared as:

The return type ModInstanceDescriptor is a common data structure which starts off all audio modules.  The function arguments are:

  • *retVal — integer return value.  The constructor should set this to E_SUCCESS (=0) upon successful completion.  The file Errors.h has a complete list of error codes to choose from.

  • nIO — packed integer specifying the number of input, output, scratch, and feedback wires.  Each number is packed into 8-bits as follows:

INPUT + (OUTPUT >> 8) + (SCRATCH >> 16) + (FEEDBACK >> 24)

Several macros are provided in Framework.h to extract the wire counts:

  • pWires – a pointer to an array of wire pointers.  The wires are ordered as input, output, and scratch. 

  • argCount – number of optional arguments supplied in the Sample array.

  • args – array of optional arguments.  This array is of type Sample which is union:

Each argument can thus be a floating-point, or signed or unsigned integer value.

The module constructor function should first call

The first argument, pClass, is a pointer to the class object for the module under construction.  The remaining arguments are identical to the Constructor arguments.  For example, in ModPeakHold.c, we have:

The BaseClassModule_Constructor allocates memory for the module instance structure and wire pointer array used by the module.  The function also initializes the module instance variables by copying argCount words from the args array starting to the first instance variable in the structure.  The class structure awe_modPeakHoldClass contains the entry

ClassModule_PackArgCounts(5, 2),    // (Public words, private words)

Thus an instance of a peak hold module has a total of 7 variables with the first 5 being set at construction time and the last 2 used internally (or arrays)  argCount must then equal 5 and BaseClassModule_Constructor checks for this.  The remaining two variables in the instance structures are pointers to arrays which are separately initialized by the constructor function.

The final step performed by BaseClassModule_Constructor is to add the newly created module instance to the linked list of objects on the target and then return a pointer to the module instance.

After calling BaseClassModule_Constructor, the module constructor performs any other initialization that is needed.  This may include initializing indirect array variables, like in the ModPeakHold.c example.  When done, the constructor function sets the return error value in *retVal and returns a pointer to the created module instance.

The module constructor function is typically called by the framework in response to a message received from the PC over the tuning interface or from a message stored in a file system.  After a module constructor function is called, the framework checks if the module has an associated Set function.  If so, the Set function is called with a mask of 0xFFFFFFFF. 

There is no need to manually call the Set function within the Constructor.  The framework takes care of this.

Processing Function

The processing function has the signature

void awe_modPeakHoldProcess(void *pInstance)

The function only gets a single argument which is a pointer to the module instance.  This is first cast to the appropriate module instance data type.  For example

awe_modPeakHoldInstance *S = (awe_modPeakHoldInstance *)pInstance;

Using macros defined in Framework.h, the processing function can obtain a pointer to the list of wires used by the module

WireInstance **pWires = ClassModule_GetWires(S);

The number of input wires

int numInWires = ClassModule_GetNInWires(S);

The number of output wires

int numOutWires = ClassModule_GetNOutWires(S);

The number of scratch wires

int numScratchWires = ClassModule_GetNScratchWires(S);

And the current run-time state of the module

int state = ClassModule_GetModuleState(S)

Where:

Note: The processing function is only called when the state is active so checking the state is meaningless in the processing function.

The wire buffers are ordered as input, output, and scratch wires.  Each wire has certain properties that you can query using macros:

where the last macro returns the total number of samples in the wire and equals blockSize x numChannels.

Each wire contains a pointer to its data samples in the 🡪buffer field.  buffer is defined as the union Sample and must be cast to a pointer of the appropriate type.  For example,

(float *) (pWires[0]->buffer)

The processing function then performs the audio processing and writes the result into the output wire buffers.  Local state variables in the instance structure are updated, as needed.

The processing function does not return an error code.  If you need to track error conditions, do this by adding an error code to the instance structure and then tracking it in the inspector.

Set Function

This function implements a module's control functionality and typically converts high-level variables to low-level variables.  The PeakHold module has the function signature

UINT awe_modPeakHoldSet(void *pInstance, UINT mask)

The first argument is a pointer to the module instance structure.  This needs to be cast to a pointer of the appropriate type:

awe_modPeakHoldInstance *S = (awe_modPeakHoldInstance *)pInstance; 

Once you have a pointer to the module instance, you have full access to the module wires using the macros defined in Framework.h.  This is useful, for example, when determining the sample rate or the block size of the module.

The second argument to the Set function is a bit mask that specifies which module variable(s) have changed.  The bit mask is set by the Server when module variables are updated while tuning.  The bit mask may also be set by control code running on the target.  Use of the bit mask is optional and is only really necessary when the module writer needs to distinguish between heavy duty changes requiring significant amounts of computation and lightweight changes.

A bit mask of 0xFFFFFFFF is used to force all variables in the Set function to update. This value of the bit mask is used in a call to the Set function after the module is constructed but before it is pumped for the first time. This ensures that all module variables are updated before they are used.

A special case of the bit mask is when it is equal to 0. When the destroy tuning command is processed by the AWECore, the Set function of every module is called with a mask of 0. This allows the module to do any necessary clean up operations. For example, if your module does file I/O you could close the file at this time. Or you might free memory that is dynamically allocated outside of the Audio Weaver heaps.

In the bit mask, bit N is set when the Nth module variable is set.  The bit counting includes the 8 word module instance header and thus the first tunable module variable is numbered 8.  The module header file contains a set of #define's indicating the bit value for each variable.  For example, ModPeakHold.h contains

A few more things to keep in mind regarding masks.  When an array variable is set, the bit mask corresponding to the array is set as expected.  However, the Set function is not told which portion of the array has been changed.  Finally, since the mask is only 32 bits in length, it is possible to run out of unique bits if the instance structure is very long.  The high bit of the mask, bit 31, has special meaning.  When this bit is set, it indicates that at least one of the high order instance variables has changed.  However, you don't know precisely which one.

Get Function

This function is rarely used in practice and translates low-level variables to higher-level variables.  This function has the same signature as the Set function

UINT awe_modClassNameGet(void *pInstance, UINT mask)

When called, the function should update instance variables and can use the mask argument to determine which variables should be updated.

BypassFunction

This function implements a module's bypass functionality.  The function signature is identical to the processing function

 void awe_modClassNameBypass(void *pInstance)

There are several standard bypass functions defined in ModCommon.h and may be used by modules:

  • IOMatchModule_Bypass() — Works with modules that have the same number of input as output wires.  It copies the data from the Nth input wire to the Nth output wire.  It assumes (and does not check) that all input and output wires have the exact same number of samples.

  • IOAcrossModule_Bypass() — Works with modules that have a single input wire and single output wire.  The wires may have a different number of channels, though.  Let INCOUNT and OUTCOUNT represent the number of channels in the input and output wires, respectively.  The function copies the first min(INCOUNT, OUTCOUNT) channels across one at a time from input to output.  If OUTCOUNT > INCOUNT, then the remaining output channels are set to 0.

  • IOAllOnesModule_Bypass() — Sets all samples in the output wire to floating point 1.0.  This is used by some dynamics processors which compute time-varying gains.

  • IOMatchUpModule_Bypass() — Suitable for modules that have an arbitrary number of input and output wires.  The function starts with the first output wire and looks for the first input wire that matches it in terms of block size and number of channels.  If a match is found, the data from that input wire is copied to the output wire, and this input wire is no longer used for subsequent matches.  If not match is found for a particular output wire, then all samples are set to 0.0.  This process repeats for all output wires.

If a module's m-file does not specify a bypass function, then the code generator assigns IOMatchUpModule_Bypass() by default.