AVAudioEngine Hangs/Locks Apps After Call to -connect:to:format:

Periodically when testing I am running into a situation where the app hangs and beach balls forever when using AVAudioEngine.

This seems to log out when this affect happens:

Now when this happens if I pause the debugger it's hanging at a call to:

  [engine connect:playerNode
                 to:engine.mainMixerNode
             format:buffer.format];

#0 0x000000019391ca9c in __psynch_mutexwait () #1 0x0000000104d49100 in _pthread_mutex_firstfit_lock_wait () #2 0x0000000104d49014 in _pthread_mutex_firstfit_lock_slow () #3 0x00000001938928ec in std::__1::recursive_mutex::lock () #4 0x00000001ef80e988 in CADeprecated::RealtimeMessenger::_PerformPendingMessages () #5 0x00000001ef818868 in AVAudioNodeTap::Uninitialize () #6 0x00000001ef7fdc68 in AUGraphNodeBase::Uninitialize () #7 0x00000001ef884f38 in AVAudioEngineGraph::PerformCommand () #8 0x00000001ef88e780 in AVAudioEngineGraph::_Connect () #9 0x00000001ef8b7e70 in AVAudioEngineImpl::Connect () #10 0x00000001ef8bc05c in -[AVAudioEngine connect:to:format:] ()

Current all my audio engine related calls are on the main queue (though I am curious about this https://forums.developer.apple.com/forums/thread/123540?answerId=816827022#816827022).

In any case, anyone know where I'm going wrong here?

I think the problem is that I called -connect:to:format: on the player node multiple times (already connected). So I'll just connect the player node to the main mixer node once and leave it. I think when I connect the player node multiple times it tears down a bunch of stuff as a side effect which looks like can cause a deadlock. Perhaps something related to the fact that I have a tap block?

Assuming this resolves the issue (need to test it more), is there any point where I should reconnect the player node to the main mixer node (as a part of error handling or a configuration change)?

So, I think the way to go is to make sure the engine isn't running before I connect the player node to the main mixer node. I will need to reconnect if the engine is paused/stopped before restarting the engine though, so I'm checking if (!isRunning) before connecting.

I think my mistake was accidentally connecting on the engine when it was already running (and was already connected).

This didn't resolve the issue. I just ran into an issue where simply invoking the isRunning getter in AVAudioEngine deadlocks.

My app started beach balling and I paused the debugger. The call stack looks like this:

#0	0x000000019391ca9c in __psynch_mutexwait ()
#1	0x00000001029a5100 in _pthread_mutex_firstfit_lock_wait ()
#2	0x00000001029a5014 in _pthread_mutex_firstfit_lock_slow ()
#3	0x00000001938928ec in std::__1::recursive_mutex::lock ()
#4	0x00000001ef8b87dc in -[AVAudioEngine isRunning] ()

So I have an if statement

if (!engine.isRunning)
{
   // Do something
}

I do have a tap block installed on the player node. Inside the tap block I read:

 if (!weakToStrongPlayerNode.isPlaying)
   {
        // Don't do anything in the tap block while the player node is paused.
         return;
    }


Which appears to be being accessed at the same time.

Thread 82 Queue : RealtimeMessenger.mServiceQueue (serial) #0 0x000000019391d3c8 in __semwait_signal ()

#1	0x00000001937fc714 in nanosleep ()
#2	0x00000001938932f4 in std::__1::this_thread::sleep_for ()
#3	0x00000001ef89a498 in AVAudioNodeImplBase::GetAttachAndEngineLock ()
#4	0x00000001ef8aa57c in -[AVAudioPlayerNode isPlaying] ()

Is it not safe to check the playing state of the player node in the tap block? What about calls to methods like -playerTimeForNodeTime: ? Can I call these in the tap block?

The documentation states that playerTimeForNodeTime: will return nil if the player node is not playing. Not sure if I can call the tap block or if it's possible to run into the same issue (like reading isRunning on AVAudioEngine on the main thread and playerNode.isPlaying on another thread)

I was thinking of just using my own atomic bool and setting it when I play/pause the player node. Then in the tap block, just read that BOOL instead of reading from playerNode directly.

But I imagine it would be possible for the system to stop the player node. And my flag could be out of sync with the true playerNode.isPlaying property. Is there a notification for such an event?

So many questions.

Alll these calls invoke AVAudioNodeImplBase::GetAttachAndEngineLock internally so I don't think I can use them in the tap block. Grr.

So ideally (at least in my case) is that I could just use the when parameter passed to the tap block to figure out where in the audio I'm at, but the when needs to be converted to player time in order to get a time that makes sense (I need to determine my time relative to the entire audio buffer I scheduled on the player node, not the buffer of the tap block).

Also when the playerNode is paused, the when parameter doesn't factor the paused time in. The tap block continues to fire while the player node is paused and the when time doesn't know anything about paused/stopped so any UI synchronization you do will just jump all over the place once you start pausing/resuming. -playerTimeForNodeTime: does know about all this, but....

I don't think it's safe to call any of the audio engine/player node APIs in the tap block without risking a deadlock. If I'm wrong about all this I'd be grateful for an education. The documentation seems a bit scarce, and the dev forums have been pretty quiet lately.

What I came up with now is to just sync my own atomic_bool with my calls that pause/play the player node. Read the atomic bool in the tap block instead of if (!playerNode.isPlaying) { return; }

Then to account for the node time/ player time situation.. every time I schedule a buffer, I reset a counter to 0 that's synchronized with the tap block. In the tap block I increment it on each invocation to compute sample time relative to the entire audio buffer. I only schedule one buffer at a time (for now). I suppose I would need to figure out a good place to reset the counter to 0 if I scheduled two buffers at the same time.

If the system ever stops my player node (on error or something) my tap block could be out of sync since my flag I use to track playerNode.isPlaying is not binded to 'the truth' but it's better than deadlocking.

If there is a cleaner way to achieve this, I'm all ears.

AVAudioEngine Hangs/Locks Apps After Call to -connect:to:format:
 
 
Q