Hi everybody, I’m new both here and in juce development, I’m trying to implement auto-gain compensation in the eq I’m building, while it has manual gain, I’d like to add auto-gain. I tried it in many ways and I tried using any possible LLM to find help but no luck, I can’t pinpoint what I’m doing wrong but the gain compensation is close to dead at low frequencies (below 500hz) and quite responsive in high frequencies (think an high shelf), it basically works less and less as I roll down in the spectrum. I also tried using the output gain knob before the auto-gain and when I boost the knob the auto-gain successfully brings back the signal at unity. Then I tried switching sample rates in PluginDoctor and I noticed the auto-gain does less and less correction as I raise the sample rate. I’m stuck at this for a while now.
This so far is my cleanest iteration (I’m not using LUFS for now, I know, K-Weighting, smoothing etc. I’m just trying to get a basic version working):
I have an AutoGainCompensator.h
#pragma once
#include <JuceHeader.h>
class AutoGainCompensator
{
public:
AutoGainCompensator() = default;
void prepare(double sampleRate, int samplesPerBlock);
void processBlock(juce::AudioBuffer<float>& buffer, bool autoGainEnabled);
void updateGainCompensation(float inputRMS, float outputRMS);
void reset();
static float calculateRMS(const juce::AudioBuffer<float>& buffer);
private:
double sampleRate = 44100.0;
juce::SmoothedValue<float> gainSmoother;
// RMS calculation variables
static constexpr float rmsTimeConstant = 0.02f; // 20ms window
static constexpr float minThreshold = 0.0001f; // -80dB
static constexpr float maxGainReduction = 0.1f; // 10:1 max reduction
static constexpr float maxGainBoost = 10.0f; // 10:1 max boost
};
the cpp file for compensation:
#include "AutoGainCompensator.h"
void AutoGainCompensator::prepare(double sr, int samplesPerBlock)
{
sampleRate = sr;
gainSmoother.reset(sampleRate, 0.0001);
gainSmoother.setCurrentAndTargetValue(1.0f);
}
void AutoGainCompensator::processBlock(juce::AudioBuffer<float>& buffer, bool autoGainEnabled)
{
if (!autoGainEnabled)
{
// When disabled, smoothly return to unity gain
gainSmoother.setTargetValue(1.0f);
// Still need to apply the smoother to avoid clicks when re-enabling
if (!gainSmoother.isSmoothing())
return; // If not smoothing and disabled, no need to process
}
// Apply the smoothed gain compensation
for (int sample = 0; sample < buffer.getNumSamples(); ++sample)
{
float currentGain = gainSmoother.getNextValue(); // Get the smoothed value ONCE per sample
for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
{
buffer.getWritePointer(channel)[sample] *= currentGain;
}
}
}
void AutoGainCompensator::updateGainCompensation(float inputRMS, float outputRMS)
{
float targetGain = 1.0f; // Default to unity gain
// Only calculates compensation if both RMS values are above minimum threshold
// This prevents boosting noise when there's no significant signal
if (outputRMS > minThreshold && inputRMS > minThreshold)
{
targetGain = inputRMS / outputRMS;
// Apply safety limits to prevent extreme corrections
// jlimit clamps the value between the specified min and max
targetGain = juce::jlimit(maxGainReduction, maxGainBoost, targetGain);
}
gainSmoother.setTargetValue(targetGain);
}
float AutoGainCompensator::calculateRMS(const juce::AudioBuffer<float>& buffer)
{
float sumSquares = 0.0f;
int totalSamples = 0;
// Iterate over all channels and samples to calculate sum of squares
for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
{
const auto* channelData = buffer.getReadPointer(channel);
for (int sample = 0; sample < buffer.getNumSamples(); ++sample)
{
const float sampleValue = channelData[sample];
sumSquares += sampleValue * sampleValue;
totalSamples++;
}
}
// Avoid division by zero
if (totalSamples == 0)
return 0.0f;
// Return the square root of the mean of squares
return std::sqrt(sumSquares / static_cast<float>(totalSamples));
}
void AutoGainCompensator::reset()
{
// Reset the smoother to its initial state (unity gain)
gainSmoother.reset(sampleRate, 0.2); // Re-initialize with current sample rate
gainSmoother.setCurrentAndTargetValue(1.0f);
}
And in my PluginProcessor.cpp I have this:
ParametricEQAudioProcessor::~ParametricEQAudioProcessor() {}
void ParametricEQAudioProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
{
if (oversamplingEnabled)
{
oversampler->initProcessing(static_cast<size_t>(samplesPerBlock));
setLatencySamples(static_cast<int>(std::ceil(oversampler->getLatencyInSamples())));
}
else
{
oversampler->reset();
setLatencySamples(0);
}
double sampleRateForBands = oversamplingEnabled
? sampleRate * oversampler->getOversamplingFactor()
: sampleRate;
int samplesPerBlockForBands = oversamplingEnabled
? samplesPerBlock * static_cast<int>(oversampler->getOversamplingFactor())
: samplesPerBlock;
for (auto& b : bands)
b->prepareToPlay(sampleRateForBands, samplesPerBlockForBands);
juce::dsp::ProcessSpec spec;
spec.sampleRate = sampleRateForBands;
spec.maximumBlockSize = static_cast<juce::uint32>(samplesPerBlockForBands);
spec.numChannels = static_cast<juce::uint32>(getTotalNumOutputChannels());
highShelf.prepare(spec);
highCut.prepare(spec);
lowShelf.prepare(spec);
lowCutStage1.prepare(spec);
lowCutStage2.prepare(spec);
outputGain->prepare(spec);
// Prepare the auto-gain compensator
autoGainCompensator.prepare(sampleRate, samplesPerBlock);
// Update filters after prepareToPlay to ensure correct sample rate is used
updateHighShelfFilter();
updateHighCutFilter();
updateLowShelfFilter();
updateLowCutFilter();
updateOutputGain();
}
void ParametricEQAudioProcessor::releaseResources()
{
oversampler->reset();
// Reset the auto-gain compensator
autoGainCompensator.reset();
}
void ParametricEQAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi)
{
juce::ScopedNoDenormals noDenormals;
bool autoGainState = *autoGainEnabled;
// Clear any garbage in output channels
auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels();
for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
buffer.clear(i, 0, buffer.getNumSamples());
// Measure input RMS (if auto-gain is enabled)
float inputRMS = 0.0f;
if (autoGainState)
{
inputRMS = AutoGainCompensator::calculateRMS(buffer);
}
// Convert the float buffer to a double buffer for internal processing
const int numChans = buffer.getNumChannels();
const int numSamples = buffer.getNumSamples();
juce::AudioBuffer<double> dblBuf(numChans, numSamples);
for (int ch = 0; ch < numChans; ++ch)
for (int i = 0; i < numSamples; ++i)
dblBuf.setSample(ch, i, buffer.getSample(ch, i));
// EXISTING PLUGIN PROCESSING HERE
processBlock(dblBuf, midi); // Call the double precision version, which contains all EQ and output gain
// Convert the double buffer back to a float buffer
for (int ch = 0; ch < numChans; ++ch)
for (int i = 0; i < numSamples; ++i)
buffer.setSample(ch, i, (float)dblBuf.getSample(ch, i));
// Measure output RMS and update compensation (if auto-gain is enabled)
if (autoGainState)
{
float outputRMS = AutoGainCompensator::calculateRMS(buffer);
autoGainCompensator.updateGainCompensation(inputRMS, outputRMS);
}
// Apply the auto-gain compensation to the final float buffer
autoGainCompensator.processBlock(buffer, autoGainState);
}
void ParametricEQAudioProcessor::processBlock(juce::AudioBuffer<double>& buffer, juce::MidiBuffer&)
{
juce::dsp::AudioBlock<double> block(buffer);
if (oversamplingEnabled)
{
auto upsampledBlock = oversampler->processSamplesUp(block);
juce::dsp::ProcessContextReplacing<double> context(upsampledBlock);
// NOT INCLUDED EQ PROCESSING CODE HAPPENS HERE
bands[i]->process(upsampledBlock, getSampleRate() * oversampler->getOversamplingFactor());
}
}
oversampler->processSamplesDown(block);
}
else
{
juce::dsp::ProcessContextReplacing<double> context(block);
// NOT INCLUDED EQ PROCESSING CODE HAPPENS HERE
bands[i]->process(block, getSampleRate());
}
}
}
// output gain at the very end of the double processing chain
juce::dsp::ProcessContextReplacing<double> outputContext(block);
outputGain->process(outputContext);
}
Any help would be appreciated, I sense is something with buffer and sample rate but I can’t spot what it is. The eq itself works great and does everything the way it should do it, only the auto-gain is broken.
