Critical section issue between ~iOSAudioIODevice and handleRouteChange


#1

I have noticed a concurrency issue in juce_ios_audio.cpp.

Using AudioDeviceSettingPannel, I was messing changing Sample Rate few times in a row very quick. After few iteration an error occurs (in AudioDeviceManager::setAudioDEviceSetup()) and the device is deleted.
(the reason why open() return an error (“Could not open the device”) is not the issue here, I am going to investigate on that afterwards).

“MessageThread” calls “~iOSAudioIODevice()” to unsubscribe the device from the session holder and then delete it. At the same time “AV Notify Thread” is still processing device->handleRouteChange().
The exact “device” that is beeing deleted from “MessageThread”.
I added a critical section in AudioSessionHolder to fix the issue.

juce_ios_Audio.cpp:

struct AudioSessionHolder
{
    AudioSessionHolder();
    ~AudioSessionHolder();

    void handleStatusChange (bool enabled, const char* reason) const;
    void handleRouteChange (const char* reason) const;

    CriticalSection devicesLock; //ADD
    Array<iOSAudioIODevice*> activeDevices;

    id nativeSession;
};

[…]

void AudioSessionHolder::handleRouteChange (const char* reason) const
{
    ScopedLock lock(devicesLock); //ADD
    for (auto device: activeDevices)
        device->handleRouteChange (reason);
}

[…]

    iOSAudioIODevice (const String& deviceName)
        : AudioIODevice (deviceName, iOSAudioDeviceName)
    {
        ScopedLock lock(sessionHolder->devicesLock); //ADD
        sessionHolder->activeDevices.add (this);
        updateSampleRateAndAudioInput();
    }

    ~iOSAudioIODevice()
    {
        ScopedLock lock(sessionHolder->devicesLock); //ADD
        sessionHolder->activeDevices.removeFirstMatchingValue (this);
        close();
    }

#2

The reason why open() in AudioDeviceManager::setAudioDeviceSetup() fails sometimes is because “category change” is summoned in open() and then handleRouteChange(“Start Audiounit”) is called.

Notify Thread is then handling “category change” message and calls handleRouteChange()
It happens that this method (handleRouteChange) is called by the 2 threads at the same time that makes AudioUnit creation to fail and be NULL. (And then generate the error mentioned in my previous post)

@Jules, If you have any idea it would be great help.
Thanks!


#3

Sorry, I didn’t get round to looking at this today but I’m pretty sure your that’s a bug in JUCE. I also think that your suggestion makes sense. I’ll have a more detailed look tomorrow or on Monday.


#4

Hi fabian,
Did you get a chance to take a look at it ?
Cheers,
Bastien


#5

Yes I’m still looking at this. I have a fix based on your suggestions but i’m still hitting some issues with this.


#6

That’s good to hear.
You should also take a look at this method, it might cause you some trouble:
Array<double> getAvailableSampleRates() override.
I believe it does not work properly. It leads my app to have no sound, I just replaced it by this:

    Array<double> getAvailableSampleRates() override
    {
        Array<double> rates;

      rates.addIfNotAlreadyThere(16000);
      rates.addIfNotAlreadyThere(32000);
      rates.addIfNotAlreadyThere(44100);
      rates.addIfNotAlreadyThere(48000);
      return rates;
}

Hope this helps you,
Thank you!
PS: I use an iPad Air.


#7

Hi BastienC,

There’s a fix for this on develop now - we’ve added a ScopedLock to the handleRouteChange method to deal with the race condition. It should fix most issues but there’s still potential for trouble if this method is called by multiple threads repeatedly as they may escape the lock out of order.

Ed