Hi All, I’m fairly new to JUCE and as a project am making a multi-band “leveler” which just allows you to adjust gains in different frequency bands, with a moving crossover. I want to do 4 bands ideally, but to get started I did just two bands and it seems to behave as expected, with what sound like some phase issues. Here are my main questions:
-
Will I necessarily need to make copies of the buffer input to processBlock()?? Right now inside the processBlock() function I create copies: band1Buffer and band2buffer, that get processed separately and then summed before output.
-
The current processing needs to loop over the channels in order to create the copy buffers, then ends the loop to execute the commands necessary for the dsp-module processor chains, then loops over the channels again to sum into the buffer for output. This loop-end-loop structure inside processBlock() feels a little inelegant, but again I’m new and maybe this is the best way, given that the addFrom and copyFrom functions are designed to work on one channel at a time.
3a. I’m aware from some research that the choice of filter is important for dealing with phase issues especially as we move into more bands. Currently I have a ProcessorChain which is a Duplicator of a StateVariableFilter, and a Gain. Is this just plain wrong?
3b. My next task is to study up on the details of DSP filtering to better understand how the phase gets affected. I have the book “Designing Audio Effects Plugins in C++” by Will Pirkle, and some other online resources, but any other tips in the right direction are much appreciated, thanks!
Below I’m copying some of the relevant segments of my code, as well as a GitHub link to the full project
Link to Project: https://github.com/langermarc19/TwoBandLeveler
Processor Chains:
//===========DSP Processing Chains=============
dsp::ProcessorChain<dsp::ProcessorDuplicator<dsp::StateVariableFilter::Filter<float>, dsp::StateVariableFilter::Parameters<float>>, dsp::Gain<float>> band1Chain;
dsp::ProcessorChain<dsp::ProcessorDuplicator<dsp::StateVariableFilter::Filter<float>, dsp::StateVariableFilter::Parameters<float>>, dsp::Gain<float>> band2Chain;
prepareToPlay and UpdateProcessorChains methods:
void TwoBandLeveler_3AudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
mSampleRate = sampleRate;
dsp::ProcessSpec spec;
spec.sampleRate = mSampleRate;
spec.maximumBlockSize = samplesPerBlock;
spec.numChannels = getMainBusNumOutputChannels();
updateProcessorChains();
band1Chain.prepare(spec);
band2Chain.prepare(spec);
}
void TwoBandLeveler_3AudioProcessor::updateProcessorChains()
{
float crossover = *pCrossover;
float band1gain = *pBand1Gain;
float band2gain = *pBand2Gain;
band1Chain.get<0>().state->type = dsp::StateVariableFilter::Parameters<float>::Type::lowPass;
band1Chain.get<0>().state->setCutOffFrequency(mSampleRate, crossover, (1.0 / MathConstants<double>::sqrt2));
band1Chain.get<1>().setGainLinear(band1gain);
band2Chain.get<0>().state->type = dsp::StateVariableFilter::Parameters<float>::Type::highPass;
band2Chain.get<0>().state->setCutOffFrequency(mSampleRate, crossover, (1.0 / MathConstants<double>::sqrt2));
band2Chain.get<1>().setGainLinear(band2gain);
}
ProcessBlock method:
void TwoBandLeveler_3AudioProcessor::processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiMessages)
{
ScopedNoDenormals noDenormals;
auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels();
auto bufferLength = buffer.getNumSamples();
for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
buffer.clear (i, 0, buffer.getNumSamples());
//update processor chains with any new settings from parameters
updateProcessorChains();
//set up copy buffers to process each band seperately
AudioBuffer<float> band1Buffer;
AudioBuffer<float> band2Buffer;
band1Buffer.setSize(totalNumInputChannels, bufferLength);
band2Buffer.setSize(totalNumInputChannels, bufferLength);
//copy input into each band's buffer
for (int channel = 0; channel < totalNumInputChannels; ++channel)
{
auto* channelData = buffer.getWritePointer (channel);
auto* bufferData = buffer.getReadPointer (channel);
band1Buffer.copyFrom(channel, 0, bufferData, bufferLength);
band2Buffer.copyFrom(channel, 0, bufferData, bufferLength);
}
//apply the filter and gain processing to each band using dsp module features
dsp::AudioBlock<float> band1Block (band1Buffer);
dsp::AudioBlock<float> band2Block (band2Buffer);
band1Chain.process(dsp::ProcessContextReplacing<float> (band1Block));
band2Chain.process(dsp::ProcessContextReplacing<float> (band2Block));
//clear input and sum the bands back into the output buffer
buffer.clear();
for (int channel = 0; channel < totalNumInputChannels; ++channel)
{
auto* band1Data = band1Buffer.getReadPointer(channel);
auto* band2Data = band2Buffer.getReadPointer(channel);
buffer.addFrom(channel, 0, band1Data, bufferLength);
buffer.addFrom(channel, 0, band2Data, bufferLength);
}
}