windowsaudiowasapi

Persistent audio discontinuity in Debug mode for WASAPI loopback capture


I am writing a program that captures the output on a Windows device using WASAPI loopback capture. In principle it works correctly, but it breaks whenever I try to debug it, after continuing from a breakpoint.

I can reproduce this in Windows' own example code: I'm using the CaptureSharedEventDriven sample.

Then I followed the instructions to change this demo to use loopback, which is simply:

This now correctly captures the audio output. However, when I breakpoint at the end of CWASAPICapture::Start(...) (line 262 in the sample) and then continue, the capture turns into rubbish from then onwards. The captured audio shows discontinuities every 1056 samples (this is also the buffer size of the IAudioClient) and misses 384 samples every iteration.

I can prove this by adding the following debug code:

[WASAPICapture.cpp, line 358]

hr = _CaptureClient->GetBuffer(&pData, &framesAvailable, &flags, NULL, NULL);
if (SUCCEEDED(hr))
{
   UINT32 framesToCopy = min(framesAvailable, static_cast<UINT32>((_CaptureBufferSize - _CurrentCaptureIndex) / _FrameSize));
   if (framesToCopy != 0)
   {
      //
      // Adding this in order to trace the output issue:
      //
      if (flags & AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY) {
         printf("Discontinuity detected, writing %d samples\n", framesAvailable);
      }
      else {
         printf("Correct render, writing %d samples\n", framesAvailable);
      }

After hitting the breakpoint, for the rest of the capture the output will now be:

Discontinuity detected, writing 480 samples
Correct render, writing 480 samples
Discontinuity detected, writing 96 samples
Discontinuity detected, writing 480 samples
Correct render, writing 480 samples
Discontinuity detected, writing 96 samples
Correct render, writing 480 samples
Correct render, writing 480 samples
Discontinuity detected, writing 96 samples
etc...

How do I make WASAPI recover from this error?


Solution

  • This MS sample contains a bug and misses a fundamental line of code. The correct version of the implementation can be found in a different sample (albeit in a slightly different use case): ApplicationLoopback

    A while-loop should be added around the GetBuffer() and ReleaseBuffer() code:

    while (SUCCEEDED(m_AudioCaptureClient->GetNextPacketSize(&FramesAvailable)) &&
           FramesAvailable > 0)   // <-- THIS LOOP SHOULD BE ADDED
    {
       hr = _CaptureClient->GetBuffer(&pData, &framesAvailable, &flags, NULL, NULL);
       if (SUCCEEDED(hr))
       {
          UINT32 framesToCopy = min(framesAvailable, static_cast<UINT32>((_CaptureBufferSize - _CurrentCaptureIndex) / _FrameSize));
           if (framesToCopy != 0)
           {
            .....
    

    In fact, this line in the ApplicationLoopback example is heavily commented with the explanation:

        // A word on why we have a loop here;
        // Suppose it has been 10 milliseconds or so since the last time
        // this routine was invoked, and that we're capturing 48000 samples per second.
        //
        // The audio engine can be reasonably expected to have accumulated about that much
        // audio data - that is, about 480 samples.
        //
        // However, the audio engine is free to accumulate this in various ways:
        // a. as a single packet of 480 samples, OR
        // b. as a packet of 80 samples plus a packet of 400 samples, OR
        // c. as 48 packets of 10 samples each.
        //
        // In particular, there is no guarantee that this routine will be
        // run once for each packet.
        //
        // So every time this routine runs, we need to read ALL the packets
        // that are now available;
        //
        // We do this by calling IAudioCaptureClient::GetNextPacketSize
        // over and over again until it indicates there are no more packets remaining.
    

    In summary: the audio engine is not required to give you nice packets of e.g. 480 samples and can give them in different chunks. For some reason, debug breakpointing the code gets it in such a state. Without this while-loop that checks for the GetNextPacketSize, it cannot recover from this.

    Adding this line to the CaptureSharedEventDriven sample fixes it.