Last time, we talked about the basics of creating three-dimensional sound using the Buffer3D class in DirectSound. While this was cool stuff, it left us slightly wanting in a few details. For example, although I mentioned that DirectSound will calculate Doppler shift for us (the high-to-low frequency shift you hear as a sound passes you by), I never actually said how we get it. I also said that DirectSound supports some more advanced positioning features – we’ll talk about those, too.
All of these advanced features rely on us properly setting up a DirectSound Listener3D object. The Listener3D object, as you might guess, models the position, velocity, and other properties of, well, the listener. As a result, it makes our lives as programmers easier: the model is much cleaner if we can say, “The player in my game is located at position (23, 17, 19), he’s facing northeast, and there’s a machine gun firing at position (4, 22, 91).” If we had to do things relying solely on the Buffer3D properties we looked at last time, it would be our responsibility to turn that machine gun position from its location in our game coordinate system into something relative to the position and orientation of the virtual “ears” in the scene.
The Listener3D object is easy enough to create. It goes something like this:
private Listener3D listener; private void CreateListener() { Buffer primary = GetPrimaryBuffer(); listener = new Listener3D(primary); }
As you can see, there’s a simple constructor for Listener3D that takes a reference to the primary DirectSound buffer, and no other arguments. You might recall from our earlier discussion that the primary buffer represents the sound card memory where everything gets mixed down before being sent to the speakers. There is only one primary buffer, where there might be many secondary buffers. I’ve written a simple function called GetPrimaryBuffer to help me retrieve a reference to the primary buffer. Here it is:
private Buffer primary; private Buffer GetPrimaryBuffer() { if (primary != null) { return primary; } BufferDescription description = new BufferDescription(); description.PrimaryBuffer = true; description.Control3D = true; primary = new Buffer(description, device); return primary; }
You can see that it looks just about like all of our other buffer creation routines, with the exception that I’ve specified a true value for the PrimaryBuffer field in the BufferDescription. I’ve also added some simple caching – since there’s only one primary buffer, we may as well save the overhead of trying to create it more than once.
With the Listener3D object created, we can now start to work with the model that DirectSound gives us. At the very least, this means setting up four vectors: position, velocity, front, and up.
The position vector means about what you’d think. This specifies where in your game world the notional listener should be located. While the coordinate system you use is technically up to you, you’ll make life easier on yourself if you use left-handed coordinates (as discussed last time), where one unit equals one meter. I’ll show you how to use other scales shortly, but the defaults assume these, so we’ll stick with them for now.
The velocity vector is nearly as straightforward. It specifies how fast the listener is traveling, and in what direction. This is used by DirectSound to calculate the Doppler shift necessary for more realistic sound. The Doppler shift is the change in frequency that happens to sounds when they are moving closer or farther relative to a listener. Imagine the sound of a train going by, or a jet zooming overhead – the change in frequency you hear is Doppler shift, and you get it in your DirectSound application for the price of merely setting a velocity vector.
As it turns out, Buffer3D objects also have a velocity. This is convenient, as sometimes you’ll want to have several sounds moving in different directions relative to a stationary or moving listener, and having velocity available on both listeners and buffers means we can set it wherever it makes sense.
There is a bit of subtlety associated with both types of velocity vectors. The gotcha is that DirectSound does not update the position of either the Listener3D or the Buffer3D based on the velocity: that’s entirely up to you. While disappointing, this is the way it is.
The last two vectors that need to be set on the Listener3D are related. The top and front vectors define an orientation for this listener. This is important: if your listener is facing east, sounds should be located 90 degrees differently from if your listener is facing north. And if they’re lying on their side, what comes out of the left or right speaker will be very different indeed from a vertically oriented listener.
This picture that I took from the DirectSound documentation might help. It shows the top vector as an arrow point out the top of the listener’s head, and the front vector as an arrow that defines the direction the listener is looking:
Specifying these two vectors is enough to uniquely identify the listener’s orientation in three-dimensional space.
So now that we’ve defined our terms, let’s look at some code:
private void SetupListener(Vector3 pos, Vector3 vel, Vector3 front, Vector3 top) { // Don't remix after every setting change listener.Deferred = true; // Set the position and velocity listener.Position = pos; listener.Velocity = vel; // Set the orientation Listener3DOrientation orientation = listener.Orientation; orientation.Front = front; orientation.Top = top; listener.Orientation = orientation; // Go ahead and remix now listener.CommitDeferredSettings(); }
As you can see, we’ve defined a function that accepts the four vectors and shoves them into the appropriate slots on the Listener3D object. The position and velocity vectors are stored directly, but the top and front vectors are set via the Orientation property, whose only purpose is to group those two vectors together.
You’ll notice that I’ve bracketed the function with the following lines of code:
listener.Deferred = true; // Set up the listener parameters here listener.CommitDeferredSettings();
The reason I’ve done this is that changing listener settings is a very heavyweight thing to do. For example, if I turn the listener to the right 90 degrees, every single sound that is now playing has to have its left/right channels adjusted. This causes a remix of every buffer into the primary buffer, and it’s expensive. Because it’s expensive, we’d like to avoid doing it after we set the position, and after we set the velocity, and after we set the orientation.
These two lines of code do exactly that. By setting the Deferred property to true, we tell DirectSound to buffer the changes until we call CommitDeferredSettings. Nice and efficient, the way we like it.
Great! That’s the basics of setting up a listener. From here, you can adjust the position and velocity of the Listener3D and the Buffer3D objects in real time as your simulation or game evolves, and you’ll hear the front/back/left/right/etc. channels update as appropriate. You’ll even get Doppler shift as things move relative to the listener. There are just a few more details I’d like to cover.
A couple of things that I noticed when I was playing around with these APIs was that the Doppler shift was not really pronounced enough for me, and that sounds didn’t get quiet fast enough as they moved away. I wanted to exaggerate these effects. DirectSound uses a fairly realistic model that simulates how sound behaves in the real world, but of course games sometimes want more real than real.
If you look at the Listener3D class, you’ll see some properties that can help us out: the distance factor, the Doppler factor, and the rolloff factor. These are multipliers that affect the way the various effects in DirectSound work.
The distance factor is a global setting that controls the way vectors are interpreted. It is the number of meters per vector unit. By default it has a value of 1.0. If you set this number to 2.0, for example, a sound at (1,0,0) will be assumed by DirectSound to be two meters from a listener at (0,0,0).
The Doppler factor is a multiplier that controls how pronounced the Doppler effect is. This value can range from 0 (no Doppler effect) to 10 (ten times the real world effect).
The rolloff factor controls how quickly sounds get quieter as they move away. If you set this value to zero (the smallest allowed), sounds will remain the same volume no matter how far away they are. At 10.0 (the largest allowable value), sounds will very quickly drop in volume as they move away. This is useful, for example, if you wanted to model a mosquito: in your ear it’s loud, but only a few meters away you can’t hear it at all.
Rolloff factor is closely related to the Buffer3D.MinDistance and MaxDistance properties.MinDistance is the distance at which the sound is at full volume. It doesn’t get louder when it gets closer than this. MaxDistance is the distance past which the sound no longer gets quieter. Its default is one billion meters, which is basically infinity. Vary these numbers along with rolloff factor to get realistic distance-based fading for the particular sound you’re working with.
Here’s the code that makes use of this stuff:
private void SetupGlobalSoundParams(float distanceFactor, float dopplerFactor, float rolloffFactor) { // Don't remix after every setting change listener.Deferred = true; // Retrieve all the settings in one go Listener3DSettings settings = listener.AllParameters; // Meters per unit vector (default 1) // Note: affects velocity only settings.DistanceFactor = distanceFactor; // How pronounced the Doppler shift is (default 1) settings.DopplerFactor = dopplerFactor; // How quickly sounds fade with distance (default 1) settings.RolloffFactor = rolloffFactor; // Put the updated settings back listener.AllParameters = settings; // Go ahead and remix now listener.CommitDeferredSettings(); }
Again, I’ve set up deferred processing with the Deferred property and the CommitDeferredSettings method. Notice also that I change settings by retrieving AllParameters, making changes, and then writing back to AllParameters. This allows me to change just the ones I want, keeping all other settings the same.
As always, I’ve included a sample app below demonstrating the concepts we’ve covered. Grab the code and play around. You’ll need chop.wav for this one – it sets up a helicopter orbiting around the listener. You won’t really hear the effect very well unless you have rear speakers.
Well, I think that about wraps it up for the DirectSound series for right now. You’ve got the tools to do some pretty sophisticated three-dimensional sound processing at this point. I rather like the DirectSound API – it’s clean, it’s simple, and it’s fairly easy. But I think it’s time for me to go back and do more Direct3D research – it’s a lot messier over there, and as a result it needs more explaining.
Please do contact me if you have any questions. I’ll do my best to answer them, although it may take me a few days to get back to you. And I always enjoy hearing from people who just want to let me know they liked (or didn’t like!) these articles. Enjoy!
Alert reader Rob Diaz contacted me with a question about why, when he looks at the values for listener.Orientation, he doesn't seem to see the values he set for listener.Orientation.Front and listener.Orientation.Top. Well, I poked around a bit, and it looks like - for whatever reason - DirectSound doesn't report back the current values. It just always tells you the same vectors ({1, 0, 0} for Front and {0, 0, 0} for Top on my machine).
The thing is, it still sets it correctly. I know this, because by spining the listener while keeping the sound stationary, I can still hear a sound move around me in a circle. So, while this makes debugging a little harder, it doesn't affect functionality.
using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; using Microsoft.DirectX; using Microsoft.DirectX.DirectSound; // Avoid name collision with System.Buffer using Buffer = Microsoft.DirectX.DirectSound.Buffer; namespace WinDev.Candera.DirectSound { public class Game : System.Windows.Forms.Form { private System.ComponentModel.IContainer components; private Device device; private System.Windows.Forms.Button button1; private Buffer buffer; private Buffer3D buffer3D; private System.Windows.Forms.Timer timer1; private Buffer primary; private Listener3D listener; public Game() { InitializeComponent(); // Set up DirectSound CreateDevice(); // Create the 3D Buffer Create3DBuffer(); // Create the listener CreateListener(); // Set up where the listener is SetupListener( new Vector3(), // Located at the origin new Vector3(), // Not moving new Vector3(0, 0, 1), // Facing forward new Vector3(0, 1, 0) // Standing upright ); // Set the doppler, distance, and rolloff factors SetupGlobalSoundParams(1, 1, 1); // Set the cooperative level SetCooperativeLevel(); } private void CreateDevice() { device = new Device(); } private void Create3DBuffer() { // Tell DirectSound we need 3D control BufferDescription desc = new BufferDescription(); desc.Control3D = true; // There are 8 overloads – here’s another one buffer = new Buffer( "chop.wav", // Filename to load desc, // Buffer description device // Device to use ); // Initialize from existing secondary buffer buffer3D = new Buffer3D(buffer); buffer3D.Mode = Mode3D.Normal; } private void SetCooperativeLevel() { device.SetCooperativeLevel( this, // The window for the application CooperativeLevel.Priority // The cooperative level ); } private void CreateListener() { Buffer primary = GetPrimaryBuffer(); listener = new Listener3D(primary); } private Buffer GetPrimaryBuffer() { if (primary != null) { return primary; } BufferDescription description = new BufferDescription(); description.PrimaryBuffer = true; description.Control3D = true; primary = new Buffer(description, device); return primary; } private void SetupListener(Vector3 pos, Vector3 vel, Vector3 front, Vector3 top) { // Don't remix after every setting change listener.Deferred = true; // Set the position and velocity listener.Position = pos; listener.Velocity = vel; // Set the orientation Listener3DOrientation orientation = listener.Orientation; orientation.Front = front; orientation.Top = top; listener.Orientation = orientation; // Go ahead and remix now listener.CommitDeferredSettings(); } private void SetupGlobalSoundParams(float distanceFactor, float dopplerFactor, float rolloffFactor) { // Don't remix after every setting change listener.Deferred = true; // Retrieve all the settings in one go Listener3DSettings settings = listener.AllParameters; // Meters per unit vector (default 1) // Note: affects velocity only settings.DistanceFactor = distanceFactor; // How pronounced the Doppler shift is (default 1) settings.DopplerFactor = dopplerFactor; // How quickly sounds fade with distance (default 1) settings.RolloffFactor = rolloffFactor; // Put the updated settings back listener.AllParameters = settings; // Go ahead and remix now listener.CommitDeferredSettings(); } #region Windows Form Designer generated code ///<summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. ///</summary> private void InitializeComponent() { this.components = new System.ComponentModel.Container(); this.button1 = new System.Windows.Forms.Button(); this.timer1 = new System.Windows.Forms.Timer(this.components); this.SuspendLayout(); // // button1 // this.button1.Font = new System.Drawing.Font("Microsoft Sans Serif", 36F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((System.Byte)(0))); this.button1.Location = new System.Drawing.Point(24, 24); this.button1.Name = "button1"; this.button1.Size = new System.Drawing.Size(248, 224); this.button1.TabIndex = 0; this.button1.Text = "Go"; this.button1.Click += new System.EventHandler(this.button1_Click); // // timer1 // this.timer1.Tick += new System.EventHandler(this.timer1_Tick); // // Game // this.AutoScaleBaseSize = new System.Drawing.Size(5, 13); this.ClientSize = new System.Drawing.Size(292, 273); this.Controls.Add(this.button1); this.Name = "Game"; this.Text = "Game"; this.ResumeLayout(false); } #endregion public static void Main() { Game g = new Game(); g.Show(); Application.Run(g); } private void button1_Click(object sender, System.EventArgs e) { timer1.Enabled = true; buffer.Play(0, BufferPlayFlags.Looping); } private void timer1_Tick(object sender, System.EventArgs e) { int t = Environment.TickCount; buffer3D.Position = new Vector3( (float) Math.Sin(t / 700.0), 0, (float) Math.Cos(t / 700.0) ); buffer3D.Velocity = new Vector3( (float) Math.Cos(t / 700.0), 0, -(float) Math.Sin(t / 700.0) ); } } }