Click here to Skip to main content
Click here to Skip to main content
 
Add your own
alternative version

Sound Experiments in Managed DirectX

, 16 Feb 2007
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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

DrGary83
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.

| Advertise | Privacy | Mobile
Web03 | 2.8.140721.1 | Last Updated 16 Feb 2007
Article Copyright 2006 by DrGary83
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid