Migrating Your Custom Wwise Plug-ins to the Wwise 2021 GUI Model

Game Audio

Introduction

Hi, it’s Robert Bantin here. I’m the audio-tech architect of the Snowdrop engine in Ubisoft, and I’ve written this guide to help you update your legacy Wwise plug-ins to the new, arguably more elegant GUI model that was introduced in Wwise 2021. While Wwise is still backwards compatible to the old model, there are some situations where it breaks: The old model doesn’t work well with the introduction of the Tab Workflow and will eventually stop working altogether. 

From the team at Audiokinetic:

Prerequisites

You already have some custom Wwise plug-ins that you have source code and Visual Studio (or some other IDE) project/solution files for, and know how they work. 

The Overview

There is, at the time of writing, three “classic” types of Wwise plug-in that you can author for Wwise 2022:

  • Source
  • Effect
  • Sink (aka "Device") 

The package of each plug-in must come as two deployables: A static lib linked directly to the game/engine; A dynamic lib that was copied into the Wwise authoring tool's plugins folder (along with an xml file showing the parameter map).

Wwise-plug-in-chart

Because the GUI has been decoupled from the plug-in container, the new structure allows the GUI to be multiply instanced. 

Wwise-plug-in-registration-flow

Steps to accomplish the goal

Step 1

Remove the function dll exports

The .dll deployable in both Source and Effect types needed the method AkCreatePlugin() to be exported, while Sink types needed both AkCreatePlugin() and AkGetSinkPluginDevices() methods to be exported. In either case this was typically done by listing them in the .DEF file.

E.g.

LIBRARY "ReallyCoolPlugin"
EXPORTS
    AkCreatePlugin
    AkGetSinkPluginDevices

This is no longer necessary, and so if you have a solution with a .DEF file remove the function names. If there are no other entry points declared, you can delete this file altogether.

E.g.

LIBRARY "ReallyCoolPlugin"
EXPORTS

Step 2

Remove all the dll bootstrap code

Usually this was declared and implemented in the ReallyCoolPlugin.cpp and .h files that lived in the WwisePlugin path.

E.g.

class ReallyCoolPluginApp : public CWinApp

In the translation unit was the AkCreatePlugin() implementation (as well as AkGetSinkPluginDevices() if this was a Sink plug-in).

Since this will be automatically taken care of later by subclassing  "AK::Wwise::Plugin::PluginMFCWindows<>" in the GUI class, you can delete these files!

jazz-club-nice-final

Step 3

Implementing the factory methods

For source and effect plug-ins, the factory methods implemented the are the same - two local functions in the ReallyCoolFX.cpp file:

AK::IAkPlugin* CreateReallyCoolFX( AK::IAkPluginMemAlloc * in_pAllocator )
{
    return AK_PLUGIN_NEW( in_pAllocator, ReallyCoolFX() );
}

AK::IAkPluginParam* CreateReallyCoolFXParams(AK::IAkPluginMemAlloc* in_pAllocator)
{
    return AK_PLUGIN_NEW(in_pAllocator, ReallyCoolFXParams());
}

AK_IMPLEMENT_PLUGIN_FACTORY(ReallyCoolFX, AkPluginTypeEffect, AKCOMPANYID_YOURSTUDIO, AKEFFECTID_YOURPLUGIN)
/*or*/
AK_IMPLEMENT_PLUGIN_FACTORY(ReallyCoolFX, AkPluginTypeSource, AKCOMPANYID_YOURSTUDIO, AKEFFECTID_YOURPLUGIN)

These macros forward declare the function names it gets as input, so the macro can appear above these function implementations too. It really doesn't matter.

However, if this was a Sink plug-in you will need to re-implement AkGetSinkPluginDevices() as a callback method within the ReallyCoolFX (sound engine class) with the following signature:

AKRESULT GetReallyCoolDeviceList(AkUInt32& io_maxNumDevices, AkDeviceDescription* out_deviceDescriptions)

There is currently no version of the AK_IMPLEMENT_PLUGIN_FACTORY macro for declaring the factory classes with this callback, so you'll have to do is manually like so:

AK::PluginRegistration ReallyCoolFXRegistration(AkPluginTypeSink,
AKCOMPANYID_YOURSTUDIO,
AKEFFECTID_YOURPLUGIN,
ReallyCoolFX,
ReallyCoolFXParams,
GetReallyCoolDeviceList);

The macro AK_STATIC_LINK_PLUGIN() that you've already used in the factory header "ReallyCoolFXFactory.h" does the rest of the linker work using the assumption that there will be a function called "ReallyCoolFXRegistration", so there's nothing else to do here so long as you've used the right naming convention.

jazz-club-great-final

Step 4

Break up the GUI code and the serialization code into two separate classes

Previously you would have had a single class for both driving the GUI and serializing the parameters during soundbank serialization. The most likely interface you would have used look like this:

#pragma once
#include <AK/Wwise/AudioPlugin.h>

class ReallyCoolPlugin : public AK::Wwise::DefaultAudioPluginImplementation

You may have overridden a lot of methods from this default implementation, but as a minimum these public methods were the most likely to be needed:

    void Destroy() override;
    void SetPluginPropertySet(AK::Wwise::IPluginPropertySet* in_pPSet) override;
    bool GetBankParameters(const GUID& in_guidPlatform, AK::Wwise::IWriteData* in_pDataWriter) const override;
    HINSTANCE GetResourceHandle() const override;
    bool GetDialog(eDialog in_eDialog, UINT& out_uiDialogID, AK::Wwise::PopulateTableItem*& out_pTable) const override;
    void NotifyPropertyChanged(const GUID&, LPCWSTR in_szPropertyName) override;
    bool WindowProc(eDialog in_eDialog, HWND in_hWnd, UINT in_message, WPARAM in_wParam, LPARAM in_lParam, LRESULT& out_lResult) override;

The new model keeps these two areas of functionality entirely separate so that the serialization code is instanced once while the GUI can be instanced multiple times.

The Serialization class

For the ReallyCoolPlugin.cpp and .h files, you can now reduce it down to a class that just handles serialization. Include the <AK/Wwise/Plugin.h> header and publicly inherit the interface AK::Wwise::Plugin::AudioPlugin: 

// ReallyCoolPlugin.h
#pragma once
#include <AK/Wwise/Plugin.h>

extern const char* const szSomeCoolIntParameter;
extern const char* const szSomeOtherCoolFloatParameter;
extern const char* const szEnableThing;

class ReallyCoolPlugin : public AK::Wwise::Plugin::AudioPlugin
{
public:
    ReallyCoolPlugin();
    bool GetBankParameters(const GUID& in_guidPlatform, AK::Wwise::Plugin::DataWriter& in_dataWriter) const override;
};

AK_DECLARE_PLUGIN_CONTAINER(ReallyCool);

Two things to really pay attention to here: The new datawriter works very similarly to the old one - save for the fact that the string IDs for parameters are now const char* and not LPCWSTR; There is now a macro for doing some boilerplate code called AK_DECLARE_PLUGIN_CONTAINER().

The implementation of this class is very straightforward.

// ReallyCoolPlugin.cpp
#include "ReallyCoolPlugin.h"

const char* const szSomeCoolIntParameter = "SomeCoolIntParameter";
const char* const szSomeOtherCoolFloatParameter = "SomeOtherCoolFloatParameter";
const char* const szEnableThing = "SomeBoolParameter";

ReallyCoolPlugin::ReallyCoolPlugin()
{
    // You _could_ add a debug message here. A modal dialog box from stdafx.h? Something that only compiles in debug perhaps?
}

bool ReallyCoolPlugin::GetBankParameters(const GUID& in_guidPlatform, AK::Wwise::Plugin::DataWriter& in_dataWriter) const
{
    in_dataWriter.WriteInt32(m_propertySet.GetInt32(in_guidPlatform, szSomeCoolIntParameter));
    in_dataWriter.WriteReal32(m_propertySet.GetReal32(in_guidPlatform, szSomeOtherCoolFloatParameter));
    in_dataWriter.WriteBool(m_propertySet.GetBool(in_guidPlatform, szEnableThing));

    return true;
}

