Problem
When using a certain Core Audio device more than once per process, closing the non-last instance takes 2 seconds.
In our case we rely on the destruction of the current device before creating an instance for the new device. This makes our application freeze for 2 seconds.
Cause
When closing a Core Audio device, the CoreAudioInternal::stop()
method will wait for the kAudioDevicePropertyDeviceIsRunning
property to become 0. However, this property will not become 0 as long as there are any other device clients active within the same process. After 2 seconds the state of kAudioDevicePropertyDeviceIsRunning
is ignored and the method returns anyway.
// wait until it's definitely stopped calling back..
for (int i = 40; --i >= 0;)
{
Thread::sleep(50);
UInt32 running = 0;
UInt32 size = sizeof(running);
AudioObjectPropertyAddress pa;
pa.mSelector = kAudioDevicePropertyDeviceIsRunning;
pa.mScope = kAudioObjectPropertyScopeWildcard;
pa.mElement = juceAudioObjectPropertyElementMain;
OK(AudioObjectGetPropertyData(deviceID, &pa, 0, nullptr, &size, &running));
if (running == 0)
break;
}
// From juce_mac_CoreAudio.cpp:721
Potential solution
There is a good reason for this mechanism to exist, a call to AudioDeviceStop()
doesn’t guarantee any ioproc callback from being made after this call returns when called from a thread other than the io thread.
On the other hand, when AudioDeviceStop()
is being called from the IO thread (from within the call to the AudioDeviceIOProc
callback) there actually is a guarantee that the callback won’t happen anymore after AudioDeviceStop()
returned.
To quote Jeff Moore from Apple’s Core Audio team:
When AudioDeviceStop() is called on the IO thread, things are different. The HAL will not call the IOProc that was stopped again after the AudioDeviceStop() call completes, but the buffer provided by that IOProc will get played (and nothing after that from that IOProc).
With this guarantee in mind the CoreAudioInternal
class could be changed to stop the device on the IO thread instead of the message thread.
My proposal would be to add an std::atomic<bool>
to the CoreAudioInternal
class and use that to signal between the message thread and the IO thread.
The CoreAudioInternal::stop()
method then becomes:
void stop (bool leaveInterruptRunning)
{
{
const ScopedLock sl (callbackLock);
callback = nullptr;
}
if (started && (deviceID != 0) && ! leaveInterruptRunning)
{
shouldStopDeviceOnIOThread = true; // Set flag for audioCallback to stop the device
started = false;
{ const ScopedLock sl (callbackLock); }
// wait until it's definitely stopped calling back..
for (int i = 40; --i >= 0;)
{
if (shouldStopDeviceOnIOThread == false)
break;
Thread::sleep (50);
}
const ScopedLock sl (callbackLock);
OK (AudioDeviceDestroyIOProcID (deviceID, audioProcID));
audioProcID = {};
}
}
And stopping the device will then be done inside the audioCallback()
method:
void audioCallback (const AudioBufferList* inInputData,
AudioBufferList* outOutputData)
{
const ScopedLock sl (callbackLock);
if (shouldStopDeviceOnIOThread)
{
if (OK (AudioDeviceStop (deviceID, audioProcID)))
shouldStopDeviceOnIOThread = false;
return;
}
// More irrelevant code not shown
}
Attached is a patch with the proposed changes.
juce_mac_CoreAudio.cpp.patch (2.5 KB)