509 lines
18 KiB
Plaintext
509 lines
18 KiB
Plaintext
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
|
|
{
|
|
/// <summary>
|
|
/// This class implements streaming wav data player.
|
|
/// </summary>
|
|
public class WaveOut : IDisposable
|
|
{
|
|
#region class PlayItem
|
|
|
|
/// <summary>
|
|
/// This class holds queued wav play item.
|
|
/// </summary>
|
|
internal class PlayItem
|
|
{
|
|
private GCHandle m_HeaderHandle;
|
|
private GCHandle m_DataHandle;
|
|
private int m_DataSize = 0;
|
|
|
|
/// <summary>
|
|
/// Default constructor.
|
|
/// </summary>
|
|
/// <param name="headerHandle">Header handle.</param>
|
|
/// <param name="header">Wav header.</param>
|
|
/// <param name="dataHandle">Wav header data handle.</param>
|
|
/// <param name="dataSize">Data size in bytes.</param>
|
|
public PlayItem(ref GCHandle headerHandle,ref GCHandle dataHandle,int dataSize)
|
|
{
|
|
m_HeaderHandle = headerHandle;
|
|
m_DataHandle = dataHandle;
|
|
m_DataSize = dataSize;
|
|
}
|
|
|
|
#region method Dispose
|
|
|
|
/// <summary>
|
|
/// Cleans up any resources being used.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
m_HeaderHandle.Free();
|
|
m_DataHandle.Free();
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region Properties Implementation
|
|
|
|
/// <summary>
|
|
/// Gets header handle.
|
|
/// </summary>
|
|
public GCHandle HeaderHandle
|
|
{
|
|
get{ return m_HeaderHandle; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets header.
|
|
/// </summary>
|
|
public WAVEHDR Header
|
|
{
|
|
get{ return (WAVEHDR)m_HeaderHandle.Target; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets wav header data pointer handle.
|
|
/// </summary>
|
|
public GCHandle DataHandle
|
|
{
|
|
get{ return m_DataHandle; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets wav header data size in bytes.
|
|
/// </summary>
|
|
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<PlayItem> m_pPlayItems = null;
|
|
private waveOutProc m_pWaveOutProc = null;
|
|
private bool m_IsDisposed = false;
|
|
|
|
/// <summary>
|
|
/// Default constructor.
|
|
/// </summary>
|
|
/// <param name="outputDevice">Output device.</param>
|
|
/// <param name="samplesPerSec">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.</param>
|
|
/// <param name="bitsPerSample">Bits per sample. For PCM 8 or 16 are the only valid values.</param>
|
|
/// <param name="channels">Number of channels.</param>
|
|
/// <exception cref="ArgumentNullException">Is raised when <b>outputDevice</b> is null.</exception>
|
|
/// <exception cref="ArgumentException">Is raised when any of the aruments has invalid value.</exception>
|
|
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<PlayItem>();
|
|
|
|
// 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() + ".");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Default destructor.
|
|
/// </summary>
|
|
~WaveOut()
|
|
{
|
|
Dispose();
|
|
}
|
|
|
|
#region method Dispose
|
|
|
|
/// <summary>
|
|
/// Cleans up any resources being used.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// This method is called when wav device generates some event.
|
|
/// </summary>
|
|
/// <param name="hdrvr">Handle to the waveform-audio device associated with the callback.</param>
|
|
/// <param name="uMsg">Waveform-audio output message.</param>
|
|
/// <param name="dwUser">User-instance data specified with waveOutOpen.</param>
|
|
/// <param name="dwParam1">Message parameter.</param>
|
|
/// <param name="dwParam2">Message parameter.</param>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Cleans up the first data block in play queue.
|
|
/// </summary>
|
|
/// <param name="state">User data.</param>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Plays specified audio data bytes. If player is currently playing, data will be queued for playing.
|
|
/// </summary>
|
|
/// <param name="audioData">Audio data. Data boundary must n * BlockSize.</param>
|
|
/// <param name="offset">Offset in the buffer.</param>
|
|
/// <param name="count">Number of bytes to play form the specified offset.</param>
|
|
/// <exception cref="ObjectDisposedException">Is raised when this object is disposed and this method is accessed.</exception>
|
|
/// <exception cref="ArgumentNullException">Is raised when <b>audioData</b> is null.</exception>
|
|
/// <exception cref="ArgumentException">Is raised when <b>audioData</b> is with invalid length.</exception>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Gets audio output volume.
|
|
/// </summary>
|
|
/// <param name="left">Left channel volume level.</param>
|
|
/// <param name="right">Right channel volume level.</param>
|
|
/// <exception cref="ObjectDisposedException">Is raised when this object is disposed and this method is accessed.</exception>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Sets audio output volume.
|
|
/// </summary>
|
|
/// <param name="left">Left channel volume level.</param>
|
|
/// <param name="right">Right channel volume level.</param>
|
|
/// <exception cref="ObjectDisposedException">Is raised when this object is disposed and this method is accessed.</exception>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Gets all available output audio devices.
|
|
/// </summary>
|
|
public static WavOutDevice[] Devices
|
|
{
|
|
get{
|
|
List<WavOutDevice> retVal = new List<WavOutDevice>();
|
|
// Get all available output devices and their info.
|
|
int devicesCount = WavMethods.waveOutGetNumDevs();
|
|
for(int i=0;i<devicesCount;i++){
|
|
WAVEOUTCAPS pwoc = new WAVEOUTCAPS();
|
|
if(WavMethods.waveOutGetDevCaps((uint)i,ref pwoc,Marshal.SizeOf(pwoc)) == MMSYSERR.NOERROR){
|
|
retVal.Add(new WavOutDevice(i,pwoc.szPname,pwoc.wChannels));
|
|
}
|
|
}
|
|
|
|
return retVal.ToArray();
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Gets if this object is disposed.
|
|
/// </summary>
|
|
public bool IsDisposed
|
|
{
|
|
get{ return m_IsDisposed; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets current output device.
|
|
/// </summary>
|
|
/// <exception cref="">Is raised when this object is disposed and this property is accessed.</exception>
|
|
public WavOutDevice OutputDevice
|
|
{
|
|
get{
|
|
if(m_IsDisposed){
|
|
throw new ObjectDisposedException("WaveOut");
|
|
}
|
|
|
|
return m_pOutDevice;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets number of samples per second.
|
|
/// </summary>
|
|
/// <exception cref="">Is raised when this object is disposed and this property is accessed.</exception>
|
|
public int SamplesPerSec
|
|
{
|
|
get{
|
|
if(m_IsDisposed){
|
|
throw new ObjectDisposedException("WaveOut");
|
|
}
|
|
|
|
return m_SamplesPerSec;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets number of buts per sample.
|
|
/// </summary>
|
|
/// <exception cref="">Is raised when this object is disposed and this property is accessed.</exception>
|
|
public int BitsPerSample
|
|
{
|
|
get{
|
|
if(m_IsDisposed){
|
|
throw new ObjectDisposedException("WaveOut");
|
|
}
|
|
|
|
return m_BitsPerSample;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets number of channels.
|
|
/// </summary>
|
|
/// <exception cref="">Is raised when this object is disposed and this property is accessed.</exception>
|
|
public int Channels
|
|
{
|
|
get{
|
|
if(m_IsDisposed){
|
|
throw new ObjectDisposedException("WaveOut");
|
|
}
|
|
|
|
return m_Channels;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets one smaple block size in bytes.
|
|
/// </summary>
|
|
/// <exception cref="">Is raised when this object is disposed and this property is accessed.</exception>
|
|
public int BlockSize
|
|
{
|
|
get{
|
|
if(m_IsDisposed){
|
|
throw new ObjectDisposedException("WaveOut");
|
|
}
|
|
|
|
return m_BlockSize;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets if wav player is currently playing something.
|
|
/// </summary>
|
|
/// <exception cref="">Is raised when this object is disposed and this property is accessed.</exception>
|
|
public bool IsPlaying
|
|
{
|
|
get{
|
|
if(m_IsDisposed){
|
|
throw new ObjectDisposedException("WaveOut");
|
|
}
|
|
|
|
if(m_pPlayItems.Count > 0){
|
|
return true;
|
|
}
|
|
else{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
}
|
|
}
|