AudioCaptureTimecodeProvider deadlock (with repro)

This has only been tested with the a Focusright 2i2 (driver version: 4.116.9, latest as of today) and Unreal 5.2.1.

It’s unclear to me whether this is a Focusright driver issue or a bug in the engine.

This hang repros on my machine and a colleague’s machine (with the same audio interface and drivers). [Side note: shoutout to my colleague for helping me figure out this out]

It would be great if someone else could follow the basic repro steps below to gather a bit more data on this, with other audio hardware.


Repro

  1. Disconnect your audio interface
  2. Run the Editor
  3. Enable Audio Capture Timecode Provider plugin
  4. Set Project Settings → Timecode Provider to AudioCaptureTimecodeProvider
  5. Relaunch the Editor (this step probably isn’t needed, but just so we’re in total sync)
  6. Set Project Settings → Timecode Provider to any other Timecode Provider
  7. The Editor hangs

Technical Details

I’ve dug into the C++ and it would appear that what’s happening is:

  1. On Editor launch (or when the Timecode Provider is set to AudioCaptureTimecodeProvider), RtApi :: RtApi() initializes stream_.mutex:
RtApi :: RtApi()
{
  stream_.state = STREAM_CLOSED;
  stream_.mode = UNINITIALIZED;
  stream_.apiHandle = 0;
  stream_.userBuffer[0] = 0;
  stream_.userBuffer[1] = 0;
  MUTEX_INITIALIZE( &stream_.mutex ); // <-- HERE
  showWarnings_ = true;
  firstErrorOccurred_ = false;
}

stream_.mutex.LockCount == -1 after this line.

  1. void RtApiDs :: callbackEvent() is called and a loop is spawned to continuously read data from the audio device’s input:
     MUTEX_LOCK( &stream_.mutex ); // <-- stream_.mutex.LockCount = -2 after this line

    if (...) {}
    else { // mode == INPUT
      while ( safeReadPointer < endRead && stream_.callbackInfo.isRunning ) {
        // See comments for playback.
        double millis = (endRead - safeReadPointer) * 1000.0;
        millis /= ( formatBytes(stream_.deviceFormat[1]) * stream_.nDeviceChannels[1] * stream_.sampleRate);
        if ( millis < 1.0 ) millis = 1.0;
        Sleep( (DWORD) millis );

        // Wake up and find out where we are now.
        result = dsBuffer->GetCurrentPosition( &currentReadPointer, &safeReadPointer );
        if ( FAILED( result ) ) {
          errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") getting current read position!";
          errorText_ = errorStream_.str();
          MUTEX_UNLOCK( &stream_.mutex );
          error( RtAudioError::SYSTEM_ERROR );
          return;
        }
      
        if ( safeReadPointer < (DWORD)nextReadPointer ) safeReadPointer += dsBufferSize; // unwrap offset
      }
    }
  1. Now if we set the Timecode Provider in Project Settings as described in repro step #6, the engine attempts to destroy the previous Timecode Provider:
bool UEngine::SetTimecodeProvider(UTimecodeProvider* InTimecodeProvider)
{
	if (InTimecodeProvider != TimecodeProvider)
			{
		if (TimecodeProvider && bIsCurrentTimecodeProviderInitialized)
		{
			TimecodeProvider->Shutdown(this); // <-- HERE
		}

		bIsCurrentTimecodeProviderInitialized = false;
		TimecodeProvider = IsValid(InTimecodeProvider) ? InTimecodeProvider : nullptr;

		if (TimecodeProvider)
			{
			bIsCurrentTimecodeProviderInitialized = TimecodeProvider->Initialize(this);
		}
		OnTimecodeProviderChanged().Broadcast();
	}
	return bIsCurrentTimecodeProviderInitialized;
}

The AudioCaptureTimecodeProvider destructor is called, resulting in calls to RtApiDs::stopStream followed by RtApiDs::closeStream. Note that closeStream sets stream_.callbackInfo.isRunning to false, which ends the while loop in #2, however we will never get this far. Why? Because RtApiDs::stopStream() does this:

    if ( stream_.mode != DUPLEX )
      MUTEX_LOCK( &stream_.mutex );  // <---DEADLOCK

stream_.mutex.LockCount is -2 when this line is reached, and it would seem that the mutex cannot be acquired, as we’re waiting for data to stop streaming in our while loop (#2).

If I set a breakpoint on this line and set LockCount to -1 instead of -2, the deadlock doesn’t occur and things appear to run normally.

It would seem that when the audio interface is disconnected, the driver (or Windows) is streaming endless amounts of useless data, and so the while loop in #2 never ends, and the mutex cannot be acquired to stop the stream.


System details

Edition Windows 10 Pro
Version 22H2
Installed on ‎6/‎29/‎2023
OS build 19045.3448
Experience Windows Feature Experience Pack 1000.19044.1000.0
Device name -
Processor 12th Gen Intel(R) Core™ i9-12900K 3.20 GHz
Installed RAM 96.0 GB (95.8 GB usable)
Device ID -
Product ID -
System type 64-bit operating system, x64-based processor
Pen and touch No pen or touch input is available for this display