Table of Contents

How to Create Wwise Sound Engine Plug-ins

Creating Wwise sound engine plug-ins is empowering, but it can be complex. We strongly recommend that you read through the following sections in tandem with the samples we provide in the Sample Plug-ins section. Our samples offer a reliable style and structure for the basis of your own creations.

Wwise Sound Engine Plug-ins Overview

Plug-ins allow you to insert custom DSP routines into the overall signal processing chain performed by the sound engine. The plug-in parameters can be controlled either from the authoring tool or in-game through RTPCs (refer to Concept: Real-Time Parameter Controls (RTPCs)).

Each plug-in consists of 2 components:

  • A runtime component integrated into the sound engine that can be developed cross-platform or optimized for a specific platform. The game will need to register this plug-in (see Plug-in Static Registration) and link against its implementation.
  • A user interface component integrated into the authoring tool. This component is found in a DLL loaded by Wwise (see How to Create a Wwise Plug-in DLL). All plug-in parameters determined in the UI are stored in banks so this component is not used in game. If a parameter is to be controlled in game through RTPCs, the necessary information will also be stored in banks.
Tip.gif
Tip: Create a common static library for the runtime component. This way it can be linked by both the game and the plug-in user interface, a DLL which is loaded by Wwise.

A parameter node interface needs to be implemented to react to changes coming from either the sound engine's RTPC manager or the Wwise authoring tool and retrieve the current plug-in instance parameters during execution. (See Parameter Node Interface Implementation.)

There are 3 main categories of plug-ins that can be integrated into the sound engine:

Plug-ins and their associated parameter interfaces are created by the sound engine through a plug-in mechanism which requires the exposure of static creation functions that return new instances of parameter nodes and new plug-in instances when required. The following code demonstrates how this can be done. The creation functions must be packaged into AK::PluginRegistration static instances that will be seen by the library users. You need one AK::PluginRegistration class per plugin class/type.

// Static parameter node creation function callback to be registered to the plug-in manager.
AK::IAkPluginParam * CreateMyPluginParams( AK::IAkPluginMemAlloc * in_pAllocator )
{
    return AK_PLUGIN_NEW( in_pAllocator, CAkMyPluginParams() );
}

// Static plug-in creation function callback to be registered to the plug-in manager.
AK::IAkPlugin * CreateMyPlugin( AK::IAkPluginMemAlloc * in_pAllocator )
{
    return AK_PLUGIN_NEW( in_pAllocator, CAkMyPlugin() );
}

//Static initializer object to automatically register the plug-in into the sound engine.
AK::PluginRegistration MyPluginRegistration(AkPluginTypeEffect, AKCOMPANYID_MYCOMPANY, EFFECTID_MYPLUGIN, CreateMyPlugin, CreateMyPluginParams);
Note.gif
Note: Set the AkPluginType argument of the AK::PluginRegistration function appropriately depending on the type of plug-in you make. For example, AkPluginTypeSource for source plug-ins and AkPluginTypeEffect for effect plug-ins. See AkPluginType
Note.gif
Note: The naming of the Registration object is important. It goes with the AK_STATIC_LINK_PLUGIN(_pluginname_) macro which will concatenate "Registration" after _pluginname_. See below Plug-in Static Registration.

If you are a plug-in provider and wish to ensure that your plug-ins will not have the same ID as plug-ins from other vendors, please contact support@audiokinetic.com to obtain a reserved company ID.

Plug-in Static Registration

Various instances of audio plug-ins are all handled by the Plug-in Manager, which recognizes different plug-in classes using both CompanyIDs and PluginIDs. Plug-ins must be registered to the Plug-in Manager before they can be used in game. The registration process binds a PluginID to the creation function callbacks provided as arguments.

The sample code below demonstrates how plug-ins can be registered by the game. Plug-ins provided by Audiokinetic must also be registered if they are used by a game.

For ease of use, all plugins also have a Factory header file which only contains the AK_STATIC_LINK_PLUGIN macro. To make plugin management easier, name your Factory header the same way as your library concatenated with "Factory". For example, the code below would be the content for MyPluginFactory.h associated with MyPlugin.lib.

AK_STATIC_LINK_PLUGIN(MyPlugin);    //This macro registers the plugin in the sound engine.