AK_DEFINE_PLUGIN_CONTAINER(ReallyCool);
AK_EXPORT_PLUGIN_CONTAINER(ReallyCool);
AK_ADD_PLUGIN_CLASS_TO_CONTAINER(ReallyCool, ReallyCoolPlugin, ReallyCoolFX);

DEFINE_PLUGIN_REGISTER_HOOK;
DEFINEDUMMYASSERTHOOK;

There are other accessors and serializers for the other POD types. It's all very logical.

Note that not only is the a lot of boilerplate code now implemented by AK_DEFINE_PLUGIN_CONTAINER() and AK_ADD_PLUGIN_CLASS_TO_CONTAINER(), but the dll export work that was done in the .DEF file is performed by AK_EXPORT_PLUGIN_CONTAINER() now.

jazz-club-wonderful-final

 The GUI class

As you might have already figured out, since the GUI code was taken out of the plug-in's serialization class, it has to live somewhere else in some form. This comes from the include <AK/Wwise/Plugin.h>, which you have already added to your ReallyCoolPlugin.h file. Below is the absolute bare minimum that you will need to implement via multiple inheritance.  

// ReallyCoolPluginGUI.h
#pragma once
#include "ReallyCoolPlugin.h" // which already includes <AK/Wwise/Plugin.h>

class ReallyCoolPluginGUI final
    : public AK::Wwise::Plugin::PluginMFCWindows<>
    , public AK::Wwise::Plugin::GUIWindows
    , public AK::Wwise::Plugin::RequestHost
    , public AK::Wwise::Plugin::RequestPropertySet
{
public:
    ReallyCoolPluginGUI() {}
    void NotifyPropertyChanged(const GUID& in_guidPlatform, const char* in_szPropertyName) override;
    bool GetDialog(AK::Wwise::Plugin::eDialog in_eDialog, UINT& out_uiDialogID, AK::Wwise::Plugin::PopulateTableItem*& out_pTable) const override;
    bool WindowProc(AK::Wwise::Plugin::eDialog in_eDialog, HWND in_hWnd, UINT in_message, WPARAM in_wParam, LPARAM in_lParam, LRESULT& out_lResult) override;

private:
    HWND myHwndPropView = nullptr;
};

If you intend to change the state of the GUI with a property like szEnableThing, consider adding a private method here to do that work of the form: 

private:
    void PrivateMethodToChangeThingState();

In implementation, the code in these functions is almost the same as before. However the types are subtly different, which is worth noting if you are adapting GUI code you already have. 

There is also one important architectural change that you need to take on board. While the GUI-driven frontend dll parameters are still coupled to the serialized parameters loaded by the backend lib, it is now possible to have member variables in the GUI class that only affect individual instances of that GUI. These should be variables that modify the GUI instance’s state that aren’t used by the backend lib at all. Conversely, all parameters intended to be used by the backend lib should be part of the shared state that propagates across all the instances of the GUI as frontend dll parameters, irrespective of which GUI instance was modified by the user. 

To quote Samuel Longchamps from Audiokinetic: “A common source of bugs is when two GUI instances each have a copy of a variable when it should be the same across all instances. Put the plug-in's shared state in the backend class instead, and access it through the backend.

You can get access to the backend instance associated with a frontend instance by requesting the LinkBackend service (make your class inherit from RequestLinkBackend). The reverse is also possible by inheriting RequestLinkFrontend in the backend class. Note that since there are potentially multiple frontend instances, when using LinkFrontend you need to iterate in all frontend instances to sync them with the shared state.”

