Version
menu

Wwise SDK 2025.1.0
Increasing simulation data throughput with the Command Buffer API

Some games frequently have to send a high volume of data to the sound engine. For example, games with lots of game objects and game syncs send hundreds of values to the sound engine at regular intervals. Although you can manage this data transfer with the C++ API, a more efficient way to do so is to use the fully asynchronous, C-compatible Command Buffer API.

The Command Buffer API has two primary advantages over the C++ API:

  • It minimizes both CPU usage and memory allocations for large data transfers.
  • It is fully compatible with any C11 compiler, and therefore its functions can be bound directly to other programming languages that have FFI (foreign function interface) capabilities, such as Rust.

Understanding the Command Buffer API

The Command Buffer API works like other command buffer-based APIs, such as Vulkan and DirectStorage. The command buffer is a region of memory that contains a sequential list of instructions for the sound engine. For example, a single command buffer can contain several unrelated commands such as:

  • Registering game objects
  • Posting events
  • Updating RTPC values

After you construct the buffer and submit it to the sound engine, the buffer is added to the sound engine's internal processing queue. The commands in the buffer are then processed asynchronously on the next audio rendering frame, in the order in which they are listed in the buffer.

Commands can fail during processing. When a command fails, the sound engine proceeds to the next command in the buffer.

Clients can be called back when all commands in a buffer are processed. When this callback occurs, the client can inspect the result code of each command in the buffer and release or re-use the memory area.

Constructing command buffers

To construct a command buffer, the size of the buffer must first be determined. The size of the buffer depends on the quantity and type of commands it contains. You can use the AK_CommandBuffer_MinSize() and AK_CommandBuffer_CmdSize() functions to calculate the buffer size.

After the buffer size is calculated, the client can allocate the buffer using AK_CommandBuffer_Create(). This function allocates the buffer and initializes it as an empty but valid command buffer.

For example, the following code demonstrates how to create a valid command buffer able to accomodate 20 RegisterGameObject commands and 20 SetRTPC commands:

void * buffer = AK_CommandBuffer_Create(buffer_size);
assert(buffer != NULL);

Note: Alternatively, clients can choose to manage the command buffer memory using their own engine's allocation hooks. To do so, replace the call to AK_CommandBuffer_Create() with a manual memory allocation of the same size, using an alignment of 4 bytes. Then, use AK_CommandBuffer_Init() to initialize the content of the buffer as an empty but valid command buffer.

Note that such buffers should be released using the usual method provided by the game engine. See Cleaning up for more details.

To add commands to the buffer, use AK_CommandBuffer_Add(). For example, the following code demonstrates how to fill the buffer with 20 RegisterGameObject commands and 20 SetRTPC commands:

for (int i=0; i < 20; i++)
{
assert(pCmd != nullptr); // If null is returned, it means the buffer is full and cannot accomodate another command.
pCmd->gameObjectID = 100 + i;
}
for (int i=0; i < 20; i++)
{
assert(pCmd != nullptr);
pCmd->rtpcID = MY_RTPC_ID;
pCmd->rtpcValue = 5.5;
pCmd->gameObjectID = 100 + i; // Because commands in a buffer are executed sequentially, this ID will have been registered by the time this command is processed.
}

Each command has a code (dictated by the enum AkCommand) and a fixed-size payload structure. Not all commands have the same payload size. The example above shows that the AkCommand_RegisterGameObject command has a payload described by the AkCmd_RegisterGameObject structure, while AkCommand_SetRTPC has the payload described by the AkCmd_SetRTPC structure. See the documentation for AkCommand to learn which payload structures are associated with command codes.

Extra payload data

Some commands require additional payload data of variable size. For example, the AkCommand_SetListeners command must be followed by an array of listener IDs. The number of listeners can vary based on the needs of the game.

To add a variable-sized payload to a command, first add the command with AK_CommandBuffer_Add, then add the variable-sized payload with the appropriate function based on the payload's type:

  • For strings, use AK_CommandBuffer_AddString. This function associates a name with a game object, which is useful during profiling.
  • For arrays of simple data types, use AK_CommandBuffer_AddArray. This function is used for commands with a "number of items" field, such as AkCmd_SetListeners and AkCmd_SetMultiplePositions.
  • For external sources, use AK_CommandBuffer_AddExternalSources. This function is used exclusively for AkCommand_PostEvent.
  • For Spatial Audio geometry data, use AK_CommandBuffer_AddGeometry. This function is used exclusively for AkCommand_SA_SetGeometry.