A game using MyPlugin would simply include the MyPluginFactory file and link with MyPlugin.lib.

#include <AK/Plugin/MyPluginFactory.h>  // Factory headers forces linking of the plugin code through AK_STATIC_LINK_PLUGIN.
Note.gif
Note: If you get a link error about symbols ending with _linkonceonly multiply defined, this means you included the Factory include in multiple CPP files. It needs to be included once only per linking unit (such as a DLL, SO, DYLIB, or EXE file).

Dynamic Libraries

There are two ways to use plug-ins in a game: through static libraries and dynamic libraries. Distributing a static library is mandatory. Distributing dynamic libraries is optional but highly recommended because the Wwise integration for Unity uses dynamic libraries. The Wwise Unreal integration, however, needs to use static libraries.

Creating a dynamic library from the library is quite easy. Audiokinetic did this for all the Effect plug-ins, so you can find many examples in the folder \SDK\samples\DynamicLibraries. You must:

  • Ensure the project links the static library you built.
  • Include one reference to AK_STATIC_LINK_PLUGIN either directly or through a header file.
  • Make sure your Dynamic library exports the symbol "g_pAKPluginList". Most platforms will export it automatically because it is explicitly declared as exportable. Some will need an explicit DEF file. Use the macro DEFINE_PLUGIN_REGISTER_HOOK to define the symbol.
  • Name your dynamic library according to your XML declaration. If you did not specify the EngineDllName attribute in the XML, name it the same name as the XML. Note that you can group multiple plug-ins in a single DLL.

In short, the code you need to transform your static library into a dynamic library is only:

To deploy your plug-in in Unity, put a copy of the dynamic library in the Wwise\Deployment\Plugins\[Platform]\DSP folder. All DSP plug-ins in that folder should be in an optimized configuration, ready for game release.

Note.gif
Note: On iOS, the build system prevents the use of dynamic libraries. Therefore, in Unity you must deploy both the .a file and the corresponding Factory.h file. The link between the usage of the dynamic library on other platforms and the static library on iOS is through its name: make sure that the DLL name is the same as the lib name (or a substring), without the "lib" prefix. For example:
  • On Windows: MyPlugin.dll
  • On iOS: libMyPlugin.a + MyPluginFactory.h
Note.gif
Note: On Mac Wwise will load only DYLIB files. However, Unity doesn't recognize DYLIB as a valid extension. Therefore, it doesn't copy and deploy those files when building the game. To work around this problem, rename the extension to BUNDLE, even if the file itself is not a BUNDLE. For example:
  • On Windows: MyPlugin.dll
  • On Mac (non-Unity game): MyPlugin.dylib
  • On Mac (Unity game): MyPlugin.bundle
Note.gif
Note: On Android you need to prefix the dynamic library with "lib", such as libMyPlugin.so.

Allocating/De-allocating Memory in Audio Plug-ins

You should perform all dynamic memory allocation or deallocation inside audio plug-ins through the provided memory allocator interface. This ensures that all memory consumed and released by plug-ins can be tracked by the Memory Manager, allocated from a specific memory pool, and displayed in the Wwise profiler.

Macros are provided to overload the new/delete operators and call malloc()/free() using the provided memory allocator. These macros are provided in IAkPluginMemAlloc.h.

To allocate objects, use the AK_PLUGIN_NEW() macro and pass the pointer to the memory allocator interface and the desired object type. The macro returns a pointer to the newly-allocated object. The corresponding AK_PLUGIN_DELETE() macro should be used to release memory.

To allocate arrays use the AK_PLUGIN_ALLOC(), which gets the requested size in bytes from the Memory Manager and returns a void pointer to the allocated memory address. Use the corresponding AK_PLUGIN_FREE() macro to release the allocated memory. The sample code below demonstrates how to use the macros.

// Allocate single CAkMyObject
CAkMykObject * pSingleObject = AK_PLUGIN_NEW( in_pAllocator, CAkMyObject );
// Release pSingleObject
AK_PLUGIN_DELETE( in_pAllocator, pSingleObject );
// Allocate an array of uNumSamples audio samples
AkSampleType * pData = static_cast<AkSampleType *>( AK_PLUGIN_ALLOC( in_pAllocator, sizeof(AkSampleType) * uNumSamples) );
// Free array of audio samples
AK_PLUGIN_FREE( in_pAllocator, pData );

