using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Runtime.InteropServices;
using LumiSoft.Media.Wave.Native;
namespace LumiSoft.Media.Wave
{
///
/// This class implements streaming wav data player.
///
public class WaveOut : IDisposable
{
#region class PlayItem
///
/// This class holds queued wav play item.
///
internal class PlayItem
{
private GCHandle m_HeaderHandle;
private GCHandle m_DataHandle;
private int m_DataSize = 0;
///
/// Default constructor.
///
/// Header handle.
/// Wav header.
/// Wav header data handle.
/// Data size in bytes.
public PlayItem(ref GCHandle headerHandle,ref GCHandle dataHandle,int dataSize)
{
m_HeaderHandle = headerHandle;
m_DataHandle = dataHandle;
m_DataSize = dataSize;
}
#region method Dispose
///
/// Cleans up any resources being used.
///
public void Dispose()
{
m_HeaderHandle.Free();
m_DataHandle.Free();
}
#endregion
#region Properties Implementation
///
/// Gets header handle.
///
public GCHandle HeaderHandle
{
get{ return m_HeaderHandle; }
}
///
/// Gets header.
///
public WAVEHDR Header
{
get{ return (WAVEHDR)m_HeaderHandle.Target; }
}
///
/// Gets wav header data pointer handle.
///
public GCHandle DataHandle
{
get{ return m_DataHandle; }
}
///
/// Gets wav header data size in bytes.
///
public int DataSize
{
get{ return m_DataSize; }
}
#endregion
}
#endregion
private WavOutDevice m_pOutDevice = null;
private int m_SamplesPerSec = 8000;
private int m_BitsPerSample = 16;
private int m_Channels = 1;
private int m_MinBuffer = 1200;
private IntPtr m_pWavDevHandle = IntPtr.Zero;
private int m_BlockSize = 0;
private int m_BytesBuffered = 0;
private bool m_IsPaused = false;
private List m_pPlayItems = null;
private waveOutProc m_pWaveOutProc = null;
private bool m_IsDisposed = false;
///
/// Default constructor.
///
/// Output device.
/// Sample rate, in samples per second (hertz). For PCM common values are
/// 8.0 kHz, 11.025 kHz, 22.05 kHz, and 44.1 kHz.
/// Bits per sample. For PCM 8 or 16 are the only valid values.
/// Number of channels.
/// Is raised when outputDevice is null.
/// Is raised when any of the aruments has invalid value.
public WaveOut(WavOutDevice outputDevice,int samplesPerSec,int bitsPerSample,int channels)
{
if(outputDevice == null){
throw new ArgumentNullException("outputDevice");
}
if(samplesPerSec < 8000){
throw new ArgumentException("Argument 'samplesPerSec' value must be >= 8000.");
}
if(bitsPerSample < 8){
throw new ArgumentException("Argument 'bitsPerSample' value must be >= 8.");
}
if(channels < 1){
throw new ArgumentException("Argument 'channels' value must be >= 1.");
}
m_pOutDevice = outputDevice;
m_SamplesPerSec = samplesPerSec;
m_BitsPerSample = bitsPerSample;
m_Channels = channels;
m_BlockSize = m_Channels * (m_BitsPerSample / 8);
m_pPlayItems = new List();
// Try to open wav device.
WAVEFORMATEX format = new WAVEFORMATEX();
format.wFormatTag = WavFormat.PCM;
format.nChannels = (ushort)m_Channels;
format.nSamplesPerSec = (uint)samplesPerSec;
format.nAvgBytesPerSec = (uint)(m_SamplesPerSec * m_Channels * (m_BitsPerSample / 8));
format.nBlockAlign = (ushort)m_BlockSize;
format.wBitsPerSample = (ushort)m_BitsPerSample;
format.cbSize = 0;
// We must delegate reference, otherwise GC will collect it.
m_pWaveOutProc = new waveOutProc(this.OnWaveOutProc);
int result = WavMethods.waveOutOpen(out m_pWavDevHandle,m_pOutDevice.Index,format,m_pWaveOutProc,0,WavConstants.CALLBACK_FUNCTION);
if(result != MMSYSERR.NOERROR){
throw new Exception("Failed to open wav device, error: " + result.ToString() + ".");
}
}
///
/// Default destructor.
///
~WaveOut()
{
Dispose();
}
#region method Dispose
///
/// Cleans up any resources being used.
///
public void Dispose()
{
if(m_IsDisposed){
return;
}
m_IsDisposed = true;
try{
// If playing, we need to reset wav device first.
WavMethods.waveOutReset(m_pWavDevHandle);
// If there are unprepared wav headers, we need to unprepare these.
foreach(PlayItem item in m_pPlayItems){
WavMethods.waveOutUnprepareHeader(m_pWavDevHandle,item.HeaderHandle.AddrOfPinnedObject(),Marshal.SizeOf(item.Header));
item.Dispose();
}
// Close output device.
WavMethods.waveOutClose(m_pWavDevHandle);
m_pOutDevice = null;
m_pWavDevHandle = IntPtr.Zero;
m_pPlayItems = null;
m_pWaveOutProc = null;
}
catch{
}
}
#endregion
#region method OnWaveOutProc
///
/// This method is called when wav device generates some event.
///
/// Handle to the waveform-audio device associated with the callback.
/// Waveform-audio output message.
/// User-instance data specified with waveOutOpen.
/// Message parameter.
/// Message parameter.
private void OnWaveOutProc(IntPtr hdrvr,int uMsg,int dwUser,int dwParam1,int dwParam2)
{
// NOTE: MSDN warns, we may not call any wav related methods here.
try{
if(uMsg == WavConstants.MM_WOM_DONE){
ThreadPool.QueueUserWorkItem(new WaitCallback(this.OnCleanUpFirstBlock));
}
}
catch{
}
}
#endregion
#region method OnCleanUpFirstBlock
///
/// Cleans up the first data block in play queue.
///
/// User data.
private void OnCleanUpFirstBlock(object state)
{
try{
lock(m_pPlayItems){
PlayItem item = m_pPlayItems[0];
WavMethods.waveOutUnprepareHeader(m_pWavDevHandle,item.HeaderHandle.AddrOfPinnedObject(),Marshal.SizeOf(item.Header));
m_pPlayItems.Remove(item);
m_BytesBuffered -= item.DataSize;
item.Dispose();
}
}
catch{
}
}
#endregion
#region method Play
///
/// Plays specified audio data bytes. If player is currently playing, data will be queued for playing.
///
/// Audio data. Data boundary must n * BlockSize.
/// Offset in the buffer.
/// Number of bytes to play form the specified offset.
/// Is raised when this object is disposed and this method is accessed.
/// Is raised when audioData is null.
/// Is raised when audioData is with invalid length.
public void Play(byte[] audioData,int offset,int count)
{
if(m_IsDisposed){
throw new ObjectDisposedException("WaveOut");
}
if(audioData == null){
throw new ArgumentNullException("audioData");
}
if((count % m_BlockSize) != 0){
throw new ArgumentException("Audio data is not n * BlockSize.");
}
//--- Queue specified audio block for play. --------------------------------------------------------
byte[] data = new byte[count];
Array.Copy(audioData,offset,data,0,count);
GCHandle dataHandle = GCHandle.Alloc(data,GCHandleType.Pinned);
// m_BytesBuffered += data.Length;
WAVEHDR wavHeader = new WAVEHDR();
wavHeader.lpData = dataHandle.AddrOfPinnedObject();
wavHeader.dwBufferLength = (uint)data.Length;
wavHeader.dwBytesRecorded = 0;
wavHeader.dwUser = IntPtr.Zero;
wavHeader.dwFlags = 0;
wavHeader.dwLoops = 0;
wavHeader.lpNext = IntPtr.Zero;
wavHeader.reserved = 0;
GCHandle headerHandle = GCHandle.Alloc(wavHeader,GCHandleType.Pinned);
int result = 0;
result = WavMethods.waveOutPrepareHeader(m_pWavDevHandle,headerHandle.AddrOfPinnedObject(),Marshal.SizeOf(wavHeader));
if(result == MMSYSERR.NOERROR){
PlayItem item = new PlayItem(ref headerHandle,ref dataHandle,data.Length);
m_pPlayItems.Add(item);
// We ran out of minimum buffer, we must pause playing while min buffer filled.
if(m_BytesBuffered < 1000){
if(!m_IsPaused){
WavMethods.waveOutPause(m_pWavDevHandle);
m_IsPaused = true;
}
//File.AppendAllText("aaaa.txt","Begin buffer\r\n");
}
// Buffering completed,we may resume playing.
else if(m_IsPaused && m_BytesBuffered > m_MinBuffer){
WavMethods.waveOutRestart(m_pWavDevHandle);
m_IsPaused = false;
//File.AppendAllText("aaaa.txt","end buffer: " + m_BytesBuffered + "\r\n");
}
/*
// TODO: If we ran out of minimum buffer, we must pause playing while min buffer filled.
if(m_BytesBuffered < m_MinBuffer){
if(!m_IsPaused){
WavMethods.waveOutPause(m_pWavDevHandle);
m_IsPaused = true;
}
}
else if(m_IsPaused){
WavMethods.waveOutRestart(m_pWavDevHandle);
}*/
m_BytesBuffered += data.Length;
result = WavMethods.waveOutWrite(m_pWavDevHandle,headerHandle.AddrOfPinnedObject(),Marshal.SizeOf(wavHeader));
}
else{
dataHandle.Free();
headerHandle.Free();
}
//--------------------------------------------------------------------------------------------------
}
#endregion
#region method GetVolume
///
/// Gets audio output volume.
///
/// Left channel volume level.
/// Right channel volume level.
/// Is raised when this object is disposed and this method is accessed.
public void GetVolume(ref ushort left,ref ushort right)
{
if(m_IsDisposed){
throw new ObjectDisposedException("WaveOut");
}
int volume = 0;
WavMethods.waveOutGetVolume(m_pWavDevHandle,out volume);
left = (ushort)(volume & 0x0000ffff);
right = (ushort)(volume >> 16);
}
#endregion
#region method SetVolume
///
/// Sets audio output volume.
///
/// Left channel volume level.
/// Right channel volume level.
/// Is raised when this object is disposed and this method is accessed.
public void SetVolume(ushort left,ushort right)
{
if(m_IsDisposed){
throw new ObjectDisposedException("WaveOut");
}
WavMethods.waveOutSetVolume(m_pWavDevHandle,(right << 16 | left & 0xFFFF));
}
#endregion
#region Properties Implementation
///
/// Gets all available output audio devices.
///
public static WavOutDevice[] Devices
{
get{
List retVal = new List();
// Get all available output devices and their info.
int devicesCount = WavMethods.waveOutGetNumDevs();
for(int i=0;i
/// Gets if this object is disposed.
///
public bool IsDisposed
{
get{ return m_IsDisposed; }
}
///
/// Gets current output device.
///
/// Is raised when this object is disposed and this property is accessed.
public WavOutDevice OutputDevice
{
get{
if(m_IsDisposed){
throw new ObjectDisposedException("WaveOut");
}
return m_pOutDevice;
}
}
///
/// Gets number of samples per second.
///
/// Is raised when this object is disposed and this property is accessed.
public int SamplesPerSec
{
get{
if(m_IsDisposed){
throw new ObjectDisposedException("WaveOut");
}
return m_SamplesPerSec;
}
}
///
/// Gets number of buts per sample.
///
/// Is raised when this object is disposed and this property is accessed.
public int BitsPerSample
{
get{
if(m_IsDisposed){
throw new ObjectDisposedException("WaveOut");
}
return m_BitsPerSample;
}
}
///
/// Gets number of channels.
///
/// Is raised when this object is disposed and this property is accessed.
public int Channels
{
get{
if(m_IsDisposed){
throw new ObjectDisposedException("WaveOut");
}
return m_Channels;
}
}
///
/// Gets one smaple block size in bytes.
///
/// Is raised when this object is disposed and this property is accessed.
public int BlockSize
{
get{
if(m_IsDisposed){
throw new ObjectDisposedException("WaveOut");
}
return m_BlockSize;
}
}
///
/// Gets if wav player is currently playing something.
///
/// Is raised when this object is disposed and this property is accessed.
public bool IsPlaying
{
get{
if(m_IsDisposed){
throw new ObjectDisposedException("WaveOut");
}
if(m_pPlayItems.Count > 0){
return true;
}
else{
return false;
}
}
}
#endregion
}
}