Sending signal/events from audio to GUI thread?

Hello,

I am a newbie on JUCE, thus, please forgive me my stupid question:
I am writing an audio app, my MainComponent is derived from AudioAppComponent. I did overwrite getNextAudioBlock() for my audio processing. That works fine. Now I need to update some visual representation in my GUI during the audio processing. Calling repaint() does not work. Well that is logical, because of the different threads.
So far the only solution I found in the turorials is to wait for a timer event. Well, this is not so nice.
So I am wondering, is there any other way to send a signal from the audio thread to the GUI thread that triggers a repaint of the GUI?

Raphael

JUCE has some other classes like the AsyncUpdater which allow for this kind of cross-thread updating, however they aren’t always real-time safe.

Timers on the GUI thread are a good way to handle this sort of thing, this post has some additional info:

What’s wrong with a timer? Presumably, your audio thread will be active most of the time, and it will be providing new data to update the GUI most of the time, so using a timer makes total sense.

1 Like

The timer is almost always the best way to go with this.

If you need a true “signal” type message, just do this:

class MyAudioProcessorClass : public AudioProcessor, etc...
{

    Atomic<bool> someSortOfSignal;
    Atomic<double> someAudioParameter;

    void processBlock (AudioBuffer<float> &buffer, MidiBuffer &midiMessages) override
    {
        // dsp code here
        const double level = buffer.getRMSLevel (0, 0, bufer.getNumSamples());
       
        // Use juce::Atomic or std::atomic to share audio parameters between threads
        someAudioParameter.set (level);

        // Send a signal if the RMS of the buffer goes above 0.5
        if (level > 0.5) {
            someSortOfSignal.set (true);
        }
    }

}

