Implemented Loopback capturing for NAudio

Mar 2, 2010 at 10:19 PM
Edited Mar 6, 2012 at 7:43 AM

I've sent Mark a mail, but I figured it wouldn't harm to drop it here in the discussions as well since I saw posts on his blog of other people wanting to do loopback-capturing.
I've implemented a Wasapi loopback capture class for NAudio, look down for code.

To use in NAudioDemo, modify RecordingForm.cs to use :

MMDeviceCollection deviceCol = deviceEnum.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active);

and

waveIn = new WasapiLoopbackCapture((MMDevice)comboDevices.SelectedItem);

I hope it's of use to anyone.
Have fun,

Lennart Denninger.

 

---<Cut Here>---

/*
  LICENSE
  -------
  Copyright (C) 2007 Ray Molenkamp
 
  Windows Vista / Windows 7 Loopback implementation by
  L.Denninger
 
  Loopback allows to capture all audio played by Windows.
  Basicly it's a software implementation of the "Record from stereomix" functionality,
  that some audiodrivers don't seem to supply anymore.

  This source code is provided 'as-is', without any express or implied
  warranty.  In no event will the authors be held liable for any damages
  arising from the use of this source code or the software it produces.

  Permission is granted to anyone to use this source code for any purpose,
  including commercial applications, and to alter it and redistribute it
  freely, subject to the following restrictions:

  1. The origin of this source code must not be misrepresented; you must not
     claim that you wrote the original source code.  If you use this source code
     in a product, an acknowledgment in the product documentation would be
     appreciated but is not required.
  2. Altered source versions must be plainly marked as such, and must not be
     misrepresented as being the original source code.
  3. This notice may not be removed or altered from any source distribution.
*/

using System;
using System.Collections.Generic;
using System.Text;

