using System;
using System.Diagnostics;
using System.Threading;
using System.Runtime.InteropServices;
// ReSharper disable once CheckNamespace
namespace NAudio.Wave
{
///
/// Alternative WaveOut class, making use of the Event callback
///
public class WaveOutEvent : IWavePlayer, IWavePosition
{
private readonly object waveOutLock;
private readonly SynchronizationContext syncContext;
private IntPtr hWaveOut; // WaveOut handle
private WaveOutBuffer[] buffers;
private IWaveProvider waveStream;
private volatile PlaybackState playbackState;
private AutoResetEvent callbackEvent;
///
/// Indicates playback has stopped automatically
///
public event EventHandler PlaybackStopped;
///
/// Gets or sets the desired latency in milliseconds
/// Should be set before a call to Init
///
public int DesiredLatency { get; set; }
///
/// Gets or sets the number of buffers used
/// Should be set before a call to Init
///
public int NumberOfBuffers { get; set; }
///
/// Gets or sets the device number
/// Should be set before a call to Init
/// This must be between -1 and DeviceCount - 1.
/// -1 means stick to default device even default device is changed
///
public int DeviceNumber { get; set; } = -1;
///
/// Opens a WaveOut device
///
public WaveOutEvent()
{
syncContext = SynchronizationContext.Current;
if (syncContext != null &&
((syncContext.GetType().Name == "LegacyAspNetSynchronizationContext") ||
(syncContext.GetType().Name == "AspNetSynchronizationContext")))
{
syncContext = null;
}
// set default values up
DesiredLatency = 300;
NumberOfBuffers = 2;
waveOutLock = new object();
}
///
/// Initialises the WaveOut device
///
/// WaveProvider to play
public void Init(IWaveProvider waveProvider)
{
if (playbackState != PlaybackState.Stopped)
{
throw new InvalidOperationException("Can't re-initialize during playback");
}
if (hWaveOut != IntPtr.Zero)
{
// normally we don't allow calling Init twice, but as experiment, see if we can clean up and go again
// try to allow reuse of this waveOut device
// n.b. risky if Playback thread has not exited
DisposeBuffers();
CloseWaveOut();
}
callbackEvent = new AutoResetEvent(false);
waveStream = waveProvider;
int bufferSize = waveProvider.WaveFormat.ConvertLatencyToByteSize((DesiredLatency + NumberOfBuffers - 1) / NumberOfBuffers);
MmResult result;
lock (waveOutLock)
{
result = WaveInterop.waveOutOpenWindow(out hWaveOut, (IntPtr)DeviceNumber, waveStream.WaveFormat, callbackEvent.SafeWaitHandle.DangerousGetHandle(), IntPtr.Zero, WaveInterop.WaveInOutOpenFlags.CallbackEvent);
}
MmException.Try(result, "waveOutOpen");
buffers = new WaveOutBuffer[NumberOfBuffers];
playbackState = PlaybackState.Stopped;
for (var n = 0; n < NumberOfBuffers; n++)
{
buffers[n] = new WaveOutBuffer(hWaveOut, bufferSize, waveStream, waveOutLock);
}
}
///
/// Start playing the audio from the WaveStream
///
public void Play()
{
if (buffers == null || waveStream == null)
{
throw new InvalidOperationException("Must call Init first");
}
if (playbackState == PlaybackState.Stopped)
{
playbackState = PlaybackState.Playing;
callbackEvent.Set(); // give the thread a kick
ThreadPool.QueueUserWorkItem(state => PlaybackThread(), null);
}
else if (playbackState == PlaybackState.Paused)
{
Resume();
callbackEvent.Set(); // give the thread a kick
}
}
private void PlaybackThread()
{
Exception exception = null;
try
{
DoPlayback();
}
catch (Exception e)
{
exception = e;
}
finally
{
playbackState = PlaybackState.Stopped;
// we're exiting our background thread
RaisePlaybackStoppedEvent(exception);
}
}
private void DoPlayback()
{
while (playbackState != PlaybackState.Stopped)
{
if (!callbackEvent.WaitOne(DesiredLatency))
{
if (playbackState == PlaybackState.Playing)
{
Debug.WriteLine("WARNING: WaveOutEvent callback event timeout");
}
}
// requeue any buffers returned to us
if (playbackState == PlaybackState.Playing)
{
int queued = 0;
foreach (var buffer in buffers)
{
if (buffer.InQueue || buffer.OnDone())
{
queued++;
}
}
if (queued == 0)
{
// we got to the end
playbackState = PlaybackState.Stopped;
callbackEvent.Set();
}
}
}
}
///
/// Pause the audio
///
public void Pause()
{
if (playbackState == PlaybackState.Playing)
{
MmResult result;
playbackState = PlaybackState.Paused; // set this here to avoid a deadlock problem with some drivers
lock (waveOutLock)
{
result = WaveInterop.waveOutPause(hWaveOut);
}
if (result != MmResult.NoError)
{
throw new MmException(result, "waveOutPause");
}
}
}
///
/// Resume playing after a pause from the same position
///
private void Resume()
{
if (playbackState == PlaybackState.Paused)
{
MmResult result;
lock (waveOutLock)
{
result = WaveInterop.waveOutRestart(hWaveOut);
}
if (result != MmResult.NoError)
{
throw new MmException(result, "waveOutRestart");
}
playbackState = PlaybackState.Playing;
}
}
///
/// Stop and reset the WaveOut device
///
public void Stop()
{
if (playbackState != PlaybackState.Stopped)
{
// in the call to waveOutReset with function callbacks
// some drivers will block here until OnDone is called
// for every buffer
playbackState = PlaybackState.Stopped; // set this here to avoid a problem with some drivers whereby
MmResult result;
lock (waveOutLock)
{
result = WaveInterop.waveOutReset(hWaveOut);
}
if (result != MmResult.NoError)
{
throw new MmException(result, "waveOutReset");
}
callbackEvent.Set(); // give the thread a kick, make sure we exit
}
}
///
/// Gets the current position in bytes from the wave output device.
/// (n.b. this is not the same thing as the position within your reader
/// stream - it calls directly into waveOutGetPosition)
///
/// Position in bytes
public long GetPosition() => WaveOutUtils.GetPositionBytes(hWaveOut, waveOutLock);
///
/// Gets a instance indicating the format the hardware is using.
///
public WaveFormat OutputWaveFormat => waveStream.WaveFormat;
///
/// Playback State
///
public PlaybackState PlaybackState => playbackState;
///
/// Volume for this device 1.0 is full scale
///
public float Volume
{
get => WaveOutUtils.GetWaveOutVolume(hWaveOut, waveOutLock);
set => WaveOutUtils.SetWaveOutVolume(value, hWaveOut, waveOutLock);
}
#region Dispose Pattern
///
/// Closes this WaveOut device
///
public void Dispose()
{
GC.SuppressFinalize(this);
Dispose(true);
}
///
/// Closes the WaveOut device and disposes of buffers
///
/// True if called from Dispose
protected void Dispose(bool disposing)
{
Stop();
if (disposing)
{
DisposeBuffers();
}
CloseWaveOut();
}
private void CloseWaveOut()
{
if (callbackEvent != null)
{
callbackEvent.Close();
callbackEvent = null;
}
lock (waveOutLock)
{
if (hWaveOut != IntPtr.Zero)
{
WaveInterop.waveOutClose(hWaveOut);
hWaveOut= IntPtr.Zero;
}
}
}
private void DisposeBuffers()
{
if (buffers != null)
{
foreach (var buffer in buffers)
{
buffer.Dispose();
}
buffers = null;
}
}
///
/// Finalizer. Only called when user forgets to call Dispose
///
~WaveOutEvent()
{
Dispose(false);
Debug.Assert(false, "WaveOutEvent device was not closed");
}
#endregion
private void RaisePlaybackStoppedEvent(Exception e)
{
var handler = PlaybackStopped;
if (handler != null)
{
if (syncContext == null)
{
handler(this, new StoppedEventArgs(e));
}
else
{
syncContext.Post(state => handler(this, new StoppedEventArgs(e)), null);
}
}
}
}
}