VST pitfalls with parameter synchronization

DSP, Plugin and Host development discussion.
RELATED
PRODUCTS

Post

I "double buffer" my coefficients, so setParmeter can go ahead and write on one copy while process is safely using another copy. Then, when setParameter is done, it sets a flag telling process to switch to the other copy on its next run. What if another setParameter call comes in before the swap? Then it unsets the swap flag and builds into the same copy it used before. Does this make a race condition? Not that I've ever caught happening (I suppose I could triple buffer if I was really worried about that - but for the plugin I did that in it wouldn't be fatal, so I didn't worry about it.)

Post

very angry mobster wrote: One of the difficulties of using the MVC pattern is finding ways to keep all the 'views' in sync with the 'model'. My code uses a ad hoc collection of techniques.

The GUI will update all parameters when the GUI is opened. Some parameters are updated on a timer. Some parameter changes will send a 'windows message' to the GUI to indicate a change.

I break with the MVC pattern to update the audio engine. My plugin parameter objects update the audio engine directly. (In a way the audio engine is a second model that mirrors the first.')
I've seen hierarchical MVC evangelized a bit, also called Presentation-Abstraction-Control, where the idea of separate models is more natural. Particularly interesting I think was that controllers are the only things that communicate across categories, so a lot of these sorts of synchronization problems end up being solved just within a tree of controllers - implementation might be vastly different but just as a logical outline I really liked this.

[e] Maybe it's useful for more rigidly defining protocol when passing messages between very different process types ... audio and GUI and host all sort of implicitly carry their own particular preferred protocols (each with technical elegances and limitations), so there's a mapping and transformation amongst all this that the controller hierarchy delineates.

Post

AdmiralQuality wrote:I "double buffer" my coefficients, so setParmeter can go ahead and write on one copy while process is safely using another copy. Then, when setParameter is done, it sets a flag telling process to switch to the other copy on its next run. What if another setParameter call comes in before the swap? Then it unsets the swap flag and builds into the same copy it used before. Does this make a race condition? Not that I've ever caught happening (I suppose I could triple buffer if I was really worried about that - but for the plugin I did that in it wouldn't be fatal, so I didn't worry about it.)
Yes, that's what I thought. You need an atomic CAS operation for this sort of thing.

Post

Big Tick wrote:
very angry mobster wrote: +1. I use the same approach as AdmiralQuality. I don't use locks for parameters that map to a float or integer value. I consider them atomic. In most cases my audio engine code doesn't care if these atomic parameters change mid process. The parameter is copied when it can't change mid process without something blowing up. Often the copying doesn't need to be explicit and happens as part of how my audio engine works.
So you're doing something like this ?

Code: Select all

processReplacing(...) {
   float newCutoff = getParameter(CUTOFF);
   if (newCutoff != this->prevCutoff) {
		this->prevCutoff = newCutoff;
		recomputeFilterCoeffs();
   }
   ...
}
Essentially yes.

Post

I got a solution now which is working for me, and which should work for whatever interpretation of the vague VST standard a host sequencer has. I want to avoid that any parameters are changed during audio processing, I want to avoid big <memcpy> of all parameters at the beginning of every audio processing call (about 10kB in my case), and I want to avoid that a parameter gets set to an old value by the host when the parameter is already at a new position.

As code is easier to understand than words (isn't it?), I pasted my code in here, plus just a few words.

An array <m_hostValues> contains all parameters as the host sees them. They are always updated immediately on user action (which also notifies the host) and on <setParameter> calls. These values are also what gets returned to the host on <getParameter> and <getParameterDisplay>. That's for example important for Ableton Live's generic sliders - if you move them Ableton Live asks us directly for the text to display, so we need to operate on the latest values for this.

And we got this struct:

Code: Select all

struct ParameterUpdate
{
    ParameterUpdate()
    : m_paramId(0), m_setByGui(false), m_guiValue(0.f) { };

    ParameterUpdate(int paramId, bool setByGui, float guiValue=0.f)
    : m_paramId(paramId), m_setByGui(setByGui), m_guiValue(guiValue)
    {
        assert(m_setByGui || m_guiValue == 0.f);
    };

    int m_paramId;
    bool m_setByGui;
    float m_guiValue;
};
And a lock-less queue and an array containing information about parameters which are blocked due to changes in the UI:

Code: Select all

CircularFifo< ParameterUpdate, PARAMETER_COUNT * 2 + 1000 > m_parameterQueue;
uint32 m_blockedParameterUntil[PARAMETER_COUNT]; // Time in ms.

When moving a UI control we do this:

Code: Select all

//--- Update the parameter within the UI control immediately.
...

//--- Block the parameter by setting some time in the future (1.2 seconds works fine for us):
m_blockedParameterUntil[index] = time.now() + 1200;

//--- Make sure the updates host values are set before we add the parameter to the queue,
// so there is no chance for it to toggle back.
m_hostValues[index] = value;

//--- Add parameter change to the queue.
assert(!m_parameterQueue.isFull());
m_parameterQueue.push(ParameterUpdate(index, true, value));

//--- Inform the host about the parameter change.
..

When the host sequencer calls <setParameter> we do this:

Code: Select all

if(value != m_hostValues[index])
{
    //--- Make sure that host values are current.
    m_hostValues[index] = value;

    //--- Add parameter change to the queue.
    assert(!m_parameterQueue.isFull());
    m_parameterQueue.push(ParameterUpdate(index, false));
}

At the beginning of each audio processing, we do this:

Code: Select all

//--- Speedup.
if(m_parameterQueue.isEmpty())
    return;

//--- Set all parameters if they had been set by the UI or if they had been
// set by the host sequencer and are not blocked.

uint32 timeNow = time.now();

std::unordered_set< const int > delayedParameters; // Use <std::unordered_set> for parameter IDs to be unique.
ParameterUpdate parameter;
while(!m_parameterQueue.isEmpty())
{
    m_parameterQueue.pop(parameter);
    if(parameter.m_setByGui)
    {
        //--- UI changes are always applied immediately. And we make sure
        // that the value from the UI change is taken.
    
        // Parameters with the same ID which had been blocked are now invalid.
        delayedParameters.erase(parameter.m_paramId);
        
        //
        const float value = parameter.m_guiValue;
        m_hostValues[parameter.m_paramId] = value;
        
        // Set the parameter in audio nodes and UI controls (UI updates
        // will be buffered for the UI thread to pick them up).
        setParameterNow(parameter.m_paramId, value);
    }
    else if(timeNow > m_blockedParameterUntil[parameter.m_paramId])
    {
        //--- The parameter has been set by the host sequencer and is not
        // blocked. We set it to the latest value that we got from the host.
        
        // Set the parameter in audio nodes and UI controls (UI updates
        // will be buffered for the UI thread to pick them up).
        setParameterNow(parameter.m_paramId, m_hostValues[parameter.m_paramId]);
    }
    else
    {
        //--- The parameter is blocked. Simply store its ID.
        delayedParameters.insert(parameter.m_paramId);
    }
}

//--- Copy all blocked parameter IDs to the parameter queue for another round.
for(std::unordered_set< const int >::iterator iter = delayedParameters.begin(); iter != delayedParameters.end(); ++iter)
{
    m_parameterQueue.push(ParameterUpdate(*iter, false));
}

That's it (well also some special handling in case of program changes and MIDI automation and some treatment for macro parameters which control other parameters, but let's ignore this for now).

Post

Very similar to what I do. The only caveat is that std::unordered_set in processReplacing() requires dynamic memory allocations, which internally, gets you back to using a lock :(

Post

Sorry to rant, but any Plugin Standard that require THAT much code just to change a parameter is absolute rubbish.

Given that ALL parameter changes in ALL plugins require all this painful code...why isn't this functionality encapsulated in the SDK in the first place?

An API should specify Processing Parameter changes are notified ONLY on the Processing thread so you just don't have to worry about concurrency..

Code: Select all

void BpmClock3::onSetParameter(void)
{
	if( GainParameter.isUpdated() )
	{
		float newParameterValue = GainParameter;
	}

	if( SomeOtherParameter.isUpdated() )
	{
		// etc.
	}
}
(likewise the GUI would be notified on the GUI thread only).

Post

Big Tick wrote:Very similar to what I do. The only caveat is that std::unordered_set in processReplacing() requires dynamic memory allocations, which internally, gets you back to using a lock :(
std::unordered_set won't be called by any other thread, I guess using it should be fine?

Post

Jeff McClintock wrote:Sorry to rant, but any Plugin Standard that require THAT much code just to change a parameter is absolute rubbish.
Well, that's VST, it has absolutely bad design. It could have been so easy by defining a well-thought-of standard instead, but well, we have to work with what is there.

Post

Jakob / Cableguys wrote:
Jeff McClintock wrote:Sorry to rant, but any Plugin Standard that require THAT much code just to change a parameter is absolute rubbish.
Well, that's VST, it has absolutely bad design. It could have been so easy by defining a well-thought-of standard instead, but well, we have to work with what is there.
:roll:

You guys think too much.

Post

Jakob / Cableguys wrote:
Jeff McClintock wrote:Sorry to rant, but any Plugin Standard that require THAT much code just to change a parameter is absolute rubbish.
Well, that's VST, it has absolutely bad design. It could have been so easy by defining a well-thought-of standard instead, but well, we have to work with what is there.
Talk is cheap. Show me! :hihi:
Grtx, Marc Jacobi.
VST.NET | MIDI.NET

Post

In which hosts does such a resending happen?

What about some parameter smoothing? That way changes won't be too abrupt. May solve the problem partially.

Then you can try minimizing GUI control value resolution. You provided example with 0.1, 0.2, 0.3 values, but one could change resolution to, for example 0.005, so changes are small and usually not noticed in sound. However, user may try explicitly to make a fast change, like from 0 to 1, but this is a rare case which shouldn't happen in most normal situations. Think there should be some appropriate smooth filter even for such a fast changes.

I think the problem should be mostly solved if you combine these 2 approaches (smoothing + increasing resolution).

Post

Jakob / Cableguys wrote:
Big Tick wrote:Very similar to what I do. The only caveat is that std::unordered_set in processReplacing() requires dynamic memory allocations, which internally, gets you back to using a lock :(
std::unordered_set won't be called by any other thread, I guess using it should be fine?
No, it won't. Deep inside the code of std::unordered_set you will find a call to malloc(), which itself, uses a mutex as explained here: http://stackoverflow.com/questions/1070 ... nvironment

Getting dynamic allocations out of the audio thread is particularly tricky - if you use std::list, std::vector, .... then you are using malloc() calls, and potentially you can get locked by the UI (or any other) thread.

In this particular case, the way I solved it is by pushing all the parameter changes on the queue, and using an unordered_set on the reader thread to single them out, before refreshing the UI (malloc calls on the UI thread are ok). Of course there is always the case where, while playing back automation, a lot of parameter changes would overflow the queue. When this happens I just stop pushing parameter changes, and instead flag the editor to do a full refresh.

Post Reply

Return to “DSP and Plugin Development”