Parameter Changes & Thread Safety

So I have some heavyweight objects (convolution engines & IIR filters) that need to be updated in the event of a change to an oversampling parameter. This involves resizing some auxiliary buffers (though I’ve made sure to allocate enough memory for the maximum possible size in the constructor of the AudioProcessor), passing a new ProcessSpec to the appropriate objects, and running some further updates to make sure the impulse responses being used by the convo engines are running at the correct sample rate.

Essentially I’m running into two possible scenarios, neither of which is ideal and they’ve both generated issues. First, there’s updating the appropriate objects in my APVTS Listener callback (parameterChanged) and using an atomic flag to check whether we’re ready to call the process method of these objects. I’ve never experienced issues with this, but some users have reported crashes pertaining to the oversampling switching. Additionally, this consistently fails pluginval during the parameter thread safety test. My guess as to the reason is: since the atomic flag might be switched in the middle of the process callback, there’s no way to guarantee that the objects doing the processing will have the new ProcessSpec passed on a parameter change in the middle of the audio callback, thus resulting in some bad memory access / data races.

Second, there’s doing the resizing and ProcessSpec passing in processBlock, which is a big no-no. On Windows, it works just fine for me, and passes pluginval with flying colors, but on my (old and slow) Mac running Logic, switching the Oversampling always results in an error where Logic halts playback to inform you that the audio couldn’t be processed in time. Makes sense, since I’m doing all that resizing and preparation in the processBlock with no reason to be there.

So my main question is: given the above, what would be a better way of ensuring thread safety of updating parameters?

You could use the objects from the process through a pointer, create the new objects on another thread, set a flag to update them in the next process callback, then switch pointers there. The new objects can be allocated while the old ones still exist. Still, if you’ve reserved memory for the maximum buffer sizes, they should be safe to resize in the audio process, and it shouldn’t be that heavy, unless you also need to clear the buffers and they’re really large.

1 Like

Thanks, I’ll have to try your suggestion re: creating new objects and switching pointers.

Still, if you’ve reserved memory for the maximum buffer sizes, they should be safe to resize in the audio process, and it shouldn’t be that heavy…

Right, and it seems that it’s mainly updating the processors via a custom prepare function that’s causing the slowdown rather than the resize.

in my last project i made it so that when processBlock starts it checks if the oversampling-order should be changed atm. if yes i manually call prepareToPlay, which reallocates everything ofc, and then leave processBlock without processing the block. this often makes the audio discontinuous at that point cause there are so many reallocations and sometimes even a new latency sent that there’s just no way for that to be smooth. it is thread-safe tho since we are definitely not using all those buffers while resizing them. in situations like that i think it’s ok to have a crappy transition in sound, because people never automate the oversampling-order, they just set and forget and then go on with the smooth operations

Don’t forget that you always can call

suspendProcessing(true);
// do something
suspendProcessing(false);

in the processor code to avoid any processing while loading something.

1 Like

why didn’t anyone ever tell me about this powerful feature before? :o

1 Like

The best is, that you can call it from every thread.

I actually was using suspendProcessing but was still getting crashes…
Seemed like in the time it took to get the processing to actually suspend, it would still run into some access issues.

I should add that I was doing this before I switched to the atomic flag method, since I figured checking a boolean would be faster than the method to stop audio callbacks.

maybe you could do something like

while(!isSuspended()){}

so it just spins for those ms where you wait for it to take action.

a cleaner solution would be if a suspended processor had an alternative method that is called then to show you that the suspension started. maybe that’s what releaseResources is for. i haven’t used that method yet

You’re saying to wrap the process block in that? Wouldn’t that make it loop over the same buffer indefinitely until processing is suspended? Or am I misunderstanding something about your suggestion?

I don’t think there’s need to call suspendProcessing() from processBlock() -you’re already there, it’s blocking itself. You call suspendProcessing() from another thread, so that if processBlock() is running your thread will have to wait, and if processBlock() needs to be called while it’s suspended, it won’t be called and the buffer will return cleared.

3 Likes

I don’t think there’s need to call suspendProcessing() from processBlock()

Right, I was calling it from parameterChanged. Still ran into thread safety issues, however. That being said, I’ve just had some luck by essentially using the method of doing resize/prepare in the process block but then returning immediately after. Passes pluginval on Windows, now I’ll just have to see if it’s still too much for Logic to handle…

UPDATE: It didn’t work in Logic. Kept triggering assertions in my dry/wet mixer, which for some reason never occurred on Windows…
My previous method of just doing resize and prepare in the process block and then pushing right along now works flawlessly…
I’m thinking the system overload issues may just have been a result of it being a debug build. I swear I tested it as a Release build, too, but alas.

2 Likes

You may have different places where it still can crash. It can not crash in the processBlock method while suspendProcessing(true) is active unless it is a JUCE bug. I’m using this method for years now and it works without any problems.

You should not do this in the processing block. It looks like your method allocates memory and does time-consuming things. This may lead to audio dropouts. Keep in mind that people use the plugins in sessions with dozens of plugins.

You also should not do it in the UI thread. I would use the JUCE ThreadPool with one thread size and do the following steps:


void prepareAsync()
{
  threadPool->addJob([this, param] { prepare(param) });
}

void prepare(int param)
{
  suspendProcessing(true);
  // do your work
  suspendProcessing(false);
}

You may have to make sure that this isn’t called all the time when you have a slider or something. You may need to use some throttling mechanism. You could do this with a timer that checks for changes every 0.2 seconds or so. depending on the work amount you have to do.

Suspend processing may cuts out audio for some processing blocks and this also isn’t welcome. As a second step you may do something like this to avoid this and keep the suspendProcessing interval as short as possible:

void prepare(int param)
{
  // prepare new buffers
  suspendProcessing(true);
  // swap pointers 
  suspendProcessing(false);
}

I think there needs to be some good consideration before resorting to new threads. It’s not free either, and if the job is not that heavy it may end up being worse. For me, the first option is always setting a flag to do the stuff in the audio thread. There are ways to avoid allocations: you can preallocate buffers and vectors, you can use polymorphic containers. If the audio thread can’t deal with it, the second option is moving the heavy stuff to the message thread. If the UI gets unresponsive, then I can consider spawning threads.

Whether you delegate to the message thread or another one, there’s no need to call suspendProcessing(). Take a look at this talk and this code. For passing large objects to the audio thread, you need what they call a non-realtime mutatable object (in the current version, RealtimeObject<T, RealtimeObjectOptions::nonRealtimeMutatable>); for the other way around, a realtime mutatable object. Both use a CAS loop in the non-realtime thread. For small structures, I use triple buffering -it spends more memory, but it’s wait-free for both sides.

template <typename T> struct TripleBuffer
{
    auto& read() const { return buf_[r_]; }
    auto& write()      { return buf_[w_]; }

    bool acquire()
    {
        int changed{ ready_.load (std::memory_order_relaxed) & 4 };
        if (changed) r_ = ready_.exchange (r_, std::memory_order_acquire) & 3;
        return changed;
    }

    void release() { w_ = ready_.exchange (w_ | 4, std::memory_order_release) & 3; };

private:
    T buf_[3]{};
    int r_{ 1 }, w_{ 2 };
    std::atomic_int ready_{};
};

You write through write(), then release() when it’s done. On the other side, you acquire() and then read(). acquire() also tells you if there’s been a new release() since the last call.

3 Likes