7 min read

3D Audio Effects: Location

Overview

This tutorial, and the one immediately following it, will discuss some of the basics of 3D audio effects. We will start this tutorial with a brief look at what 3D audio effects are, and when we might want to use them. We will then spend the rest of this tutorial looking at how to perform a simple 3D audio effect. We will make our sound cue seem like it is coming from a point in space.

If you haven’t done anything with XACT yet, you should probably go through the tutorials on Using XACT and Playing Sound. Also, when I made this tutorial, I used the information in the XACT sound loops tutorial. You won’t need this, but it might come in handy anyway.

3D Audio Effects

A 3D audio effect is simply altering how your sound is played, to give the illusion that they are happening in 3D space. In other words, an explosion isn’t just an explosion in the game, but the player can tell where the explosion happened.

3D audio effects help players feel much more involved in the game. They feel like the sound is coming from all around them, rather than from just the computer screen.

3D audio effects also give players an indication of where events are taking place. Imagine you are playing a first-person shooter, and someone is shooting at you. Without 3D effects, you would be able to hear that you are being shot at. With 3D audio effects though, you could tell where the other player is, and you would know which way to turn to find them.

One of the coolest examples that I’ve seen of what 3D audio effects can do is the Virtual Barbershop. I’ve included the “video” below. It works best if you use headphones, and if you close your eyes.Give it a try!

Setup

In a minute we will discuss how to write the code that does 3D audio effects. You can perform these effects with any sound cue. I felt like it would be a good idea to explain what I used for this tutorial, though.

In order to do this, you will need to already have an XACT project ready to use. (And it must have at least one sound cue!) If you haven’t done this yet, take a look at the tutorial about using XACT.

I’ve picked a sound effect that is just a helicopter that can loop. I wanted it to loop so that I could have the sound keep going and going (and sound nice) while the source of the sound moved around. I found the sound effect at flashkit.com. That should link to the page that has it on it, but if the link ever gets broken, you should be able to find it again by searching their ‘Sound FX’ page for ‘helicopter loop’. Also, I’ve set up my helicopter cue to loop forever, as discussed in the sound loops tutorial.

Finally, I’ve already written the code to play my helicopter sound without any 3D audio effects, as discussed in the Playing Sound tutorial.

Coding the Effect

The first thing that we need to do is get a reference to our sound cue. This means that we will also want to tell our sound cue to play in a slightly different way. So first, let’s add the following line as an instance variable to our game:

private Cue cue;

Now go to the place where you told the sound to play. We will want to change this. The code used to say:

soundBank.PlayCue("helicopter");

We want to change this to:

cue = soundBank.GetCue("helicopter");
cue.Play();

At this point, you should be able to run your game again, and it should all work as it did before.

So now let’s move on to the next step: telling our game where the sound is being emitted from, and where the listener is located at. I’m going to set up my game so that the source of the sound moves in a circle around the listener. So to do this, the first thing I’m going to do is add the following code as instance variables to the main game class:

private AudioEmitter emitter = new AudioEmitter();
private AudioListener listener = new AudioListener();

private float angle = 0;
private float distance = 5;

The AudioEmitter class will tell us information about the emitter. In this tutorial, we will only look at the emitter’s location, but there are several other options available that you can experiment with. The AudioListener class will specify similar information about where the listener (player) is located at. The angle and distance variables will be used to calculate the location of the emitter as it spins around the listener.

Next, I’ve created a method that will convert the emitter’s angle and distance into a Vector3 location in 3D space, as shown below:

private Vector3 CalculateLocation(float angle, float distance)
{
    return new Vector3(
        (float)Math.Cos(angle) * distance,
        0,
        (float)Math.Sin(angle) * distance);
}

This method basically converts polar coordinates into cartesian coordinates (which if you’re interested in knowing a bit more about, see the math tutorial on 3D coordinate systems.

Next I’m going to add the following code to my Update() method. This is the code that will change the location of my audio emitter, and also tell the cue to apply the new 3D audio effect:

angle += 0.01f;  // rotate the emitter around a little bit
listener.Position = Vector3.Zero;  // the listener just stays at the origin the whole time
emitter.Position = CalculateLocation(angle, distance);  // calculate the location of the emitter again

        cue.Apply3D(listener, emitter); // apply the 3D transform to the cue

This should do everything that we need it to do. We have only one other change left. If you want to apply a 3D effect to a Cue object, you have to have told it to apply a 3D effect at some point before you tell the cue to play. It gets the cue to be initialized in a different way. So be sure to add something like the code below, just before you tell the cue to start playing initially:

// tell it to apply a 3D transform even before you play the cue
cue.Apply3D(listener, emitter);

// and then you can tell it to start playing
cue.Play();

With all of this, you should now be able to play your project and hear your sound moving around! I’ve added all of the code for my project below, because I realize this is kind of a complicated tutorial. It brings in things from multiple other tutorials, add adds new stuff on.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

namespace SoundDemo
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        private AudioEngine audioEngine;
        private WaveBank waveBank;
        private SoundBank soundBank;

        private Cue cue;

        private AudioEmitter emitter = new AudioEmitter();
        private AudioListener listener = new AudioListener();

        private float angle = 0;
        private float distance = 5;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            LoadAudioContent();
        }

        private void LoadAudioContent()
        {
            audioEngine = new AudioEngine("Content/Sound/SoundDemoAudio.xgs");
            waveBank = new WaveBank(audioEngine, "Content/Sound/Wave Bank.xwb");
            soundBank = new SoundBank(audioEngine, "Content/Sound/Sound Bank.xsb");

            cue = soundBank.GetCue("helicopter");

            // tell it to apply a 3D transform even before you play the cue
            cue.Apply3D(listener, emitter);

            // and then you can tell it to start playing
            cue.Play();
        }

        private Vector3 CalculateLocation(float angle, float distance)
        {
            return new Vector3(
                (float)Math.Cos(angle) * distance,
                0,
                (float)Math.Sin(angle) * distance);
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            angle += 0.01f;  // rotate the emitter around a little bit
            listener.Position = Vector3.Zero;  // the listener just stays at the origin the whole time
            emitter.Position = CalculateLocation(angle, distance);  // calculate the location of the emitter again

            cue.Apply3D(listener, emitter); // apply the 3D transform to the cue

            audioEngine.Update();

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here

            base.Draw(gameTime);
        }
    }
}