Saturday, April 11, 2009

XNA, XACT, and Sound

Our project had a lot of difficulty in getting sound to work in XNA and in communication between the programmers and the sound artists.
This tutorial is intended as instructions for the audio folks at Sandswept Studios, but feel free to use it too.

Key Concepts

XACT lets the audio engineers tweak all sorts of sound parameters without the programmers having to worry about it.
The programmer just plays the cue.
The audio engineer can tweak volume, sound fall off distance, etc.

First and most important, you must have the correct version of XACT to work with XNA!
This means that you must use the XACT that comes with XNA Studio.
On my machine XACT is at C:\Program Files\Microsoft XNA\XNA Game Studio\v3.0\Tools\Xact.exe

When you are editing sounds in XACT it is nice to be able to hear them, so you have to run AudConsole.exe
that is in the same directory that Xact.exe is in.
You would think that XACT could just play sounds, but no, you have to run AudConsole.exe

Create An XACT Project

So lets get started. Run XACT and create a new project.
File ==> New project
I put the new project at C:\Users\rkeene\Documents\AudioTest\ and called it AudioTest

XACT Uses wave banks which are the 'raw sound' usualy from a .wav or .aif file.
We will use the regular old windows chimes sound. So create a wave bank
Wave Banks ==> New Wave Bank
You should get an empty wave bank window.
Now go find the .wav file we want. On my Vista machine it is in
C:\WINDOWS\Media\chimes.wav
So drag-n-drop that file onto the WaveBank area.

Now we want a sound bank, which ties a raw wave bank entry to all sorts of settings like volume and pitch.
Sound Banks ==> New Sound Bank
Then drag and drop the chimes.wav wave bank entry to the upper part of the Sound Banks window.
Sounds let you add event like volume changes, pitch changes, etc. creating nifty effects.

We also need an actual cue.
This is the name the programmer will use to find the cue and play it in the C# code in XNA.
So, drag-n-drop the sound to the lower part of the cues area.

Now we have a wave bank, a sound bank, and a project. So click on the save icon to save the project.

Sound Fade With Distance

In most games it is nifty to have sounds fade as your view point gets furthur away from the sound source.
Like in DETOUR the factory makes machine noises and we want them to fade out as the player's viewpoitn gets furthur from the factory.
But there are 'big' sound like nukes going off, and 'small' sounds like a dog barking. Big sounds fade out less with distance, and small sounds fade quickly.
In XACT there are RPC Presets that let you setup this fade out.

You need to determine what units distance is in in your game. Is it meters? Inches? Pixels?
On the DETOUR board tiles are 254x254 game units. These are also called World Coordinates.
So while the view point may be on tile 12,5 the world coordinates will be 12*254, 200, 5*254
which is X,Y,Z coordinates. The board is in the XZ plane and Y is 'up'.
Thus for our factory sound we want to be at full volume if you are looking at the factory and fade out
to silent in about 2000 units of distance.

First lets look at Variables. On the left side there is Variables ==> Cue Instance ==> Distance
This is the variable that we will relate to volume. Since our largest board is about 40x40 and thus
10,000 world units max, we want to limit distance ranges to 0 to 10000.
So click on Distance and down below set the range to 0 and 10000.

Next drag and drop your cue on to the RPC Presets item on the left of the screen.
This attaches the cue to the Presets.
Click on RPC Presets and a window comes up with a black window and lines on it.
The lines are (or will be) the sound level related to distance.

So in the RPC Presets window you first select the Variable whic is Distance.
Then select the Object whic is Sound, and then the Parameter which is Volume.
Thus "The distance effects sounds by changing colume."

Now you can drage the line around and get whatever sound vs. distance function you like.
You can double click on the line to make a new drag point. And Ctrl-Z undoes mistakes.

Testing 1 2 3 Testing

Make sure AudConsole is running, and up at the center top of the Xact tools there is a play button.
Click on the Cue, and then click play. Do you hear it?
Also, in the RPC Presets tool you can set the distance, click on the line, and then click the play button
in the RPC window and hear the sounds volume at a given distance.

Exporting

Next we need to export from XACT for XNA.
At the top of the XACT windows is a View menu and the first two items are View XBox36 and View Windows.
This is how you switch between compiling and exporting for the XBox and the PC.

You want to be able to tell the XBox vs. Windows files appart so click on the Wave Bank in the left tree,
and on the lower left there should be fields for
XBox 360 Build Path and Windows Build Path.

We use the convention that the file names end with _w or _x. So change these to be differnet.
Do the same for the sound banks.
Do the same for the top most level project too.

In my test project the top level is AudioTest
so the build path is AudioTest_w.xgs and SudioTest_x.xgs
The sound banks are SB1_w.xsb and SB1_x.xsb
and wave banks are WB1_w.xwb and WB1_x.xwb

Now you build, so make sure View is on View Windows Properties, and click the Builds The Current Project
icon in the top center of the XACT windows.
It will ask about the report you might want. I check Concise and enter rpt_w.txt

The reason for the report is that as your game gets bigger and has zillions of cues, the report helps you keep thing straight.
You can also give the report to the programmer so they know what the cue names are.
This is where all those 'notes' fields come in handy.
It is wise to add a note entry to every cue describing what it is intended for in the game.

Phew! We have exported the windows version.

You can set the View to View XBox Properties and export again to get the XBox version of the files.
Windows version files will not work on the XBox. Period.

Note: sound effects (sfx) are usually loaded into memory in advance so there is no delay to play them.
Music is HUGE so it is streamed from disk to the audio hardware on-the-fly with buffers.
If you click on the Wave Bank, the properties below will include Streaming or InMemory.
Sound effect should be InMemory, and Music should be Streaming.