On top of all this, AK uses a precompiled header that (for historical reasons) is called “stdafx.h”. It declares some common headers like <AK/Wwise/TargetVer.h> and <AK/AkPlatforms.h>, and sets up some defines that affect what is declared from the Windows SDK (e.g. #include <afxwin.h> and #define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS).

Assuming then that you used the hilariously long-deprecated MFC GUI designer tool in Visual Studio, you will have an auto generated "resource.h" containing a bunch of IDs for the GUI widgets. Include that header here too.

There is also the replace macro for updating GUI widgets that aren't replaced by the authoring tool's own sliders. As you might recall, horizontal and vertical faders are drawn as MFC Text Control widgets with a caption looking like "Class=Fader;Prop=SomeXMLDefinedProperty" or "Class=SuperRange;Prop=SomeOtherXMLDefinedProperty". As weird/convenient as this is, it is never applied to other MFC widgets like radio buttons or check-boxes, and so a collection of macros was used to define a property table that the plug-in could listen to changes for.

Previously the macros were:  

AK_BEGIN_POPULATE_TABLE(...)
AK_POP_ITEM(...,...)
AK_END_POPULATE_TABLE()

They are now:

AK_WWISE_PLUGIN_GUI_WINDOWS_BEGIN_POPULATE_TABLE(...)
AK_WWISE_PLUGIN_GUI_WINDOWS_POP_ITEM(..., ...)
AK_WWISE_PLUGIN_GUI_WINDOWS_END_POPULATE_TABLE()

That all said and done, let's look at the implementation of this GUI: 

// ReallyCoolPluginGUI.cpp
#include "stdafx.h" // make sure this one is first
#include "ReallyCoolPluginGUI.h"
#include "resource.h"

AK_WWISE_PLUGIN_GUI_WINDOWS_BEGIN_POPULATE_TABLE(ReallyCoolPluginProp)
    AK_WWISE_PLUGIN_GUI_WINDOWS_POP_ITEM(IDC_CHECK_ENABLETHING_ID, szEnableThing) // IDC_CHECK_ENABLETHING_ID is defined in "resource.h"
AK_WWISE_PLUGIN_GUI_WINDOWS_END_POPULATE_TABLE()

// Take necessary action on property changes.
// Note: user also has the option of catching the appropriate message in the WindowProc function.
void ReallyCoolPluginGUI::NotifyPropertyChanged(const GUID& /*in_guidPlatform*/, const char* in_szPropertyName)
{
    if (!myHwndPropView)
    {
        return;
    }
    if (!strcmp(in_szPropertyName, szEnableThing))
    {
        PrivateMethodToChangeThingState();
    }
}

void ReallyCoolPluginGUI::PrivateMethodToChangeThingState()
{
    bool thingEnabled = m_propertySet.GetBool(m_host.GetCurrentPlatform(), szEnableThing);
    HWND hwndItem = GetDlgItem(myHwndPropView, IDC_THING_ID); // get handle for the MFC widget that will be ghosted/unghosted

    AKASSERT(hwndItem);

    // ghost or un-ghost the MFC widget pointed to by hwndItem
    ::EnableWindow(hwndItem, MKBOOL(thingEnabled)); // MKBOOL converts C++ bool to MFC BOOL
}

// Set the property names to UI control binding populated table.
bool ReallyCoolPluginGUI::GetDialog(AK::Wwise::Plugin::eDialog in_eDialog, UINT& out_uiDialogID, AK::Wwise::Plugin::PopulateTableItem*& out_pTable) const
{
    AKASSERT(in_eDialog == AK::Wwise::Plugin::SettingsDialog);

    if (in_eDialog == AK::Wwise::Plugin::SettingsDialog)
    {
        out_uiDialogID = IDD_REALLYCOOL_PANEL; // IDD_REALLYCOOL_PANEL is defined in "resource.h"
        out_pTable = ReallyCoolPluginProp;

        return true;
    }

    return false;
}

// Standard window function, user can intercept whatever message that is of interest to him to implement UI behavior.
bool ReallyCoolPluginGUI::WindowProc(AK::Wwise::Plugin::eDialog /*in_eDialog*/, HWND in_hWnd, UINT in_message, WPARAM /*in_wParam*/, LPARAM /*in_lParam*/, LRESULT& out_lResult)
{
    switch (in_message)
    {
        case WM_INITDIALOG:
        {
            myHwndPropView = in_hWnd;
            break;
        }
        case WM_DESTROY:
        {
            myHwndPropView = NULL;
            break;
        }
    }

    out_lResult = 0;

    return false;
}

AK_ADD_PLUGIN_CLASS_TO_CONTAINER(ReallyCool, ReallyCoolPluginGUI, ReallyCoolFX); // Add our GUI class to the PluginContainer

 

And that's really it. When in doubt, look into the AK headers to see what these parameters are supposed to do (they're quite well documented in the comments). 

