Click here to Skip to main content
15,881,424 members
Articles / Multimedia / DirectX

Sound Experiments in Managed DirectX

Rate me:
Please Sign up or sign in to vote.
4.85/5 (46 votes)
16 Feb 200726 min read 267.2K   4K   118  
Using static and streaming sound buffers in Managed DirectX.
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//  THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
//  KIND, EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, WARRANTIES
//  OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//  � 2006 Gary W. Schwede  Stream Computers, Inc. All rights reserved.
//  Contact: gary at streamcomputers dot com. Permission to incorporate
//  all or part of this code in your application is given on the condition
//  that this notice accompanies it in your code and documentation.
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
using System;
using System.Diagnostics;
using System.Threading;
using System.Windows.Forms;
using Microsoft.DirectX.DirectSound;
using StreamComputers.Riff;
using StreamComputers.Utilities;

namespace StreamComputers.MdxDirectSound
{
	#region StreamingSoundBuffer Class
	/// <summary>
	/// A streaming sound buffer associated with a specific RIFF WAVE file.
	/// </summary>
	public class StreamingSoundBuffer : MdxSoundBuffer
	{
		#region private fields

		// A note on buffer size: I assume CD audio; i.e., 16-bit stereo frames at 44100 frames/sec.
		// 64k * 4 bytes per frame is about 1.5 sec. There's about 370 msec between notification events.
		private const int			m_StreamBufferSize = 262144; 
		private const int			m_numNotifyPositions = 4;	 
		private BufferPositionNotify[] m_PositionNotify = 
			new BufferPositionNotify[m_numNotifyPositions];  
		private AutoResetEvent		m_NotificationEvent;
		private const int			m_TransferBlockSize = m_StreamBufferSize / m_numNotifyPositions;
		private byte[]				m_TransferBuf = new byte[m_TransferBlockSize] ;
		private Notify				m_Notify;
		private Thread				m_DataTransferThread;

		private RiffWaveReader		m_RiffReader;
		private bool				m_WaveDataAvailable;
		private bool				m_AbortDataTransfer;
		private int					m_bytesSoFar;		// data bytes transferred to the SecondaryBuffer so far
		private int					m_SecBufWritePos;	// byte index to start next write()
		private int					m_numSpuriousNotifies;
		private int					m_numBlocksTransferred;

		#endregion

		public StreamingSoundBuffer(string fileName, Device dev)
		{
			// UI button should be disabled if no file
			if(fileName == null)
			{
				// If you use my code, I suggest you change this message...
				// or make very sure the UI won't get here!
				MessageBox.Show("Select a sound file first, moron.");
				return;
			}
			try
			{
				// Use a RiffWaveReader to access the WAVE file
				m_RiffReader = new RiffWaveReader(fileName);
			}
			catch(RiffParserException)
			{
				if(m_RiffReader != null)
				{
					m_RiffReader.Dispose();
				}
				throw;
			}	
			WaveFormat format = m_RiffReader.GetMDXWaveFormat();
			DebugConsole.WriteLine(MdxInfo.WaveFormatAsString(format));

			// create a SecondaryBuffer suitable for streaming use and with sticky focus
			BufferDescription bdesc = new BufferDescription(format);
			bdesc.BufferBytes = m_StreamBufferSize;
			bdesc.ControlPositionNotify = true;
			bdesc.CanGetCurrentPosition = true;
			bdesc.StickyFocus = true;

			m_SecondaryBuffer = new SecondaryBuffer(bdesc, dev);
			m_SecondaryBuffer.SetCurrentPosition(0) ;
			m_SecBufWritePos = 0;
			DebugConsole.WriteLine(MdxInfo.BufferCapsAsString(m_SecondaryBuffer.Caps) );

			SetUpNotifications();				
		}

		/// <summary>
		/// Create & initialize the Notify object and notification positions in the streaming   
		/// SecondaryBuffer.  These persist through multiple Play/Pause/Stop events.
		/// </summary>
		private void SetUpNotifications()
		{
			// Create a notification Event object, to fire at each notify position
			m_NotificationEvent = new AutoResetEvent(false);

			// Set up the notification positions.
			// All use the same event, in contrast to recipe in DX9.0 SDK Aug 2005 titled 
			// "DirectSound Buffers | Using Streaming Buffers" 
			for (int i = 0; i < m_numNotifyPositions; i++)
			{
				m_PositionNotify[i].Offset = (i + 1)*m_TransferBlockSize - 1;
				m_PositionNotify[i].EventNotifyHandle = m_NotificationEvent.Handle;
			}

			// create the buffer's Notify object
			m_Notify = new Notify(m_SecondaryBuffer);

			// set the buffer to fire events at the notification positions
			m_Notify.SetNotificationPositions(m_PositionNotify, m_numNotifyPositions);
		}

		#region Data Transfer Thread code

		/// <summary>
		/// Instantiate and start the server thread.  It will catch events from the Notify object, 
		/// and call TransferBlockToSecondaryBuffer() each time a Notification position is crossed.
		/// Thread terminates at end of playback, or upon Stop event.
		/// </summary>
		private void CreateDataTransferThread()
		{
			// No thread should exist yet.
			Debug.Assert(m_DataTransferThread == null,
				"CreateDataTransferThread() saw thread non-null." );
			
			// Create a thread to monitor the notify events.
			m_DataTransferThread = new Thread(new ThreadStart(DataTransferActivity));
			m_DataTransferThread.Name = "DataTransferThread";
			m_DataTransferThread.Priority = ThreadPriority.Highest;
			m_AbortDataTransfer = false;
			m_WaveDataAvailable = true;
			m_numSpuriousNotifies = 0;
			m_numBlocksTransferred = 0;
			m_DataTransferThread.Start();
	
			// thread will wait for notification events
		}

		/// <summary>
		/// The DataTransferThread's work function.  Returns, and thread ends, when
		/// wave data transfer has ended and buffer has been filled with silence, or 
		/// when Stop event does EndDataTransferThread().
		/// </summary>
		private void DataTransferActivity()
		{
			while(m_WaveDataAvailable)
			{
				//wait here for a notification event
				m_NotificationEvent.WaitOne(Timeout.Infinite, true);
				if(m_AbortDataTransfer)
				{
					return;
				}

				// The following logic defends against occasional spurious notification events
				// which seem to be due to a bug in DirectSound.
				int currentPlayPos, currentWritePos;
				m_SecondaryBuffer.GetCurrentPosition(out currentPlayPos, out currentWritePos);
				int nextWriteSector = m_SecBufWritePos / m_TransferBlockSize;
				int currentPlaySector = currentPlayPos / m_TransferBlockSize;
				if(nextWriteSector != currentPlaySector)	// don't overwrite the play segment!
				{
					m_WaveDataAvailable = TransferBlockToSecondaryBuffer();
				}
				else
				{
					DebugConsole.WriteLine("Spurious Notification detected.");
					m_numSpuriousNotifies++;
				}
			}
			
			// Wave data transfer has ended.  
			// Fill the SecondaryBuffer with silence as it finishes playing the last blocks.
			Array.Clear(m_TransferBuf, 0, m_TransferBuf.Length);
			for (int i = 0; i < m_numNotifyPositions; i++)
			{
				m_NotificationEvent.WaitOne(Timeout.Infinite, true);	//true to exit synch domain before wait ?
				m_SecondaryBuffer.Write(m_SecBufWritePos, m_TransferBuf, LockFlag.None);
				m_SecBufWritePos += m_TransferBlockSize;
				m_SecBufWritePos %= m_StreamBufferSize;
				DebugConsole.WriteLine("{0, 12}    {1,12}    {2,12}", 
					m_bytesSoFar, 0, m_SecBufWritePos);
			}

			// DON'T DO THIS!
			// MDX doc suggests to set a notify flag at the end of the real data
			//m_EndOfDataEvent = new AutoResetEvent(false);
			//m_PositionNotify[m_numNotifyPositions].Offset = m_SecBufWritePos;
			//m_PositionNotify[m_numNotifyPositions].EventNotifyHandle = m_EndOfDataEvent.Handle;
			// try this: MDX doc contradictory on whether it can be done. BOMBS with INVALID CALL exception
			//m_SecondaryBufferNotify.SetNotificationPositions(m_PositionNotify, m_numNotifyPositions +1);
			//m_EndOfDataEvent.WaitOne(Timeout.Infinite, true);

			m_SecondaryBuffer.Stop();
			m_SecondaryBuffer.SetCurrentPosition(0);
			DebugConsole.WriteLine("Stopped.");

			DebugReportAnomalies();
		}

		[Conditional("DEBUG")]
		private void DebugReportAnomalies()
		{
			// Testing code to ensure that no notification events were missed
			int numBlocksExpected = (int) Math.Ceiling(
				(double)m_RiffReader.DataLength / (double)m_TransferBlockSize );
			if(m_numBlocksTransferred != numBlocksExpected )
			{
				MessageBox.Show(String.Format("{0} blocks transferred\n{1} blocks expected.",
				                              m_numBlocksTransferred, numBlocksExpected ),
				                "Data Transfer Error.",
				                MessageBoxButtons.OK,  
				                MessageBoxIcon.Exclamation );
			}
	
			// Testing code to report spurious notify events
			if(m_numSpuriousNotifies > 0)
			{
				MessageBox.Show(String.Format("{0} Spurious notify events detected during play.\n",
				                              m_numSpuriousNotifies ),
				                "Possible DirectSound Bug Detected",
				                MessageBoxButtons.OK,  
				                MessageBoxIcon.Exclamation );
			}
		}

		/// <summary>
		/// Transfers a block of data from the transfer buffer to the secondary buffer.
		/// </summary>
		/// <returns>true if the whole block is wave data, false if end of data with zero-fill</returns>
		private bool TransferBlockToSecondaryBuffer()
		{
			/// If the file has run out of wave data, the block was zero-filled to the end. 
			int bytesThisTime = m_RiffReader.GetDataBytes(ref m_TransferBuf, m_bytesSoFar, m_TransferBlockSize);

			// actual number of data bytes transferred so far, not including zero-fill
			m_bytesSoFar += bytesThisTime;

			// write the block to the buffer, possibly including zero-fill
			m_SecondaryBuffer.Write(m_SecBufWritePos, m_TransferBuf, LockFlag.None);
			m_numBlocksTransferred++;

			// update write position 
			m_SecBufWritePos += m_TransferBlockSize;
			m_SecBufWritePos %= m_StreamBufferSize;
			DebugConsole.WriteLine("{0, 12}    {1,12}    {2,12}", 
				m_bytesSoFar, bytesThisTime, m_SecBufWritePos);

			if(bytesThisTime < m_TransferBlockSize)
			{
				return false;
			}
			return true;
		}

		// Safely end the DataTransferThread if it is alive.  Sets ref to null.
		// Called from Stop() and Dispose() ie Cleanup()
		private void EndDataTransferThread()
		{
			if (m_DataTransferThread == null)
			{
				return;
			}
			if (m_DataTransferThread.IsAlive)
			{
				m_WaveDataAvailable = false;
				m_AbortDataTransfer = true;
				m_NotificationEvent.Set();
				m_DataTransferThread.Join(200);
			}
			m_DataTransferThread = null;
		}
		#endregion

		#region IDisposable Members
		
		protected override void Cleanup()
		{
			EndDataTransferThread();
			if(m_NotificationEvent != null)
			{
				m_NotificationEvent.Close() ;
			}
			if(m_Notify != null)
			{
				m_Notify.Dispose();
			}
			if (m_RiffReader != null)
			{
				m_RiffReader.Close();
			}
			base.Cleanup();
		}
		#endregion

		#region IPlayable methods

		public override void Play()
		{
			base.Play();
			if(m_SecondaryBuffer.PlayPosition != 0)		// already playing or paused
			{
				Debug.Assert(m_DataTransferThread != null && m_DataTransferThread.IsAlive,
					"DataTransferThread not available while playing or paused.");
				if(m_Paused)
				{
					Pause();		// toggle to unpaused state
				}
				return;
			}

			Stop();
			Debug.Assert(m_DataTransferThread == null);
			CreateDataTransferThread();		
			m_SecondaryBuffer.SetCurrentPosition(0) ;
			m_SecBufWritePos = 0;

			// load entire buffer
			DebugConsole.WriteLine("bytes so far    bytes this time SecBuf Write Pos");
			m_bytesSoFar = 0;
			for(int i = 0; i < m_numNotifyPositions; i++)
			{
				// get a block of bytes, possibly including zero-fill
				TransferBlockToSecondaryBuffer();
			}

			m_SecondaryBuffer.Play(0,BufferPlayFlags.Looping);
			return;
		}

		public override int Pause()
		{
			base.Pause();
			int playPosition = m_SecondaryBuffer.PlayPosition;
			if (m_SecondaryBuffer.Status.Playing)
			{
				m_SecondaryBuffer.Stop();
				m_Paused = true;
			}
			else if(m_Paused)
			{
				m_SecondaryBuffer.Play(0,BufferPlayFlags.Looping);
				m_Paused = false;
			}
			return playPosition ;
		}

		public override void Stop()
		{
			base.Stop();
			m_SecondaryBuffer.Stop();
			m_SecondaryBuffer.SetCurrentPosition(0);
			EndDataTransferThread();
			return;
		}

		#endregion
		#endregion
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.


Written By
Software Developer (Senior)
United States United States
My life and career have been a bit unusual (mostly in good ways). So, I'm grateful every day for the opportunities God's given me to do different things and see different aspects of life.

Education: B.S. Physics '73 (atmospheric physics, sounding rockets), M.S. Computer Science '76 (radio astronomy, fuzzy controllers, music pattern recognition and visualization) New Mexico Tech; Ph.D. Engineering '83 (parallel computer architecture, digital signal processing, economics) U.C. Berkeley.

I'm married to Susan, a wonderful woman whom I met in a Computer Architecture class at U.C. Berkeley.

Professional activities: Digital systems engineer, digital audio pioneer, founder or key in several tech startups, consulting engineer, expert witness. I'm currently developing a multithreading framework in C# .NET, that makes it almost easy to write correct programs for multicore processors. I'm also implementing a new transform for recognizing, editing, and processing signals, especially sound.

I'm an occasional essayist, public speaker, and podcaster, and free-market space advocate. I enjoy good wine, good music, good friends, and cats.

If you think your project could use a different point of view, I'm available for consulting work in the San Francisco Bay area, or (preferrably) via the net.

Comments and Discussions