Messages are only being handled while the editor is open

When using JUCE 6.0.8 to create a VST3 plugin on Linux, none of JUCE’s asynchronous messaging utilities relying on the global MessageManager work while the editor is closed. This means that among other things AsyncUpdaters, Timers, and calls to juce::MessageManager::callAsync() won’t do anything until the user opens the plugin’s editor. This seems to be consistent across Bitwig Studio, REAPER, Ardour, Renoise and Carla (which uses JUCE’s own plugin hosting). A minimal example of this (courtesy of @eyalamir) would involve adding a simple default constructed timer to an audio processor class like so:

struct MyTimer : juce::Timer {
    MyTimer() { startTimerHz(1); }

    void timerCallback() override { std::cout << "Hello, world!" << std::endl; }
};

class MyProcessor : public juce::AudioProcessor {
    ...

    MyTimer t;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MyProcessor)
}

This timer only procs (and prints the message) while the editor is open.

1 Like

What if your processor class inherits from Timer directly?

class myProcessor : public juce::AudioProcessor, private juce::Timer 
{
public:
  // Constructor
  myProcessor()
  {
    startTimerHz(25);
  }

private:
  void timerCallback() override { std::cout << "Hello, world!" << std::endl; }
};

That also doesn’t do anything unless the editor is open.

On Linux there is no global event loop available like the other platforms so plug-ins need to hook into the host’s. In VST3 plug-ins this is done via the IRunLoop interface which is retrieved from the IPlugFrame callback interface of the IPlugView. This is only available when an editor is active so there’s no way to get event loop callbacks like Timers, AsyncUpdaters etc. without it.

How is a plugin supposed to perform potentially expensive computations when a parameter changes? AudioProcessorParameter::Listener suggests using AsyncUpdater, but if JUCE’s message loop not running while the editor is closed is intentional behavior and not a bug then that’s not an option. Should I just ignore JUCE’s whole messaging system and roll my own?

What is preventing a ‘SharedMessageThread’ as the one used in the VST2 wrapper ?

2 Likes

What is your use case? Are you not providing an editor at all or do you need to do processing whilst the editor is closed?

Yes, we roll our own message thread in the VST2 wrapper because there is no explicit support for hooking into the host’s run loop. It’s hard to say without investigating what the implications of doing this alongside the IRunLoop would be in the VST3 wrapper.

In this case it’s about handling computations triggered by parameter changes. Let’s say I have an FFT window size parameter. When that changes, I need to potentially do a bunch of allocations and some locking to setup new buffers so that on the next audio processing cycle the old and the newly resized buffers can just get swapped using a pointer. The JUCE docs suggest doing such potentially expensive and blocking operations from an AsyncUpdater, but those won’t fire unless the editor is open. This for instance makes it impossible to change those parameters from the host’s generic editor (or through automation, although a setting like this of course shouldn’t be automated).

EDIT: Another thing worth adding is that IRunLoop really is supposed to only be used for GUI operations, and in fact it can only be used for those things since the interface is implemented by IPlugFrame. So for general message handling JUCE really has to spin up its own message thread on Linux.

To me personally, the main advantage would be that all of my existing JUCE-based code expects the message thread to always run (for similar reasons as the OP), and if it won’t I’d have to modify a lot of my shared code whenever I choose to deploy Linux plugins (which I haven’t yet).

5 Likes

We’ll look into this. It sounds like the right solution is to do something similar to the VST2 message loop but it’ll require a bit of testing to see how it interacts with the host’s IRunLoop.

3 Likes

We’ve pushed some commits to the develop branch which should fix this. As predicted, the interaction between the host’s run loop and the shared message thread is a little tricky and there is some variation in how hosts implement the run loop interface so any feedback on the new changes would be appreciated.

3 Likes

I’ve only briefly experimented with this by reintroducing AsyncUpdater to a Linux VST3 plugin, but everything seems to be working great now! Tested with JUCE commit 1a5fb5992a1a4e28e998708ed8dce2cc864a30d7 in Bitwig Studio 3.3.7 and REAPER 6.26. Thanks a lot for the quick fix!