Draining the WASAPI capture buffer fully


About six years ago I wrote a blog post about how to do WASAPI loopback capture. Since then, a few issues have come to light.

One big issue is that we’re using MMCSS registration directly. Nowadays the much-preferred approach is to use a Media Foundation work queue; yes, you can use a Media Foundation work queue even if you’re doing WASAPI directly. The official WASAPI sample demonstrates how to use this approach.

Another big issue is that the code isn’t draining the buffer fully; it assumes there is only a single packet per engine period. Here’s the capture loop as it stands:

for (…) {
    Wait();
    if (data available) { ReadAPacket(); }
}

There are two reasons this is a problem. First, if you miss a period, you will never catch up. Second, the engine is free to write many small packets into the buffer if it likes.

The code SHOULD look like this:

for (…) {
    while (data available) { ReadAPacket(); }
    Wait();
}

Updated source and binaries attached.

EDIT September 22 2015: moved source to github https://github.com/mvaneerde/blog/tree/master/loopback-capture

loopback-capture.zip

Comments (11)

  1. Lukas says:

    You are a complete boss!!

    I've been scavenging the internet looking for information on this for quite a while now.

    YOU'RE THE BOMB!

  2. asteven says:

    when I stop playing why it still capture

  3. asteven says:

    in win7 after capture for few minits the flag was set to AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY

    do you have any solutions,I'm in china and my english is poor my email:8569504@qq.com

    thinks

  4. Sunny says:

    Hi. Just wondering is this open source? I'm looking for something like this for my school project.

  5. @Sunny do with the source as you like.

  6. zhengfish says:

    I failed to compile/build this example code.

    Could you please help to tell me how to build it? use what compiler/WDK?

    my error message is as below?

    path contains nonexistant c:userssamappdataroamingpythonscripts, removing

    BUILD: Compile and Link for x86

    BUILD: Loading c:winddk7600.16385.1build.dat…

    BUILD: Computing Include file dependencies:

    BUILD: Start time: Sat Aug 01 16:24:47 2015

    BUILD: Examining e:studytemploopback-capturesource directory for files to compile.

       e:studytemploopback-capturesource Invalidating OACR warning log for 'root:x86chk'

    BUILD: Saving c:winddk7600.16385.1build.dat…

    BUILD: Compiling and Linking e:studytemploopback-capturesource directory

    Configuring OACR for 'root:x86chk' – <OACR on>

    Compiling – guid.cpp

    Compiling – main.cpp

    Compiling – loopback-capture.cpp

    1>errors in directory e:studytemploopback-capturesource

    1>e:studytemploopback-capturesourceloopback-capture.cpp(6) : error C1083: Cannot open include file: 'audioclie

    nt.h': No such file or directory

    Compiling – prefs.cpp

    Compiling – generating code…

    1>NMAKE : fatal error U1073: don't know how to make 'C:WinDDK7600.16385.1libwin7i386avrt.lib'

    1>nmake.exe /nologo BUILDMSG=Stop. -i /nologo /f c:winddk7600.16385.1binmakefile.def BUILD_PASS=PASS2 LINKONLY=

    1 NOPASS0=1 MAKEDIR_RELATIVE_TO_BASEDIR= failed – rc = 2

    BUILD: Finish time: Sat Aug 01 16:24:51 2015

    BUILD: Done

       6 files compiled – 1 Warning – 3 Errors – 1,114 LPS

    e:studytemploopback-capturesource>

  7. > Cannot open include file: 'audioclient.h': No such file or directory

    audioclient.h ships as part of the SDK (not the WDK.)

  8. zhengfish says:

    Yes, I installed the MicroSoft SDK as below:

    c:Program Files (x86)Microsoft SDKsWindowsv7.1AIncludeAudioclient.h

    How can I build your example code? use Visual Studio? or WDK?

    If I use WDK, it will give me this error:

    C:temp4loopback-capturesource>build

    path contains nonexistant c:appsactivepython34, removing

    path contains nonexistant c:appsactivepython34scripts, removing

    path contains nonexistant c:appsanaconda3, removing

    path contains nonexistant c:appsanaconda3scripts, removing

    path contains nonexistant c:userscz1936appdataroamingpythonscripts, removing

    path contains nonexistant c:program files (x86)dbankclickup, removing

    path contains nonexistant c:klocworkinsight_10.1_command_linebin, removing

    BUILD: Compile and Link for x86

    BUILD: Loading c:winddk7600.16385.1build.dat…

    BUILD: Computing Include file dependencies:

    BUILD: Start time: Tue Aug 04 13:07:00 2015

    BUILD: Examining c:temp4loopback-capturesource directory for files to compile.

       c:temp4loopback-capturesource Invalidating OACR warning log for 'root:x86chk'

    BUILD: Saving c:winddk7600.16385.1build.dat…

    BUILD: Compiling and Linking c:temp4loopback-capturesource directory

    Configuring OACR for 'root:x86chk' – <OACR on>

    Compiling – guid.cpp

    Compiling – main.cpp

    Compiling – loopback-capture.cpp

    1>errors in directory c:temp4loopback-capturesource

    1>c:temp4loopback-capturesourceloopback-capture.cpp(6) : error C1083: Cannot open include file: 'audioclient.h': No such file or directory

    Compiling – prefs.cpp

    Compiling – generating code…

    1>NMAKE : fatal error U1073: don't know how to make 'C:WinDDK7600.16385.1libwin7i386avrt.lib'

    1>nmake.exe /nologo BUILDMSG=Stop. -i /nologo /f c:winddk7600.16385.1binmakefile.def BUILD_PASS=PASS2 LINKONLY=1 NOPASS0=1 MAKEDIR_RELATIVE_TO_BASEDIR= failed – rc = 2

    BUILD: Finish time: Tue Aug 04 13:07:14 2015

    BUILD: Done

       6 files compiled – 7 Warnings – 3 Errors – 1,114 LPS

  9. Utkarsh says:

    Hi,

    I am trying to capture an audio stream like you do and as explained at https://msdn.microsoft.com/en-us/library/windows/desktop/dd370800%28v=vs.85%29.aspx, but I am facing some issues. As per https://msdn.microsoft.com/en-us/library/windows/desktop/dd370872%28v=vs.85%29.aspx, “When creating a shared-mode stream for an audio endpoint device, the Initialize method always accepts the stream format obtained from a GetMixFormat call on the same device”, but I am observing a different behavior. When I call GetMixFormat, I get the format with following properties:

    Format tag: 0xfffe, channels: 0x0008, sampling rate: 0x0000bb80, bytes per sec: 0x000bb800, block align: 0x0010, bits per sample: 0x0010

    If I pass the same format to the Initialize call, it fails with the AUDCLNT_E_UNSUPPORTED_FORMAT error. I have noticed that although the GetMixFormat function returns 8 channels, the actual audio hardware supports only 4 channel audio. One more interesting observation is that this happens only when Nahimic software is enabled on the MSI notebook, and if that software is disabled/uninstalled, then GetMixFormat returns only 2 channels and Initialize call also succeeds with that format.

    I want my application to work even when Nahimic is enabled. If I send 2 channels (and update the block align and other fields accordingly) in the format call sent to Initialize, then it succeeds but I am unable to capture any audio. What should be the right way to capture audio when Nahimic is enabled?

    Thanks.

    1. > Format tag: 0xfffe

      This is WAVE_FORMAT_EXTENSIBLE. Can you cast the pointer to WAVEFORMATEXTENSIBLE* and log the rest of the members too?

  10. Rama says:

    Thank You Matthew!

    Dear Matthew,

    I just wanted to thank you so much for providing this sample code on github, I was able to compile on the first try.

    I wanted to give back to you in some way, and the community, so I hope my code offering here is seen as a very respective gift.

    I had to modify only a small amount of code in order to prevent two issues from occurring:

    1. The .exe ending prematurely when no sound is playing, which makes it the responsability of the calling code to know if there truly is active audio incoming or if the loopback-capture really should exit.

    2. The occasional occurrence of the flag AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY or the flag AUDCLNT_BUFFERFLAGS_SILENT causing an early exit and without the wave file being fully created.

    Again with these small changes your awesome offering, Matthew, becomes industry-standard WASAPI loopback code immediately. It already is of course, but it becomes easy for new users to implement into their audio projects :)

    Please note I printed out the actual error names to make it easy for inexperienced developers to know what to look up on MSDN if they get one of these errors, rather than a numeric error code which is harder to understand (at least it was for me)

    ~~~

    Very Slightly Modified Code So No Early Exit and no Exit on AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY

    In loopback-capture.cpp

    hr = pAudioCaptureClient->GetBuffer(
    &pData,
    &nNumFramesToRead,
    &dwFlags,
    NULL,
    NULL
    );
    if (FAILED(hr)) {
    ERR(L”IAudioCaptureClient::GetBuffer failed on pass %u after %u frames: hr = 0x%08x”, nPasses, *pnFrames, hr);
    if(hr == AUDCLNT_S_BUFFER_EMPTY) {
    ERR(L”GetBuffer Error is ~ AUDCLNT_S_BUFFER_EMPTY”);
    }
    else if(hr == AUDCLNT_E_BUFFER_ERROR) {
    ERR(L”GetBuffer Error is ~ AUDCLNT_E_BUFFER_ERROR”);
    }
    else if(hr == AUDCLNT_E_OUT_OF_ORDER) {
    ERR(L”GetBuffer Error is ~ AUDCLNT_E_OUT_OF_ORDER”);
    }
    else if(hr == AUDCLNT_E_DEVICE_INVALIDATED) {
    ERR(L”GetBuffer Error is ~ AUDCLNT_E_DEVICE_INVALIDATED”);
    }
    else if(hr == AUDCLNT_E_BUFFER_OPERATION_PENDING) {
    ERR(L”GetBuffer Error is ~ AUDCLNT_E_BUFFER_OPERATION_PENDING”);
    }
    else if(hr == AUDCLNT_E_SERVICE_NOT_RUNNING) {
    ERR(L”GetBuffer Error is ~ AUDCLNT_E_SERVICE_NOT_RUNNING”);
    }
    else if(hr == E_POINTER){
    ERR(L”GetBuffer Error is ~ E_POINTER”);
    }
    return hr;
    }

    if (bFirstPacket && AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY == dwFlags) {
    //LOG(L”%s”, L”Probably spurious glitch reported on first packet”);
    }

    else if (0 != dwFlags) {

    if(AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY == dwFlags)
    {
    LOG(L”Flag Discontinuity”);
    }
    else if(AUDCLNT_BUFFERFLAGS_SILENT == dwFlags)
    {
    LOG(L”Flag Silent”);
    }
    else if(AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR == dwFlags)
    {
    LOG(L”Time Stamp Error”);
    }
    else
    {
    LOG(L”Audio not started yet (or unknown error), waiting for 0.01 seconds”);
    Sleep(10);
    }
    //LOG(L”IAudioCaptureClient::GetBuffer set flags to 0x%08x on pass %u after %u frames”, dwFlags, nPasses, *pnFrames);
    //return E_UNEXPECTED;
    hr = pAudioCaptureClient->ReleaseBuffer(nNumFramesToRead);
    if (FAILED(hr)) {
    ERR(L”IAudioCaptureClient::ReleaseBuffer failed on pass %u after %u frames: hr = 0x%08x”, nPasses, *pnFrames, hr);
    return hr;
    }
    continue;
    }

    if (0 == nNumFramesToRead) {
    //…. rest of code as already perfect

    Code Modification Notes:
    Please note I still return an error if the buffer could not be released, which indicates a more fundamental problem than the more likely cases of silence or discontinuities which are unusual states that can be recovered from.

    I chose to continue rather than to pass through because Matthew’s awesome code is expecting valid data at that point and I’ve intercepted a reason for which the pass through should not occur.

    Further Analysis and Improvement:
    One thing to note, I am actually dropping the discontinuous frame entirely, I am not sure if that is a good idea or not. I don’t know if that actually helps “catch up” or simply is a loss of data or some combination of both, but in my tests it was inaudible whenever I got discontinuities, I got about 4 in one test with no auditory impact.

    Thank you again Matthew, and I hope my code offering is seen as helpful and does help people use your awesome handiwork to achieve their audio software dreams and goals

    Rama

Skip to main content