Files
InstaLPEQ/Source/PluginProcessor.cpp
hariel1985 9c5b5a3957 v1.2.2: Live spectrum analyzer, makeup gain, drag-and-drop signal chain
- Real-time FFT spectrum analyzer drawn behind EQ curves
- Makeup gain knob (+/- 24 dB) after limiter
- Draggable signal chain panel: reorder Master Gain / Limiter / Makeup Gain
- Chain order saved/restored with DAW session
- Scaled fonts in signal chain panel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:44:27 +01:00

327 sor
10 KiB
C++

#include "PluginProcessor.h"
#include "PluginEditor.h"
InstaLPEQProcessor::InstaLPEQProcessor()
: AudioProcessor (BusesProperties()
.withInput ("Input", juce::AudioChannelSet::stereo(), true)
.withOutput ("Output", juce::AudioChannelSet::stereo(), true))
{
}
InstaLPEQProcessor::~InstaLPEQProcessor()
{
firEngine.stop();
}
bool InstaLPEQProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
if (layouts.getMainInputChannelSet() != juce::AudioChannelSet::stereo() ||
layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
return false;
return true;
}
void InstaLPEQProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
currentSampleRate = sampleRate;
currentBlockSize = samplesPerBlock;
juce::dsp::ProcessSpec spec { sampleRate, (juce::uint32) samplesPerBlock, 2 };
convolution.prepare (spec);
limiter.prepare (spec);
limiter.setThreshold (0.0f);
limiter.setRelease (50.0f);
firEngine.start (sampleRate);
updateFIR();
setLatencySamples (firEngine.getLatencySamples());
}
void InstaLPEQProcessor::releaseResources()
{
firEngine.stop();
convolution.reset();
}
void InstaLPEQProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer&)
{
juce::ScopedNoDenormals noDenormals;
// Check for new FIR from background thread
if (auto newFIR = firEngine.getNewFIR())
{
convolution.loadImpulseResponse (std::move (*newFIR), currentSampleRate,
juce::dsp::Convolution::Stereo::no,
juce::dsp::Convolution::Trim::no,
juce::dsp::Convolution::Normalise::no);
firLoaded = true;
}
if (bypassed.load() || ! firLoaded)
return;
// Process through convolution
juce::dsp::AudioBlock<float> block (buffer);
juce::dsp::ProcessContextReplacing<float> context (block);
convolution.process (context);
// Apply chain in configured order
std::array<ChainStage, numChainStages> order;
{
const juce::SpinLock::ScopedTryLockType lock (chainLock);
if (lock.isLocked())
order = chainOrder;
else
order = { MasterGain, Limiter, MakeupGain };
}
for (auto stage : order)
{
switch (stage)
{
case MasterGain:
{
float gain = juce::Decibels::decibelsToGain (masterGainDb.load());
if (std::abs (gain - 1.0f) > 0.001f)
buffer.applyGain (gain);
break;
}
case Limiter:
{
if (limiterEnabled.load())
{
juce::dsp::AudioBlock<float> limBlock (buffer);
juce::dsp::ProcessContextReplacing<float> limContext (limBlock);
limiter.process (limContext);
}
break;
}
case MakeupGain:
{
float mkGain = juce::Decibels::decibelsToGain (makeupGainDb.load());
if (std::abs (mkGain - 1.0f) > 0.001f)
buffer.applyGain (mkGain);
break;
}
default: break;
}
}
// Feed spectrum analyzer (mono mix of output)
const int numSamples = buffer.getNumSamples();
const int numChannels = buffer.getNumChannels();
for (int i = 0; i < numSamples; ++i)
{
float sample = 0.0f;
for (int ch = 0; ch < numChannels; ++ch)
sample += buffer.getSample (ch, i);
sample /= (float) numChannels;
fifoBuffer[fifoIndex++] = sample;
if (fifoIndex >= spectrumFFTSize)
{
fifoIndex = 0;
std::copy (fifoBuffer.begin(), fifoBuffer.end(), fftData.begin());
std::fill (fftData.begin() + spectrumFFTSize, fftData.end(), 0.0f);
spectrumWindow.multiplyWithWindowingTable (fftData.data(), spectrumFFTSize);
spectrumFFT.performFrequencyOnlyForwardTransform (fftData.data());
{
const juce::SpinLock::ScopedLockType lock (spectrumLock);
for (int b = 0; b < spectrumFFTSize / 2; ++b)
{
float mag = fftData[b] / (float) spectrumFFTSize;
float dbVal = juce::Decibels::gainToDecibels (mag, -100.0f);
// Smooth: 70% old + 30% new
spectrumMagnitude[b] = spectrumMagnitude[b] * 0.7f + dbVal * 0.3f;
}
}
spectrumReady.store (true);
}
}
}
// ============================================================
// Band management
// ============================================================
std::vector<EQBand> InstaLPEQProcessor::getBands() const
{
const juce::SpinLock::ScopedLockType lock (bandLock);
return bands;
}
void InstaLPEQProcessor::setBand (int index, const EQBand& band)
{
{
const juce::SpinLock::ScopedLockType lock (bandLock);
if (index >= 0 && index < (int) bands.size())
bands[index] = band;
}
updateFIR();
}
void InstaLPEQProcessor::addBand (float freq, float gainDb)
{
{
const juce::SpinLock::ScopedLockType lock (bandLock);
if ((int) bands.size() >= maxBands)
return;
EQBand b;
b.frequency = freq;
b.gainDb = gainDb;
bands.push_back (b);
}
updateFIR();
}
void InstaLPEQProcessor::removeBand (int index)
{
{
const juce::SpinLock::ScopedLockType lock (bandLock);
if (index >= 0 && index < (int) bands.size())
bands.erase (bands.begin() + index);
}
updateFIR();
}
int InstaLPEQProcessor::getNumBands() const
{
const juce::SpinLock::ScopedLockType lock (bandLock);
return (int) bands.size();
}
bool InstaLPEQProcessor::getSpectrum (float* dest, int maxBins) const
{
if (! spectrumReady.load())
return false;
const juce::SpinLock::ScopedTryLockType lock (spectrumLock);
if (! lock.isLocked())
return false;
int bins = std::min (maxBins, spectrumFFTSize / 2);
std::copy (spectrumMagnitude.begin(), spectrumMagnitude.begin() + bins, dest);
return true;
}
std::array<InstaLPEQProcessor::ChainStage, InstaLPEQProcessor::numChainStages> InstaLPEQProcessor::getChainOrder() const
{
const juce::SpinLock::ScopedLockType lock (chainLock);
return chainOrder;
}
void InstaLPEQProcessor::setChainOrder (const std::array<ChainStage, numChainStages>& order)
{
const juce::SpinLock::ScopedLockType lock (chainLock);
chainOrder = order;
}
void InstaLPEQProcessor::updateFIR()
{
auto currentBands = getBands();
firEngine.setBands (currentBands);
}
void InstaLPEQProcessor::setQuality (int fftOrder)
{
firEngine.setFFTOrder (fftOrder);
setLatencySamples (firEngine.getLatencySamples());
updateFIR();
}
// ============================================================
// State save/restore
// ============================================================
void InstaLPEQProcessor::getStateInformation (juce::MemoryBlock& destData)
{
juce::XmlElement xml ("InstaLPEQ");
xml.setAttribute ("bypass", bypassed.load());
xml.setAttribute ("masterGain", (double) masterGainDb.load());
xml.setAttribute ("limiter", limiterEnabled.load());
xml.setAttribute ("makeupGain", (double) makeupGainDb.load());
auto order = getChainOrder();
juce::String chainStr;
for (int i = 0; i < numChainStages; ++i)
{
if (i > 0) chainStr += ",";
chainStr += juce::String ((int) order[i]);
}
xml.setAttribute ("chainOrder", chainStr);
auto currentBands = getBands();
for (int i = 0; i < (int) currentBands.size(); ++i)
{
auto* bandXml = xml.createNewChildElement ("Band");
bandXml->setAttribute ("freq", (double) currentBands[i].frequency);
bandXml->setAttribute ("gain", (double) currentBands[i].gainDb);
bandXml->setAttribute ("q", (double) currentBands[i].q);
bandXml->setAttribute ("type", (int) currentBands[i].type);
bandXml->setAttribute ("enabled", currentBands[i].enabled);
}
copyXmlToBinary (xml, destData);
}
void InstaLPEQProcessor::setStateInformation (const void* data, int sizeInBytes)
{
auto xml = getXmlFromBinary (data, sizeInBytes);
if (xml == nullptr || ! xml->hasTagName ("InstaLPEQ"))
return;
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));
auto chainStr = xml->getStringAttribute ("chainOrder", "0,1,2");
auto tokens = juce::StringArray::fromTokens (chainStr, ",", "");
if (tokens.size() == numChainStages)
{
std::array<ChainStage, numChainStages> order;
for (int i = 0; i < numChainStages; ++i)
order[i] = static_cast<ChainStage> (tokens[i].getIntValue());
setChainOrder (order);
}
std::vector<EQBand> loadedBands;
for (auto* bandXml : xml->getChildIterator())
{
if (! bandXml->hasTagName ("Band"))
continue;
EQBand b;
b.frequency = (float) bandXml->getDoubleAttribute ("freq", 1000.0);
b.gainDb = (float) bandXml->getDoubleAttribute ("gain", 0.0);
b.q = (float) bandXml->getDoubleAttribute ("q", 1.0);
b.type = static_cast<EQBand::Type> (bandXml->getIntAttribute ("type", 0));
b.enabled = bandXml->getBoolAttribute ("enabled", true);
loadedBands.push_back (b);
}
{
const juce::SpinLock::ScopedLockType lock (bandLock);
bands = loadedBands;
}
updateFIR();
}
// ============================================================
// Editor
// ============================================================
juce::AudioProcessorEditor* InstaLPEQProcessor::createEditor()
{
return new InstaLPEQEditor (*this);
}
// This creates new instances of the plugin
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
return new InstaLPEQProcessor();
}