Parameter Node Interface Implementation

The parameter node essentially centralizes read and write access to parameters. The plug-in parameter interface consists of the following methods:

AK::IAkPluginParam::Clone()

This method should create a duplicate of the parameter instance and adjust necessary internal state variables to be ready to be used with a new plug-in instance. This situation occurs, for example, when an event creates a new playback instance. The function must return a new parameter node instance using the AK_PLUGIN_NEW() macro. In many cases, a copy constructor call is sufficient (as in the code example below). In cases where memory is allocated within the parameter node, a deep copy should be implemented.

// Creating a shared parameters duplicate.
AK::IAkPluginParam * CAkMyPluginParams::Clone( AK::IAkPluginMemAlloc * in_pAllocator )
{
    return AK_PLUGIN_NEW( in_pAllocator, CAkMyPluginParams(*this) );
}

AK::IAkPluginParam::Init()

This function initializes the parameters with the provided parameter block. When the provided parameter block size is zero (i.e. when the plug-in is used within the authoring tool), AK::IAkPluginParam::Init() should initialize the parameter structure using default values.

Tip.gif
Tip: Call AK::IAkPluginParam::SetParamsBlock() to initialize the parameter block when it is valid.
// Parameter node initialization.
AKRESULT CAkMyPluginParams::Init( AK::IAkPluginMemAlloc * in_pAllocator, void * in_pParamsBlock, AkUInt32 in_ulBlockSize )
{
    if ( in_ulBlockSize == 0)
    {
        // Init with default values if we got an invalid parameter block.
        return AK_Success;
    }

    return SetParamsBlock( in_pParamsBlock, in_ulBlockSize );
}

AK::IAkPluginParam::SetParamsBlock()

This method sets all plug-in parameters at once using a parameter block that was stored through AK::Wwise::IAudioPlugin::GetBankParameters() during bank creation in Wwise. The parameters will read in the same format they were written into the bank by the Wwise counterpart of the plug-in. Note that data is in packed format and thus variables may not be aligned based on the data type as required for some target platforms. Use the READBANKDATA helper macro provided in AkBankReadHelpers.h to avoid theses platform specific concerns. There is no need to worry about endianness issues for the plugin parameters as data is properly byte swapped by the application.

// Read and parse parameter block.
AKRESULT CAkMyPluginParams::SetParamsBlock( void * in_pParamsBlock, AkUInt32 in_uBlockSize )
{
    // Read data in the order it was put in the bank
    AKRESULT eResult = AK_Success;
    AkUInt8 * pParamsBlock = (AkUInt8 *)in_pParamsBlock;
    m_fFloatParam1  = READBANKDATA( AkReal32, pParamsBlock, in_ulBlockSize );
    m_bBoolParam2 = READBANKDATA( bool, pParamsBlock, in_ulBlockSize );
    CHECKBANKDATASIZE( in_ulBlockSize, eResult );
    return eResult;
}

AK::IAkPluginParam::SetParam()

This method updates a single parameter at a time. This is called whenever a parameter value changes, either from the Plugin UI, RTPCs, and so on. The parameter to update is specified by an argument of type AkPluginParamID and corresponds to the AudioEnginePropertyID defined in the Wwise XML plugin description file. (Refer to Wwise Plug-in XML Description Files for more information).

Tip.gif
Tip: We recommend binding each AudioEngineParameterID defined in the XML file to a constant variable of type AkPluginParamsID.
Note.gif
Note: Parameters that support RTPCs are assigned the type AkReal32 regardless of the property type specified in the XML file.
// Parameters IDs for Wwise or RTPCs.
static const AkPluginParamID AK_MYFLOATPARAM1_ID    = 0;
static const AkPluginParamID AK_MYBOOLPARAM2_ID     = 1;

