Click here to Skip to main content
15,896,348 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 270K   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.Text;
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_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 readonly bool m_inHardware; 
		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_numSpuriousNotifies;
		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>
		/// <param name="fileName"></param>
		/// <param name="dev"></param>
		public StreamingSoundBuffer(string fileName, Device dev) : this(fileName, dev, false)
		{
		}

		public StreamingSoundBuffer(string fileName, Device dev, bool inHardware)
		{
			m_inHardware = inHardware;

			// 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();
			DebugConsole.WriteLine(MdxInfo.WaveFormatAsString(format));
			DebugConsole.WriteLine("WaveDataLength: {0,12} bytes", m_RiffReader.DataLength);

			// describe a SecondaryBuffer suitable for streaming, and very selfish focus
			BufferDescription bdesc = new BufferDescription(format);
			bdesc.BufferBytes = m_StreamBufferSize;
			bdesc.ControlPositionNotify = true;
			bdesc.CanGetCurrentPosition = true;
			bdesc.ControlVolume = true;
			bdesc.LocateInHardware = m_inHardware;
			bdesc.LocateInSoftware = !m_inHardware;
			bdesc.GlobalFocus = true;
			bdesc.StickyFocus = true;

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

				// 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 = (m_RiffReader.DataLength > m_dataBytesSoFar);
				m_State = BufferPlayState.Idle;
			}
			catch (ApplicationException e)
			{
				// This may be due to lack of hardware accellerator: let GUI deal with it.
				DebugConsole.WriteLine("Failed to create specified StreamingSoundBuffer.");
				StringBuilder msg = new StringBuilder("Cannot create specified StreamingSoundBuffer:\n");
				msg.Append("ApplicationException encountered:\n");
				msg.Append(e.ToString());
				MessageBox.Show( msg.ToString(),
					"Buffer Specification Error.",
					MessageBoxButtons.OK,
					MessageBoxIcon.Exclamation);
				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() from Idle state.
		/// </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;
				DebugConsole.WriteLine("Fill Notification set at {0,12}", m_NotificationPositionArray[i].Offset);
				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_numSpuriousNotifies = 0;
			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 for notification events
		}

		/// <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()
		{
			bool badEvent;
			int endWaveSector = 0;
			while (m_MoreWaveDataAvailable)
			{
				if (m_AbortDataTransfer)
				{
					return;
				}
				//wait here for a notification event
				m_NotificationEvent.WaitOne(Timeout.Infinite, true);
				badEvent = NotificationInPlaySector();
				if (!badEvent)
				{
					endWaveSector = m_secondaryBufferWritePosition / m_SectorSize;
					m_MoreWaveDataAvailable = TransferBlockToSecondaryBuffer();
				}
			}

			DebugConsole.WriteLine("Wave Data Ended.");

			// 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);
			badEvent = true;
			while (badEvent)
			{
				m_NotificationEvent.WaitOne(Timeout.Infinite, true);
				badEvent = NotificationInPlaySector();
			}
			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);
			// report end of data
			DebugConsole.WriteLine("Total Wave Bytes {0,12}", m_dataBytesSoFar);
			DebugConsole.WriteLine("Offset in Buffer {0,12}", dataEndInBuffer);
			DebugConsole.WriteLine("Predicted EndPos {0,12}", m_predictedEndIndex);
			Debug.Assert(dataEndInBuffer == m_predictedEndIndex,
			             "Wave Data Stream end is not at predicted position.");

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

				// Was this a spurious event?  If not within 
				// last wave segment or silent segment, it must have been.
				int currentPlayPos, unused;
				m_SecondaryBuffer.GetCurrentPosition(out currentPlayPos, out unused);
				int currentPlaySector = currentPlayPos / m_SectorSize;
				DebugConsole.WriteLine("CurrentPlaySector{0,4}", currentPlaySector);
				DebugConsole.WriteLine("End Wave Sector  {0,4}", endWaveSector);
				DebugConsole.WriteLine("Silent Sector    {0,4}", silentSector);

				notificationWithinEndSectors = currentPlaySector == endWaveSector
				                               | currentPlaySector == silentSector;
				if (!notificationWithinEndSectors)
				{
					DebugConsole.WriteLine("Spurious Notification detected.");
					m_numSpuriousNotifies++;
				}
			}

			// normal stop at end of file
			m_SecondaryBuffer.Stop();
			m_State = BufferPlayState.Idle;
			DebugConsole.WriteLine("Stopped.");
			DebugReportAnomalies();
		}

		// Detect a premature fill-notification event: the next sector to be filled is the 
		// segment now being played. 
		private bool NotificationInPlaySector()
		{
			int currentPlayPos, allowableWritePos;
			m_SecondaryBuffer.GetCurrentPosition(out currentPlayPos, out allowableWritePos);
			int nextWriteSector = m_secondaryBufferWritePosition / m_SectorSize;
			int currentPlaySector = currentPlayPos / m_SectorSize;
			if (nextWriteSector != currentPlaySector)
			{
				return false;
			}
			else
			{
				DebugConsole.WriteLine("Spurious Notification detected. playPos = {0}  writePos = {1}",
										currentPlayPos, m_secondaryBufferWritePosition);
				m_numSpuriousNotifies++;
				return true;
			}
		}

		/// <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 dataBytesThisTime = m_RiffReader.GetDataBytes(ref m_transferBuffer,
			                                                  m_dataBytesSoFar, m_SectorSize);
			m_dataBytesSoFar += dataBytesThisTime;
			WriteBlockToSecondaryBuffer();
			m_numberOfDataSectorsTransferred++;

			DebugConsole.WriteLine("{0, 12}    {1,12}    {2,12}",
			                       m_dataBytesSoFar, dataBytesThisTime, m_secondaryBufferWritePosition);

			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;
		}

		/// <remarks>
		/// 1.System.Windows.Forms.MessageBox.Show() is a thread-safe public static member.
		/// So it should be OK to call it from the data transfer thread.
		/// </remarks>
		[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_SectorSize);
			if (m_numberOfDataSectorsTransferred != numBlocksExpected)
			{
				MessageBox.Show(String.Format("{0} blocks transferred\n{1} blocks expected.",
				                              m_numberOfDataSectorsTransferred, 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),
				                "Device driver / DirectSound Bug Detected",
				                MessageBoxButtons.OK,
				                MessageBoxIcon.Exclamation);
			}
		}

		#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()
		{
			DebugConsole.WriteLine("Play command in {0} state",m_State.ToString());
			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
				DebugConsole.WriteLine("bytes so far    bytes this time  SecBuf Write Pos");
				for (int i = 0; i < m_numberOfSectorsInBuffer; 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()
		{
			DebugConsole.WriteLine("Pause command in {0} state",m_State.ToString());
			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()
		{
			DebugConsole.WriteLine("Stop command in {0} state",m_State.ToString());
			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