03 Three-Dimensional Sound

Last time, we talked about the part of DirectSound that lets you set things like volume, frequency, and pan. While these are useful things, they’re also a bit low-level to be generally useful. For example, if you’re writing a tank game and you want to produce the sound of a fighter jet screaming by overhead, the last thing you want to do is figure out all the math necessary to make the sound shift from left to right as appropriate. Not to mention changing the frequency to reflect the Doppler shift (Doppler shift is the change in frequency from high to low that occurs when a fast moving object passes by you).

DirectSound takes care of you on this count. In fact, it goes one better: not only will it figure out things like Doppler shift and left-right balance automatically, but on systems where rear speakers are present, it will even automatically shift sounds from the front to the back as appropriate. What this means is that the people playing your game will hear the footsteps behind them when the bad guy is sneaking up…very cool!

The basics of playing a three-dimensional sound are fairly straightforward. We simply create a Buffer, then additionally create a Buffer3D object on top of it. There are only two caveats: one, you must remember to specify the Control3D flag when you create the Buffer; two, if you want to get three-dimensional sound processing done for you, you must work with mono sounds. This latter constraint makes sense, as stereo sounds already have left/right assumptions baked in that inhibit DirectSound’s ability to calculate left/right and front/rear pan automatically.

Here’s the code that creates a Buffer with three-dimensional capabilities:

private Buffer buffer; private Buffer3D buffer3D; private void Create3DBuffer() { // Tell DirectSound we need 3D control BufferDescription desc = new BufferDescription(); desc.Control3D = true; 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; }

You can see that I specified my desire to have 3D capabilities by passing in a BufferDescription object to the constructor of Buffer, where I’ve set the Control3D flag to be true. Then I created a new Buffer3D object, passing in the Buffer for which I’d like to specify three-dimensional properties.

It’s a little bit weird that there are two objects involved – you’d think that there’d just be one Buffer3D class that would inherit from Buffer. As near as I can tell, it’s done this way as a consequence of the fact that Managed DirectSound is really just a wrapper around the underlying COM-based DirectSound API. Since COM deals in interfaces and not objects, things get a little weird when mapping between the two. Regardless of the reason, though, this is how it works, so we just have to get used to it.

The last line in my Create3DBuffer method sets the buffer’s 3D positioning mode. I’d like to leave discussion of this property until we talk about Listeners in a future tutorial. For now, let’s just set it to Normal and ignore it.

Now that we’ve created a Buffer with 3D capabilities, how do we play it? As it turns out, that part hasn’t changed: we still call Play on the Buffer object. Not on the Buffer3D object, on the Buffer object. We use the Buffer3D object to control the sound’s position, via its Position property. It goes something like this:

private void PlayPositionalSound(float x, float y, float z) { buffer3D.Position = new Vector3(x, y, z); buffer.Play(0, BufferPlayFlags.Default); }

Given an x, y, and z position, this method will play our sound as if it were coming from that location – in front of, behind, to the side, or even above/below the listener! Note that the position actually has to take the form of a Vector3, which we construct from the x, y, and z that are passed in to this method.

One thing you should understand about coordinates in DirectSound is that they use what’s called a left-handed coordinate system. In this particular system, x represents left and right, y represents up and down, and z represents in front/behind as described in this table:

This positioning model will get you a fair way down the road, and may even be completely adequate for the needs of your application, but since it assumes the listener is located at position 0, 0, 0, it might not be appropriate in all situations. We’ll talk about how to deal with this next time.

I’ve included sample code below that allows you to play a sound at an arbitrary position in space by typing in coordinates into the text boxes and hitting the “Play Sound” button. You’ll need chop.wav in the directory you compile the program to, or else you can change the code to play a sound of your own choosing.

The Code

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 Candera.DirectSound { public class Game : System.Windows.Forms.Form { private Device device; private System.Windows.Forms.Button button1; private Buffer buffer; private System.Windows.Forms.Label label3; private System.Windows.Forms.Label label2; private System.Windows.Forms.Label label1; private System.Windows.Forms.TextBox tbZ; private System.Windows.Forms.TextBox tbY; private System.Windows.Forms.TextBox tbX; private Buffer3D buffer3D; public Game() { InitializeComponent(); // Set up DirectSound CreateDevice(); // Create the 3D Buffer Create3DBuffer(); // 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; 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 ); } #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.button1 = new System.Windows.Forms.Button(); this.label3 = new System.Windows.Forms.Label(); this.label2 = new System.Windows.Forms.Label(); this.label1 = new System.Windows.Forms.Label(); this.tbZ = new System.Windows.Forms.TextBox(); this.tbY = new System.Windows.Forms.TextBox(); this.tbX = new System.Windows.Forms.TextBox(); this.SuspendLayout(); // // button1 // this.button1.Location = new System.Drawing.Point(48, 48); this.button1.Name = "button1"; this.button1.Size = new System.Drawing.Size(208, 23); this.button1.TabIndex = 0; this.button1.Text = "Play Sound"; this.button1.Click += new System.EventHandler(this.button1_Click); // // label3 // this.label3.Location = new System.Drawing.Point(40, 157); this.label3.Name = "label3"; this.label3.TabIndex = 12; this.label3.Text = "Z:"; this.label3.TextAlign = System.Drawing.ContentAlignment.MiddleRight; // // label2 // this.label2.Location = new System.Drawing.Point(40, 122); this.label2.Name = "label2"; this.label2.TabIndex = 11; this.label2.Text = "Y:"; this.label2.TextAlign = System.Drawing.ContentAlignment.MiddleRight; // // label1 // this.label1.Location = new System.Drawing.Point(40, 93); this.label1.Name = "label1"; this.label1.TabIndex = 10; this.label1.Text = "X:"; this.label1.TextAlign = System.Drawing.ContentAlignment.MiddleRight; // // tbZ // this.tbZ.Location = new System.Drawing.Point(152, 157); this.tbZ.Name = "tbZ"; this.tbZ.TabIndex = 9; this.tbZ.Text = "0"; // // tbY // this.tbY.Location = new System.Drawing.Point(152, 125); this.tbY.Name = "tbY"; this.tbY.TabIndex = 8; this.tbY.Text = "0"; // // tbX // this.tbX.Location = new System.Drawing.Point(152, 93); this.tbX.Name = "tbX"; this.tbX.TabIndex = 7; this.tbX.Text = "0"; // // Game // this.AutoScaleBaseSize = new System.Drawing.Size(5, 13); this.ClientSize = new System.Drawing.Size(292, 273); this.Controls.Add(this.label3); this.Controls.Add(this.label2); this.Controls.Add(this.label1); this.Controls.Add(this.tbZ); this.Controls.Add(this.tbY); this.Controls.Add(this.tbX); 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) { buffer3D.Mode = Mode3D.Normal; float x = float.Parse(tbX.Text); float y = float.Parse(tbY.Text); float z = float.Parse(tbZ.Text); buffer3D.Position = new Vector3(x, y, z); buffer.Play(0, BufferPlayFlags.Looping); } } }