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