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