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