Bonus details

Implementing the (?) button for the plug-in's documentation

When the custom GUI you've made for the plug-in is windowed, the (?) button should open to some relevant documentation about it. If you reference one of the bundled effects like Delay, you'll see it's done like this: 

// Implement online help when the user clicks on the "?" icon .
bool DelayGUI::Help( HWND in_hWnd, AK::Wwise::Plugin::eDialog in_eDialog, const char* in_szLanguageCode ) const
{
    AFX_MANAGE_STATE( ::AfxGetStaticModuleState() ) ;
    if ( in_eDialog == AK::Wwise::Plugin::SettingsDialog )
        ::SendMessage( in_hWnd, WM_AK_PRIVATE_SHOW_HELP_TOPIC, ONLINEHELP::Delay_Properties, 0 );
    else
        return false;
    return true;
}

The only problem with trying to repeat what their code does here is that it is sending an application specific windows message to the parent window. Since you don't have source code of the Authoring tool you won't be able to hook up a similar message id to whatever is listening to this. Instead then, perhaps try a Windows shell command:  

HINSTANCE ShellExecuteA(
    [in, optional] HWND   hwnd,
    [in, optional] LPCSTR lpOperation,
    [in]           LPCSTR lpFile,
    [in, optional] LPCSTR lpParameters,
    [in, optional] LPCSTR lpDirectory,
    [in]           INT    nShowCmd
);

You will need to implement the Help() method as is done by the Delay effect. If you look inside one of the classes you inherited from to declare the ReallyCoolPluginGUI class...

AK::Wwise::Plugin::GUIWindows

...there is indeed a virtual method called Help(), but it's stubbed. You'll need to override it and add an implementation to open your docs. 

Firstly in the header file:

public:
    ...
    bool Help(HWND in_hWnd, AK::Wwise::Plugin::eDialog in_eDialog, const char* in_szLanguageCode) const override;

...And secondly in the cpp file before AK_ADD_PLUGIN_CLASS_TO_CONTAINER:

// Implement online help when the user clicks on the "?" icon .
bool ReallyCoolPluginGUI::Help(HWND in_hWnd, AK::Wwise::Plugin::eDialog in_eDialog, const char* in_szLanguageCode) const
{
    AFX_MANAGE_STATE(::AfxGetStaticModuleState());
    if (in_eDialog == AK::Wwise::Plugin::SettingsDialog)
        ShellExecute(0, 0, L"https://confluence.yourcompany.com/x/4YkVoQ", 0, 0, SW_SHOW);
    else
        return false;
    return true;
}

So what address to use in that ShellExecute() call? Let's say your documentation on Confluence. Open that page in a browser and hit the share button.

Hit the Copy button, and paste the resulting permalink into the string in quotes. Using the permalink generated here is safe to embed that the normal browser path because it won't change if you edit the title on the page.

It's time to rebuild and deploy your plug-in. Now when the user hits the (?) button it will open that page in a new browser window!

Showing debug messages to the user while in the Wwise Authoring Tool

Let's say you have everything working now, but wait - you had a WinAPI call somewhere in your code for messaging the user of the dll, and that still needs LPWSTR string data. You can't pass a const char* of a parameter name into that call, so how do you convert it?

