Hi Folks,
Fair warning: this post is quite long. The questions I posed at the end might make sense to some people without having to read all my implementation details, so you can try skipping to those if you’d like. I apologize in advance for wasting anyone’s time.
I have a few general questions to discuss about best practices for structuring my audio plugins.
I’ve completed a few projects in JUCE, but none that are optimized to a level that is release-ready IMO.
There are two questions that have always plagued me through these designs that I have not been able to find a good answer to:
- What is the best way to pass down parameter update calls from the DAW?
- What is the most efficient way to process audio by different modules within a class?
My gut feeling says they are related, and I think my inability to find the optimal solution is based on my lack of knowledge of processing threads.
I’ll give a simple example, and tell you how I currently handle the implementation.
I’m making a basic Chorus effect. Right now, its stereo only, and it uses two delay lines for each channel -> 4 total delay lines. Also, each delay line has an LFO that modulates the delay time. Since the L & R channels require a phase offset, there are 4 LFO modules, one for each delay line. Eventually, there will be a filter, but we can ignore that for now.
So my current implementation looks something like this:
class DelayLine {
public:
float process(float inputSample, float delayLengthInSamples);
float feedback;
private:
float* buffer;
float writeIndex;
};
class Lfo {
public:
float getNextSample();
void setFrequency(float f);
private:
const float* currentWaveTable;
float readIndex;
float increment;
};
class Chorus {
public:
void process(AudioBuffer<float>& blockToProcess);
void setDelayLength(int delayIndex, float delayLength);
void setLfoFrequency(int lfoIndex, float frequency);
void setLfoAmount(int lfoIndex, float lfoAmount);
void setFeedback(float feedback);
float dryWetMix;
private:
float sampleRate;
Array<float> currentDelayLengths;
Array<float> lfoAmounts;
OwnedArray<Lfo> lfoArray;
OwnedArray<DelayLine> delayArray;
};
Then, in my class that inherits from AudioProcessor, I have a Chorus object and an AudioProcessorValueTreeState for all the parameters that the DAW can interact with.
In the ‘processBlock’ function of AudioProcessor class, I just call the ‘process’ function from the Chorus class and pass it the AudioBuffer from processBlock. Then, in the Chorus ‘process’ function, there is a loop that goes through each sample, gets the LFO sample, and then gives the sample to the DelayLine to get the delayed output.
Finally, I handle parameter updating by having my class that inherits from AudioProcessor also inherit AudioProcessorValueTreeState::Listener, then implementing the parameterChanged() function with a switch statement to call the appropriate set function in my Chorus class, which in turn calls the appropriate set functions in the Lfo and DelayLine classes, like this:
void ChorusAudioProcessor::parameterChanged(const String& parameterID, float newValue)
{
// tree is a reference to the AudioProcessorValueTreeState
int parameterIdx = tree.getParameter(parameterID)->getParameterIndex();
switch (parameterIdx) {
// LFO #1 frequency
case 0:
chorus->setLfoFrequency(0, newValue);
break;
// LFO #2 frequency
case 1:
chorus->setLfoFrequency(1, newValue);
break;
// DelayLine #1 length
case 2:
chorus->setDelayLength(0, newValue);
break;
... etc ...
}
}
I’m interested in a few specific situations relating to this implementation, and to my two more broad questions I posed at the start:
-
If the DAW wants to update a parameter (I’ll use feedback as an example) while the Chorus object is in the middle of its sample-by-sample processing loop, does this present any issues? In my novice programming brain, the functionality would look like: Processing audio in loop -> setFeedback called -> Pause audio processing loop -> update feedback -> resume audio processing
-
If I continue to construct new components from these subcomponents, it would seem that I would start to get long chains of set functions. Will this cause issues with performance, and if so, how might I change my program structure to reduce these chains of function calls?
-
Excessive function calling also seems to occur in my Lfo and DelayLine classes, due to them processing single samples. Would a better method be to have the Lfo class fill a buffer with output samples, then pass this buffer along with the input audio to the DelayLine class, which fills the input buffer with delayed samples?
-
Related to (3), if processing blocks is indeed more efficient, how might this affect signals from the DAW to change parameters in the middle of processing? Using the feedback example above and my current understanding of this implementation, if the DAW wants to change the feedback in the middle of processing a block, the sample-by-sample loop will get paused, the feedback will be updated, and then processing will resume. If I switch to block processing, then it seems to me that the DelayLine would rarely update its feedback in sync with the signals from the DAW, and instead the feedback will more likely be updated between blocks. For some parameters this is likely okay, but for others such as the frequency of a filter, it would seem that using a block size of 512 or more could produce audible artifacts by being unable to adjust the frequency during the processing of a particular block.
I don’t necessarily need answers to all of these questions. Rather, I just put them there as an outline of my thoughts and the logical issues that seem to stem from them. If any of you have any insight regarding any of these topics, whether it answers my questions directly or not, that would be very much appreciated. Also, if anyone knows resources that can help me maximize the efficiency of my plugins, specifically regarding parameter updating and block processing, then I would love to take a look at those.
As I said before, I really have not been able to find much on these topics, which is why I decided to outline my current understanding, so that a kind soul can tell me where it is wrong and where I might go to improve it.