Integrating Wwise Into a Custom Engine I
The Wwise audio engine created by Audiokinetic is an extremely powerful and helpful piece of middleware used by hundreds of sound designers across the game industry and beyond. The documentation for Wwise provides easy step-by-step procedures for integration into the most commonly used game engines, Unity and Unreal, so that programmers can easily add sound to their games, but for teams creating their own custom game engine, the process is much more difficult. Even though the documentation provided by Audiokinetic gives most of the information, it doesn't have everything. The goal of this article is to:
- Integrate the core of Wwise in a custom engine.
- Integrate audio objects and their controls into different styles of engine architecture.
- Use the preprocessor to implement Wwise Events, RTPCs, Switches, and other Wwise controls into the engine at compile time. This step will be shown in a follow-up article.
The content of this article assumes that the reader has knowledge of C++ and game engine architectures. Much of this information is probably most useful to students implementing custom game engines who want to integrate Wwise as their middleware.
Integrating the Wwise Core
Most of the instructions can be found at this link, but there are some steps that are missing from it.
The first missing step:
In the "Initializing the Streaming Manager" step of the integration, the example code uses a global variable of the type
CAkFilePackageLowLevelIOBlocking g_lowLevelIO;
contained in the file "AkFilePackageLowLevelIOBlocking.h". However, you will notice that this particular header file is actually not contained in the include directory provided in the Wwise SDK download. It is actually buried elsewhere in the directory that you originally downloaded.
If you open the directory where the SDK is located (most likely in C:\Program Files (x86)\Audiokinetic\Wwise ($VERSION NUMBER)\SDK on a computer running Windows), in addition to the "include" directory, there should also be a directory called "samples." In SDK\samples\SoundEngine, there will be three subdirectories called "Common", "POSIX", and "Win32".
If you're running Wwise on a Windows machine, copy the Win32 directory, otherwise copy the POSIX directory, and paste it into the AK\SoundEngine directory in your local include directory for your custom engine. \
Next go back to the Common directory in SDK\samples\SoundEngine, and copy all the files in that directory, and paste them in the include version of the directory in your custom engine in AK\SoundEngine\Common.
You'll notice that in the files that you copied over, there were both .cpp and .h files included. You'll have to include those files to be compiled by your engine.
The second missing step:
In order to initialize the communications module, the module used to do live profiling from Wwise, your project must link with ws2_32.lib.
Common Practices
If your engine is a C-style API, then using everything the documentation says verbatim is perfectly fine. However, organizing the core Wwise Integration into a class in C++ is pretty typical. An example interface might look like this:
class AudioCore : public Singleton<AudioCore> // assume Singleton CRTP exists
{
public:
void Init();
void Update(float dt);
void Exit();
void LoadAsset(const char* bankName);
void UnloadAsset(const char* bankName);
// to be referenced later
AudioObjectID RegisterAudioObject();
void UnregisterAudioObject(AudioObjectID id);
private:
bool m_renderAudio;
CAkFilePackageLowLevelIOBlocking m_lowLevelIO;
std::vector<std::string> m_currentBanksLoaded;
};
Another best-practice is to use the Wwise generated AkUniqueIDs as identifiers over using strings. As a programmer, I don't have to tell you why using integral types is preferable over strings. To grab the AkUniqueIDs, your sound designer will have to check the flag in their Wwise build process to generate the Wwise_IDs.h file. Using this file, we'll be able to use the preprocessor later to abstract these out.
Integrating Audio Objects
Objects that emit and listen to sound in Wwise are internally denoted by AkGameObjects. Each AkGameObject has an AkGameObjectID, which is a typedef'ed unsigned __int64. They are assigned as you give them to Wwise. Depending on your engine architecture, there are different ways that you might implement audio emitters and listeners. Before we go over two common ways for two common engine architectures, here is a base class that will come in handy for both architectures:
using AudioObjectID = AkGameObjectID;
class AudioObject
{
public:
AudioObject() : m_audioObjectID{-1} {}
virtual ~AudioObject() {}
void Init(){
m_audioObjectID = AudioCore::Instance().RegisterAudioObject();
ASSERT(m_audioObjectID != AK_INVALID_GAME_OBJECT);
}
void Exit(){
AudioCore::Instance().UnregisterAudioObject(m_audioObjectID);
}
AudioObjectID GetAudioObjectID() const { return m_audioObjectID; };
// these will be changed to use integral values later using the preprocessorvoid SetRTPC(const char* name, float value);void SetSwitch(const char* switchGroupName, const char* switchName);
protected:
AudioObjectID m_audioObjectID;
};
Component Based/Entity-Component-System Architectures
If your custom engine is component based, the implementation of audio objects is pretty simple. You'll only need to make two components. If you use the ECS style of components, then you'll also need to write only one system for both.
The components are intuitively called the AudioEmitterComponent and the AudioListenerComponent (which can both be shortened to AudioEmitter and AudioListener). They are very simple to implement:
class AudioEmitter
: public Component<AudioEmitter>, // assume CRTP component base classpublic AudioObject
{
public:
//... engine specific component constructors/destructors here
// asume components are activated/deactivated via init/exit// instead of using ctors and dtors
void Init()
{
AudioObject::Init(); // call base class init.
m_positionOffset = {0, 0,0};
}
void Update(float dt); // used in Unity-style component based engines
void Exit()
{
AudioObject::Exit(); // call base class exit.
}
// this will change to use an integral type later
void Emit(const char* eventName);
// so that you can position the exact position of the emitter within// game object space.// for advanced implementation, you could have multiple emitter positions// indexed by names on a game object. could be used for animations, etc.
vec3 m_positionOffset;
};
// I usually just define ID 1 to always be the Listener.
// In general, only 1 listener is needed to exist at a time.
// However, there always need to exist one listener in order to hear sound.
const AudioObjectID DEFAULT_LISTENER_OBJECT = 1;
class AudioListener
: public Component<AudioListener>,
public AudioObject
{
public:
//...
void Init()
{
m_audioObjectID = DEFAULT_LISTENER_OBJECT;
AKRESULT result = AK::SoundEngine::RegisterGameObj(DEFAULT_LISTENER_OBJECT);
ASSERT(result == AK_Success);
result = AK::SoundEngine::SetDefaultListeners(&m_audioObjectID, 1);
ASSERT(result == AK_Success);
m_positionOffset = {0,0,0};
}
void Update(float dt); // used in Unity-style component based engines
void Exit()
{
AudioObject::Exit(); // call base class Exit
}
vec3 m_positionOffset;
};
Now the only thing left for the AudioObjects is for them to be able to tell Wwise where they are each frame. In a Unity-style component-based engine, the Update methods of each component will look identical; I'll implement the AudioEmitter Update below:
void AudioEmitter::Update(float dt)
{
// assume Transform is the component that contains // the world position information
Transform* transform = this->GetSiblingComponent<Transform>();
// retrieve position and normalized up and forward vectors from transform.
vec3 position = transform->GetPosition();
vec3 upVector = transform->GetUpVector().Normalize();
vec3 forwardVector = transform->GetForwardVector().Normalize();
AkSoundPosition audioTransform;
audioTransform.SetOrientation(forwardVector, upVector);
audioTransform.SetPosition(position + this->m_positionOffset);
AKRESULT result = AK::SoundEngine::SetPosition(
this->GetAudioObjectID(), audioTransform);
ASSERT(result == AK_Success);
}
Because the Update methods are identical, implementing them for an ECS architecture makes a lot of sense; an AudioPositioningSystem can be used for both AudioListeners and AudioEmitters. This Update can definitely be optimized to make sure that the position is only updated to Wwise if the object has moved within the last frame, but this is the basic implementation.
Entity and Event Based Architectures
I will first admit that I am inexperienced when it comes to working with engines that are Entity or Event based, simply because the modern engines that I've used and the engines that I've written have all been ECS or Component based architectures.
We'll simply be using the same AudioObject class from above; if an object wants to be able to emit sound effects, it logically makes sense for it to have an "is-a" relationship with the AudioObject. If your Entity-based engine has attachable components, like Unreal, then you might make an AudioEmitter component that can emit similar to how its done above, and instead of an AudioListener component, you might have a "RegisterAsListener" function on AudioObject.
For Event based architectures, emitting sound effects as events is probably the easiest. By its nature, Wwise is Event based, so you simply have to bind the AudioCore into your event system to receive audio emitting events. With sound effects that require positioning, you can send the events with the position of the object that's emitting the sound!
Conclusions:
Most of the information required for binding Wwise is already in their documentation. They give step by step processes on how to integrate it into your architecture. This article is for improving your integration process and for giving ideas on how to best add it to your game.
In the next article, I'll go in depth on how I used the preprocessor to get rid of any usage of strings in the pipeline, and control Wwise using only integral types in a readable and clean way at compile time.