For example, the following code adds a AkCommand_SetListeners command with three listener IDs:

AkGameObjectID listenerIDs[3] = { 200, 300, 400 };
pCmd->gameObjectID = 100;
pCmd->numListenerIDs = 3;
AK_CommandBuffer_AddArray(buffer, sizeof(AkGameObjectID), pCmd->numListenerIDs, listenerIDs);

If you do not add extra payload data to a command that requires it, the command fails with an AK_InvalidParameter result. To learn whether a particular command requires extra payload data, refer to the documentation of the command.

Executing commands and inspecting results

To execute the commands, you must call AK_CommandBuffer_Submit() to submit the buffer to the sound engine. This function places the buffer in the sound engine's pending message queue. Its commands are processed asynchronously on the next audio rendering frame, in the order in which they were added to the buffer.

Warning: Avoid releasing or writing to the buffer until all commands in the buffer are processed.

When a command is processed, the result of the command is written in the command buffer itself. Clients must designate a callback function before submitting the buffer in order to inspect command results. When this callback is called by the sound engine, clients can iterate through the commands to check result codes.

You can also use the callback to determine when the buffer can be re-used to submit additional commands, or when it can be released.

The following example shows how to register the command buffer callback, submit the buffer, and check results when processing is complete:

// This simple example uses a global lock to protect the buffer on the client side.
// You can replace this with your preferred constructs
CAkLock g_bufferLock;
void OnCommandBufferDone(void* in_pCookie);
void SubmitCommandBuffer(void* buffer)
{
g_bufferLock.Lock();
AkCommandBufferHeader* pHeader = static_cast<AkCommandBufferHeader*>(buffer);
pHeader->completionCallback = OnCommandBufferDone;
pHeader->completionCallbackCookie = buffer; // This is a user-defined value passed to the callback.
}
void OnCommandBufferDone(void* in_pCookie)
{
AK_CommandBuffer_Begin(in_pCookie, &it);
while (AK_CommandBuffer_Next(&it))
{
if (it.header->result != AK_Success)
{
AKPLATFORM::OutputDebugMsgV("Command with code %d failed: Result = %d\n", it.header->code, it.header->result);
}
}
g_bufferLock.Unlock(); // Buffer can be re-used now!
}

Cleaning up

As demonstrated above, it is customary to re-use the same buffer for multiple command submissions to avoid churn on the memory manager. However, as with all other memory resources, command buffers must eventually be released. To release a command buffer, call AK_CommandBuffer_Destroy().

Warning: Do not use AK_CommandBuffer_Destroy() to free a manually-allocated command buffer! Use your engine's memory allocation hooks instead. Only buffers created by AK_CommandBuffer_Create() should be released using AK_CommandBuffer_Destroy().

Events and Playing IDs

When you use the Command Buffer API to post Events, the user generates Playing IDs before the Events are posted. In contrast, the C++ AK::SoundEngine::PostEvent API returns a valid Playing ID after the Event is posted.

To post an event with Command Buffer, first generate a Playing ID with AK_SoundEngine_GeneratePlayingID(). Next, use this ID when you construct the AkCmd_PostEvent command. Finally, submit the buffer that contains the commands.

The PostEvent command consumes its Playing ID when processed. Therefore, it is not possible to share the same Playing ID for multiple PostEvents. A new Playing ID must be generated for each PostEvent command. PostEvent commands that refer to Playing IDs that were previously consumed will fail to process.

Here is an example of a command buffer that posts two distinct Events:

Command buffer memory layout

Clients do not have to use the AK_CommandBuffer_Init() and AK_CommandBuffer_Add() functions to construct a command buffer. The content of the buffer can be constructed manually. Its memory layout is defined by the various structures defined in AK/command_types.h.

The following diagram shows the sequential layout of a valid command buffer in memory.

+-----------------------------------------------------+
| CBH | CH1 | CP1 | CH2 | CP2 | ... | CHN | CPN | EOB |
+-----------------------------------------------------+

CBH = Command Buffer Header (AkCommandBufferHeader)
CHN = Nth Command Header (AkCommandHeader)
CPN = Nth Command Payload. Its size is dictated by the command header's "size" member.
EOB = End-of-buffer marker. AkCommandHeader structure with the command code AkCommand_EndOfBuffer (0) and payload size 0.