MyPluginGuiClass : private Timer, public Component, etc...
{
    ...

    void paint (Graphics& g) override
    {
        // Color changes when the volume goes above 0.5
        g.fillAll (Colours::purple.withRotatedHue (rmsLevel);
    }
    
    void timerCallback() override
    {
        // Get your audio parameter
       rmsLevel = processor.someAudioParameter.get();

        // If Signal is activated
        if (processor.someSortOfSignal.get() == true) {
           
            // Deactivate the signal when you read it so you can send it again later.
            processor.someSortOfSignal.set (false);
            
            // Respond to the signal
            // If the RMS Level is > 0.5, repaint some level meter or something
            repaint();
        }
    }

    double rmsLevel = 0;
    ...
}

Always use juce::Atomic or std::atomic when sharing values between different threads! If you don’t, your app will crash at random times.

This method is probably the simplest and safest way to go and can help you make you UI performant.

3 Likes

I have been reworking some older code lately, and have been using a similar technique with the Timer for “sending” some types of signal to the GUI. My thoughts at this point are, as a general rule for plugins using AudioProcessorValueTreeState for parameter management:

  1. If you just need Sliders and Buttons to link to parameter values, use the Attachment classes.

  2. If you need other GUI components to update in response to parameter values, make the Editor an AudioProcessorValueTreeState::Listener and trigger those GUI changes in the parameterChanged callback.

  3. If you need GUI components to update in response to audio signals, use this poll-with-a-Timer approach. There are limits on how fast you can update the GUI, and any audio signal is “moving” way faster than a GUI can respond. There is always effectively “downsampling” involved
 so you might as well let the GUI set the rate of that, because any faster and it’s just wasted effort.

Any other cases that I’m missing there?

1 Like

You probably don’t want to be doing GUI things in the parameter changed callback, as this callback can (and often will) be called in the audio thread.

Instead. you probably want to raise an atomic flag/int counter in response to that flag, and do the actual update on a timer on the message thread.

2 Likes

Oh? Maybe I misunderstood that part. I thought the AudioProcessorValueTreeState::Listener was made thread-safe by doing async callbacks not on the audio thread? Maybe I have that wrong.

Of the 3 cases I outlined above, I have only actually implemented #1 and #3, since I haven’t had a need yet for #2
 so I’ll admit that that part was based on reading the documentation rather than on firsthand experience with it.

Inspired by @cpenny’s sharing an example using atomics, I thought I’d share a thread-safe way to trigger a simple peak light in the GUI in response to an audio signal. In this example, it’s just triggering off the input signal, but obviously you could set the value of the peakLevel member from any point in the DSP code. (If all you did want was the peak level of the input, it would be simpler to use AudioBuffer::getMagnitude rather than iterate through the buffer as this code does.)

Note that this does not do any “peak decay” processing – but if you are running your GUI Timer at 30 Hz, that’s an average of 33ms per analysis window, which should be long enough to see even if the peak is only momentary. And this approach also guarantees that if there IS a peak, it won’t be missed by the display (since it’s the display timer itself that resets the peak value).

class MyAudioProcessorClass : public AudioProcessor, etc...
{
private:
    Atomic<float> peakLevel;
    
public:
	float getAndResetPeak()
	{
	    // The exchange() method atomically grabs the old peak level, and resets it to 0
		return peakLevel.exchange (0.0f);
	}

    void processBlock (AudioBuffer<float> &buffer, MidiBuffer &midiMessages) override
    {        
        for (int i = 0; i < buffer.getNumSamples(); i++)
    	{
    		const float audioLevelAbs = fabsf (buffer.getSample (0, i));  // this only looks at 1st buffer channel
    	
    		if (audioLevelAbs > peakLevel.get())
    			peakLevel.set (audioLevelAbs);
    	}
    }

}

MyPluginGuiClass : private Timer, public Component, etc...
{
	const float peakThreshold = Decibels::decibelsToGain<float> (-12.0f);
	ImageButton peakLight;  // set this up in constructor with normal/down images, and setClickingTogglesState (false)
    MyAudioProcessorClass& processor;

	// include in constructor: startTimerHz (30);

    void timerCallback() override
    {
    	const bool lightOn = processor.getAndResetPeak() > peakThreshold;
    	peakLight.setState (lightOn ? Button::buttonDown : Button::buttonNormal);
    	repaint(); 
	}
}

You probably want to avoid using an atomic in per-sample loops.

The reason is: atomics prevent almost all compiler optimizations, vectorization, etc.

Instead, write the result into a regular (non-atomic) variable in the per-sample loop, and then write it into the atomic at the end of the block.

2 Likes

OK, thanks for the feedback. I figured there was some downside to using atomics, and there you have it. No such thing as a free lunch.

I’ll leave the code example as-is, so your reply makes sense. But instead, I’d create a float peakLevelTemp within the scope of processBlock, update that per-sample, and then call peakLevel.set (peakLevelTemp) once, at the end of processBlock.

Is that also true for reading from atomics? Or just writing to them?

Yes.
Atomics create a ‘memory barrier’ that forbids the compiler to reorder or optimize away the operation, which is why they are safe to use in a multi threaded environment, but also exactly why they aren’t great in per-sample loops.

1 Like

Hmm, if reading from atomics has a negative impact on realtime performance, maybe that should be a caveat mentioned in the context of JUCE’s AudioProcessorValueTreeState::getRawParameterValue()? Since JUCE v5.4.6, that method was changed to return a std::atomic<float>*. The comments on that say:

“Returns a pointer to a floating point representation of a particular parameter which a realtime process can read to find out its current value.”

Nothing in that documentation suggests that you shouldn’t be using that pointer as often as you please, including within a per-sample loop.

std::atomic’s behavior (and performance hit) is well documented, there’s no need for JUCE to document that.

After further thought - for the code I posted above, wouldn’t this break the atomicity of sharing the peakLevel between threads?

Say there’s Processor members std::atomic<float> peakLevel, and float peakLevelTemp, to implement this idea.

When the Editor’s timerCallback fires, it calls getAndResetPeak(), which atomically gets the peak value from peakLevel, AND resets it to 0, using the atomic exchange method. That is all thread-safe and good.

But then what would you do to reset the peakLevelTemp (non-atomic) float? If you have getAndResetPeak() set it to 0, then that’s not thread-safe (Message thread would be modifying a Processor member, possibly while the audio thread is mid-processBlock).

If you don’t have getAndResetPeak() set peakLevelTemp to 0, then at the end of the per-sample loop, when you write that value into the atomic, that peakLevelTemp is no longer a correct peak value (and possibly the peak light is On when it should be Off).

I think your implementation in this case needs to be rethought regardless of atomics.

If the editor is ‘allowed’ to reset the peak, that reset should be ‘signaled’ to the processor before the block starts (using a parameter/atomic variable, for example).

Then the processor can reset it before the block, calculate the peak and after it’s done, send the current value out to the second shared atomic.

Using the same atomic for read/write in the processor and editor is probably not what you want to do in this case.

I am unclear what that would gain you.

The peak level value calculated by the processor is a running peak measurement, not a peak-per-block measurement, so there’s no need to coordinate its calculation with when block processing starts or ends.

And the peak value exists only for the purpose of “publishing” that value, so that the GUI can use it for user feedback/eye candy
 in other words, nothing else in processBlock uses it or depends on it. So I don’t see the harm in letting the Editor’s timerCallback be (directly) in charge of the peak reset.

What you’d gain, is a much faster loop that calculates the peaks. :slight_smile:

You might not care as much about performance in this particular loop, but in most sample-loops you care quite a lot about it.

And since sample loops are not run in any musical speed (they’re just done as fast they can) probably not lose any accuracy when doing the peak reset per block.

I don’t think that would work either. With two atomics, between getting one and setting the other in the UI thread a whole block may have passed in the audio thread, and you’d lose a peak:

        1 2 3 4
        5 6 7 8 -> get: 8
        9 8 7 6
reset-> 5 4 3 2 -> get: 5

I think you may solve it with one atomic setting it to an out-of-range value, say -1, as a flag. So in the UI thread:

// actually you should keep the old value if exchange returns -1
return blockPeak.exchange (-1.0f);

and at the end of each block:

if (blockPeak.exchange (runningPeak) == -1.0f)
    runningPeak = 0.0f;
1 Like

@kamedin when communicating a ‘reset’ command from the editor, that runs on a slower rate, you’re gonna miss out on some peaks anyway
 all that matters is that the reset message would come in sometime later, as you can’t trust your mouse clicks to be sample accurate.

Having that said, if you’re getting that message from a MIDI event, you can split the block so it would be sample accurate, but when talking about automation parameters/UI thread messages the ‘correct’ place to read them is at the start of the block, as that would produce the fastest sample block processing.

If you absolutely need all the values in the buffer to be in the GUI thread (say, if you’re calculating FFT) you’re better off with passing that full buffer via FIFO, and calculating that in the GUI thread, vs trying to use an atomic during the sample loop.