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