Here's an example. We had an effect plug-in that deprecated some parameters, and I wanted to make sure that if the Wwise plug-in xml file was not up to date, that the designer didn't accidentally generate settings for those parameters and serialize them into a soundbank. That would cause a "check bank data size" assert when the soundengine lib read in the data in debug builds. Most likely only a graphics programmer would notice, but they are easily startled so let's help them out. 

The two deprecated parameters are now declared as:

const char* const szRightInputSource = "RightInputSource";
const char* const szInputSourceszOutputMode = "OutputMode";

You can then use the standard strcmp() function to detect if an incoming property change is either of these. Make a simple wchar_t array on the stack and use that for a temporary conversion of that property string that the MessageBoxEx() function can read via a LPWSTR (as it would have done before). The message box is also on the stack and will go out of scope before the temporary array does.

    if (!strcmp(in_szPropertyName, szRightInputSource) || !strcmp(in_szPropertyName, szOutputMode))
    {
        wchar_t wtext[20]; // make it large enough to hold any of the property names

        int wchars_num = MultiByteToWideChar(CP_UTF8, 0, in_szPropertyName, -1, NULL, 0); // determine equivalent length for wchar

        MultiByteToWideChar(CP_UTF8, 0, in_szPropertyName, -1, wtext, wchars_num); // convert utf8 to wchar using new length

        LPWSTR wcptr = wtext;

        ::MessageBoxEx(nullptr, L"ChannelSwitcherPlugin::NotifyPropertyChanged - property not supported", wcptr, IDOK, 0);
    }

jazz-club-mellow-final

Related Content

Reference info from Audiokinetic: https://www.audiokinetic.com/en/library/edge/?source=SDK&id=effectplugin_tools_newplugin.html
The migration guide from Audiokinetic: https://www.audiokinetic.com/en/library/edge/?source=SDK&id=plugin_21_1_migration.html

 

 

Robert Bantin

Snowdrop Audio Architect

Massive Entertainment - A Ubisoft Studio

Robert Bantin

Snowdrop Audio Architect

Massive Entertainment - A Ubisoft Studio

Rob is a software engineer specializing in audio systems and DSP. They are a Salford Acoustics graduate, and over the course of 25 years have worked on MPEG codecs, watermarking, 3D spatialization, effects processors, musical synthesis, network streaming, device drivers, game-audio middleware, and a few video games.

 @cubusaddendum

Comments

Leave a Reply

Your email address will not be published.

More articles

Implementation Guide Series for the Wwise Unreal Integration

From our friends at Game Audio Resource, here is a Wwise 2019.1.4 Unreal 4 Audio Game implementation...

2.4.2020 - By Game Audio Resource

WAAPI is for Everyone | Part 2: wwise.core

Hello. I’m Thomas Wang (also known as Xi Ye).In part 1, I used mind maps to summarize WAAPI...

27.11.2020 - By Thomas Wang (汪洋)

Audiokinetic Theater Sessions Recap | GDC 2024

It was an incredible honour to host the line-up of audio professionals who presented in the...

30.5.2024 - By Audiokinetic

Six Days of Season: A Letter to the Future

'Tis the Season! We’re so excited to share our in-depth exploration of the sonic storytelling behind...

9.12.2024 - By Audiokinetic

Naming Convention Best Practices

This article was originally published by Can Uzer on his website.What makes a good naming...

23.1.2025 - By Can Uzer

Dustborn: Crafting the Soundscape of a Comic Book-Inspired Roadtrip

Dustborn: a Comic Book-Inspired Road Trip Dustborn, developed by Red Thread Games and published by...

31.1.2025 - By Simon Poole

More articles

Implementation Guide Series for the Wwise Unreal Integration

From our friends at Game Audio Resource, here is a Wwise 2019.1.4 Unreal 4 Audio Game implementation...

WAAPI is for Everyone | Part 2: wwise.core

Hello. I’m Thomas Wang (also known as Xi Ye).In part 1, I used mind maps to summarize WAAPI...

Audiokinetic Theater Sessions Recap | GDC 2024

It was an incredible honour to host the line-up of audio professionals who presented in the...