//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// 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
}
}