using System;
using System.Threading;
using System.Runtime.InteropServices;
using NAudio.Wave;
// for consistency this should be in NAudio.Wave namespace, but left as it is for backwards compatibility
// ReSharper disable once CheckNamespace
namespace NAudio.CoreAudioApi
{
///
/// Audio Capture using Wasapi
/// See http://msdn.microsoft.com/en-us/library/dd370800%28VS.85%29.aspx
///
public class WasapiCapture : IWaveIn
{
private const long ReftimesPerSec = 10000000;
private const long ReftimesPerMillisec = 10000;
private volatile CaptureState captureState;
private byte[] recordBuffer;
private Thread captureThread;
private AudioClient audioClient;
private int bytesPerFrame;
private WaveFormat waveFormat;
private bool initialized;
private readonly SynchronizationContext syncContext;
private readonly bool isUsingEventSync;
private EventWaitHandle frameEventWaitHandle;
private readonly int audioBufferMillisecondsLength;
///
/// Indicates recorded data is available
///
public event EventHandler DataAvailable;
///
/// Indicates that all recorded data has now been received.
///
public event EventHandler RecordingStopped;
///
/// Initialises a new instance of the WASAPI capture class
///
public WasapiCapture() :
this(GetDefaultCaptureDevice())
{
}
///
/// Initialises a new instance of the WASAPI capture class
///
/// Capture device to use
public WasapiCapture(MMDevice captureDevice)
: this(captureDevice, false)
{
}
///
/// Initializes a new instance of the class.
///
/// The capture device.
/// true if sync is done with event. false use sleep.
public WasapiCapture(MMDevice captureDevice, bool useEventSync)
: this(captureDevice, useEventSync, 100)
{
}
///
/// Initializes a new instance of the class.
///
/// The capture device.
/// true if sync is done with event. false use sleep.
/// Length of the audio buffer in milliseconds. A lower value means lower latency but increased CPU usage.
public WasapiCapture(MMDevice captureDevice, bool useEventSync, int audioBufferMillisecondsLength)
{
syncContext = SynchronizationContext.Current;
audioClient = captureDevice.AudioClient;
ShareMode = AudioClientShareMode.Shared;
isUsingEventSync = useEventSync;
this.audioBufferMillisecondsLength = audioBufferMillisecondsLength;
waveFormat = audioClient.MixFormat;
}
///
/// Share Mode - set before calling StartRecording
///
public AudioClientShareMode ShareMode { get; set; }
///
/// Current Capturing State
///
public CaptureState CaptureState { get { return captureState; } }
///
/// Capturing wave format
///
public virtual WaveFormat WaveFormat
{
get
{
// for convenience, return a WAVEFORMATEX, instead of the real
// WAVEFORMATEXTENSIBLE being used
return waveFormat.AsStandardWaveFormat();
}
set { waveFormat = value; }
}
///
/// Gets the default audio capture device
///
/// The default audio capture device
public static MMDevice GetDefaultCaptureDevice()
{
var devices = new MMDeviceEnumerator();
return devices.GetDefaultAudioEndpoint(DataFlow.Capture, Role.Console);
}
private void InitializeCaptureDevice()
{
if (initialized)
return;
long requestedDuration = ReftimesPerMillisec * audioBufferMillisecondsLength;
var streamFlags = GetAudioClientStreamFlags();
// If using EventSync, setup is specific with shareMode
if (isUsingEventSync)
{
// Init Shared or Exclusive
if (ShareMode == AudioClientShareMode.Shared)
{
// With EventCallBack and Shared, both latencies must be set to 0
audioClient.Initialize(ShareMode, AudioClientStreamFlags.EventCallback | streamFlags, requestedDuration, 0,
waveFormat, Guid.Empty);
}
else
{
// With EventCallBack and Exclusive, both latencies must equals
audioClient.Initialize(ShareMode, AudioClientStreamFlags.EventCallback | streamFlags, requestedDuration, requestedDuration,
waveFormat, Guid.Empty);
}
// Create the Wait Event Handle
frameEventWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset);
audioClient.SetEventHandle(frameEventWaitHandle.SafeWaitHandle.DangerousGetHandle());
}
else
{
// Normal setup for both sharedMode
audioClient.Initialize(ShareMode,
streamFlags,
requestedDuration,
0,
waveFormat,
Guid.Empty);
}
int bufferFrameCount = audioClient.BufferSize;
bytesPerFrame = waveFormat.Channels * waveFormat.BitsPerSample / 8;
recordBuffer = new byte[bufferFrameCount * bytesPerFrame];
//Debug.WriteLine(string.Format("record buffer size = {0}", this.recordBuffer.Length));
initialized = true;
}
///
/// To allow overrides to specify different flags (e.g. loopback)
///
protected virtual AudioClientStreamFlags GetAudioClientStreamFlags()
{
// enable auto-convert PCM
return AudioClientStreamFlags.AutoConvertPcm | AudioClientStreamFlags.SrcDefaultQuality;
}
///
/// Start Capturing
///
public void StartRecording()
{
if (captureState != CaptureState.Stopped)
{
throw new InvalidOperationException("Previous recording still in progress");
}
captureState = CaptureState.Starting;
InitializeCaptureDevice();
captureThread = new Thread(() => CaptureThread(audioClient))
{
IsBackground = true,
};
captureThread.Start();
}
///
/// Stop Capturing (requests a stop, wait for RecordingStopped event to know it has finished)
///
public void StopRecording()
{
if (captureState != CaptureState.Stopped)
captureState = CaptureState.Stopping;
}
private void CaptureThread(AudioClient client)
{
Exception exception = null;
try
{
DoRecording(client);
}
catch (Exception e)
{
exception = e;
}
finally
{
client.Stop();
// don't dispose - the AudioClient only gets disposed when WasapiCapture is disposed
}
captureThread = null;
captureState = CaptureState.Stopped;
RaiseRecordingStopped(exception);
}
private void DoRecording(AudioClient client)
{
//Debug.WriteLine(String.Format("Client buffer frame count: {0}", client.BufferSize));
int bufferFrameCount = client.BufferSize;
// Calculate the actual duration of the allocated buffer.
long actualDuration = (long)((double)ReftimesPerSec *
bufferFrameCount / waveFormat.SampleRate);
int sleepMilliseconds = (int)(actualDuration / ReftimesPerMillisec / 2);
int waitMilliseconds = (int)(3 * actualDuration / ReftimesPerMillisec);
var capture = client.AudioCaptureClient;
client.Start();
// avoid race condition where we stop immediately after starting
if (captureState == CaptureState.Starting)
{
captureState = CaptureState.Capturing;
}
while (captureState == CaptureState.Capturing)
{
if (isUsingEventSync)
{
frameEventWaitHandle.WaitOne(waitMilliseconds, false);
}
else
{
Thread.Sleep(sleepMilliseconds);
}
if (captureState != CaptureState.Capturing)
break;
// If still recording
ReadNextPacket(capture);
}
}
private void RaiseRecordingStopped(Exception e)
{
var handler = RecordingStopped;
if (handler == null) return;
if (syncContext == null)
{
handler(this, new StoppedEventArgs(e));
}
else
{
syncContext.Post(state => handler(this, new StoppedEventArgs(e)), null);
}
}
private void ReadNextPacket(AudioCaptureClient capture)
{
int packetSize = capture.GetNextPacketSize();
int recordBufferOffset = 0;
//Debug.WriteLine(string.Format("packet size: {0} samples", packetSize / 4));
while (packetSize != 0)
{
IntPtr buffer = capture.GetBuffer(out int framesAvailable, out AudioClientBufferFlags flags);
int bytesAvailable = framesAvailable * bytesPerFrame;
// apparently it is sometimes possible to read more frames than we were expecting?
// fix suggested by Michael Feld:
int spaceRemaining = Math.Max(0, recordBuffer.Length - recordBufferOffset);
if (spaceRemaining < bytesAvailable && recordBufferOffset > 0)
{
DataAvailable?.Invoke(this, new WaveInEventArgs(recordBuffer, recordBufferOffset));
recordBufferOffset = 0;
}
// if not silence...
if ((flags & AudioClientBufferFlags.Silent) != AudioClientBufferFlags.Silent)
{
Marshal.Copy(buffer, recordBuffer, recordBufferOffset, bytesAvailable);
}
else
{
Array.Clear(recordBuffer, recordBufferOffset, bytesAvailable);
}
recordBufferOffset += bytesAvailable;
capture.ReleaseBuffer(framesAvailable);
packetSize = capture.GetNextPacketSize();
}
DataAvailable?.Invoke(this, new WaveInEventArgs(recordBuffer, recordBufferOffset));
}
///
/// Dispose
///
public void Dispose()
{
StopRecording();
if (captureThread != null)
{
captureThread.Join();
captureThread = null;
}
if (audioClient != null)
{
audioClient.Dispose();
audioClient = null;
}
}
}
}