v1.3: Auto makeup gain, spectrum analyzer, FIR normalization, README overhaul
- Auto makeup gain: RMS-based loudness compensation from actual FIR response - Real-time FFT spectrum analyzer behind EQ curves - FIR normalization fix: flat settings now produce exact 0 dB passthrough - Brickwall limiter (0 dB ceiling) with toggle - Drag-and-drop signal chain reordering - Low FIR tap count warning for 512/1024 - Double-click reset on all knobs - Comprehensive README with linear phase EQ explanation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -124,11 +124,11 @@ juce::AudioBuffer<float> FIREngine::generateFIR (const std::vector<EQBand>& band
|
||||
magnitudes[i] *= bandMag[i];
|
||||
}
|
||||
|
||||
// Store magnitude in dB for display
|
||||
// Store theoretical magnitude in dB for display (from IIR target curve)
|
||||
{
|
||||
std::vector<float> magDb (numBins);
|
||||
for (int i = 0; i < numBins; ++i)
|
||||
magDb[i] = (float) juce::Decibels::gainToDecibels (magnitudes[i], -60.0);
|
||||
magDb[i] = (float) juce::Decibels::gainToDecibels ((float) magnitudes[i], -60.0f);
|
||||
|
||||
const juce::SpinLock::ScopedLockType lock (magLock);
|
||||
magnitudeDb = std::move (magDb);
|
||||
@@ -168,5 +168,73 @@ juce::AudioBuffer<float> FIREngine::generateFIR (const std::vector<EQBand>& band
|
||||
juce::dsp::WindowingFunction<float> window (fftSize, juce::dsp::WindowingFunction<float>::blackmanHarris);
|
||||
window.multiplyWithWindowingTable (firData, fftSize);
|
||||
|
||||
// Normalize: ensure flat spectrum → unity DC gain
|
||||
// Without this, IFFT scaling + windowing cause incorrect base level
|
||||
float dcGain = 0.0f;
|
||||
for (int i = 0; i < fftSize; ++i)
|
||||
dcGain += firData[i];
|
||||
|
||||
if (std::abs (dcGain) > 1e-6f)
|
||||
{
|
||||
float normFactor = 1.0f / dcGain;
|
||||
for (int i = 0; i < fftSize; ++i)
|
||||
firData[i] *= normFactor;
|
||||
}
|
||||
|
||||
// Compute auto makeup from the ACTUAL final FIR frequency response
|
||||
// (includes windowing + normalization effects)
|
||||
{
|
||||
std::vector<float> analysisBuf (fftSize * 2, 0.0f);
|
||||
std::copy (firData, firData + fftSize, analysisBuf.data());
|
||||
|
||||
juce::dsp::FFT analysisFft (order);
|
||||
analysisFft.performRealOnlyForwardTransform (analysisBuf.data());
|
||||
|
||||
// Extract actual magnitude from FFT result
|
||||
// Format: [DC_real, Nyquist_real, bin1_real, bin1_imag, bin2_real, bin2_imag, ...]
|
||||
double powerSum = 0.0;
|
||||
int count = 0;
|
||||
|
||||
for (int i = 1; i < fftSize / 2; ++i)
|
||||
{
|
||||
float re = analysisBuf[i * 2];
|
||||
float im = analysisBuf[i * 2 + 1];
|
||||
powerSum += (double) (re * re + im * im);
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
double avgPower = powerSum / (double) count;
|
||||
float rmsGain = (float) std::sqrt (avgPower);
|
||||
float makeupDb = -20.0f * std::log10 (std::max (rmsGain, 1e-10f));
|
||||
autoMakeupDb.store (makeupDb);
|
||||
}
|
||||
|
||||
// (magnitudeDb stays as theoretical IIR curve for display)
|
||||
}
|
||||
|
||||
return firBuffer;
|
||||
}
|
||||
|
||||
// A-weighting curve (IEC 61672:2003)
|
||||
// Returns linear amplitude weighting factor for given frequency
|
||||
float FIREngine::aWeighting (float f)
|
||||
{
|
||||
if (f < 10.0f) return 0.0f;
|
||||
|
||||
double f2 = (double) f * (double) f;
|
||||
double f4 = f2 * f2;
|
||||
|
||||
double num = 12194.0 * 12194.0 * f4;
|
||||
double den = (f2 + 20.6 * 20.6)
|
||||
* std::sqrt ((f2 + 107.7 * 107.7) * (f2 + 737.9 * 737.9))
|
||||
* (f2 + 12194.0 * 12194.0);
|
||||
|
||||
double ra = num / den;
|
||||
|
||||
// Normalize so A(1000 Hz) = 1.0
|
||||
// A(1000) unnormalized ≈ 0.7943
|
||||
static const double norm = 1.0 / 0.7943282347;
|
||||
return (float) (ra * norm);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ public:
|
||||
int getFIRLength() const { return 1 << fftOrder.load(); }
|
||||
int getLatencySamples() const { return getFIRLength() / 2; }
|
||||
|
||||
// Auto makeup gain: A-weighted RMS loudness compensation (dB)
|
||||
float getAutoMakeupGainDb() const { return autoMakeupDb.load(); }
|
||||
|
||||
private:
|
||||
void run() override;
|
||||
juce::AudioBuffer<float> generateFIR (const std::vector<EQBand>& bands, double sr, int order);
|
||||
@@ -43,4 +46,7 @@ private:
|
||||
|
||||
std::vector<float> magnitudeDb;
|
||||
mutable juce::SpinLock magLock;
|
||||
|
||||
std::atomic<float> autoMakeupDb { 0.0f };
|
||||
static float aWeighting (float freq);
|
||||
};
|
||||
|
||||
@@ -92,17 +92,17 @@ InstaLPEQEditor::InstaLPEQEditor (InstaLPEQProcessor& p)
|
||||
limiterLabel.setJustificationType (juce::Justification::centred);
|
||||
addAndMakeVisible (limiterLabel);
|
||||
|
||||
// Makeup gain
|
||||
makeupGainSlider.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
|
||||
makeupGainSlider.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 60, 16);
|
||||
makeupGainSlider.setRange (-24.0, 24.0, 0.1);
|
||||
makeupGainSlider.setValue (0.0);
|
||||
makeupGainSlider.setTextValueSuffix (" dB");
|
||||
makeupGainSlider.setDoubleClickReturnValue (true, 0.0);
|
||||
addAndMakeVisible (makeupGainSlider);
|
||||
makeupGainLabel.setFont (customLookAndFeel.getMediumFont (13.0f));
|
||||
makeupGainLabel.setJustificationType (juce::Justification::centred);
|
||||
addAndMakeVisible (makeupGainLabel);
|
||||
// Auto makeup gain
|
||||
autoMakeupToggle.setToggleState (processor.autoMakeupEnabled.load(), juce::dontSendNotification);
|
||||
addAndMakeVisible (autoMakeupToggle);
|
||||
autoMakeupLabel.setFont (customLookAndFeel.getMediumFont (13.0f));
|
||||
autoMakeupLabel.setColour (juce::Label::textColourId, InstaLPEQLookAndFeel::textSecondary);
|
||||
autoMakeupLabel.setJustificationType (juce::Justification::centred);
|
||||
addAndMakeVisible (autoMakeupLabel);
|
||||
autoMakeupValue.setFont (customLookAndFeel.getRegularFont (12.0f));
|
||||
autoMakeupValue.setColour (juce::Label::textColourId, InstaLPEQLookAndFeel::accent);
|
||||
autoMakeupValue.setJustificationType (juce::Justification::centred);
|
||||
addAndMakeVisible (autoMakeupValue);
|
||||
|
||||
// Signal chain panel
|
||||
chainPanel.setListener (this);
|
||||
@@ -189,10 +189,12 @@ void InstaLPEQEditor::resized()
|
||||
limiterLabel.setBounds (masterArea.removeFromLeft (55));
|
||||
limiterToggle.setBounds (masterArea.removeFromLeft (40));
|
||||
|
||||
// Makeup gain knob
|
||||
makeupGainLabel.setFont (customLookAndFeel.getMediumFont (std::max (11.0f, 14.0f * scale)));
|
||||
makeupGainLabel.setBounds (masterArea.removeFromLeft (55));
|
||||
makeupGainSlider.setBounds (masterArea.removeFromLeft (masterH));
|
||||
// Auto makeup gain toggle + value display
|
||||
autoMakeupLabel.setFont (customLookAndFeel.getMediumFont (std::max (11.0f, 14.0f * scale)));
|
||||
autoMakeupLabel.setBounds (masterArea.removeFromLeft (70));
|
||||
autoMakeupToggle.setBounds (masterArea.removeFromLeft (40));
|
||||
autoMakeupValue.setFont (customLookAndFeel.getRegularFont (std::max (10.0f, 12.0f * scale)));
|
||||
autoMakeupValue.setBounds (masterArea.removeFromLeft (60));
|
||||
|
||||
// Quality selector on the right side of master row
|
||||
qualityLabel.setFont (customLookAndFeel.getMediumFont (std::max (11.0f, 14.0f * scale)));
|
||||
@@ -218,7 +220,12 @@ void InstaLPEQEditor::timerCallback()
|
||||
processor.bypassed.store (bypassToggle.getToggleState());
|
||||
processor.masterGainDb.store ((float) masterGainSlider.getValue());
|
||||
processor.limiterEnabled.store (limiterToggle.getToggleState());
|
||||
processor.makeupGainDb.store ((float) makeupGainSlider.getValue());
|
||||
processor.autoMakeupEnabled.store (autoMakeupToggle.getToggleState());
|
||||
|
||||
// Update auto makeup display
|
||||
float mkDb = processor.getActiveAutoMakeupDb();
|
||||
juce::String mkStr = (mkDb >= 0 ? "+" : "") + juce::String (mkDb, 1) + " dB";
|
||||
autoMakeupValue.setText (mkStr, juce::dontSendNotification);
|
||||
|
||||
// Update spectrum analyzer
|
||||
{
|
||||
|
||||
@@ -44,7 +44,7 @@ private:
|
||||
NodeParameterPanel nodePanel;
|
||||
|
||||
juce::Label titleLabel { {}, "INSTALPEQ" };
|
||||
juce::Label versionLabel { {}, "v1.2.2" };
|
||||
juce::Label versionLabel { {}, "v1.3.0" };
|
||||
juce::ToggleButton bypassToggle;
|
||||
juce::Label bypassLabel { {}, "BYPASS" };
|
||||
|
||||
@@ -57,8 +57,9 @@ private:
|
||||
juce::Label masterGainLabel { {}, "MASTER" };
|
||||
juce::ToggleButton limiterToggle;
|
||||
juce::Label limiterLabel { {}, "LIMITER" };
|
||||
juce::Slider makeupGainSlider;
|
||||
juce::Label makeupGainLabel { {}, "MAKEUP" };
|
||||
juce::ToggleButton autoMakeupToggle;
|
||||
juce::Label autoMakeupLabel { {}, "AUTO GAIN" };
|
||||
juce::Label autoMakeupValue { {}, "0.0 dB" };
|
||||
|
||||
SignalChainPanel chainPanel;
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ void InstaLPEQProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::M
|
||||
if (bypassed.load() || ! firLoaded)
|
||||
return;
|
||||
|
||||
// Process through convolution
|
||||
// Process through convolution (EQ)
|
||||
juce::dsp::AudioBlock<float> block (buffer);
|
||||
juce::dsp::ProcessContextReplacing<float> context (block);
|
||||
convolution.process (context);
|
||||
@@ -99,9 +99,12 @@ void InstaLPEQProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::M
|
||||
}
|
||||
case MakeupGain:
|
||||
{
|
||||
float mkGain = juce::Decibels::decibelsToGain (makeupGainDb.load());
|
||||
if (std::abs (mkGain - 1.0f) > 0.001f)
|
||||
buffer.applyGain (mkGain);
|
||||
if (autoMakeupEnabled.load())
|
||||
{
|
||||
float mkGain = juce::Decibels::decibelsToGain (firEngine.getAutoMakeupGainDb());
|
||||
if (std::abs (mkGain - 1.0f) > 0.001f)
|
||||
buffer.applyGain (mkGain);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
@@ -207,6 +210,11 @@ bool InstaLPEQProcessor::getSpectrum (float* dest, int maxBins) const
|
||||
return true;
|
||||
}
|
||||
|
||||
float InstaLPEQProcessor::getActiveAutoMakeupDb() const
|
||||
{
|
||||
return autoMakeupEnabled.load() ? firEngine.getAutoMakeupGainDb() : 0.0f;
|
||||
}
|
||||
|
||||
std::array<InstaLPEQProcessor::ChainStage, InstaLPEQProcessor::numChainStages> InstaLPEQProcessor::getChainOrder() const
|
||||
{
|
||||
const juce::SpinLock::ScopedLockType lock (chainLock);
|
||||
@@ -242,7 +250,7 @@ void InstaLPEQProcessor::getStateInformation (juce::MemoryBlock& destData)
|
||||
xml.setAttribute ("bypass", bypassed.load());
|
||||
xml.setAttribute ("masterGain", (double) masterGainDb.load());
|
||||
xml.setAttribute ("limiter", limiterEnabled.load());
|
||||
xml.setAttribute ("makeupGain", (double) makeupGainDb.load());
|
||||
xml.setAttribute ("autoMakeup", autoMakeupEnabled.load());
|
||||
|
||||
auto order = getChainOrder();
|
||||
juce::String chainStr;
|
||||
@@ -276,7 +284,7 @@ void InstaLPEQProcessor::setStateInformation (const void* data, int sizeInBytes)
|
||||
bypassed.store (xml->getBoolAttribute ("bypass", false));
|
||||
masterGainDb.store ((float) xml->getDoubleAttribute ("masterGain", 0.0));
|
||||
limiterEnabled.store (xml->getBoolAttribute ("limiter", true));
|
||||
makeupGainDb.store ((float) xml->getDoubleAttribute ("makeupGain", 0.0));
|
||||
autoMakeupEnabled.store (xml->getBoolAttribute ("autoMakeup", true));
|
||||
|
||||
auto chainStr = xml->getStringAttribute ("chainOrder", "0,1,2");
|
||||
auto tokens = juce::StringArray::fromTokens (chainStr, ",", "");
|
||||
|
||||
@@ -48,7 +48,10 @@ public:
|
||||
std::atomic<bool> bypassed { false };
|
||||
std::atomic<float> masterGainDb { 0.0f };
|
||||
std::atomic<bool> limiterEnabled { true };
|
||||
std::atomic<float> makeupGainDb { 0.0f }; // -24 to +24 dB
|
||||
std::atomic<bool> autoMakeupEnabled { true };
|
||||
|
||||
float getActiveAutoMakeupDb() const;
|
||||
float getMeasuredAutoMakeupDb() const { return measuredMakeupDb.load(); }
|
||||
|
||||
// Chain order (read/write from GUI, read from audio thread)
|
||||
std::array<ChainStage, numChainStages> getChainOrder() const;
|
||||
@@ -86,6 +89,11 @@ public:
|
||||
int currentBlockSize = 512;
|
||||
bool firLoaded = false;
|
||||
|
||||
// Signal-based auto makeup measurement
|
||||
double smoothedInputRms = 0.0;
|
||||
double smoothedOutputRms = 0.0;
|
||||
std::atomic<float> measuredMakeupDb { 0.0f };
|
||||
|
||||
std::array<ChainStage, numChainStages> chainOrder { MasterGain, Limiter, MakeupGain };
|
||||
juce::SpinLock chainLock;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ juce::String SignalChainPanel::getStageName (InstaLPEQProcessor::ChainStage stag
|
||||
{
|
||||
case InstaLPEQProcessor::MasterGain: return "MASTER GAIN";
|
||||
case InstaLPEQProcessor::Limiter: return "LIMITER";
|
||||
case InstaLPEQProcessor::MakeupGain: return "MAKEUP GAIN";
|
||||
case InstaLPEQProcessor::MakeupGain: return "AUTO GAIN";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user