/NAudio/Wave/WaveInputs/WasapiCapture.cs

# · C# · 342 lines · 243 code · 38 blank · 61 comment · 32 complexity · 482cbc640710d5ffbd91627dd7efffd5 MD5 · raw file

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using NAudio.Wave;
  5. using System.Threading;
  6. using System.Diagnostics;
  7. using System.Runtime.InteropServices;
  8. // for consistency this should be in NAudio.Wave namespace, but left as it is for backwards compatibility
  9. namespace NAudio.CoreAudioApi
  10. {
  11. /// <summary>
  12. /// Audio Capture using Wasapi
  13. /// See http://msdn.microsoft.com/en-us/library/dd370800%28VS.85%29.aspx
  14. /// </summary>
  15. public class WasapiCapture : IWaveIn
  16. {
  17. private const long REFTIMES_PER_SEC = 10000000;
  18. private const long REFTIMES_PER_MILLISEC = 10000;
  19. private volatile bool requestStop;
  20. private byte[] recordBuffer;
  21. private Thread captureThread;
  22. private AudioClient audioClient;
  23. private int bytesPerFrame;
  24. private WaveFormat waveFormat;
  25. private bool initialized;
  26. private readonly SynchronizationContext syncContext;
  27. private readonly bool isUsingEventSync;
  28. private EventWaitHandle frameEventWaitHandle;
  29. /// <summary>
  30. /// Indicates recorded data is available
  31. /// </summary>
  32. public event EventHandler<WaveInEventArgs> DataAvailable;
  33. /// <summary>
  34. /// Indicates that all recorded data has now been received.
  35. /// </summary>
  36. public event EventHandler<StoppedEventArgs> RecordingStopped;
  37. /// <summary>
  38. /// Initialises a new instance of the WASAPI capture class
  39. /// </summary>
  40. public WasapiCapture() :
  41. this(GetDefaultCaptureDevice())
  42. {
  43. }
  44. /// <summary>
  45. /// Initialises a new instance of the WASAPI capture class
  46. /// </summary>
  47. /// <param name="captureDevice">Capture device to use</param>
  48. public WasapiCapture(MMDevice captureDevice)
  49. : this(captureDevice, false)
  50. {
  51. }
  52. /// <summary>
  53. /// Initializes a new instance of the <see cref="WasapiCapture"/> class.
  54. /// </summary>
  55. /// <param name="captureDevice">The capture device.</param>
  56. /// <param name="useEventSync">true if sync is done with event. false use sleep.</param>
  57. public WasapiCapture(MMDevice captureDevice, bool useEventSync)
  58. {
  59. syncContext = SynchronizationContext.Current;
  60. audioClient = captureDevice.AudioClient;
  61. ShareMode = AudioClientShareMode.Shared;
  62. isUsingEventSync = useEventSync;
  63. waveFormat = audioClient.MixFormat;
  64. }
  65. /// <summary>
  66. /// Share Mode - set before calling StartRecording
  67. /// </summary>
  68. public AudioClientShareMode ShareMode { get; set; }
  69. /// <summary>
  70. /// Recording wave format
  71. /// </summary>
  72. public virtual WaveFormat WaveFormat
  73. {
  74. get
  75. {
  76. // for convenience, return a WAVEFORMATEX, instead of the real
  77. // WAVEFORMATEXTENSIBLE being used
  78. var wfe = waveFormat as WaveFormatExtensible;
  79. if (wfe != null)
  80. {
  81. try
  82. {
  83. return wfe.ToStandardWaveFormat();
  84. }
  85. catch (InvalidOperationException)
  86. {
  87. // couldn't convert to a standard format
  88. }
  89. }
  90. return waveFormat;
  91. }
  92. set { waveFormat = value; }
  93. }
  94. /// <summary>
  95. /// Gets the default audio capture device
  96. /// </summary>
  97. /// <returns>The default audio capture device</returns>
  98. public static MMDevice GetDefaultCaptureDevice()
  99. {
  100. var devices = new MMDeviceEnumerator();
  101. return devices.GetDefaultAudioEndpoint(DataFlow.Capture, Role.Console);
  102. }
  103. private void InitializeCaptureDevice()
  104. {
  105. if (initialized)
  106. return;
  107. long requestedDuration = REFTIMES_PER_MILLISEC * 100;
  108. if (!audioClient.IsFormatSupported(ShareMode, waveFormat))
  109. {
  110. throw new ArgumentException("Unsupported Wave Format");
  111. }
  112. var streamFlags = GetAudioClientStreamFlags();
  113. // If using EventSync, setup is specific with shareMode
  114. if (isUsingEventSync)
  115. {
  116. // Init Shared or Exclusive
  117. if (ShareMode == AudioClientShareMode.Shared)
  118. {
  119. // With EventCallBack and Shared, both latencies must be set to 0
  120. audioClient.Initialize(ShareMode, AudioClientStreamFlags.EventCallback, requestedDuration, 0,
  121. this.waveFormat, Guid.Empty);
  122. }
  123. else
  124. {
  125. // With EventCallBack and Exclusive, both latencies must equals
  126. audioClient.Initialize(ShareMode, AudioClientStreamFlags.EventCallback, requestedDuration, requestedDuration,
  127. this.waveFormat, Guid.Empty);
  128. }
  129. // Create the Wait Event Handle
  130. frameEventWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset);
  131. audioClient.SetEventHandle(frameEventWaitHandle.SafeWaitHandle.DangerousGetHandle());
  132. }
  133. else
  134. {
  135. // Normal setup for both sharedMode
  136. audioClient.Initialize(ShareMode,
  137. streamFlags,
  138. requestedDuration,
  139. 0,
  140. this.waveFormat,
  141. Guid.Empty);
  142. }
  143. int bufferFrameCount = audioClient.BufferSize;
  144. this.bytesPerFrame = this.waveFormat.Channels * this.waveFormat.BitsPerSample / 8;
  145. this.recordBuffer = new byte[bufferFrameCount * bytesPerFrame];
  146. Debug.WriteLine(string.Format("record buffer size = {0}", this.recordBuffer.Length));
  147. initialized = true;
  148. }
  149. /// <summary>
  150. /// To allow overrides to specify different flags (e.g. loopback)
  151. /// </summary>
  152. protected virtual AudioClientStreamFlags GetAudioClientStreamFlags()
  153. {
  154. return AudioClientStreamFlags.None;
  155. }
  156. /// <summary>
  157. /// Start Recording
  158. /// </summary>
  159. public void StartRecording()
  160. {
  161. if (captureThread != null)
  162. {
  163. throw new InvalidOperationException("Previous recording still in progress");
  164. }
  165. InitializeCaptureDevice();
  166. ThreadStart start = () => CaptureThread(this.audioClient);
  167. this.captureThread = new Thread(start);
  168. Debug.WriteLine("Thread starting...");
  169. this.requestStop = false;
  170. this.captureThread.Start();
  171. }
  172. /// <summary>
  173. /// Stop Recording (requests a stop, wait for RecordingStopped event to know it has finished)
  174. /// </summary>
  175. public void StopRecording()
  176. {
  177. this.requestStop = true;
  178. }
  179. private void CaptureThread(AudioClient client)
  180. {
  181. Exception exception = null;
  182. try
  183. {
  184. DoRecording(client);
  185. }
  186. catch (Exception e)
  187. {
  188. exception = e;
  189. }
  190. finally
  191. {
  192. client.Stop();
  193. // don't dispose - the AudioClient only gets disposed when WasapiCapture is disposed
  194. }
  195. captureThread = null;
  196. RaiseRecordingStopped(exception);
  197. Debug.WriteLine("Stop wasapi");
  198. }
  199. private void DoRecording(AudioClient client)
  200. {
  201. Debug.WriteLine(String.Format("Client buffer frame count: {0}", client.BufferSize));
  202. int bufferFrameCount = client.BufferSize;
  203. // Calculate the actual duration of the allocated buffer.
  204. long actualDuration = (long)((double)REFTIMES_PER_SEC *
  205. bufferFrameCount / waveFormat.SampleRate);
  206. int sleepMilliseconds = (int)(actualDuration / REFTIMES_PER_MILLISEC / 2);
  207. int waitMilliseconds = (int)(3 * actualDuration / REFTIMES_PER_MILLISEC);
  208. AudioCaptureClient capture = client.AudioCaptureClient;
  209. client.Start();
  210. if (isUsingEventSync)
  211. {
  212. Debug.WriteLine(string.Format("wait: {0} ms", waitMilliseconds));
  213. }
  214. else
  215. {
  216. Debug.WriteLine(string.Format("sleep: {0} ms", sleepMilliseconds));
  217. }
  218. while (!this.requestStop)
  219. {
  220. bool readBuffer = true;
  221. if (isUsingEventSync)
  222. {
  223. readBuffer = frameEventWaitHandle.WaitOne(waitMilliseconds, false);
  224. }
  225. else
  226. {
  227. Thread.Sleep(sleepMilliseconds);
  228. }
  229. // If still playing and notification is ok
  230. if (!this.requestStop && readBuffer)
  231. {
  232. ReadNextPacket(capture);
  233. }
  234. }
  235. }
  236. private void RaiseRecordingStopped(Exception e)
  237. {
  238. var handler = RecordingStopped;
  239. if (handler == null) return;
  240. if (this.syncContext == null)
  241. {
  242. handler(this, new StoppedEventArgs(e));
  243. }
  244. else
  245. {
  246. this.syncContext.Post(state => handler(this, new StoppedEventArgs(e)), null);
  247. }
  248. }
  249. private void ReadNextPacket(AudioCaptureClient capture)
  250. {
  251. int packetSize = capture.GetNextPacketSize();
  252. int recordBufferOffset = 0;
  253. //Debug.WriteLine(string.Format("packet size: {0} samples", packetSize / 4));
  254. while (packetSize != 0)
  255. {
  256. int framesAvailable;
  257. AudioClientBufferFlags flags;
  258. IntPtr buffer = capture.GetBuffer(out framesAvailable, out flags);
  259. int bytesAvailable = framesAvailable * bytesPerFrame;
  260. // apparently it is sometimes possible to read more frames than we were expecting?
  261. // fix suggested by Michael Feld:
  262. int spaceRemaining = Math.Max(0, recordBuffer.Length - recordBufferOffset);
  263. if (spaceRemaining < bytesAvailable && recordBufferOffset > 0)
  264. {
  265. if (DataAvailable != null) DataAvailable(this, new WaveInEventArgs(recordBuffer, recordBufferOffset));
  266. recordBufferOffset = 0;
  267. }
  268. // if not silence...
  269. if ((flags & AudioClientBufferFlags.Silent) != AudioClientBufferFlags.Silent)
  270. {
  271. Marshal.Copy(buffer, recordBuffer, recordBufferOffset, bytesAvailable);
  272. }
  273. else
  274. {
  275. Array.Clear(recordBuffer, recordBufferOffset, bytesAvailable);
  276. }
  277. recordBufferOffset += bytesAvailable;
  278. capture.ReleaseBuffer(framesAvailable);
  279. packetSize = capture.GetNextPacketSize();
  280. }
  281. if (DataAvailable != null)
  282. {
  283. DataAvailable(this, new WaveInEventArgs(recordBuffer, recordBufferOffset));
  284. }
  285. }
  286. /// <summary>
  287. /// Dispose
  288. /// </summary>
  289. public void Dispose()
  290. {
  291. StopRecording();
  292. if (captureThread != null)
  293. {
  294. captureThread.Join();
  295. captureThread = null;
  296. }
  297. if (audioClient != null)
  298. {
  299. audioClient.Dispose();
  300. audioClient = null;
  301. }
  302. }
  303. }
  304. }