AudioDeviceManager changeListenerCallback not firing in Release on device remove/add

Hey fellow JUCE devs. I’ve been enjoying using JUCE so far, but have a minor issue that’s turned up. I’d love to hear if I’m doing something obviously wrong before I dig too deeply into this issue.

I am using JUCE as a static library with the following modules:

  • juce_audio_basics
  • juce_audio_devices
  • juce_audio_formats
  • juce_core
  • juce_events

I am using juce::AudioDeviceManager as a member of my own DeviceManager class, and as such my own class inherits from juce::ChangeListener and implements:
void changeListenerCallback(ChangeBroadcaster* source);

My class is added as a change listener to the instance of juce::AudioDeviceManager.

Running on macOS in Debug it works! When I plug in a USB device I get this stack (as expected):

However, in Release build the changeListenerCallback function is not called! Is this to do with my use of JUCE as a static library? Has anyone else seen anything like this?

I’m using JUCE 5.3.2 on macOS.

I am also struggling with this issue.

Right, so we have found and have a “fix” for this issue, we found it also occurred on Windows and we needed a fix by now…

We noticed this:

void ChangeBroadcaster::addChangeListener (ChangeListener* const listener)
    // Listeners can only be safely added when the event thread is locked
    // You can  use a MessageManagerLock if you need to call this from another thread.
    jassert (MessageManager::getInstance()->currentThreadHasLockedMessageManager());

    changeListeners.add (listener);

jassert has a call to MessageManager::getInstance() (which will thus get called in Debug and not Release). We noticed that getInstance() creates the MessageManager singleton if it does not exist.

So we added a call to this before our code:

m_audioDeviceManager.initialiseWithDefaultDevices(2, 2);

And it “fixed” it. So this is likely something to do with code within jasserts leading to (unsurprising) issues in Release. One can only assume in non-static library builds, there are calls to the MessageManager::getInstance() during JUCE bootstrap.

This needs a proper fix from JUCE’s end and perhaps a review of code that is being called within jassert.

Hmm, that’s an interesting edge-case, but we’d expect MessageManager to have been created before any significant user code gets run.

Were you perhaps running this code inside a static constructor? That’s the only way I can think that you might end up doing this before the system has properly initialised, and it’s pretty dangerous in general to be doing complex work like creating audio devices at a point where the C++ runtime still hasn’t finished its basic initialisation tasks.

…however, I do think it’d probably worth me doing a quick check for other places like this and changing them to something like

jassert (MessageManager::getInstanceWithoutCreating() != nullptr && MessageManager::MessageManager::getInstanceWithoutCreating()->isThisTheMessageThread());

…so your code would still have been problematic and you’d still need to change it, but you’d at least have hit an assertion

Just moved the initialisation code to be much later on in the chain to check, and no still has the same issue in Release. Perhaps there is the expectation that:

we’d expect MessageManager to have been created before any significant user code gets run

is only valid with JUCEApplication or Component derived code?

Either way, I think the addition of the assertion for this particular use case would be good.

I think any code that has this assertion in it really shouldn’t be running unless an MM has been started.

If you find some code where it could legitimately be used in another context, e.g. a console app without an event loop, then we should remove the assertion from that code, or make it more lenient. But certainly something like ChangeBroadcaster::addChangeListener doesn’t make any sense in that kind of situation.

I just had a look and will push some improvements to those assertions, it’s something that needs doing anyway.