Problems with locking and the audio thread

I’m on iOS, and my audio is glitching out- it drops out entirely for around 10ms at a time, which is about the length of an audiobuffer I think. it does not seem to be an underrun since performance is not an issue I think. I think it’s a threading bug, but I’m confused about how I might solve it.

Here’s my setup:

I made a polyphonic synthesizer class. I have several instruments playing at a time, where each instrument is an instance of the synth class. I’m using ofxMaxim, so the synth class has a function that is called in the main app’s audioRequested callback to generate audio samples. so far so good.

I noticed, though, that the synth would occasionally randomly drop notes. I eventually realized this was due to a race condition. the main thread, due to the user pressing a note, adds a note the synth’s queue, a vector of notes to be played. in the audio thread (since the synth’s function is getting called in audiorequested), the synth reads through that vector, then clears it. I think notes getting added by the main thread at just the wrong moment can be missed by the audio thread (i.e. cleared from the vector, without getting played). the only certain way, I think, to avoid this problem is to use a lock… right? I’ve never used thread locks before.

so, in the synth class, I simply lock the main thread as I add a note to the queue, and then lock the audio thread as I read and then clear the queue. no more dropped notes, ever! as far as I can tell! (it’s hard to wait for something not to happen).

but… now I’m having these audio dropouts. they occur specifically and only when at least two of the instruments (i.e. two different instances of the synth class) are playing a bunch of notes simultaneously. so I think I’m making some kind of horrible error in the locking, where two of the synths try to lock one of the threads at the same time… or something? any tips at all would be super helpful…

Hi Eric,

Yeah your description sounds about right. The audio callback is quite strict about it’s deadlines. If the callback ends up waiting around on a lock for any significant amount of time, there’s a very good chance you’ll miss the deadline. Hence, dropouts.

IIRC in older versions of iOS it’d flip out if you missed a few and stop all sound from your app entirely :confused:

Anyway, the way to tackle this is to get the lock to hold for the minimum time possible. Instead of designing the callback like this:

myMutex.lock();
expensiveAudioCalculation();
myMutex.unlock();

Try something like this:

newBuffer = expensiveAudioCalculation();
myMutex.lock();
audioBuffer = newBuffer;
myMutex.unlock();

One way that works pretty well is to introduce a circular buffer, like TPCircularBuffer. In your update() you fill the circular buffer, and the audioRequested() callback takes audio out of it. You set the circular buffer’s size to be a nice tradeoff between latency and reliability. It’s not bulletproof, but it’s better than trying to always do expensive audio stuff right in the callback.

Basically, though, the trick is to design your audio system in such a way that when the audioRequested callback happens, the audio is pretty much ready to go. It’s not straightforward, unfortunately :slight_smile: