Click here to Skip to main content
15,893,668 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 269.6K   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.
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//  � 2007 Gary W. Schwede and 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 Microsoft.DirectX.DirectSound;
using StreamComputers.Riff;

namespace StreamComputers.MdxDirectSound
{
	#region StreamingSoundBuffer Class

	/// <summary>
	/// A simplified streaming sound buffer associated with a specific RIFF WAVE file.
	/// Its DirectSound.SecondaryBuffer has the property "LocateInSoftware", with which  
	/// spurious notifications have not been observed to occur, so they are not tested for.
	/// </summary>
	public class SimpleStreamingSoundBuffer : 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_numberOfSectorsInBuffer = 4;

		private BufferPositionNotify[] m_NotificationPositionArray
			= new BufferPositionNotify[m_numberOfSectorsInBuffer];

		private AutoResetEvent m_NotificationEvent;
		private const int m_SectorSize = m_StreamBufferSize / m_numberOfSectorsInBuffer;
		private byte[] m_transferBuffer = new byte[m_SectorSize];
		private Notify m_Notifier;
		private Thread m_DataTransferThread;

		private RiffWaveReader m_RiffReader;
		private bool m_MoreWaveDataAvailable;
		private bool m_AbortDataTransfer;
		private int m_dataBytesSoFar; // data bytes transferred to the SecondaryBuffer so far
		private int m_secondaryBufferWritePosition; // byte index to start next Write()
		private int m_numberOfDataSectorsTransferred;

		private BufferPositionNotify[] m_EndNotificationPosition = new BufferPositionNotify[1];
		private int m_predictedEndIndex;

		#endregion

		/// <summary>
		/// Constructs a streaming sound buffer associated with a specific RIFF WAVE file
		/// </summary>
		public SimpleStreamingSoundBuffer(string fileName, Device dev)
		{
			// UI button should be disabled if no file
			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();

			// create a SecondaryBuffer suitable for streaming use.  Locate (mix) in software
			// and specify global and sticky focus
			BufferDescription bdesc = new BufferDescription(format);
			bdesc.BufferBytes = m_StreamBufferSize;
			bdesc.ControlPositionNotify = true;
			bdesc.CanGetCurrentPosition = true;
			bdesc.ControlVolume = true;
			bdesc.LocateInSoftware = true;
			bdesc.GlobalFocus = true;
			bdesc.StickyFocus = true;

			try
			{
				m_SecondaryBuffer = new SecondaryBuffer(bdesc, dev);
				m_SecondaryBuffer.SetCurrentPosition(0);
				m_secondaryBufferWritePosition = 0;
				m_SecondaryBuffer.Volume = 0;			// ie not attenuated

				// Create a notification Event object, to fire at each notify position
				m_NotificationEvent = new AutoResetEvent(false);

				// Preset as much of the EndNotificationPosition array as possible to avoid doing it 
				// in real-time.
				m_EndNotificationPosition[0].EventNotifyHandle = m_NotificationEvent.Handle;
				m_predictedEndIndex = (int) (m_RiffReader.DataLength % m_StreamBufferSize); //[bytes]

				// ready to go:
				m_MoreWaveDataAvailable = true;
				m_State = BufferPlayState.Idle;
			}
			catch (ApplicationException e)
			{
				// This may be due to lack of sound device: let GUI deal with it.
				throw (e);									
			}
		}

		/// <summary>
		/// Create & initialize the Notifier object and notification positions in the 
		/// streaming SecondaryBuffer.  These fill positions persist through multiple
		/// Play/Pause events; however, they are replaced by the single endPosition 
		/// at end of data, and are reloaded by Play() if Stopped (position == 0).
		/// </summary>
		/// <param name="numberOfSectors">number of equal-sized sectors in the buffer</param>
		private void SetFillNotifications(int numberOfSectors)
		{
			// Set up the fill-notification positions at last byte of each sector.
			// 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 < numberOfSectors; i++)
			{
				m_NotificationPositionArray[i].Offset = (i + 1) * m_SectorSize - 1;
				m_NotificationPositionArray[i].EventNotifyHandle = m_NotificationEvent.Handle;
			}

			m_Notifier = new Notify(m_SecondaryBuffer);

			// set the buffer to fire events at the notification positions
			m_Notifier.SetNotificationPositions(m_NotificationPositionArray, numberOfSectors);
		}

		private void SetEndNotification(int indexInBuffer)
		{
			Debug.Assert(m_Notifier != null, "SetEndNotification(int) called with null Notifier");
			m_EndNotificationPosition[0].Offset = indexInBuffer;

			// do this quickly to avoid possible sound glitch
			m_SecondaryBuffer.Stop();
			m_Notifier.SetNotificationPositions(m_EndNotificationPosition, 1);
			m_SecondaryBuffer.Play(0, BufferPlayFlags.Looping);
		}

		#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.");

			m_AbortDataTransfer = false;
			m_MoreWaveDataAvailable = (m_RiffReader.DataLength > m_dataBytesSoFar);
			m_numberOfDataSectorsTransferred = 0;

			// 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_DataTransferThread.Start();

