Tutorial 3: Making Sounds with XNA Game Studio Express and XACT
This article details how to use the Microsoft Cross-Platform Audio Creation Tool (XACT) to make sounds for an XNA Game Studio Express game, and how to use the XNA Framework Audio API to play them.
Note
This tutorial builds on code you have written during the previous tutorial: Tutorial 2: Making Your Model Move Using Input. Follow the steps in the previous tutorial before starting this tutorial.
- Step 1: Get Some Wave Files
- Step 2: Create an XACT Project with Wave Files
- Step 3: Load an XACT Project Through the Content Pipeline
- Step 4: Play Sounds Using the Audio API
- Congratulations!
- The Complete Example (contents of Game1.cs)
Step 1: Get Some Wave Files
Audio in XNA Game Studio Express is, at heart, wave-based. Wave files (.wav) are assembled into an XACT project and built into wave banks that are loaded into your game.
The first thing to do is get some wave files. This is where the Spacewar project you created in Tutorial 1: Displaying a 3D Model on the Screen will come in handy. Let's go get the wave files from that project.
- Make sure your project from Tutorial 2: Making Your Model Move Using Input is open. If it isn't, open it by clicking the File menu, and then click Open Project and browse to your project.
- In the Solution Explorer, right-click the Content folder, click Add, and then click New Folder. Name this folder Audio.
- Right-click the Audio folder you just created, click Add, and then click New Folder. Name this folder Waves.
- Open Windows Explorer. Browse to the Spacewar Starter Kit project folder and from there to the Content\Audio\Waves folder. Inside that folder, you will see several subcategories of folders, containing many sounds for you to use.
- Copy these files from the Spacewar Waves folder to the Content\Audio\Waves folder in Windows Explorer: Ships\engine_2.wav, and Weapons\hyperspace_activate.wav.
Step 2: Create an XACT Project with Wave Files
If you've noticed that you don't add wave files like other content files, via the Solution Explorer, you're right. The Content Pipeline processes XACT projects, which are compilations of wave files, and not just the raw wave files themselves. We must now create that compilation using the Microsoft Cross-Platform Audio Creation Tool (XACT).
Let's launch XACT.
- From your Start Menu, browse to All Programs, then Microsoft XNA Game Studio Express, then Tools.
- Click Microsoft Cross-Platform Audio Creation Tool (XACT).
XACT will launch and immediately open a new project. Once you see the XACT window, save this new project to your game project folder in the Content\Audio folder, as follows:
- From the XACT window, click the File menu, and then click Save Project As.
- From the dialog box that appears, browse to your project folder, then into the Content\Audio folder.
- Type a name for your project; use MyGameAudio.
- Click OK.
The project is saved in your game project folder under Contents\Audio. We will load this XACT project through the Content Pipeline later. First, let's put our wave files in the project so that when the XACT project loads in our game, there will be sounds to play.
The screen contains an empty project. In the empty project, you must first create a wave bank. To do this, click Wave Banks, and then click New Wave Bank. A new wave bank appears in the tree view under Wave Banks with the default name Wave Bank.
Create a new sound bank. To do this, click Sound Banks, and then click New Sound Bank. A new sound bank appears in the tree view under Sound Banks with the default name Sound Bank.
At this point, two new windows have appeared: one for the wave bank and one for the sound bank. To arrange these windows for easier viewing, click Window, and then click Tile Horizontally. The windows should now look similar to the following:
Add both of your wave files to the wave bank window. Make sure the wave bank window is active by clicking on it, and then click Wave Banks and click Insert Wave File(s). Browse to your game project folder, and into the Content\Audio\Waves folder. Select both wave files. If you successfully added them, they appear in the wave bank window, which will look similar to the following:
For each wave listed in the wave bank window, drag the wave from the wave bank window to the sound bank window, and drop it on the Cue Name panel. XACT automatically creates a new cue that is linked to a new sound that plays this wave file. It should look similar to the following:
If the lower-left corner of the sound bank window does not have any entries, you must add entries to it by dragging each sound from the upper-left panel of the sound bank to the lower-left panel of the sound bank. This action creates cues that correspond to the sounds.
The last thing to do before saving is make sure our engine_2 sound loops when we play it. We don't want it to just play once and stop. Click engine_2 in the upper-left panel of the sound bank. When you do this, a tree structure will appear in the upper-right panel of the sound bank.
In the upper-right panel of the sound bank, click the Play Wave item. Notice that the property pane in the far lower-left panel of your XACT window will change and list a set of properties.
In this property panel, find the LoopEvent property. Set it to Infinite. It should look something like this:
Save the XACT project by clicking the File menu and then clicking Save Project.
Step 3: Load an XACT Project Through the Content Pipeline
You've just created and saved an XACT project that contains two wave files. The next step is to load this XACT project into the Content Pipeline.
- Return to Visual C# 2005 Express Edition, and make sure your game project is loaded. If it isn't, open it by clicking the File menu, and then click Open Project and browse to your project.
- Right-click the Content\Audio folder in the Solution Explorer, click Add and then Existing Item. Using the dialog box that appears, browse to your game project's (not Spacewar's) Content\Audio folder, and select MyGameAudio.xap. If you can't see any files, make sure you change the Files of type selection box to read Content Pipeline Files. Click OK.
The .xap file will be built automatically by the Content Pipeline as part of building your game. Now, all that's left is to load the output files from your XACT project into your game. Let's code!
View the code by double-clicking Game1.cs in Solution Explorer.
Find the Initialize method. Modify it to look like this:
AudioEngine audioEngine; WaveBank waveBank; SoundBank soundBank; protected override void Initialize() { audioEngine = new AudioEngine( "Content\\Audio\\MyGameAudio.xgs" ); waveBank = new WaveBank( audioEngine, "Content\\Audio\\Wave Bank.xwb" ); soundBank = new SoundBank( audioEngine, "Content\\Audio\\Sound Bank.xsb" ); base.Initialize(); }
Notice that you need to load three files for your audio.
- An AudioEngine by passing in the name of the XACT project file with an .xgs extension.
- A WaveBank by passing in your AudioEngine and the name of the wave bank you created in the XACT project with an .xwb extension.
- A SoundBank by passing in your AudioEngine and the name of the sound bank you created in the XACT project with an .xsb extension.
Once you have done this, you're ready to add the code to play your sounds when game events happen!
Step 4: Play Sounds Using the Audio API
Sounds you wish to play in your game are accessed via a Cue object, which you can get by calling GetCue, or play directly by calling PlayCue.
In this tutorial, we do both. For our looping engine sound, we will call GetCue to get and hold the engine cue, and pause and resume the cue as our engines turn on and off when the user holds the trigger. When the player presses the A button to warp, we will play the hyperspace sound by calling PlayCue.
Find the UpdateInput method. Modify it to look like this:
// Cue so we can hang on to the sound of the engine. Cue engineSound = null; protected void UpdateInput() { // Get the game pad state. GamePadState currentState = GamePad.GetState( PlayerIndex.One ); if (currentState.IsConnected) { // Rotate the model using the left thumbstick, and scale it down. modelRotation -= currentState.ThumbSticks.Left.X * 0.10f; // Create some velocity if the right trigger is down. Vector3 modelVelocityAdd = Vector3.Zero; // Find out what direction we should be thrusting, using rotation. modelVelocityAdd.X = -(float)Math.Sin( modelRotation ); modelVelocityAdd.Z = -(float)Math.Cos( modelRotation ); // Now scale our direction by how hard the trigger is down. modelVelocityAdd *= currentState.Triggers.Right; // Finally, add this vector to our velocity. modelVelocity += modelVelocityAdd; GamePad.SetVibration( PlayerIndex.One, currentState.Triggers.Right, currentState.Triggers.Right ); // Set some audio based on whether we're pressing a trigger. if (currentState.Triggers.Right > 0) { if (engineSound == null) { engineSound = soundBank.GetCue( "engine_2" ); engineSound.Play(); } else if (engineSound.IsPaused) { engineSound.Resume(); } } else { if (engineSound != null && engineSound.IsPlaying) { engineSound.Pause(); } } // In case you get lost, press A to warp back to the center. if (currentState.Buttons.A == ButtonState.Pressed) { modelPosition = Vector3.Zero; modelVelocity = Vector3.Zero; modelRotation = 0.0f; // Make a sound when we warp. soundBank.PlayCue( "hyperspace_activate" ); } } }
Many things are happening here. Here's a breakdown of what we're doing.
// Cue so we can hang on to the sound of the engine. Cue engineSound = null;
The Cue represents an instance of a sound. In this case, this Cue will represent the sound of our engines when we hold the right trigger.
// Set some audio based on whether we're pressing a trigger. if (currentState.Triggers.Right > 0) { if (engineSound == null) { engineSound = soundBank.GetCue( "engine_2" ); engineSound.Play(); } else if (engineSound.IsPaused) { engineSound.Resume(); } } else { if (engineSound != null && engineSound.IsPlaying) { engineSound.Pause(); } }
This code manages the engine sound. Since we enter this code once every frame, we have to make sure we don't continually try to play the same sound. We only want to modify the state of the Cue if there's a change, such as the trigger being released after being held. This code uses GetCue the very first time through the loop to ready the Cue to play, and play it if the trigger is down.
From that point forward, each release of the trigger will call Pause and halt playback of the Cue. Subsequently holding the trigger again will call Resume, and playback will continue.
// In case you get lost, press A to warp back to the center. if (currentState.Buttons.A == ButtonState.Pressed) { modelPosition = Vector3.Zero; modelVelocity = Vector3.Zero; modelRotation = 0.0f; // Make a sound when we warp. soundBank.PlayCue( "hyperspace_activate" ); }
Finally, when the user presses the A button to warp, get and play a sound all at once using PlayCue. Since we don't need to stop or pause this sound, but can just let it play, there's no reason to hold onto the sound in a Cue object.
Congratulations!
At this point, you have a spaceship floating in 3D space, that moves around when you use your Xbox 360 controller, and makes sounds and gives you feedback in your controller. You have created the very beginnings of a 3D game using XNA Game Studio Express, and you're only just getting started. There's so much more to explore!
Ideas to Expand
If you're ready to go further with this sample, why not try a few of these ideas?
- Use some of the advanced runtime parameter control (RPC) functionality of XACT to change the volume and pitch of your engines as you change pressure on your right trigger. (For more information about how to do this, see XACT Audio Authoring.)
- Add some background music and try setting volumes using categories. (See How to: Change Sound Volume Levels.)
Finally...
At this point, you've been given many of the basic elements you need to build a game: graphics, input, and sound. Even so, you may be wondering, "How do I build a game?"
Games are an expressive process, with plenty of room for creative problem solving. There is truly no one right way to make a game. With the example we have created, there are still many missing elements. What does the ship interact with? Does it have a goal? What obstacles prevent the ship from reaching the goal?
Answering these questions will define your game, and make it your own. Play some games that inspire you, check out the XNA Team Blog, read up on the Programming Guide, explore the XNA Framework, and have fun building a game of your very own. We hope you enjoy XNA Game Studio Express!
The Complete Example (contents of Game1.cs)
#region Using Statements
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;
#endregion
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
ContentManager content;
public Game1()
{
graphics = new GraphicsDeviceManager( this );
content = new ContentManager( Services );
}
AudioEngine audioEngine;
WaveBank waveBank;
SoundBank soundBank;
protected override void Initialize()
{
audioEngine = new AudioEngine( "Content\\Audio\\MyGameAudio.xgs" );
waveBank = new WaveBank( audioEngine, "Content\\Audio\\Wave Bank.xwb" );
soundBank = new SoundBank( audioEngine, "Content\\Audio\\Sound Bank.xsb" );
base.Initialize();
}
// Specify the 3D model to draw.
Model myModel;
// The aspect ratio determines how to scale 3d to 2d projection.
float aspectRatio;
protected override void LoadGraphicsContent(bool loadAllContent)
{
if (loadAllContent)
{
myModel = content.Load<Model>("Content\\Models\\p1_wedge");
}
aspectRatio = graphics.GraphicsDevice.Viewport.Width /
graphics.GraphicsDevice.Viewport.Height;
}
protected override void UnloadGraphicsContent( bool unloadAllContent )
{
if (unloadAllContent == true)
{
content.Unload();
}
}
// Set the velocity of the model, applied each frame to the model's position.
Vector3 modelVelocity = Vector3.Zero;
protected override void Update( GameTime gameTime )
{
if (GamePad.GetState( PlayerIndex.One ).Buttons.Back == ButtonState.Pressed)
this.Exit();
// Get some input.
UpdateInput();
// Update audioEngine.
audioEngine.Update();
// Add velocity to the current position.
modelPosition += modelVelocity;
// Bleed off velocity over time.
modelVelocity *= 0.95f;
base.Update( gameTime );
}
// Cue so we can hang on to the sound of the engine.
Cue engineSound = null;
protected void UpdateInput()
{
// Get the game pad state.
GamePadState currentState = GamePad.GetState( PlayerIndex.One );
if (currentState.IsConnected)
{
// Rotate the model using the left thumbstick, and scale it down.
modelRotation -= currentState.ThumbSticks.Left.X * 0.10f;
// Create some velocity if the right trigger is down.
Vector3 modelVelocityAdd = Vector3.Zero;
// Find out what direction we should be thrusting, using rotation.
modelVelocityAdd.X = -(float)Math.Sin( modelRotation );
modelVelocityAdd.Z = -(float)Math.Cos( modelRotation );
// Now scale our direction by how hard the trigger is down.
modelVelocityAdd *= currentState.Triggers.Right;
// Finally, add this vector to our velocity.
modelVelocity += modelVelocityAdd;
GamePad.SetVibration( PlayerIndex.One, currentState.Triggers.Right,
currentState.Triggers.Right );
// Set some audio based on whether we're pressing a trigger.
if (currentState.Triggers.Right > 0)
{
if (engineSound == null)
{
engineSound = soundBank.GetCue( "engine_2" );
engineSound.Play();
}
else if (engineSound.IsPaused)
{
engineSound.Resume();
}
}
else
{
if (engineSound != null && engineSound.IsPlaying)
{
engineSound.Pause();
}
}
// In case you get lost, press A to warp back to the center.
if (currentState.Buttons.A == ButtonState.Pressed)
{
modelPosition = Vector3.Zero;
modelVelocity = Vector3.Zero;
modelRotation = 0.0f;
// Make a sound when we warp.
soundBank.PlayCue( "hyperspace_activate" );
}
}
}
// Set the position of the model in world space, and set the rotation.
Vector3 modelPosition = Vector3.Zero;
float modelRotation = 0.0f;
// Set the position of the camera in world space, for our view matrix.
Vector3 cameraPosition = new Vector3( 0.0f, 50.0f, -5000.0f );
protected override void Draw( GameTime gameTime )
{
graphics.GraphicsDevice.Clear( Color.CornflowerBlue );
// Copy any parent transforms.
Matrix[] transforms = new Matrix[myModel.Bones.Count];
myModel.CopyAbsoluteBoneTransformsTo( transforms );
// Draw the model. A model can have multiple meshes, so loop.
foreach (ModelMesh mesh in myModel.Meshes)
{
// This is where the mesh orientation is set, as well as our camera and projection.
foreach (BasicEffect effect in mesh.Effects)
{
effect.EnableDefaultLighting();
effect.World = transforms[mesh.ParentBone.Index] * Matrix.CreateRotationY( modelRotation )
* Matrix.CreateTranslation( modelPosition );
effect.View = Matrix.CreateLookAt( cameraPosition, Vector3.Zero, Vector3.Up );
effect.Projection = Matrix.CreatePerspectiveFieldOfView( MathHelper.ToRadians( 45.0f ),
aspectRatio, 1.0f, 10000.0f );
}
// Draw the mesh, using the effects set above.
mesh.Draw();
}
base.Draw( gameTime );
}
}