AKRESULT CAkMyPluginParams::SetParam( AkPluginParamID in_ParamID, void * in_pValue, AkUInt32 in_uParamSize )
{
        // Set parameter value.
        switch ( in_ParamID )
        {
        case AK_MYFLOATPARAM1_ID:
            m_fFloatParam1.fParam2 = *(AkReal32*)( in_pValue );
            break;
        case AK_MYBOOLPARAM2_ID:
            m_bBoolParam2 = *(bool*)( in_pValue ); // Parameter does not support RTPC
                or ...
            // Note RTPC parameters are always of type float regardless of property type in XML plugin description
            m_bBoolParam2 = (*(AkReal32*)(in_pValue)) != 0;
            break;
        
        default:
            return AK_InvalidParameter;
    }

    return AK_Success;
}

AK::IAkPluginParam::Term()

This method is called by the sound engine when a parameter node is terminated. Any memory resources used must be released and the parameter node instance is responsible for self-destruction.

// Shared parameters termination.
AKRESULT CAkMyPluginParams::Term( AK::IAkPluginMemAlloc * in_pAllocator )
{
    AK_PLUGIN_DELETE( in_pAllocator, this );
    return AK_Success;
}

Communication Between Parameter Nodes and Plug-ins.

Each plug-in has an associated parameter node from which it can retrieve parameter values from to update its DSP accordingly. The associated parameter node interface is passed in at plug-in initialization and will remain valid for the lifespan of the plug-in. The plug-in can then query information from the parameter node as often as required by the DSP process. Because of the unidirectional relationship between a plug-in to its associated parameter node, responding to parameter value query by the parameter node is left to the discretion of the implementer (e.g. using accessor methods).

Audio Plug-in Interface Implementation

To develop an audio plug-in, you must implement certain functions to allow the plug-in to function properly within the engine's audio data flow. For source plug-ins you should derive from the AK::IAkSourcePlugin interface, for effects that can replace the input buffer with the output (e.g. no need for unordered access or change in data rate) you should derive from the AK::IAkInPlaceEffectPlugin. For other effects that require out of place implementation, you should derive from the AK::IAkOutOfPlaceEffectPlugin interface.

A plug-in life cycle always begins with a call to the AK::IAkPlugin::Init() function, immediately followed by a call to AK::IAkPlugin::Reset(). As long as the plug-in needs to output more data, AK::IAkPlugin::Execute() is called with new buffers. When the plug-in is no longer needed, AK::IAkPlugin::Term() is called.

AK::IAkPlugin::Term()

This method is called when the plug-in is terminated. AK::IAkPlugin::Term() must release all memory resources used by the plug-in and self-destruct the plug-in instance.

AKRESULT CAkMyPlugin::Term( AK::IAkPluginMemAlloc * in_pAllocator )
{
    if ( m_pMyDelayLine != NULL )
    {
        AK_PLUGIN_FREE( in_pAllocator, m_pMyDelayLine );
        m_pMyDelayLine = NULL;
    }

    AK_PLUGIN_DELETE( in_pAllocator, this );
    return AK_Success;
}

AK::IAkPlugin::Reset()

The reset method must reinitialize the state of the plug-in to prepare it to accommodate new unrelated audio content. The sound engine pipeline will call AK::IAkPlugin::Reset() immediately after initialization and at any other time the state of the object needs to be reset. Typically all memory allocations should be performed at initialization but the status of delay lines and sample counts for example should be cleared on AK::IAkPlugin::Reset().

// Reset internal state of plug-in
AKRESULT CAkMyPlugin::Reset( )
{
    // Reset delay line
    if ( m_pMyDelayLine != NULL )
        memset( m_pMyDelayLine, 0, m_uNumDelaySamples * sizeof(AkSampleType) );
    return AK_Success;
}

AK::IAkPlugin::GetPluginInfo()

This plug-in information query mechanism is used when the sound engine requires information about the plug-in. Fill in the correct information in the AkPluginInfo structure to describe the type of plug-in implemented (e.g. source or effect), its buffer usage scheme (e.g. in place), and processing mode (e.g synchronous).

Note.gif

Note: Effect plug-ins should be synchronous on all platforms.

// Effect info query from sound engine.
AKRESULT CAkMyPlugin::GetPluginInfo( AkPluginInfo & out_rPluginInfo )
{
    out_rPluginInfo.eType = AkPluginTypeSource; // Source plug-in.
    out_rPluginInfo.bIsInPlace = true;          // In-place plug-in.
    out_rPluginInfo.bIsAsynchronous = false;    // Synchronous plug-in.
    return AK_Success;
}

