I develop a plugin that has the ability to add and delete processors to create synth patches. I have previously ignored deleting processors to focus on other features and am now rolling back to consider this problem alongside things such as preset loading and unloading.
I understand that I do not want the processors to ever be deleted on the audio thread and have set up a Timer in my backend base class. In this class I am already using moodycamel::ReaderWriterQueue to pass processor creation to the backend in a thread-safe manner.
void SynthBase::processAudio(AudioSampleBuffer* buffer, int channels, int samples, int offset) {
AudioThreadAction action;
while (processorInitQueue.try_dequeue (action))
action();
...
}
I like this pattern as it allows me to use lambdas of the type
AudioThreadAction = juce::dsp::FixedSizeFunction<actionSize, void()>;
sidenote: this idea is lifted from jatin chowdhury’s wonderful chowdsp_utils
I want to implement a similar setup for the deletion of processors to be triggered in the SynthBase::timerCallback. Here is my current setup
void SynthBase::timerCallback() {
DeleteThreadAction action;
bool succeeded = true;
while (succeeded) {
auto * front = processorDeleteQueue.peek();
if (front!=nullptr && (*front)()) {
succeeded = processorDeleteQueue.try_dequeue(action);
}else {
succeeded = false;
}
}
The complication here is that the processor is held by the backend class with a shared_ptrs and one class holding a raw ptr so the lambda in this ReaderWriterQueue is using DeleteThreadAction = juce::dsp::FixedSizeFunction<actionSize, bool()>; and would return false if the shared_ptr is not the final reference to the object meaning it is still pending removal from the backend.
The raw ptr is held by a tracktion::engine::ValueTreeObjectList<ProcessorBase>. I use this to manage creation and deletion so that the delete would be triggered from the UI through ValueTree removal.
so my `deleteObject’ code would look something like this
//trigger UI listeners to delete UI component
for (auto listener: listeners_) {
listener->removeModule(processor_base);
}
//add lambda to poorly named ProcessorInitQueue to remove from backend vector
...
// add lambda to processorDeleteQueue to trigger actual deletion of the object
...
}
now there are some questions here.
Firstly, could I just have one lambda the passes to the processorInitQueue and have that lambda moving the ProcessorBase into the processorDeleteQueue. If I did this then I could possibly turn the shared_ptr<ProcessorBase> into a unique_ptr<ProcessorBase> which is probably what it should be. This would eliminate the need for having an action that returns bool because I can guarantee it is ready to be deleted once it is passed into that function which is much cleaner.
Would that cause a copy on the audio thread? This copy question which I will investigate as I implement is more is my main concern for this use case.
My other question is how will this affect preset loading as well as app close. I currently have the deletion timer set to something very low like every 500 milliseconds. This would obviously slow down preset loading and ideally when a user is loading a preset I can guarantee audio processing is turned off (at least in my case I could and probably should(?)). Would something as simple as a check for preset loading be the solution here and then instead of calling the lambdas I could make a direct delete of these things. I guess in the case of program close I would probably want the deletion of the backend object to be handling the clean up of those processors so maybe
if (close)
...
else if (presetLoad)
...
else
...
but that’s probably overkill.
anyway thought I would pop this thought here as I implement it to see how other people solve this and similar problems.
