Parameter synchronization in GainPlugin example


#1

Hi all,

I am looking at the Juce example GainPlugIn.
It seems to me that the gain parameter (variable of type AudioParameterFloat*) is being accessed by the audio thread in processBlock() and by the UI thread in GenericEditor.h, both in the Timer callback and in the sliderValueChange() callback.

However I could not find any synchronization mechanism for the access to the variable.
Is this maybe handled automatically by the AudioParameter Class or by the plugin framework ?

thanks


#2

You are right: the synchronisation of the parameters between the UI thread and the audio thread is not clean. In fact, according to the C++ standard, it is undefined behaviour to access the same memory address from multiple threads without using std::atomic (or JUCE’s Atomic) or some kind of locks (which is a bad idea in your audio thread as your audio thread should be lock-free - so use atomics instead).

However, in practice, it currently works to share a float like this in plug-ins. This is because - on all platforms that support desktop DAWs (basically x86) - reading/writing a float (or anything that is 64-bits or less) is a single machine instruction and is atomic [1][2], i.e. it will never be in a teared state - an update to a float was either executed successfully or not.

In fact, on these platforms, I’ve found that std::atomic will generate the exact same load and store machine instructions compared to not using std::atomic at all. On arm, however, the situation is different: there you are likely to need std::atomic (or JUCE’s Atomic class) as floating point access is not guaranteed to be atomic.

I say currently, because future (and in rare cases, even current) compiler optimizations may break your code if you do not use std::atomic. Consider the following example:

 void processBlock (...)
 {
      float localCopy = myParam;

      for (int i = 0; i < numSamples; ++i)
      {
            sample[i] *= localCopy;
      }
 }

 ....
 float myParam;

In this code, you want to avoid tearing and therefore copy the parameter value to localCopy before you do any processing. For arguments sake, let’s say your processor depends on localCopy being constant during the for loop - otherwise it will crash.

The above code will currently work on x86 - even if you update myParam from the UI thread. However, technically, the compiler optimizer is allowed to replace any reference to localCopy with a reference to myParam . This could be because the compiler optimizer wants to save stack-space or save valuable registers for other operations and therefore tries to optimize away the storage for localCopy. The result is that localCopy will not remain constant during the loop if myParam is updated from the UI thread.

The standard says that the C++ compiler is allowed to do this as changing myParam outside of processBlock would be undefined behaviour. Therefore it can assume that myParam will remain constant in processBlock as the compiler sees that processBlock itself (or any of the sub-routines it calls) does not change myParam.

Wrapping myParam in std::atomic explicitly tells the compiler that myParam may change from outside of the thread and therefore the above optimization is not allowed.

In essence, if you want to play it safe and be future-proof, use JUCE’s Atomic class or std::atomic - currently, however, it is very unlikely to make any difference for plug-ins running on desktop OSes.

[1] There are some other requirements for it to be atomic like proper padding and alignment. The compiler will do that for you anyway though unless you are using some crazy pragmas
[2] If an operation is expressed as a single machine instruction, it does not mean that it is atomic. However, load/stores of anything that is a 64-bit word or smaller is atomic on x86 when properly padded and aligned.


#3

I remember that jules said that atomics haven’t been used in GainPlugin on purpose, to keep the code simple for novices to understand without too much “side quests” to get the overall big picture.

However, since this is not the first time that this subject appears in the forum, I’d think that adding a short comment in the GainPlugin code that references this discussion in the forum, will be beneficial for all the developers who will get to the next level and wonder why no synchronization primitives are in place between the two threads.


#4

Thank you for such a detailed and exhaustive answer !


#5

Having dug a bit more into this, I have got another couple of questions.

How to use atomic in conjunction with AudioProcessorParameters ? The ownership of the AudioProcessorParameter objects belongs to the plugin, and the only access one has from user code is through the pointer returned by new in addParameter().

For example:
addParameter (gainParam = new AudioParameterFloat (“gain”, “Gain”, 0.0f, 1.0f, 0.9f));

One possible way to go could be to roll your own AudioParameter classes that use atomics in their guts…maybe ?


#6

Yeah you need to create a derived class of AudioProcessorParameter with an atomic float as a private data member.

Then you can override setValue and getValue to use the atomic.

i.e. something along the lines of:

MyParameter::setValue(newValue)
{
    paramValue.store(float newValue);
}

float MyParameter::getValue()
{
    return paramValue.load();
}

Provided your derived class (MyParameter) inherits from AudioProcessorParameter you can still add it to the processor just using

addParameter(new MyParameter("gain", "Gain", 0.0f, 1.0f, 0.9f);

The situation is a little different if you start using the ValueTreeState stuff.

Depending on what your C++ experience is like maybe have a quick read up on polymorphism and virtual functions just so you understand the concept behind this.

NOTE: Your inheriting from AudioProcessorParameter if you do this NOT AudioProcessorParameterFloat

Cheers


#7

Thanks !:heart: :heart: