mirror of https://github.com/naudio/NAudio.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
355 lines
11 KiB
355 lines
11 KiB
using System;
|
|
using System.Diagnostics;
|
|
using System.Threading;
|
|
using System.Runtime.InteropServices;
|
|
|
|
// ReSharper disable once CheckNamespace
|
|
namespace NAudio.Wave
|
|
{
|
|
/// <summary>
|
|
/// Alternative WaveOut class, making use of the Event callback
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Indicates playback has stopped automatically
|
|
/// </summary>
|
|
public event EventHandler<StoppedEventArgs> PlaybackStopped;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the desired latency in milliseconds
|
|
/// Should be set before a call to Init
|
|
/// </summary>
|
|
public int DesiredLatency { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the number of buffers used
|
|
/// Should be set before a call to Init
|
|
/// </summary>
|
|
public int NumberOfBuffers { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the device number
|
|
/// Should be set before a call to Init
|
|
/// This must be between -1 and <see>DeviceCount</see> - 1.
|
|
/// -1 means stick to default device even default device is changed
|
|
/// </summary>
|
|
public int DeviceNumber { get; set; } = -1;
|
|
|
|
/// <summary>
|
|
/// Opens a WaveOut device
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initialises the WaveOut device
|
|
/// </summary>
|
|
/// <param name="waveProvider">WaveProvider to play</param>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Start playing the audio from the WaveStream
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pause the audio
|
|
/// </summary>
|
|
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");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resume playing after a pause from the same position
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stop and reset the WaveOut device
|
|
/// </summary>
|
|
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
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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)
|
|
/// </summary>
|
|
/// <returns>Position in bytes</returns>
|
|
public long GetPosition() => WaveOutUtils.GetPositionBytes(hWaveOut, waveOutLock);
|
|
|
|
/// <summary>
|
|
/// Gets a <see cref="Wave.WaveFormat"/> instance indicating the format the hardware is using.
|
|
/// </summary>
|
|
public WaveFormat OutputWaveFormat => waveStream.WaveFormat;
|
|
|
|
/// <summary>
|
|
/// Playback State
|
|
/// </summary>
|
|
public PlaybackState PlaybackState => playbackState;
|
|
|
|
/// <summary>
|
|
/// Volume for this device 1.0 is full scale
|
|
/// </summary>
|
|
public float Volume
|
|
{
|
|
get => WaveOutUtils.GetWaveOutVolume(hWaveOut, waveOutLock);
|
|
set => WaveOutUtils.SetWaveOutVolume(value, hWaveOut, waveOutLock);
|
|
}
|
|
|
|
#region Dispose Pattern
|
|
|
|
/// <summary>
|
|
/// Closes this WaveOut device
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
GC.SuppressFinalize(this);
|
|
Dispose(true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closes the WaveOut device and disposes of buffers
|
|
/// </summary>
|
|
/// <param name="disposing">True if called from <see>Dispose</see></param>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finalizer. Only called when user forgets to call <see>Dispose</see>
|
|
/// </summary>
|
|
~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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|