Accessing Data Using AkAudioBuffer Structure

Audio data buffers are passed to plug-ins at execution time through a pointer to an AkAudioBuffer structure. All audio buffers passed to plug-ins use a fixed format. For platforms supporting software effects, audio buffers’ channels are not interleaved, and all samples are normalized 32-bit floating point in the (-1.f,1.f) range running at a 48 kHz sampling rate.

The AkAudioBuffer structure provides means for accessing both interleaved and deinterleaved data. It contains a field to specify the number of sample frames valid in each channel buffer (AkAudioBuffer::uValidFrames) along with the maximum number of sample frames that these buffers can contain (returned by AkAudioBuffer::MaxFrames()).

The AkAudioBuffer structure also contains the buffer's channel mask, which defines the channels that are present in the data. If you just need the number of channels, use AkAudioBuffer::NumChannels().

Retrieving Interleaved Data.

A plug-in can access a buffer of interleaved data through AkAudioBuffer::GetInterleavedData(). Only source plug-ins should access and output interleaved data. In order to do so, they must correctly prepare the sound engine during initialization (see AK::IAkSourcePlugin::Init()). The sound engine will instantiate the DSP processed in order to properly convert the data into the native pipeline format.

Tip.gif
Tip: You can get better performance, if the source plug-in outputs data that is already in the sound engine's native format.

Retrieving Deinterleaved Data.

A plug-in accesses individual deinterleaved channels through AkAudioBuffer::GetChannel(). The data type always conforms to the sound engine's native format (AkSampleType). The example code below shows how all deinterleaved channels can be retrieved for processing.

// Process all channels independently.
void CAkMyPlugin::Execute( AkAudioBuffer * io_pBuffer )     // Input/Output buffer (processing is done in place).
{
    AkUInt32 uNumChannels = io_pBuffer->NumChannels();
    
    for ( AkUInt32 uChan=0; uChan<uNumChannels; uChan++ )
    {
        AkSampleType * pChannel = io_pBuffer->GetChannel( uChan );
        // Process data of pChannel...
    }
}
Caution.gif
Caution: A plug-in must never take for granted that buffers of each channel are contiguous in memory.

Channel Ordering

The channels for processing audio are ordered as follows: Front Left, Front Right, Center, Rear Left, Rear Right, and LFE. The Low-Frequency channel (LFE) is always placed at the end (except for Source plug-ins), so that it can be handled separately because many DSP processings require it. A plug-in can query if the LFE channel is present in the audio buffer with AkAudioBuffer::HasLFE(). It can access the LFE channel directly by calling AkAudioBuffer::GetLFE(). The following code demonstrates two different ways for handling the LFE channel separately.

void CAkMyPlugin::Execute( AkAudioBuffer * io_pBuffer )     // Input/Output buffer (processing is done in place).
{
    // Get LFE channel with GetLFE().
    AkSampleType * pLFE = io_pBuffer->GetLFE();
    if ( pLFE )
    {
        // Process data of pLFE...
    }

    OR ...
    
    // Get LFE channel with GetChannel().
    if ( io_pBuffer->HasLFE() )
    {
        AkSampleType * pLFE = io_pBuffer->GetChannel( io_pBuffer->NumChannels() - 1 );
        // Process data of pLFE...
    }
}

If you want to apply specific processing to a channel that is not the LFE, then you need to use the channel index defines of AkCommonDefs.h. For example, if you want to process only the center channel of a 5.x configuration, you would do the following:

// Process only the center channel of a 5.x configuration.
void CAkMyPlugin::Execute( AkAudioBuffer * io_pBuffer )     // Input/Output buffer (processing is done in place).
{
    // Query for specific channel configuration.
    if ( io_pBuffer->GetChannelMask() == AK_SPEAKER_SETUP_5 || io_pBuffer->GetChannelMask() == AK_SPEAKER_SETUP_5POINT1 )
    {
        // Access channel using the index defined for the appropriate configuration.
        AkSampleType * pCenter = io_pBuffer->GetChannel( AK_IDX_SETUP_5_CENTER );
        // Process data of pCenter...
    }
}
Tip.gif
Tip: Notice that the channel index defines are independent of whether the configuration is N.0 or N.1. This is because the LFE channel is always the last one, except for Source plug-ins (AK_IDX_SETUP_N_LFE should not be used if the channel configuration has no LFE).
Note.gif
Note: In the case of 7.1, the channel ordering for interleaved data of source plug-ins is L-R-C-LFE-BL-BR-SL-SR.

