Audio Device Manager bug MacOS

Hello everyone, I am trying to test juce::AudioDeviceManager on MacOS and check whether connections with Bluetooth devices are working correctly.

Here, my application crashes when I select my Bluetooth device as the input and output device.
Sometimes when I want to connect my Airpods, for example, to another input or output device, the application crashes.


I received this explanation using AI:

The core issue is a critical Thread Safety / Deadlock problem within the AudioIODeviceCombinerclass in JUCE’s CoreAudio backend.

Here is the global explanation of why it crashes and the fix:

1. The Problem:
When you use separate input and output devices (e.g., AirPods + Built-in Mic), JUCE creates a virtual AudioIODeviceCombiner

  1. The Trigger: When a device sends a notification (like AirPods connecting or changing sample rate), CoreAudio fires a callback (on a high-priority HALC_ShellPlugIn thread).

  2. The Trap: This callback eventually calls AudioIODeviceCombiner::restartAsync()

  3. The Crash: In the original code,

    In JUCE/modules/juce_audio_devices/native/juce_CoreAudio_mac.cpp
    restartAsync() did this:

    void restartAsync() override
        {
            {
                const ScopedLock sl (closeLock);
    
                if (active)
                {
                    if (callback != nullptr)
                        previousCallback = callback;
    
                    close();
                }
            }
    
            startTimer (100);
        }
    

    Calling close() synchronously inside this high-priority CoreAudio callback is illegal.

    close() tries to shut down the audio engine and waits for threads to stop. But since the calling thread is part of that audio system, it causes a Race Condition (leading to your SIGBUS at 0x1) or a Deadlock (the “skipping cycle due to overload” errors).

2. The Solution: Make it Truly “Async”

The fix forces the restart logic to happen only on the main thread, where it is safe to close and open devices.

The Modification: We remove the synchronous close() call from restartAsync(). The method now simply “schedules” the work by starting a timer. The actual closing happens millieconds later inside timerCallback(), which runs safely on the Message Thread.

Corrected Code:

void restartAsync() override
{
    {
        const ScopedLock sl (closeLock);
        if (active && callback != nullptr)
            previousCallback = callback;
    }
    // "Please restart me later" - safe!
    startTimer (100); 
}

This prevents the CoreAudio thread from ever being blocked or destroyed while it’s executing code.

If any JUCE admins can test the solution or find a more robust backup, I’m interested.

Cheers.