using NAudio.Wave;
using System.Threading;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace NAudio.CoreAudioApi
{
    /// <summary>
    /// Audio Capture using Wasapi
    /// See http://msdn.microsoft.com/en-us/library/dd370800%28VS.85%29.aspx
    /// </summary>
    public class WasapiLoopbackCapture : IWaveIn
    {
        private const long REFTIMES_PER_SEC = 10000000;
        private const long REFTIMES_PER_MILLISEC = 10000;
        private volatile bool stop;
        private byte[] recordBuffer;
        private Thread captureThread;
        private AudioClient audioClient;
        private int bytesPerFrame;

        /// <summary>
        /// Indicates recorded data is available 
        /// </summary>
        public event EventHandler<WaveInEventArgs> DataAvailable;

        /// <summary>
        /// Indicates that all recorded data has now been received.
        /// </summary>
        public event EventHandler RecordingStopped;

        /// <summary>
        /// Initialises a new instance of the WASAPI capture class
        /// </summary>
        public WasapiLoopbackCapture() :
            this(GetDefaultCaptureDevice())
        {
        }

        /// <summary>
        /// Initialises a new instance of the WASAPI capture class
        /// </summary>
        /// <param name="captureDevice">Capture device to use</param>
        public WasapiLoopbackCapture(MMDevice captureDevice)
        {
            this.audioClient = captureDevice.AudioClient;
        }

        /// <summary>
        /// Recording wave format
        /// </summary>
        public WaveFormat WaveFormat
        {
            get
            {
                return audioClient.MixFormat;
            }
            set
            {
                throw new Exception("Setting of Wave Format not supported for loopback device !");
            }
        }

        /// <summary>
        /// Gets the default audio capture device
        /// </summary>
        /// <returns>The default audio capture device</returns>
        public static MMDevice GetDefaultCaptureDevice()
        {
            MMDeviceEnumerator devices = new MMDeviceEnumerator();
            return devices.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
        }

        private void InitializeCaptureDevice()
        {
            long requestedDuration = REFTIMES_PER_MILLISEC * 100;

            audioClient.Initialize(AudioClientShareMode.Shared,
                AudioClientStreamFlags.Loopback,
                requestedDuration,
                0,
                WaveFormat,
                Guid.Empty);

            int bufferFrameCount = audioClient.BufferSize;
            bytesPerFrame = WaveFormat.BlockAlign;
            recordBuffer = new byte[bufferFrameCount * bytesPerFrame];
            Debug.WriteLine(string.Format("record buffer size = {0}", recordBuffer.Length));
        }

        /// <summary>
        /// Start Recording
        /// </summary>
        public void StartRecording()
        {
            InitializeCaptureDevice();
            ThreadStart start = delegate { this.CaptureThread(this.audioClient); };
            this.captureThread = new Thread(start);

            Debug.WriteLine("Thread starting...");
            this.stop = false;
            this.captureThread.Start();
        }

        /// <summary>
        /// Stop Recording
        /// </summary>
        public void StopRecording()
        {
            if (this.captureThread != null)
            {
                this.stop = true;

                Debug.WriteLine("Thread ending...");

                // wait for thread to end
                this.captureThread.Join();
                this.captureThread = null;

                Debug.WriteLine("Done.");

                this.stop = false;
            }
        }

        private void CaptureThread(AudioClient client)
        {
            Debug.WriteLine(client.BufferSize);
            int bufferFrameCount = audioClient.BufferSize;

            // Calculate the actual duration of the allocated buffer.
            long actualDuration = (long)((double)REFTIMES_PER_SEC *
                             bufferFrameCount / WaveFormat.SampleRate);
            int sleepMilliseconds = (int)(actualDuration / REFTIMES_PER_MILLISEC / 2);

            AudioCaptureClient capture = client.AudioCaptureClient;
            client.Start();

            try
            {
                Debug.WriteLine(string.Format("sleep: {0} ms", sleepMilliseconds));
                while (!this.stop)
                {
                    Thread.Sleep(sleepMilliseconds);
                    ReadNextPacket(capture);
                }

                client.Stop();

                if (RecordingStopped != null)
                {
                    RecordingStopped(this, EventArgs.Empty);
                }
            }
            finally
            {
                if (capture != null)
                {
                    capture.Dispose();
                }
                if (client != null)
                {
                    client.Dispose();
                }

                client = null;
                capture = null;
            }

            System.Diagnostics.Debug.WriteLine("stop wasapi");
        }

        private void ReadNextPacket(AudioCaptureClient capture)
        {
            IntPtr buffer;
            int framesAvailable;
            AudioClientBufferFlags flags;
            int packetSize = capture.GetNextPacketSize();
            int recordBufferOffset = 0;
            //Debug.WriteLine(string.Format("packet size: {0} samples", packetSize / 4));

            while (packetSize != 0)
            {
                buffer = capture.GetBuffer(out framesAvailable, out flags);

                int bytesAvailable = framesAvailable * bytesPerFrame;

                //Debug.WriteLine(string.Format("got buffer: {0} frames", framesAvailable));

                // 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();
            }
            if (DataAvailable != null)
            {
                DataAvailable(this, new WaveInEventArgs(recordBuffer, recordBufferOffset));
            }
        }

        /// <summary>
        /// Dispose
        /// </summary>
        public void Dispose()
        {
            StopRecording();
        }
    }
}

Sep 18, 2010 at 1:37 PM

This is great!  Just what I was looking for.  I have implemented it in my own program and it is working.  One question.  If no sound is playing in the speakers the resulting wave file is only 80 bytes long (wav header and nothing else).  If I then start playing sound, then and only then does it record sound.  Is this a feature or a bug? :)

 

Thanks

Sep 18, 2010 at 4:53 PM

Also I'm trying to convert the resulting wav file into mp3.  I'm using a c# wrapper for lame_enc I found (http://www.codeproject.com/KB/audio-video/MP3Compressor.aspx) but it doesn't seem to want the wave files generated by the loopback capture.

I'm thinking it has something to do with the 32bit wave files generated but I'm not sure.  Is there any way to adjust the format of the wave file capture, play around with the resulting bit rate (bit, khz, channels etc)?

Sep 18, 2010 at 5:31 PM

I probably found the root of the silence issue, seems like a nice feature:

// if not silence...                

if ((flags & AudioClientBufferFlags.Silent) != AudioClientBufferFlags.Silent)

 

Still trying to figure out why lame won't accept the output wave

Dec 8, 2010 at 5:19 PM

I'm having issues with buffer overruns in above code. The following proposal does not catch all possibilities, but I could not experience any issues any more.

