This tutorial covers the basics of the Microsoft Cross-Platform Audio Creation Tool (XACT) API, including understanding XACT components and file formats, and basic steps for using XACT, such as loading wave banks, using notifications, and playing cues.
The full working source code for this tutorial is in:
<Installed SDK Location>\Samples\C++\XACT\Tutorials\Tut01_PlayCue
Before we dive into programming, let's cover some of the basic background of XACT.
XACT is composed of three connected parts:
When creating an audio solution for a game, it is important to understand which aspects are in the hands of the programmer and which are the responsibility of the composer or sound designer. To avoid confusion and potential responsibility "clashes," XACT attempts to provide a single way to perform most tasks. Properties and behaviors that are specified in content cannot generally be "overridden" programmatically, and conversely, the composer does not control resource management or dynamic pausing.
When initiating a project, the sound designer and the programmer decide on important cues, also known as "triggers" or "sound cues," in the game. The composer will then create a sound (or perhaps multiple sounds, one of which will be chosen randomly to play) that that cue plays, and the programmer makes the game plays those cues when the proper game events occur. This allows the programmer and the sound designer can go their separate ways.
To summarize, the programmer's responsibilities:
Meanwhile, the sound designer or composer:
XACT relies on two types of files for audio content: wave banks (*.xwb) and sound banks (*.xsb).
Wave banks are just bundled wave data, formatted for optimal playback performance. For in-memory wave banks, waves are DWORD aligned. For streamed wave banks, the individual waves are sector aligned (2048-byte boundaries for DVD streaming). All of this data formatting is managed automatically in the XACT build process, based on settings specified by the content author. These settings can be changed easily enough, but you should communicate with your content creator as to what kind of in-memory budget you have for audio, as well as how much streaming bandwidth you can grant to audio during game play.
Sound banks are where waves are assembled on a timeline into sounds by the content creator. The content creator then assigns a cue to play a sound (or in some cases, pick a random sound from a list). The developer then plays and stops these cues based on game events. Beyond controlling cues, the developer can also manipulate some properties on a category level. Each sound can be assigned to a category by the content creator. The developer can then globally adjust the relative volume of categories as well as pause or resume them.
While we're talking about file formats, let's also mention the XACT project file (*.xap). This is the file that contains all of the instructions for building the sound banks and wave banks for a project-every setting that the content creator has specified in the XACT authoring tool. Typically a developer will never have to actually read or edit this file. However, because the sound bank format could change in a future release, you should make sure to rebuild your sound banks with each new SDK, even if the content is no longer being edited. The command line tool xactbld.exe allows you to rebuild sound banks (and, optionally, wave banks) from the XACT project file without needing to open the editing tool. It's quite common to integrate this into a game's automated build process, rather than needing to remember to manually rebuild.
Here is a quick overview of the basic steps to use XACT, and afterward we'll look each of these steps in detail.
First, to initialize XACT it just requires making this call:
XACT_RUNTIME_PARAMETERS xrParams = {0}; xrParams.fnNotificationCallback = XACTNotificationCallback; hr = pXACTEngine->Initialize( &xrParams );
The XACT_RUNTIME_PARAMETERS structure contains a number of advanced options such as the global settings file and look-ahead time which are out of the scope of this tutorial, but they can all be safely set to 0 to use the default settings. The fnNotificationCallback member is the callback function used to notify the application about XACT notifications. Notifications will be covered in detail later in this tutorial.
Next, you need to create the wave banks and sound banks. In this tutorial, we only create one XACT wave bank and one XACT sound bank. For more complex games that have lots of sounds, they can break their sound effects into multiple wave and sound banks. This allows them to more easily partition the sound data by level, by character, by material, or however they desire to keep only the working set they desire loaded at the same time.
To create XACT wave banks and XACT sound banks you must use the following methods of IXACTEngine:
STDAPI IXACTEngine_CreateInMemoryWaveBank( void *pvBuffer, DWORD dwSize, DWORD dwFlags, DWORD dwAllocAttributes, IXACTWaveBank **ppWaveBank ); STDAPI IXACTEngine_CreateStreamingWaveBank( const XACT_WAVEBANK_STREAMING_PARAMETERS *pParms, IXACTWaveBank **ppWaveBank ); STDAPI IXACTEngine_CreateSoundBank( void *pvBuffer, DWORD dwSize, DWORD dwFlags, DWORD dwAllocAttributes, IXACTSoundBank **ppSoundBank );
IXACTEngine::CreateStreamingWaveBank is slightly more advanced and will be discussed in the next tutorial.
Both IXACTEngine::CreateSoundBank and IXACTEngine::CreateInMemoryWaveBank need a pointer to the data to read and the size of the data in bytes. Typically this data will come from reading an XACT file but this does not have to be the case. For example, the data could be part of an exe resource or contained inside a bundled file format.
When reading a file, there are 2 primary methods of doing so. The most typical is to use ReadFile() to read the file into a buffer like so:
HANDLE hFile = CreateFile(str, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); DWORD dwFileSize = GetFileSize(hFile , NULL); VOID* pSoundBankData = new BYTE [dwFileSize]; ReadFile(hFile, pSoundBankData, dwFileSize, &dwBytesRead, NULL)) HRESULT hr = pXACTEngine->CreateSoundBank(pSoundBankData, dwFileSize, 0, 0, &pSoundBank);
The other method is called memory-mapped files and uses MapViewOfFile to map a view of a file into the address space of the calling process. Memory-mapped files tend to be faster for most situations assuming you have enough virtual address space for a full map of the file thus our examples will use the faster memory mapped file method:
HANDLE hFile = CreateFile( str, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL ); DWORD dwFileSize = GetFileSize( hFile, NULL ); HANDLE hMapFile = CreateFileMapping( hFile, NULL, PAGE_READONLY, 0, dwFileSize, NULL ); VOID* pSoundBankData = MapViewOfFile( hMapFile, FILE_MAP_READ, 0, 0, 0 ); HRESULT hr = pXACTEngine->CreateSoundBank( pSoundBankData, dwFileSize, 0, 0, &pSoundBank );
The most important concept to understand is that you can not free the data block passed to these create functions until XACT has destroyed that wave bank or sound bank. The file data passed to these functions is not cached by XACT and is needed by XACT when sounds are played. Simply calling Release on the bank does not mean the bank is immediately destroyed and the memory can be freed because there could be sound(s) still playing when Release was called.
It is only safe to free the data after calling either IXACTEngine::ShutDown or a XACTNOTIFICATIONTYPE_SOUNDBANKDESTROYED or XACTNOTIFICATIONTYPE_WAVEBANKDESTROYED notification for the bank has been received. We will look at how to create and handle notifications shortly. Alternatively, if you set XACT_FLAG_API_CREATE_MANAGEDATA and do not use the memory-mapped file method then XACT will automatically free the file buffer when the bank is destroyed.
After the wave and sound banks have been created, an XACT cue index is needed used by your game to play a cue event. There are a few methods for retrieving these index cues. The most common method is to use IXACTSoundBank::GetCueIndex like so:
iZapCueIndex = g_audioState.pSoundBank->GetCueIndex( "zap" );
Note that the string "zap" used here is just the friendly string name of the cue as you defined in the XACT project. Also note that if the cue does not exist in the sound bank, the index will be XACTINDEX_INVALID however this is not tragic especially during development as it will simply cause the IXACTCue::Play or IXACTSoundBank::Prepare call to fail.
Another method is to enable the BuildProjectHeader setting in XACT authoring tool. This will make the XACT project create a C style header which can be included by your game. The header will contain something like this depending on the names of your cues:
typedef enum { XACT_CUE_SOUND_BANK_ZAP = 0, } XACT_CUE_SOUND_BANK; #define XACT_CUE_SOUND_BANK_ENTRY_COUNT 1
Now that the XACT is initialized, the wave and sound banks have been created and your engine has indices to all the cues, you are ready to use XACT.
You can optionally register for any number of notifications depending on the game's needs. The process simply involves filling out the XACT_NOTIFICATION_DESCRIPTION structure and calling IXACTEngine::RegisterNotification like so:
XACT_NOTIFICATION_DESCRIPTION desc = {0}; desc.type = XACTNOTIFICATIONTYPE_CUESTOP; desc.pSoundBank = pSoundBank; desc.cueIndex = iZapCueIndex; pXACTEngine->RegisterNotification(&desc);
The structure is described in detail in the docs along with all of the notification types (XACTNOTIFICATIONTYPE) available but it should be noted that you don't have to tie the notification to a one time event or a specific cue or bank. For example, you can setup a persistent notification for all sound banks like so:
XACT_NOTIFICATION_DESCRIPTION desc = {0}; desc.flags = XACT_FLAG_NOTIFICATION_PERSIST; desc.type = XACTNOTIFICATIONTYPE_CUESTOP; desc.cueIndex = XACTINDEX_INVALID; pXACTEngine->RegisterNotification(&desc);
This notification will trigger whenever any cue stops on any sound bank. If you have multiple sound or wave banks and want to swap them in and out as the game progresses, you can use a notification like this:
XACT_NOTIFICATION_DESCRIPTION desc = {0}; desc.flags = XACT_FLAG_NOTIFICATION_PERSIST; desc.type = XACTNOTIFICATIONTYPE_SOUNDBANKDESTROYED; pXACTEngine->RegisterNotification(&desc); desc.flags = XACT_FLAG_NOTIFICATION_PERSIST; desc.type = XACTNOTIFICATIONTYPE_WAVEBANKDESTROYED; pXACTEngine->RegisterNotification(&desc);
So you will be notified whenever a wave or sound bank is destroyed so you can free its associated file memory.
To play sounds using XACT, the game plays a specific XACT cue when a game event occurs. This cue is defined in the XACT project by the sound designer to play a certain set of XACT sounds which in turn play events which trigger the playback of a certain set of wave data with possibly a variation of volumes and pitches. Because of the way XACT is designed, the audio that is heard when a cue is played can be changed without recompiling the game. This allows the audio to be designed and tweaked independently once the game events are defined and the cues are triggered by the game engine.
The simplistic method to play a cue is done by calling IXACTSoundBank::Play with the appropriate cue index when an event happens in the game. We'll look at more complex methods later. For the typical case the code is simply:
pSoundBank->Play( iCueIndex, 0, NULL );
Note that it is often not necessary to retrieve the pointer to the IXACTCue object unless your game needs to stop or pause the cue so NULL can be used as the 3rd parameter.
Notifications are received in a notification callback function that looks like this:
void WINAPI XACTNotificationCallback(const XACT_NOTIFICATION* pNotification) { if( pNotification->type == XACTNOTIFICATIONTYPE_CUESTOP ) { // Handle the cue stop notification } // .... }
This callback is set when calling IXACTEngine::Initialize. This callback can be executed on a different thread than the application thread, so any shared data referenced in this callback must be made thread safe. The application also needs to minimize the amount of time spent in this callback to avoid glitching, and a limited subset of XACT APIs can be called from inside the callback (see XACT_NOTIFICATION_CALLBACK for a list of acceptable APIs) so it is sometimes necessary to handle the notification outside of this callback.
The XACT_NOTIFICATION structure will contain all the information the game needs to process the notification. If in the rare case the game wants to remove all pending notifications from the queue without handling them it can flush them like so:
XACT_NOTIFICATION_DESCRIPTION desc = {0}; desc.type = XACTNOTIFICATIONTYPE_CUESTOP; XACTFlushNotification(&desc);
You can also use IXACTEngine::UnRegisterNotification to stop receiving a certain type of notification.
It is important to allow XACT to do periodic work by calling IXACTEngine::DoWork. However this function must be called often enough. If you call it too infrequently, streaming will suffer and resources will not be managed promptly. On the other hand if you call it too frequently, it will negatively affect performance. Calling it once per frame is usually a good balance as shown below:
while( WM_QUIT != msg.message ) { // Use PeekMessage() so we can use idle time to render the scene and call DoWork() bGotMsg = ( PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE ) != 0 ); if( bGotMsg ) { // Translate and dispatch the message TranslateMessage( &msg ); DispatchMessage( &msg ); } else { RenderFrame(); pXACTEngine->DoWork(); } }
To shut down and clean up XACT, first release all the XACT interfaces and then call IXACTEngine::ShutDown and free the sound and wave bank data like shown below. Be aware that IXACTEngine::ShutDown is synchronous and will take some time to complete if there are still playing cues. Also IXACTEngine::ShutDown is generally only called when a game exits and not the preferred method of changing audio resources. To unload resources while game is running, use notifications.
VOID CleanupXACT() { // Release XACT interfaces if( g_pSoundBank ) g_pSoundBank->Release(); if( g_pWaveBank ) g_pWaveBank->Release(); // Shut down XACT pXACTEngine->ShutDown(); // Release memory mapped files if( g_pbSoundBank ) UnmapViewOfFile( g_pbSoundBank ); if( g_pbWaveBank ) UnmapViewOfFile( g_pbWaveBank ); }