Note the following about the preceding diagram:

  • You could divide the "Command Payload" into a fixed-size payload (the command's payload struct) and a variable-size payload. In this case, the command header's "size" member must equal the sum of the fixed-size and variable-size payloads.
  • External sources and geometry data have a complex serialization scheme that might change between Wwise versions. We recommend that you always use AK_CommandBuffer_AddExternalSources and AK_CommandBuffer_AddGeometry to serialize these structures to a command buffer.
  • The memory address of each segment that forms the buffer must be aligned on 4 bytes. You must pad variable-size payloads to ensure alignment.
Warning: Submitting or iterating over a malformed command buffer can lead to undefined behavior or crashes.
@ AkCommand_RegisterGameObject
See AkCmd_RegisterGameObject.
AKSOUNDENGINE_API void * AK_CommandBuffer_Add(void *in_buffer, enum AkCommand in_cmd_id)
AkGameObjectID gameObjectID
(optional) Game Object ID; specify AK_INVALID_GAME_OBJECT for global scope or if using playingID
AKSOUNDENGINE_API struct AkCommandBufferHeader * AK_CommandBuffer_Create(size_t in_size)
AKSOUNDENGINE_API size_t AK_CommandBuffer_CmdSize(enum AkCommand in_cmd_id)
@ AkCommand_PostEvent
See AkCmd_PostEvent.
AkUInt64 AkGameObjectID
Game object ID.
Definition: AkTypedefs.h:47
@ AkCommand_SetRTPC
See AkCmd_SetRTPC.
AkRtpcValue rtpcValue
Value to set.
void * completionCallbackCookie
User cookie for done_callback.
void OutputDebugMsgV(const char *in_pszFmt,...)
Output a debug message on the console (variadic function).
AkCommandCallbackFunc completionCallback
At that point in time, the client can free the command buffer memory, or re-use it for something else...
@ AkCommand_SetListeners
See AkCmd_SetListeners.
AKSOUNDENGINE_API void AK_CommandBuffer_Submit(void *in_buffer)
AKSOUNDENGINE_API AkUInt32 AK_SoundEngine_GetIDFromString(const char *in_pszString)
Definition: AkLock.h:38
struct AkCommandHeader * header
Pointer to header of command. Use this to check the code of the command and the result code after pro...
AKRESULT Unlock(void)
Unlock.
Definition: AkLock.h:71
#define NULL
Definition: AkTypedefs.h:33
AKSOUNDENGINE_API void * AK_CommandBuffer_AddArray(void *in_buffer, size_t item_size, AkUInt16 num_items, const void *items)
AKSOUNDENGINE_API int AK_CommandBuffer_Next(struct AkCommandBufferIterator *inout_iterator)
AkRtpcID rtpcID
Game parameter ID.
AkGameObjectID gameObjectID
Game Object ID.
AKSOUNDENGINE_API size_t AK_CommandBuffer_MinSize(void)
AKSOUNDENGINE_API void AK_CommandBuffer_Begin(void *in_buffer, struct AkCommandBufferIterator *out_iterator)
AkGameObjectID gameObjectID
Associated game object ID.
AKRESULT Lock(void)
Lock.
Definition: AkLock.h:61
Describes the data written at the beginning of any initialized command buffer.
AkUniqueID eventID
Unique ID of the event.
AkUInt16 code
Unique ID of the command, as listed in AkCommand enum.
AkGameObjectID gameObjectID
ID of the game object to be registered. Valid range is [0 to 0xFFFFFFFFFFFFFFDF].
@ AK_Success
The operation was successful.
Definition: AkEnums.h:34
AKSOUNDENGINE_API AkPlayingID AK_SoundEngine_GeneratePlayingID(void)
Generates a new playing ID. This is guaranteed to return a different value every time this is called.
AkUInt32 numListenerIDs
Number of listeners.
AkPlayingID playingID
Unique ID that will be associated with this playback. Use AK_SoundEngine_GeneratePlayingID() to gener...

Was this page helpful?

Need Support?

Questions? Problems? Need more info? Contact us, and we can help!

Visit our Support page

Tell us about your project. We're here to help.

Register your project and we'll help you get started with no strings attached!

Get started with Wwise