Okay! I had a think about things. In order of the replys…
When you get assert failures, the first thing to check is, what actually failed. Look the AsyncUpdater:
I did find this when I was debugging, which led me to the MessageManager for the first time. At first glance, it just forwarded to’MessageManager::getInstance();', which I just directly called in my first project.
After taking another look after you brought it up I see it’s doing more. Especially the call to shutdownJuce_GUI()
, which is in the ScopedJuceInitialiser_GUI
destructor. I learned when main() reaches the end of execuction, objects created in main() (that are created with ‘automatic storage duration’) will have there destructor called before the program completely terminates.
So ScopedJuceInitialiser_GUI
in your main would call shutdownJUCE_GUI
when main() ends. As you said ‘If you are lucky, the existence of the initialiser might solve your leaks too’. Perhaps shutdownJUCE_GUI()
deallocates juce objects (like Audio*
related objects, the ones that were leaking) gracefully? Will expand on shutdownJuce_GUI()
in the codebase later.
Meanwhile I mapped out initialiseJuce_GUI()
for the record/any curious lurkers:
Why that requirement? C++ modules are new and unproven technology and you will likely encounter all kinds of extraneous issues that few people will be able to help you with. (I for example didn’t completely understand what was going on in your source code repo you linked to.)
I just heard it was the new way forward. That new codebases should adopt it, and old ones should consider migrating (its not going well https://arewemodulesyet.org/). Not a requirement here, cpp modules/qml is only a side quest, this is about juce architecture!
I don’t know how familiar you are with modules and I don’t want to go on a long tangent, but I will say this for anyone interested : One benefit of cpp modules is build time gains.
If you declare a class in the public fragment (like you would in a header) and your implementations in the private fragment and build your project. Then change a implementation and build again - only that module gets re-compiled, not everything that imports it.
Modules spit out a preliminary ‘interface file’ alongside the usual object file, into your build output tree (on clang its .pcm I think). They contain declarations of symbols that were marked with export (or inside a parent export{} braces) from the public fragment, that is - everything you can import elsewhere.
During compilation, anything that imports a module will find the interface file, and create ‘Stubs’ of the symbols in its object file. During linking, stubs are replaced with implementations from that modules object file. So if you change a module class method after initial build - things that import it never knew, and doesnt trigger recompilation. It only sees the public fragment via the interface file during compilation, and updates during linking. I like the design more then header/source personally. C++ trying to be more pythonic lol. Anyways!
here’s a simple example I wrote that uses Juce for the audio and the Choc library for the event loop/GUI :
the MyAudioCallback is very interesting, it seems you bypassed the need for (or reimplemented) AudioSourcePlayer/AudioSource. Looks like your combining them, alongside AsyncUpdater, which I have been wondering about. Its a bit over my head at first glance, but I’m going to mark this and come back to it, I think im missing some fundamentals at the moment. And I see your using ScopedJuceInitialiser_GUI
, not calling MessageManager::getInstance()
directly, as dan also hinted to. Thanks!
literally every one of these questions would best be answered by doing the JUCE tutorials
hehe, I have gone through a bunch of them (they got me this far), but many are left. Maybe I jumped the gun on this post a bit. But if I didn’t, we wouldn’t have this nice post from you
will definetly be finishing those up tho.
The MessageManager primarily manages JUCE’s GUI thread and message dispatching
Noted! thanks. When you look at the main() from JUCEApplicationBase
, it creates the juce app (and thus the interface) and the MessageManager
there. So just getting it locked the ‘GUI Thread’ must be the thread main() runs on, acoording to normal juce convention.
AudioDeviceManager handles audio device selection and callbacks
thanks for confirming, I had dug into AudioDeviceManager
a bit from tangets off tutorials, and came to some understanding of that.
Just for the record and if anyone wants to double check me - when you call AudioDeviceManager.initialise()
. Basically it will find a subclass of AudioIODevice
that matches your system, on my linux box its class ALSAAudioIODevice final : public AudioIODevice
. cross platform juce in action!
It will create a instance of that class, and assign it to this internal pointer in AudioDeviceManager
, 510: std::unique_ptr<AudioIODevice> currentAudioDevice;
.
Then it calls currentAudioDevice->open()
, and then currentAudioDevice->start()
. You see in currentAudioDevice->start (callbackHandler.get());
, it passes the AudioDeviceManagers
nested 546:std::unique_ptr<CallbackHandler> callbackHandler;
into the function.
The CallBackHandler
was first created, when you created AudioDeviceManager
- thats all it’s constructor does :
AudioDeviceManager::AudioDeviceManager()
{
callbackHandler.reset (new CallbackHandler (*this));
}
What is confusing is how AudioDeviceManager
holds a CallBackHandler
, and the CallBackHandler
holds owner
, which is the AudioDeviceManager
LOL. Typical confusing C++ design patterns.
And when you call currentAudioDevice->start()
, It passes in the CallBackHandler
. Inside, is passes itself (the AudioIODevice
subclass) into a method of the CallBackHandler
. See what I mean? its almost like a ‘recursive double loop’, hard to grasp. A class method takes in a input, then calls a method of the input and plugs itself into that. /psyduck.gif.
Ultimately, AudioDeviceManager.initialise()
chain of events seems (?) to end at calling callbacks.audioDeviceAboutToStart()
. callbacks
is a array of AudioIODeviceCallBack
objects, nested in the AudioDeviceManager
. You added to the array with AudioDeviceManager.addAudioCallback()
. Note AudioSourcePlayer
inherits from AudioIODeviceCallback
, it is inside the callbacks
.
I tried to map it out here. Probably missed a bunch of stuff, you may need to blow up the image on desktop :
With all that aside, the goal of the AudioDeviceManager
(or at least one of the primary ones), is to fetch your samples, and forward them to the audio driver on your OS, right?
Typically AudioSource
holds the samples. with AudioSourcePlayer.setSource(AudioSource)
, and then AudioDeviceManager.addAudioCallback(AudioSourcePlayer)
, I see the path from your samples to the AudioDeviceManager
.
But what method is called on AudioDeviceManager
for it to return samples (or ‘trigger the callbacks’), and what calls it? I tried digging in the codebase and maybe found the answer to ‘what method’ : AudioDeviceManager.audioDeviceIOCallbackInt()
. I have a diagram to explain…
So I see the path in the code for your AudioDeviceManager
to return the samples - leaving
Question 1 : What calls AudioDeviceManager.audioDeviceIOCallbackInt and how does it call it?.
man this stuff is super complicated… You will never grasp it the first time. For now I am going to leave the pieces on the ground and walk away like a coward…
Instead of thinking in terms of “sectors,” consider the core component of each module as its own thread.
Ok, got it. That makes some sense because MessageManager
is in juce_events, and AudioDeviceManager
is in juce_audio_devices. Since we know juce modules are kind of self contained (right?) You shouldn’t think there is a strict dependancy between them. As you said the audio subsystem can work without the messaging system.
When interaction is needed, JUCE provides mechanisms like AsyncUpdater to safely communicate between these threads without compromising real-time audio.
Ok good to have a high level point of AsyncUpdater - it handles communication between threads. It was a mystery. I will probably learn more in one of the juce tutorials.
Yes, the high-priority audio thread runs processBlock(), handling the audio resources of your application or plugin as efficiently as possible. In plugin contexts, it also manages other essential real-time tasks like AudioProcessorcallbacks.
Copy! hadn’t got to that tutorial yet my bad.
Direct modification of MessageManager for custom event processing isn’t typical in JUCE. For QML integration, it’s better to create a bridge that aligns JUCE and QML event loops without altering MessageManager.
and
Integrating QML with JUCE’s threading and messaging system typically requires a separate thread or synchronization method rather than direct integration via MessageManager.
Roger that… try to be as ‘hands off’ when screwing with juce, and let it do it’s thing. Try to make clean, seperate threads that link to juce as it is, instead of trying to weave crazy integrations into the existing juce classes. Actually I tried a project where I subclassed juce::Thread
and created MessageManager
in it, and called runDispatchLoop()
inside the juce::Thread.run()
. No idea what I was doing, but it become clear I totally broke juce::Thread
, because it’s designed to check threadShouldExit()
in a while loop, and all the other thread methods work with it. So yea, i see what your saying…
–
Okay! checkpoint. I think I got a grasp of everything above to some degree. Just the question about what calls AudioDeviceManager.audioDeviceIOCallbackInt()
, Not important right now…
Just to clarify two things before a conclusion :
First of all, the MessageManager is not a thread
Right, it is a object. A thread and a object are totally different concepts. And even if a object was designed to have ‘stuff’ that runs on a different thread, just creating that object doesn’t mean the ‘stuff’ will start running on that thread it was instanced on, or any other. That’s how juce::Thread
works right. What is inside juce::Thread.run()
, is what gets executed on a different thread. and you have to call start()
to trigger run()
. I think I saw the AudioSampleBuffer (Advanced) tutorial goes over juce::Thread
. I’ll get to it…
except it runs on the “main thread”
So I confirmed this by looking at the usual juce main(), here again :
int JUCEApplicationBase::main()
{
ScopedJuceInitialiser_GUI libraryInitialiser;
jassert (createInstance != nullptr);
const std::unique_ptr<JUCEApplicationBase> app (createInstance());
jassert (app != nullptr);
if (! app->initialiseApp())
return app->shutdownApp();
JUCE_TRY
{
// loop until a quit message is received..
MessageManager::getInstance()->runDispatchLoop();
}
JUCE_CATCH_EXCEPTION
return app->shutdownApp();
}
and when you expand runDispatchLoop
:
```cpp
void MessageManager::runDispatchLoop()
{
jassert (isThisTheMessageThread()); // must only be called by the message thread
while (quitMessageReceived.get() == 0)
{
JUCE_TRY
{
if (! detail::dispatchNextMessageOnSystemQueue (false))
Thread::sleep (1);
}
JUCE_CATCH_EXCEPTION
}
}
// must only be called by the message thead ← note this. and, in the MessageManager constructor :
MessageManager::MessageManager() noexcept
: messageThreadId (Thread::getCurrentThreadId())
sets messageThreadId
to the current thread (we instanced it in main via ScopedJuceInitialiser_GUI libraryInitialiser
) and :
bool MessageManager::isThisTheMessageThread() const noexcept
{
const std::lock_guard<std::mutex> lock { messageThreadIdMutex };
return Thread::getCurrentThreadId() == messageThreadId;
}
The point is, and what I just want to have on the record for any other beginners that find this in the future : The GUI Thread is the Message Thread is the Thread main() runs on, in a normal juce application. They are all the same.
Finally,
JUCE’s audio engine can operate without MessageManager if there’s no GUI.
Understood, but given you want a gui, even if it is a different framework (qml), that means you should have a MessageManager. Ok got it. And as you said, if your using a different gui framework, make a seperate thread instead of trying to integrate. Right, we went over this, moving on…
Question 2 : How does MessageManager.runDispatchLoop() fit into the picture, with a application that uses a different gui framework? And, is runDispatchLoop() critial for the MessageManager to function?
I’m still a bit vague on what the messaging system is actually sending, I know it work with the GUI tho. The confusion comes, because runDispatchLoop
is a blocking loop. It is the native juce event loop. Usually, it runs in main(). But we can’t run it in main(). Or else it would block the QML event loop. And, it’s best to not mix them in one. This is why I did that project to runDispatchLoop()
in a juce::Thread.run()
, to leave main() open for QML. But this can’t be the right way. If we don’t need runDispatchLoop
for MessageManager to function, problem solved. Maybe the answer is found traversing runDispatchLoop
code, maybe in tutorials. I noticed in Xenokios project, he never called runDispatchLoop()
. Is having two event loops in one application even a thing people do?
In summary, there is really only two outstanding questions. I did a bit of a brain dump, of course you can correct anything If I’m mistaken. From here, I’m going to put a pin in this train of thought and finish all the tutorials. I’m sure they will fill in the blanks somewhat.
But first I will now go explore this place they call the ‘out-side’.