It would be great, if someone could direct me on the right way, to create a stream compatible for DLNA "http-get:*:audio/L16;rate=44100;channels=2:DLNA.ORG_PN=LPCM" from the captured data.

 

                while (!this.stop)
                {
                    if (!ReadNextPacket(capture))
                        Thread.Sleep(sleepMilliseconds);
                }

        private bool ReadNextPacket(AudioCaptureClient capture)
        {
            bool moreData = false;
            IntPtr buffer;
            int framesAvailable;
            AudioClientBufferFlags flags;
            int packetSize = capture.GetNextPacketSize();
            int recordBufferOffset = 0;
            //Debug.WriteLine(string.Format("packet size: {0} samples", packetSize / 4));

            while (packetSize != 0)
            {
                buffer = capture.GetBuffer(out framesAvailable, out flags);

                int bytesAvailable = framesAvailable * bytesPerFrame;

                Debug.WriteLine(string.Format("got buffer: {0} frames; Offset={1}", framesAvailable, recordBufferOffset));

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

                if (recordBufferOffset >= recordBuffer.Length)
                {
                    Debug.WriteLine("would overflow");
                    moreData = true;
                    break;
                }

                packetSize = capture.GetNextPacketSize();
            }
            if (DataAvailable != null)
            {
                DataAvailable(this, new WaveInEventArgs(recordBuffer, recordBufferOffset));
            }

            return moreData;
        }

Thanks,
Steffen 

Dec 15, 2010 at 3:09 PM

Allskoner,

I am trying to do the same thing as you in regards to converting to mp3 stream.  I then send it to a device that is waiting for mp3 files to plays.  The device doesn't play anything.  Have you resolved you issue?  Any help would be greatly appreciated.

Thanks

Mike

Jan 18, 2011 at 1:43 PM

Hello!

Has anyone figured out how to capture the loopback in 16 bit? I'm trying to find a way to convert the 'recordBuffer' to 16 bit on the fly. Any help is really appreciated!

 

Thanks

R.

Coordinator
Jan 18, 2011 at 4:06 PM

WASAPI won't let you capture in 16 bit - you'll have to convert it yourself. Every four bytes captured should be turned into a float sample value in the range -1 to 1. Then multiply by 32768 and cast to a short to get a 16 bit integer sample value.

Jan 19, 2011 at 1:14 AM
Edited Jan 19, 2011 at 1:51 AM

Thanks for taking the time to answer my question Mark! That was exactly what I needed! It works beautifully!

Your help is greatly appreciated!

R.

Mar 5, 2011 at 2:48 AM

The WasapiLoopbackCapture did the trick for me too, as long as I play it with Windows Media Player.

If I play it with the NAudio WaveOut demo app, an exception is thrown in AcmStream.SuggestPcmFormat. "NoDriver calling acmFormatSugget".

NAudio is well designed, which is such a nice change from the C++ codebase I was experimenting with previously.

Coordinator
Mar 7, 2011 at 4:16 PM

what format are you trying to convert from and to? The no driver error means there is no ACM codec installed on your machine that can do the requested conversion?

Mark

Coordinator
Dec 26, 2011 at 9:51 AM

by the way, WasapiLoopbackCapture is now part of NAudio in the latest code (for NAudio 1.6)

Feb 26, 2015 at 2:20 PM
I have lot of problem brohter pls help me. i add all above code in class and recieve build time error. pls guid me how i can start and stop recording from window form in c#???

Error 1 'NAudio.CoreAudioApi.WasapiLoopbackCapture' does not implement interface member 'NAudio.Wave.IWaveIn.RecordingStopped'. 'NAudio.CoreAudioApi.WasapiLoopbackCapture.RecordingStopped' cannot implement 'NAudio.Wave.IWaveIn.RecordingStopped' because it does not have the matching return type of 'System.EventHandler<NAudio.Wave.StoppedEventArgs>'. E:\Student\std\proj\WMP\WMP\WMP\WasapiLoopbackCapture.cs 17 18 WMP
Coordinator
Feb 27, 2015 at 6:37 AM
You should be referencing the latest version of NAudio rather than creating your own copy of WasapiLoopbackCapture.
Feb 27, 2015 at 10:04 AM
Hi markhealth thnk you . i am making a c# .net window application. add reference of 1.7.2.19 version. i want to record what i hear. can you guid me pls how i start,stop and save on hard drive in window form using C# ??? thnk you
Coordinator
Mar 2, 2015 at 4:00 PM
Yes, use WasapiLoopbackCapture to capture the audio, and WaveFileWriter to save it to disk. Look at the NAudio demo application source code if you want to see examples of how to do this.