			// thread will wait here for the first notification event
		}

		/// <summary>
		/// The DataTransferThread's work function.  Returns, and thread ends,
		/// when wave data transfer has ended and buffer has played out, or 
		/// when Stop event does EndDataTransferThread().
		/// </summary>
		private void DataTransferActivity()
		{
			int endWaveSector = 0;
			while (m_MoreWaveDataAvailable)
			{
				if (m_AbortDataTransfer)
				{
					return;
				}
				//wait here for a notification event
				m_NotificationEvent.WaitOne(Timeout.Infinite, true);
				endWaveSector = m_secondaryBufferWritePosition / m_SectorSize;
				m_MoreWaveDataAvailable = TransferBlockToSecondaryBuffer();
			}

			// Fill one more sector with silence, to avoid playing old data during the
			// time between end-event-notification and SecondaryBuffer.Stop().
			Array.Clear(m_transferBuffer, 0, m_transferBuffer.Length);
			m_NotificationEvent.WaitOne(Timeout.Infinite, true);
			int silentSector;
			silentSector = m_secondaryBufferWritePosition / m_SectorSize;
			WriteBlockToSecondaryBuffer();

			// No more blocks to write: Remove fill-notify points, and mark end of data.
			int dataEndInBuffer = m_dataBytesSoFar % m_StreamBufferSize;
			SetEndNotification(dataEndInBuffer);
			Debug.Assert(dataEndInBuffer == m_predictedEndIndex,
				"Wave Data Stream end is not at predicted position.");

			bool notificationWithinEndSectors = false;	// end of data or the silent sector
			// Wait for play to reach the end
			while (!notificationWithinEndSectors)
			{
				m_NotificationEvent.WaitOne(Timeout.Infinite, true);

				int currentPlayPos, unused;
				m_SecondaryBuffer.GetCurrentPosition(out currentPlayPos, out unused);
				int currentPlaySector = currentPlayPos / m_SectorSize;

				notificationWithinEndSectors = currentPlaySector == endWaveSector
												| currentPlaySector == silentSector;
			}
			m_SecondaryBuffer.Stop();
			m_State = BufferPlayState.Idle;
		}

		/// <summary>
		/// Reads and transfers a block of data from the file, beginning at m_dataBytesSoFar,
		/// into the transfer buffer; then writes to the secondary buffer, beginning at 
		/// m_secondaryBufferWritePosition.
		/// </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 dataBytesThisTime = m_RiffReader.GetDataBytes(ref m_transferBuffer,
																m_dataBytesSoFar, m_SectorSize);
			m_dataBytesSoFar += dataBytesThisTime;
			WriteBlockToSecondaryBuffer();
			m_numberOfDataSectorsTransferred++;

			if (dataBytesThisTime < m_SectorSize)
			{
				return false;
			}
			return true;
		}

		// Write a block from the Transfer Buffer to the Secondary Buffer, 
		// possibly including zero-fill.
		private void WriteBlockToSecondaryBuffer()
		{
			m_SecondaryBuffer.Write(m_secondaryBufferWritePosition, m_transferBuffer, LockFlag.None);
			m_secondaryBufferWritePosition += m_SectorSize;
			m_secondaryBufferWritePosition %= m_StreamBufferSize;
		}

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


		#endregion

		#region IDisposable Members

		protected override void Cleanup()
		{
			EndDataTransferThread();
			if (m_NotificationEvent != null)
			{
				m_NotificationEvent.Close();
			}
			if (m_Notifier != null)
			{
				m_Notifier.Dispose();
			}
			if (m_RiffReader != null)
			{
				m_RiffReader.Close();
			}
			base.Cleanup();
		}

		#endregion

		#region IPlayable methods

		/// <summary>
		/// If in Idle state, attempts to play from the beginning.  
		/// If Paused, resumes play from current position.  Otherwise has no effect.
		/// </summary>
		public override void Play()
		{
			base.Play();
			if (m_State == BufferPlayState.Paused) 
			{
				Debug.Assert(m_DataTransferThread != null && m_DataTransferThread.IsAlive,
					"DataTransferThread not available while paused.");
				Pause(); // toggle to unpaused state
				return;
			}
			else if(m_State == BufferPlayState.Idle)
			{
				EndDataTransferThread();
				Debug.Assert(m_DataTransferThread == null);
				SetFillNotifications(m_numberOfSectorsInBuffer);	// includes clearing end notify
				m_dataBytesSoFar = 0;
				m_secondaryBufferWritePosition = 0;
				m_SecondaryBuffer.SetCurrentPosition(0);		
				m_MoreWaveDataAvailable = (m_RiffReader.DataLength > m_dataBytesSoFar);
				CreateDataTransferThread();

				// load all sectors
				for (int i = 0; i < m_numberOfSectorsInBuffer -1; i++)			
				{
					// get a block of bytes, possibly including zero-fill
					TransferBlockToSecondaryBuffer();
				}

				m_SecondaryBuffer.SetCurrentPosition(0);
				m_SecondaryBuffer.Volume = 0;
				m_SecondaryBuffer.Play(0, BufferPlayFlags.Looping);
				m_State = BufferPlayState.Playing;
				return;
			}
		}
		/// <summary>
		/// Pause playing the sound file from Playing state, or resume playing from Paused state.
		/// If state is not Playing nor Paused, has no effect.
		/// </summary>
		/// <returns>Buffer.PlayPosition at time of call [bytes]</returns>
		public override int Pause()
		{
			base.Pause();
			int playPosition = m_SecondaryBuffer.PlayPosition;
			if (m_State == BufferPlayState.Playing)
			{
				m_SecondaryBuffer.Stop();
				m_State = BufferPlayState.Paused;
			}
			else if (m_State == BufferPlayState.Paused)
			{
				m_SecondaryBuffer.Play(0, BufferPlayFlags.Looping);
				m_State = BufferPlayState.Playing;
			}
			return playPosition;
		}

		private void UnPause()
		{
			if (m_State == BufferPlayState.Paused)
			{
				m_SecondaryBuffer.Play(0, BufferPlayFlags.Looping);
				m_State = BufferPlayState.Playing;
			}			
		}

		public override void Stop()
		{
			base.Stop();
			m_SecondaryBuffer.Volume = -10000;
			UnPause();
			EndDataTransferThread();
			m_SecondaryBuffer.Stop();					
			m_State = BufferPlayState.Idle;
			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