C# and XNA and Sounds

Ok, now you have all the sounds build for XNA.

Now we move to the XNA C# world.
In XNA you have to load the project (.xgs) the sound bank (.xsb) and the wave bank (.xwb)
Then you find the cue, give it 3D position (optional), and play it.
If your sound person did things correctly, then that is all.

First we need to get the files into the XNA project.
So copy the three files into the Content area. (Ok, get organized and use a subdirectory, etc.)
Now in the Content part of the Solution Explorer, Add ==> Existing Item
You will need to set the pop up file chooser to All Files (*.*). It initially filters for non-sound files.
Select the three files and hit Ok.

They are now in the project, but you need to tell XNA and Visual Studio how to use them.
Select all three under the Content area and in the properties area set...
Build Action: Content
Content Importer: XACT Project - XNA FrameWork
Content ProcessorXACT Project - XNA FrameWork
Copy To Output Directory: Copy if Newer


Ok, now build the project to see if everything is Ok.

Now we need to load the sounds into our game so
add three new class instance level variables to your Game.cs file

public AudioEngine TheAudioEngineTest;
public WaveBank TheWaveBankTest;
public SoundBank TheSoundBankTest;

And in the LoadContent method put something like...

TheAudioEngineTest = new AudioEngine("Content\\AudioTest_w.xgs");
TheWaveBankTest = new WaveBank(TheAudioEngineTest, "Content\\WB1_w.xwb");
TheSoundBankTest = new SoundBank(TheAudioEngineTest, "Content\\SB1_w.xsb");
TheAudioEngineTest.Update();

The Update there is to 'fail fast' if there is a problem, and is not required.

Note: If you play a cue and never call Update on the audio engine, you not hear anything.
Note: The above is for in-memory sounds like sound effects. Music is streamed and the code might
then look like...

TheAudioEngine = new AudioEngine("Content\\music_w.xgs");
TheWaveBank = new WaveBank(TheAudioEngine, "Content\\music_w_wb.xwb", 0, 4);
TheSoundBank = new SoundBank(TheAudioEngine, "Content\\music_w_sb.xsb");
TheAudioEngine.Update();
Note the 0,4 on the end of the WaveBank entry. This is critical.It is the offset and packet size
and effects how smoothly the music plays back.

Oops - We need to differentiate between XBOX and not XBOX code so we need a
#if XBOX
// Load the XBox versions of the files.
#else
// Load the Windows versions of the files.
#endif
around our loads where the _w becomes _x

Now lets play our sound.
The simplest way is...

TheSoundBank.PlayCue("mycue");

Also in the main Update in your game you need to Update TheSoundEngine

You can also do...

Cue c = TheSoundBank.GetCue("mycue");
c.Play();

But of course, we want possitional sound so we do...
Object o = Gm.TheAudioEngineTest.RendererDetails;
AudioListener aListen = new AudioListener();
aListen.Position = FocusTile.LocationWorld;
AudioEmitter aEmit = new AudioEmitter();
aEmit.Position = FactoryTile.LocationWorld;
Cue c = Gm.TheSoundBankTest.GetCue("TestCue1");
float d = (aEmit.Position - aListen.Position).Length();
c.SetVariable("Distance", d);
c.Apply3D(aListen, aEmit);
c.Play();

Ok, the FocusTile.LocationWorld gives me the X,Y,Z coordinates of the tile in world units.
The FactoryTile is where my factory is.
The d is the distance between the two.
We have to set the Distance variable on the cue, and the Apply3D does the stereo
left-right speaker shift.
(You left and right speakers are in the correct place on your desk top?
The one with the volume knob is always the right speaker.)

Wow, that is how sound is done.

XBox Note: When you make the XNA project for the XBox you must remove the wave bank, project, and sound bank files in Content
and re-add the XBox versions.

Tips and Tricks

Randomize Cue Sound Choice
You can make a single cue play several different sound randomly. The programmer does not need to even know which one will play.
Cues have a weight or probability. You can give each sound in the cue a different weight and that is how often they will get picked.
This is cool if you have a repetitive sound, like gun fire, and you want to randomly choose between several different gun sounds to make it not-so-boring.

C# Code Structure
You can have several Audio Engines at the same time. We break out one music engine, and 3 different Sfx engines. Then we have a PlaySfx call that takes an enum flag as to which Sfx set the cue is in. This makes our code control checkins much faster since we don't have one giant Sfx Sound Bank and Wave Bank.

It is also handy to have a currently playing music track, and a NextMusicCue that will get played when the current one finishes.

We also have an SfxItem class that hold info about a currently playing item and each of the Sfx engines. So our overall update method loos like this...

private void UpdateAudioEngines()
{
if (TheAudioEngineTest != null)
{
TheAudioEngineTest.Update();
}
if (TheAudioEngine != null)
{
TheAudioEngine.Update();
if (CurrentMusicCue != null && CurrentMusicCue.IsStopped && NextMusicCue != null)
{
CurrentMusicCue = NextMusicCue;
NextMusicCue = null;
CurrentMusicCue.Play();
}
}
if (SfxActions != null && SfxActions.TheAudioEngine != null)
{
SfxActions.TheAudioEngine.Update();
}
SfxItem.UpdateAll();
}

Enjoy

T.F.

2 comments:

Andreas said...

Great XNA article. How about cross-posting on the www.xnawiki.com site (in the relevant section)?

Thomas said...

Hey, I'm thinking about licensing your FBX importer but I can't find your email address anywhere. Send me an email if you're interested.