I like DirectSound. Compared to its cousin Direct3D, it’s a straightforward API. Certainly, the more advanced features – such as the support for three-dimensional sound effects – require you to set up a few things and make more than a couple method calls, but just to get a sound playing is actually quite easy. There are five steps:
Create a Device object
Set a cooperative level
Create a Buffer object
Load the Buffer with sound data
Play the Buffer
Let’s examine each of these steps in detail.
The Microsoft.DirectX.DirectSound.Device object – which lives in the Microsoft.DirectX.DirectSound assembly, so you’ll need to add a reference to that – is the class that models the sound hardware in your system. It’s important to remember that, because it means that when you change the properties of this object, you’re affecting how all of the sounds that run through DirectSound in your application will behave. These sorts of “global parameters” may seem a bit weird to those of us that are used to the type of APIs usually used in information processing systems, but you see this pattern throughout the DirectX APIs.
Creating a Device object is quite simple: we simply “new” one up, like so:
// private field to hold the Device for later use private Device device; privatevoid CreateDevice() { device = new Device(); }
Notice that we’ve also created a private field to hold a reference to the object we create so we can use it later. We’ll also want a using statement at the top of the file to make sure that the compiler knows we’re talking about the DirectSound Device object, and not some other sort of Device (say,Microsoft.DirectX.Direct3D.Device). Like so:
using Microsoft.DirectX.DirectSound;
Every application that makes use of DirectSound must set a cooperative level. This is simply a flag that tells DirectX how much control we’re going to take of the sound hardware. We have three options that range from “I’m going to control absolutely everything – no one else on this machine gets access to sound functionality” to “I’ll play really nicely with others – only give me limited control when I’m in the foreground.”
Generally speaking, we’ll want the option that falls between these two extremes. It’s represented by the flag CooperativeLevel.Priority. This flag is a member of the CooperativeLevel enumeration, whose values are listed in the table below.
I’ll talk a bit more about the WritePrimary cooperative level when we discuss buffers in a minute. For now, it’s enough to note that we’ll generally choose the Priority level, which still lets other applications play sounds.
Here’s the code that sets the cooperative level:
private void SetCooperativeLevel() { device.SetCooperativeLevel( this, // The window for the application CooperativeLevel.Priority // The cooperative level ); }
It’s pretty straightforward: we simply call SetCooperativeLevel on the Device, passing in the cooperative level we’d like set. There is one small twist, though. Notice that I’m passing in the this reference. If you look at the complete code for our application, listed at the end of this article, you’ll see that I’m writing a Windows Forms application, and that the this reference actually refers to a Form object.
In essence, we’re telling DirectSound which window we’re associated with. This is important because DirectSound needs to integrate with Windows. Among other things, if we’ve set a cooperative level lower than CooperativeLevel.WritePrimary, DirectSound needs to know when our application loses focus so it can give up control of the sound hardware in case another application needs it.
All sounds in DirectSound are contained in buffers. These buffers can live in the main memory of the computer, or they can live in the memory on the sound card. They can hold data of various formats. And of course they can play the data they hold back through your sound hardware.
There are actually two different types of buffers in DirectSound: primary buffers and secondary buffers. The primary buffer is the buffer that represents the sound the hardware is about to play. There is only one primary buffer. Secondary buffers are all the other buffers. Sound in the buffer has already been mixed together from all the other secondary buffers, had effects applied, etc. etc.
Since DirectSound is more than happy to take care of managing the primary buffer for you – applying effects or mixing in all the secondary buffers you have playing – there’s generally no need for applications to interact with it. In fact, writing data into the primary buffer is a lot harder than just working with secondary buffers, since you have to take into account all the other activity going on that might be affecting it. If you do need to modify the contents of the primary buffer, you’ll need to specify theWritePrimary cooperative level. But you’ll generally only do this if you have advanced needs that dictate having full control over the sound hardware; CooperativeLevel.Priority is good enough for most applications.
Both types of buffers in DirectSound are modeled by the Microsoft.DirectX.DirectSound.Buffer class. This class has methods and properties that let you start and stop playing, specify whether you’d like a primary or secondary buffer, modify the data in the buffer, and much more.
The easiest way to work with a Buffer is to simply use the constructor that takes a filename as an argument. This particular constructor both initializes a new Buffer object and populates it with the sound found in the file you name. The file must contain a sound in .WAV format - MP3 and other file formats are not supported out of the box. Here’s the code:
// Avoid name collision with System.Buffer using Buffer = Microsoft.DirectX.DirectSound.Buffer; // Hold onto the buffer for later use private Buffer buffer; privatevoid CreateBuffer() { buffer = new Buffer( "explosion.wav", // Filename to load device // Device to use ); }
Note that we simply give it the name of a .WAV file and a reference to the Device, and DirectSound takes care of the rest: allocating a buffer somewhere in memory, figuring out what format the sound in the file is, and loading that file into the buffer.
One other thing to point out is a little annoyance with an easy workaround. If you were to try to compile code with a reference to the DirectSound Buffer object in it, you’d confuse the compiler. That’s because there’s a class called System.Buffer as well, and the compiler doesn’t know which one to use. Luckily, we can fix this with the following line of code:
// Avoid name collision with System.Buffer using Buffer = Microsoft.DirectX.DirectSound.Buffer;
This tells the compiler that when we say Buffer, we mean the DirectSound one. This turns out to be convenient elsewhere, since (for example) Direct3D also has a class called Device.
At this point, we’re almost done – all that’s left is to actually play the sound.
Playing the buffer is even easier than creating it: we simply call Play on the buffer object. Here’s the code:
private void button1_Click(object sender, System.EventArgs e) { buffer.Play( 0, // Sound priority BufferPlayFlags.Default // Play flags ); }
I’ve put the call to Play in a handler for a button, so that the sound will play when I click it. The call to Play takes two parameters: a priority and a set of one or more flags. The priority is used to sort out which sounds should get dropped if too much is going on, but passing anything other than zero here is an advanced operation that we’ll cover another time.
The second parameter is one or more flags that control how the sound is played. Most of these are a bit beyond us at this stage, but one bears mentioning: BufferPlayFlags.Looping. As you can probably guess, setting this flag will make the buffer play over and over again, until you call Buffer.Stop.
The call to Play returns immediately – it doesn’t wait for the buffer to finish playing. This is a good thing, as sounds can easily be several seconds long. As I mentioned, you can call Buffer.Stop to stop the sound from playing any time you like. However, what happens if you call Play again before it has finished playing the first time? Or if it’s looping?
You might guess that the sound hardware would start a second copy of the sound, keeping the first one running. But that’s not what happens. It turns out that what Playreally means is “put this buffer into the ‘play’ mode, starting from its current position.” Since the buffer is already in the ‘play’ mode, calling Play a second time does nothing. Try running the application I’ve provided at the end of this article. Click the “Play Sound” button several times in rapid succession – you should hear only one copy of the sound playing at a time. If you want to have the same sound playing more than once simultaneously, you’ll need a second Buffer with the same data in it.
Understanding that a Buffer has a concept of a current position helps us understand the behavior of Stop as well. If you add a button to the form that calls Stop on the buffer while it’s playing, and then call Play again, you’ll notice that the buffer plays from the point where it was stopped. This fits well with our model of how a Buffer works. The only other thing we need to remember is that DirectSound conveniently “rewinds” the buffer if it is allowed to play all the way to the end, so we don’t have to remember to change the current position (called the play cursor) before calling Play again.
The basics of DirectSound are fairly simple. The absolute minimum required is to create a Device, set its cooperative level, load a sound into a new Buffer, and then call Play on that Buffer. There’s much more to DirectSound, obviously, but I like that we can actually get stuff coming out of the speakers with so little code. Next time, we’ll talk about how to control some of the playing parameters, like volume and pan.
I’ve included the code for a complete application below. In order for it to work, you’ll either need to change the name of the .WAV file the application tries to load, or you’ll need to drop explosion.wav into the directory where the application is run from. You can download explosion.wav here.
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 field to hold the Device for later use private Device device; private System.Windows.Forms.Button button1; private Buffer buffer; public Game() { InitializeComponent(); // Set up DirectSound CreateDevice(); // Load the sound CreateBuffer(); // Set the cooperative level SetCooperativeLevel(); } private void CreateDevice() { device = new Device(); } private void CreateBuffer() { // There are 8 overloads buffer = new Buffer( "explosion.wav", // Filename to load device // Device to use ); } 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.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); // // 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) { buffer.Play( 0, // Sound priority BufferPlayFlags.Default // Play flags ); } } }