Using Global Sound Engine Callbacks From Plugins

Plugins may use AK::IAkGlobalPluginContext::RegisterGlobalCallback() to register to various global sound engine callbacks. For example, a plug-in class may need to be called once per audio frame, via a singleton, of which plug-in instances are aware. You may also use global hooks for initializing data structures that remain during the whole lifetime of the sound engine.

To do so, replace the default AK_IMPLEMENT_PLUGIN_FACTORY macro with your own implementation, to utilize the AK::PluginRegistration's "RegisterCallback". In the code snippet below, a static callback called MyRegisterCallback is defined for this purpose and passed to the AK::PluginRegistration object MyPluginRegistration.

Note.gif
Note: The AK_IMPLEMENT_PLUGIN_FACTORY macro declares factory functions and an AK::PluginRegistration object by appending strings to the plug-in name that is passed to it as an argument. In the example below, AK_IMPLEMENT_PLUGIN_FACTORY has been reimplemented following the same naming convention. For your own plug-in, you should replace "MyPlugin" by its actual name. Furthermore, AK_IMPLEMENT_PLUGIN_FACTORY's sister macro, AK_STATIC_LINK_PLUGIN, follows the same naming convention. If you diverge from AK_IMPLEMENT_PLUGIN_FACTORY's naming convention, then you also need to reimplement AK_STATIC_LINK_PLUGIN accordingly.
// A global callback for initializing and terminating MyPluginManager (AkGlobalCallbackFunc).
static void MyRegisterCallback(
    AK::IAkGlobalPluginContext * in_pContext,   ///< Engine context.
    AkGlobalCallbackLocation in_eLocation,      ///< Location where this callback is fired.
    void * in_pCookie                           ///< User cookie passed to AK::SoundEngine::RegisterGlobalCallback().
    )
{
    if (in_eLocation == AkGlobalCallbackLocation_Register)
    {
        // Registration time: called when sound engine is initialized, or when dynamic plugin lib is loaded.

        // Create our singleton. Use the allocator provided by the global context for allocations that should span the whole lifetime of the sound engine.
        MyPluginManager * pMyPluginManager = AK_PLUGIN_NEW(in_pContext->GetAllocator(), MyPluginManager);
        if (pMyPluginManager)
        {
            // Succeeded. Register to the "Term" callback for terminating our manager.
            AKRESULT eResult = in_pContext->RegisterGlobalCallback(MyRegisterCallback, AkGlobalCallbackLocation_Term, pMyPluginManager);
            
            // Handle registration failure.
            if (eResult != AK_Success)
            {
                // ...
            }
        }
    }
    else if (in_eLocation == AkGlobalCallbackLocation_Term)
    {
        // Termination time: called when sound engine is terminated. 
        // The cookie is our instance of MyPluginManager, as was registered above.
        AKASSERT(in_pCookie);
        // Destroy it.
        AK_PLUGIN_DELETE(in_pContext->GetAllocator(), (MyPluginManager*)in_pCookie);
    }
}

// Replace AK_IMPLEMENT_PLUGIN_FACTORY(MyPlugin, AkPluginTypeEffect, MY_COMPANY_ID, MY_PLUGIN_ID)
// Here, "MyPlugin" should be replaced by the name of your plugin.
AK::IAkPlugin* CreateMyPlugin(AK::IAkPluginMemAlloc * in_pAllocator);
AK::IAkPluginParam * CreateMyPluginParams(AK::IAkPluginMemAlloc * in_pAllocator);
AK::PluginRegistration MyPluginRegistration(AkPluginTypeEffect, MY_COMPANY_ID, MY_PLUGIN_ID, CreateMyPlugin, CreateMyPluginParams, MyRegisterCallback, NULL);

For more information, refer to the following sections: