02 Controlling Sounds - Pan, Volume, Frequency

Last time, we talked about how to initialize DirectSound and play a buffer. Not a bad start, but it doesn’t offer a whole lot of variety – the sound plays like it was recorded, whether that’s loud or soft, left or right, fast or slow. So I’d like to talk about how to take a little more control.

We’re going to look at controlling three different aspects of sound playback: volume, pan, and frequency. The programming model for all three of these parameters is exactly the same, and is dead simple: we simply set the appropriate property on the Buffer object, like so:

private void SetupSound(int pan, int volume, int frequency) { buffer.Pan = pan; buffer.Volume = volume; buffer.Frequency = frequency; }

As you can see, each of Pan, Volume, and Frequency are set to an int value. I’ll cover what the ranges of these values are when we discuss each of the properties in turn. Let’s do that now.

Volume is the most obvious one, and it means pretty much what you’d expect. About the only surprise around the way we control volume in DirectSound is that there’s really no simple way to make the sound louder. We can make it quieter, but not louder. So if you want to control the volume of the sounds you play back, it’s important to make sure you record them at least as loud as the loudest you want to play it back. The Volume property can be set in a range from 0 (full volume) to -10,000 (completely silent).

Pan is fairly straightforward as well. It controls whether the sound comes from the left speaker, the right speaker, or both. On a stereo, the knob that does this is usually labeled “balance”. Unlike Volume, Pan ranges from -10,000 (full left) to +10,000 (full right), with zero being center.

Note that if you populate your Buffer with stereo sounds, you might not get quite what you expect. Setting the Pan to full left on a stereo file that contains only sounds in the right channel will actually produce silence – since DirectSound just implements Pan by increasing the volume of the left channel and decreasing the volume of the right channel. As it turns out, 3D sound effects (which we’ll cover in a later article), pretty much require you to work from mono WAV files anyway, so you probably won’t run into this problem when writing your game.

The Frequency property is a bit more subtle than the other two. You might think that this setting makes the sound higher or lower in pitch…and it does. But it does this by controlling the playback speed of the Buffer, not by doing any complex pitch-shifting sound processing. The value for Frequency is the number of samples in the Buffer you’d like to play per second, and it can range from 1 to as high as you like, although some systems may not support values higher than 100,000.

Note that the value you pass to this property is an absolute one, not a relative one like for Volume and Pan. That is, if you set Frequency to 10,000, you’re saying, “Play this sound back at 10,000 samples per second” regardless of what frequency it was recorded at. Depending on how the sound was recorded, this might make it sound higher and faster (more like “The Chipmunks”), lower and slower (more like James Earl Jones), or it might not change the sound at all.

Fortunately, there’s a trick you can use to find out what Frequency value represents “unchanged”. The following code, for example, will set the Frequency back to whatever is “normal” for that particular Buffer:

buffer.Frequency = buffer.Format.SamplesPerSecond;

The Buffer’s Format property is of type WaveFormat, a structure that describes properties of a WAV sound. Here’s we’re grabbing the SamplesPerSecond property to figure out what frequency the sound normally plays at.

So there you have it – you can now control frequency, pan, and volume for a sound. Not too bad. As usual, I’ve included a little sample app at the end of the article. It allows you to use slider controls to change the three properties we talked about. You’ll just need to compile it, and ensure that you have a file called “sample.wav” in the directory where you run it. You can download my sample.wav here.

Next time, we’ll talk about how to take DirectSound to the next level: we’ll discuss three-dimensional sound.

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.TrackBar tbPan; private System.Windows.Forms.TrackBar tbVolume; private System.Windows.Forms.TrackBar tbFreq; private System.Windows.Forms.Label label1; private System.Windows.Forms.Label label2; private System.Windows.Forms.Label label3; private System.Windows.Forms.Button bReset; private System.Windows.Forms.Button bPlaySound; private Buffer buffer; public Game() { InitializeComponent(); // Set up DirectSound CreateDevice(); // Load the sound CreateSecondaryBuffer(); // Set the cooperative level SetCooperativeLevel(); } private void CreateDevice() { device = new Device(); } private void CreateSecondaryBuffer() { // There are 8 overloads buffer = new Buffer( "sample.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.bPlaySound = new System.Windows.Forms.Button(); this.tbPan = new System.Windows.Forms.TrackBar(); this.tbVolume = new System.Windows.Forms.TrackBar(); this.tbFreq = new System.Windows.Forms.TrackBar(); this.label1 = new System.Windows.Forms.Label(); this.label2 = new System.Windows.Forms.Label(); this.label3 = new System.Windows.Forms.Label(); this.bReset = new System.Windows.Forms.Button(); ((System.ComponentModel.ISupportInitialize)(this.tbPan)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.tbVolume)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.tbFreq)).BeginInit(); this.SuspendLayout(); // // bPlaySound // this.bPlaySound.Location = new System.Drawing.Point(48, 16); this.bPlaySound.Name = "bPlaySound"; this.bPlaySound.Size = new System.Drawing.Size(208, 23); this.bPlaySound.TabIndex = 0; this.bPlaySound.Text = "Play Sound"; this.bPlaySound.Click += new System.EventHandler(this.pPlaySound_Click); // // tbPan // this.tbPan.Location = new System.Drawing.Point(96, 88); this.tbPan.Maximum = 10000; this.tbPan.Minimum = -10000; this.tbPan.Name = "tbPan"; this.tbPan.TabIndex = 1; this.tbPan.TickFrequency = 1000; this.tbPan.Scroll += new System.EventHandler(this.tbPan_Scroll); // // tbVolume // this.tbVolume.Location = new System.Drawing.Point(16, 136); this.tbVolume.Maximum = 0; this.tbVolume.Minimum = -10000; this.tbVolume.Name = "tbVolume"; this.tbVolume.Orientation = System.Windows.Forms.Orientation.Vertical; this.tbVolume.Size = new System.Drawing.Size(42, 104); this.tbVolume.TabIndex = 2; this.tbVolume.TickFrequency = 1000; this.tbVolume.Scroll += new System.EventHandler(this.tbVolume_Scroll); // // tbFreq // this.tbFreq.Location = new System.Drawing.Point(232, 136); this.tbFreq.Maximum = 200; this.tbFreq.Minimum = 10; this.tbFreq.Name = "tbFreq"; this.tbFreq.Orientation = System.Windows.Forms.Orientation.Vertical; this.tbFreq.Size = new System.Drawing.Size(42, 104); this.tbFreq.TabIndex = 3; this.tbFreq.TickFrequency = 10; this.tbFreq.Value = 100; this.tbFreq.Scroll += new System.EventHandler(this.tbFreq_Scroll); // // label1 // this.label1.Location = new System.Drawing.Point(96, 64); this.label1.Name = "label1"; this.label1.TabIndex = 4; this.label1.Text = "Pan"; this.label1.TextAlign = System.Drawing.ContentAlignment.BottomCenter; // // label2 // this.label2.Location = new System.Drawing.Point(-16, 112); this.label2.Name = "label2"; this.label2.TabIndex = 5; this.label2.Text = "Vol"; this.label2.TextAlign = System.Drawing.ContentAlignment.BottomCenter; // // label3 // this.label3.Location = new System.Drawing.Point(192, 112); this.label3.Name = "label3"; this.label3.TabIndex = 6; this.label3.Text = "Freq"; this.label3.TextAlign = System.Drawing.ContentAlignment.BottomCenter; // // bReset // this.bReset.Location = new System.Drawing.Point(104, 176); this.bReset.Name = "bReset"; this.bReset.TabIndex = 7; this.bReset.Text = "Reset"; this.bReset.Click += new System.EventHandler(this.bReset_Click); // // Game // this.AutoScaleBaseSize = new System.Drawing.Size(5, 13); this.ClientSize = new System.Drawing.Size(292, 273); this.Controls.Add(this.bReset); this.Controls.Add(this.label3); this.Controls.Add(this.label2); this.Controls.Add(this.label1); this.Controls.Add(this.tbFreq); this.Controls.Add(this.tbVolume); this.Controls.Add(this.tbPan); this.Controls.Add(this.bPlaySound); this.Name = "Game"; this.Text = "Game"; ((System.ComponentModel.ISupportInitialize)(this.tbPan)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.tbVolume)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.tbFreq)).EndInit(); this.ResumeLayout(false); } #endregion public static void Main() { Game g = new Game(); g.Show(); Application.Run(g); } private void bReset_Click(object sender, System.EventArgs e) { tbVolume.Value = buffer.Volume = 0; tbPan.Value = buffer.Pan = 0; tbFreq.Value = 100; buffer.Frequency = buffer.Format.SamplesPerSecond; } private void tbPan_Scroll(object sender, System.EventArgs e) { buffer.Pan = tbPan.Value; } private void tbVolume_Scroll(object sender, System.EventArgs e) { buffer.Volume = tbVolume.Value; } private void tbFreq_Scroll(object sender, System.EventArgs e) { buffer.Frequency = tbFreq.Value * buffer.Format.SamplesPerSecond / 100; } private void pPlaySound_Click(object sender, System.EventArgs e) { buffer.Play( 0, // Sound priority BufferPlayFlags.Default // Play flags ); } } }