Initial commit: InstaLPEQ linear phase EQ plugin

Full-featured linear phase EQ with interactive graphical curve display.
FIR-based processing (8192-tap), 8 parametric bands, multi-platform
CI/CD (Windows/macOS/Linux), InstaDrums visual style.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hariel1985
2026-03-25 10:17:59 +01:00
commit b11135f786
21 fájl változott, egészen pontosan 2859 új sor hozzáadva és 0 régi sor törölve

12
Source/EQBand.h Normal file
Fájl megtekintése

@@ -0,0 +1,12 @@
#pragma once
struct EQBand
{
enum Type { Peak, LowShelf, HighShelf };
float frequency = 1000.0f; // 20 Hz - 20000 Hz
float gainDb = 0.0f; // -24 dB to +24 dB
float q = 1.0f; // 0.1 to 18.0
Type type = Peak;
bool enabled = true;
};

420
Source/EQCurveDisplay.cpp Normal file
Fájl megtekintése

@@ -0,0 +1,420 @@
#include "EQCurveDisplay.h"
#include "LookAndFeel.h"
EQCurveDisplay::EQCurveDisplay() {}
void EQCurveDisplay::setBands (const std::vector<EQBand>& newBands)
{
bands = newBands;
repaint();
}
void EQCurveDisplay::setMagnitudeResponse (const std::vector<float>& magnitudesDb, double sampleRate, int fftSize)
{
magnitudeResponseDb = magnitudesDb;
responseSampleRate = sampleRate;
responseFftSize = fftSize;
repaint();
}
void EQCurveDisplay::setSelectedBand (int index)
{
if (selectedBand != index)
{
selectedBand = index;
repaint();
if (listener != nullptr)
listener->selectedBandChanged (index);
}
}
// ============================================================
// Coordinate mapping
// ============================================================
juce::Rectangle<float> EQCurveDisplay::getPlotArea() const
{
float marginL = 38.0f;
float marginR = 12.0f;
float marginT = 10.0f;
float marginB = 22.0f;
return getLocalBounds().toFloat().withTrimmedLeft (marginL)
.withTrimmedRight (marginR)
.withTrimmedTop (marginT)
.withTrimmedBottom (marginB);
}
float EQCurveDisplay::freqToX (float freq) const
{
auto area = getPlotArea();
float normLog = std::log10 (freq / minFreq) / std::log10 (maxFreq / minFreq);
return area.getX() + normLog * area.getWidth();
}
float EQCurveDisplay::xToFreq (float x) const
{
auto area = getPlotArea();
float normLog = (x - area.getX()) / area.getWidth();
return minFreq * std::pow (maxFreq / minFreq, normLog);
}
float EQCurveDisplay::dbToY (float db) const
{
auto area = getPlotArea();
float norm = (maxDb - db) / (maxDb - minDb);
return area.getY() + norm * area.getHeight();
}
float EQCurveDisplay::yToDb (float y) const
{
auto area = getPlotArea();
float norm = (y - area.getY()) / area.getHeight();
return maxDb - norm * (maxDb - minDb);
}
// ============================================================
// Paint
// ============================================================
void EQCurveDisplay::paint (juce::Graphics& g)
{
auto bounds = getLocalBounds().toFloat();
// Background gradient
{
juce::ColourGradient bgGrad (InstaLPEQLookAndFeel::bgDark.darker (0.4f), 0, bounds.getY(),
InstaLPEQLookAndFeel::bgDark.darker (0.2f), 0, bounds.getBottom(), false);
g.setGradientFill (bgGrad);
g.fillRoundedRectangle (bounds, 4.0f);
}
// Border
g.setColour (InstaLPEQLookAndFeel::bgLight.withAlpha (0.3f));
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
drawGrid (g);
drawPerBandCurves (g);
drawResponseCurve (g);
drawNodes (g);
}
void EQCurveDisplay::drawGrid (juce::Graphics& g)
{
auto area = getPlotArea();
auto* lf = dynamic_cast<InstaLPEQLookAndFeel*> (&getLookAndFeel());
juce::Font labelFont = lf ? lf->getRegularFont (11.0f) : juce::Font (juce::FontOptions (11.0f));
g.setFont (labelFont);
// Vertical lines — frequency markers
const float freqs[] = { 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000 };
const char* freqLabels[] = { "20", "50", "100", "200", "500", "1k", "2k", "5k", "10k", "20k" };
for (int i = 0; i < 10; ++i)
{
float xPos = freqToX (freqs[i]);
bool isMajor = (freqs[i] == 100 || freqs[i] == 1000 || freqs[i] == 10000);
g.setColour (InstaLPEQLookAndFeel::bgLight.withAlpha (isMajor ? 0.2f : 0.08f));
g.drawVerticalLine ((int) xPos, area.getY(), area.getBottom());
g.setColour (InstaLPEQLookAndFeel::textSecondary.withAlpha (0.7f));
g.drawText (freqLabels[i], (int) xPos - 16, (int) area.getBottom() + 2, 32, 16,
juce::Justification::centredTop, false);
}
// Horizontal lines — dB markers
for (float db = minDb; db <= maxDb; db += 6.0f)
{
float yPos = dbToY (db);
bool isZero = (std::abs (db) < 0.1f);
g.setColour (isZero ? InstaLPEQLookAndFeel::accent.withAlpha (0.15f)
: InstaLPEQLookAndFeel::bgLight.withAlpha (0.1f));
g.drawHorizontalLine ((int) yPos, area.getX(), area.getRight());
if (std::fmod (std::abs (db), 12.0f) < 0.1f || isZero)
{
g.setColour (InstaLPEQLookAndFeel::textSecondary.withAlpha (0.7f));
juce::String label = (db > 0 ? "+" : "") + juce::String ((int) db);
g.drawText (label, (int) area.getX() - 36, (int) yPos - 8, 32, 16,
juce::Justification::centredRight, false);
}
}
}
void EQCurveDisplay::drawResponseCurve (juce::Graphics& g)
{
if (magnitudeResponseDb.empty())
return;
auto area = getPlotArea();
int numBins = (int) magnitudeResponseDb.size();
juce::Path curvePath;
juce::Path fillPath;
float zeroY = dbToY (0.0f);
bool started = false;
for (float px = area.getX(); px <= area.getRight(); px += 1.0f)
{
float freq = xToFreq (px);
if (freq < 1.0f || freq > responseSampleRate * 0.5)
continue;
// Convert frequency to bin index
float binFloat = freq * (float) responseFftSize / (float) responseSampleRate;
int bin = (int) binFloat;
float frac = binFloat - (float) bin;
if (bin < 0 || bin >= numBins - 1)
continue;
// Linear interpolation between bins
float dbVal = magnitudeResponseDb[bin] * (1.0f - frac) + magnitudeResponseDb[bin + 1] * frac;
dbVal = juce::jlimit (minDb - 6.0f, maxDb + 6.0f, dbVal);
float yPos = dbToY (dbVal);
if (! started)
{
curvePath.startNewSubPath (px, yPos);
fillPath.startNewSubPath (px, zeroY);
fillPath.lineTo (px, yPos);
started = true;
}
else
{
curvePath.lineTo (px, yPos);
fillPath.lineTo (px, yPos);
}
}
if (! started)
return;
// Close fill path
fillPath.lineTo (area.getRight(), zeroY);
fillPath.closeSubPath();
// Fill under curve
g.setColour (InstaLPEQLookAndFeel::accent.withAlpha (0.1f));
g.fillPath (fillPath);
// Glow
g.setColour (InstaLPEQLookAndFeel::accent.withAlpha (0.2f));
g.strokePath (curvePath, juce::PathStrokeType (4.0f));
// Core curve
g.setColour (InstaLPEQLookAndFeel::accent);
g.strokePath (curvePath, juce::PathStrokeType (2.0f));
}
void EQCurveDisplay::drawPerBandCurves (juce::Graphics& g)
{
if (bands.empty())
return;
auto area = getPlotArea();
for (int bandIdx = 0; bandIdx < (int) bands.size(); ++bandIdx)
{
const auto& band = bands[bandIdx];
if (! band.enabled || std::abs (band.gainDb) < 0.01f)
continue;
float gainLinear = juce::Decibels::decibelsToGain (band.gainDb);
juce::dsp::IIR::Coefficients<float>::Ptr coeffs;
switch (band.type)
{
case EQBand::Peak:
coeffs = juce::dsp::IIR::Coefficients<float>::makePeakFilter (responseSampleRate, band.frequency, band.q, gainLinear);
break;
case EQBand::LowShelf:
coeffs = juce::dsp::IIR::Coefficients<float>::makeLowShelf (responseSampleRate, band.frequency, band.q, gainLinear);
break;
case EQBand::HighShelf:
coeffs = juce::dsp::IIR::Coefficients<float>::makeHighShelf (responseSampleRate, band.frequency, band.q, gainLinear);
break;
}
if (coeffs == nullptr)
continue;
juce::Path bandPath;
bool started = false;
auto colour = nodeColours[bandIdx % 8].withAlpha (bandIdx == selectedBand ? 0.4f : 0.15f);
for (float px = area.getX(); px <= area.getRight(); px += 2.0f)
{
float freq = xToFreq (px);
if (freq < 1.0f)
continue;
double mag = coeffs->getMagnitudeForFrequency (freq, responseSampleRate);
float dbVal = (float) juce::Decibels::gainToDecibels (mag, -60.0);
dbVal = juce::jlimit (minDb - 6.0f, maxDb + 6.0f, dbVal);
float yPos = dbToY (dbVal);
if (! started) { bandPath.startNewSubPath (px, yPos); started = true; }
else bandPath.lineTo (px, yPos);
}
g.setColour (colour);
g.strokePath (bandPath, juce::PathStrokeType (1.5f));
}
}
void EQCurveDisplay::drawNodes (juce::Graphics& g)
{
for (int i = 0; i < (int) bands.size(); ++i)
{
const auto& band = bands[i];
float nx = freqToX (band.frequency);
float ny = dbToY (band.gainDb);
auto colour = nodeColours[i % 8];
bool isSel = (i == selectedBand);
float r = isSel ? 10.0f : 8.0f;
// Glow for selected
if (isSel)
{
for (int gl = 0; gl < 3; ++gl)
{
float t = (float) gl / 2.0f;
float gr = r * (2.0f - t * 0.6f);
float alpha = 0.05f + t * t * 0.15f;
g.setColour (colour.withAlpha (alpha));
g.fillEllipse (nx - gr, ny - gr, gr * 2, gr * 2);
}
}
// Fill
g.setColour (band.enabled ? colour : colour.withAlpha (0.4f));
g.fillEllipse (nx - r, ny - r, r * 2, r * 2);
// Border
g.setColour (isSel ? juce::Colours::white : colour.brighter (0.3f));
g.drawEllipse (nx - r, ny - r, r * 2, r * 2, isSel ? 2.0f : 1.0f);
// Band number
auto* lf = dynamic_cast<InstaLPEQLookAndFeel*> (&getLookAndFeel());
juce::Font numFont = lf ? lf->getBoldFont (11.0f) : juce::Font (juce::FontOptions (11.0f));
g.setFont (numFont);
g.setColour (juce::Colours::white);
g.drawText (juce::String (i + 1), (int) (nx - r), (int) (ny - r), (int) (r * 2), (int) (r * 2),
juce::Justification::centred, false);
}
}
// ============================================================
// Mouse interaction
// ============================================================
int EQCurveDisplay::findNodeAt (float x, float y, float radius) const
{
for (int i = 0; i < (int) bands.size(); ++i)
{
float nx = freqToX (bands[i].frequency);
float ny = dbToY (bands[i].gainDb);
float dist = std::sqrt ((x - nx) * (x - nx) + (y - ny) * (y - ny));
if (dist <= radius)
return i;
}
return -1;
}
void EQCurveDisplay::mouseDown (const juce::MouseEvent& e)
{
auto pos = e.position;
int hit = findNodeAt (pos.x, pos.y);
if (e.mods.isRightButtonDown() && hit >= 0)
{
// Right-click context menu
juce::PopupMenu menu;
menu.addItem (1, "Delete Band");
menu.addItem (2, "Reset to 0 dB");
menu.addSeparator();
menu.addItem (3, "Peak", true, bands[hit].type == EQBand::Peak);
menu.addItem (4, "Low Shelf", true, bands[hit].type == EQBand::LowShelf);
menu.addItem (5, "High Shelf", true, bands[hit].type == EQBand::HighShelf);
menu.showMenuAsync (juce::PopupMenu::Options(), [this, hit] (int result)
{
if (result == 1)
{
if (listener) listener->bandRemoved (hit);
}
else if (result == 2)
{
auto band = bands[hit];
band.gainDb = 0.0f;
if (listener) listener->bandChanged (hit, band);
}
else if (result >= 3 && result <= 5)
{
auto band = bands[hit];
band.type = (result == 3) ? EQBand::Peak : (result == 4) ? EQBand::LowShelf : EQBand::HighShelf;
if (listener) listener->bandChanged (hit, band);
}
});
return;
}
if (hit >= 0)
{
draggedBand = hit;
setSelectedBand (hit);
}
else if (e.mods.isLeftButtonDown() && (int) bands.size() < 8)
{
// Add new band
float freq = juce::jlimit (minFreq, maxFreq, xToFreq (pos.x));
float gain = juce::jlimit (minDb, maxDb, yToDb (pos.y));
if (listener)
listener->bandAdded ((int) bands.size(), freq, gain);
}
}
void EQCurveDisplay::mouseDrag (const juce::MouseEvent& e)
{
if (draggedBand < 0 || draggedBand >= (int) bands.size())
return;
auto pos = e.position;
auto band = bands[draggedBand];
band.frequency = juce::jlimit (minFreq, maxFreq, xToFreq (pos.x));
band.gainDb = juce::jlimit (minDb, maxDb, yToDb (pos.y));
if (listener)
listener->bandChanged (draggedBand, band);
}
void EQCurveDisplay::mouseUp (const juce::MouseEvent&)
{
draggedBand = -1;
}
void EQCurveDisplay::mouseDoubleClick (const juce::MouseEvent& e)
{
int hit = findNodeAt (e.position.x, e.position.y);
if (hit >= 0)
{
auto band = bands[hit];
band.gainDb = 0.0f;
if (listener)
listener->bandChanged (hit, band);
}
}
void EQCurveDisplay::mouseWheelMove (const juce::MouseEvent& e, const juce::MouseWheelDetails& wheel)
{
int hit = findNodeAt (e.position.x, e.position.y, 20.0f);
if (hit >= 0)
{
auto band = bands[hit];
float delta = wheel.deltaY * 2.0f;
band.q = juce::jlimit (0.1f, 18.0f, band.q + delta);
if (listener)
listener->bandChanged (hit, band);
}
}

71
Source/EQCurveDisplay.h Normal file
Fájl megtekintése

@@ -0,0 +1,71 @@
#pragma once
#include <JuceHeader.h>
#include "EQBand.h"
class EQCurveDisplay : public juce::Component
{
public:
struct Listener
{
virtual ~Listener() = default;
virtual void bandAdded (int index, float freq, float gainDb) = 0;
virtual void bandRemoved (int index) = 0;
virtual void bandChanged (int index, const EQBand& band) = 0;
virtual void selectedBandChanged (int index) = 0;
};
EQCurveDisplay();
void setListener (Listener* l) { listener = l; }
void setBands (const std::vector<EQBand>& bands);
void setMagnitudeResponse (const std::vector<float>& magnitudesDb, double sampleRate, int fftSize);
int getSelectedBandIndex() const { return selectedBand; }
void setSelectedBand (int index);
void paint (juce::Graphics& g) override;
void mouseDown (const juce::MouseEvent& e) override;
void mouseDrag (const juce::MouseEvent& e) override;
void mouseUp (const juce::MouseEvent& e) override;
void mouseDoubleClick (const juce::MouseEvent& e) override;
void mouseWheelMove (const juce::MouseEvent& e, const juce::MouseWheelDetails& w) override;
private:
std::vector<EQBand> bands;
std::vector<float> magnitudeResponseDb;
double responseSampleRate = 44100.0;
int responseFftSize = 8192;
int selectedBand = -1;
int draggedBand = -1;
Listener* listener = nullptr;
static constexpr float minFreq = 20.0f;
static constexpr float maxFreq = 20000.0f;
static constexpr float minDb = -24.0f;
static constexpr float maxDb = 24.0f;
// Node colours (8 distinct colours for up to 8 bands)
static inline const juce::Colour nodeColours[8] = {
juce::Colour (0xffff6644), // orange-red
juce::Colour (0xff44bbff), // sky blue
juce::Colour (0xffff44aa), // pink
juce::Colour (0xff44ff88), // green
juce::Colour (0xffffff44), // yellow
juce::Colour (0xffaa44ff), // purple
juce::Colour (0xff44ffff), // cyan
juce::Colour (0xffff8844), // orange
};
juce::Rectangle<float> getPlotArea() const;
float freqToX (float freq) const;
float xToFreq (float x) const;
float dbToY (float db) const;
float yToDb (float y) const;
void drawGrid (juce::Graphics& g);
void drawResponseCurve (juce::Graphics& g);
void drawPerBandCurves (juce::Graphics& g);
void drawNodes (juce::Graphics& g);
int findNodeAt (float x, float y, float radius = 14.0f) const;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EQCurveDisplay)
};

172
Source/FIREngine.cpp Normal file
Fájl megtekintése

@@ -0,0 +1,172 @@
#include "FIREngine.h"
FIREngine::FIREngine() : Thread ("FIREngine") {}
FIREngine::~FIREngine()
{
stop();
}
void FIREngine::start (double sr)
{
sampleRate.store (sr);
needsUpdate.store (true);
startThread (juce::Thread::Priority::normal);
}
void FIREngine::stop()
{
signalThreadShouldExit();
notify();
stopThread (2000);
}
void FIREngine::setBands (const std::vector<EQBand>& newBands)
{
{
const juce::SpinLock::ScopedLockType lock (bandLock);
currentBands = newBands;
}
needsUpdate.store (true);
notify();
}
void FIREngine::setFFTOrder (int order)
{
fftOrder.store (juce::jlimit (12, 14, order));
needsUpdate.store (true);
notify();
}
std::unique_ptr<juce::AudioBuffer<float>> FIREngine::getNewFIR()
{
const juce::SpinLock::ScopedTryLockType lock (firLock);
if (lock.isLocked() && pendingFIR != nullptr)
return std::move (pendingFIR);
return nullptr;
}
std::vector<float> FIREngine::getMagnitudeResponseDb() const
{
const juce::SpinLock::ScopedLockType lock (magLock);
return magnitudeDb;
}
void FIREngine::run()
{
while (! threadShouldExit())
{
if (needsUpdate.exchange (false))
{
std::vector<EQBand> bands;
{
const juce::SpinLock::ScopedLockType lock (bandLock);
bands = currentBands;
}
auto fir = generateFIR (bands, sampleRate.load(), fftOrder.load());
{
const juce::SpinLock::ScopedLockType lock (firLock);
pendingFIR = std::make_unique<juce::AudioBuffer<float>> (std::move (fir));
}
}
wait (50); // Check every 50ms
}
}
juce::AudioBuffer<float> FIREngine::generateFIR (const std::vector<EQBand>& bands, double sr, int order)
{
const int fftSize = 1 << order;
const int numBins = fftSize / 2 + 1;
// Compute frequency for each FFT bin
std::vector<double> frequencies (numBins);
for (int i = 0; i < numBins; ++i)
frequencies[i] = (double) i * sr / (double) fftSize;
// Start with flat magnitude response (1.0 = 0dB)
std::vector<double> magnitudes (numBins, 1.0);
// For each active band, compute its magnitude contribution and multiply in
for (const auto& band : bands)
{
if (! band.enabled || std::abs (band.gainDb) < 0.01f)
continue;
float gainLinear = juce::Decibels::decibelsToGain (band.gainDb);
// Create IIR coefficients just for magnitude response analysis
juce::dsp::IIR::Coefficients<float>::Ptr coeffs;
switch (band.type)
{
case EQBand::Peak:
coeffs = juce::dsp::IIR::Coefficients<float>::makePeakFilter (sr, band.frequency, band.q, gainLinear);
break;
case EQBand::LowShelf:
coeffs = juce::dsp::IIR::Coefficients<float>::makeLowShelf (sr, band.frequency, band.q, gainLinear);
break;
case EQBand::HighShelf:
coeffs = juce::dsp::IIR::Coefficients<float>::makeHighShelf (sr, band.frequency, band.q, gainLinear);
break;
}
if (coeffs == nullptr)
continue;
// Get magnitude for each bin
std::vector<double> bandMag (numBins);
coeffs->getMagnitudeForFrequencyArray (frequencies.data(), bandMag.data(), numBins, sr);
for (int i = 0; i < numBins; ++i)
magnitudes[i] *= bandMag[i];
}
// Store magnitude in dB for display
{
std::vector<float> magDb (numBins);
for (int i = 0; i < numBins; ++i)
magDb[i] = (float) juce::Decibels::gainToDecibels (magnitudes[i], -60.0);
const juce::SpinLock::ScopedLockType lock (magLock);
magnitudeDb = std::move (magDb);
}
// Build complex spectrum: magnitude only, zero phase (linear phase)
// JUCE FFT expects interleaved [real, imag, real, imag, ...] for complex
// For performRealOnlyInverseTransform, input is fftSize*2 floats
std::vector<float> fftData (fftSize * 2, 0.0f);
// Pack magnitude into real parts (positive frequencies)
// performRealOnlyInverseTransform expects the format from performRealOnlyForwardTransform:
// data[0] = DC real, data[1] = Nyquist real, then interleaved complex pairs
fftData[0] = (float) magnitudes[0]; // DC
fftData[1] = (float) magnitudes[numBins - 1]; // Nyquist
for (int i = 1; i < numBins - 1; ++i)
{
fftData[i * 2] = (float) magnitudes[i]; // real
fftData[i * 2 + 1] = 0.0f; // imag (zero = linear phase)
}
// Inverse FFT to get time-domain impulse response
juce::dsp::FFT fft (order);
fft.performRealOnlyInverseTransform (fftData.data());
// The result is in fftData[0..fftSize-1]
// Circular shift by fftSize/2 to center the impulse (make it causal)
juce::AudioBuffer<float> firBuffer (1, fftSize);
float* firData = firBuffer.getWritePointer (0);
int halfSize = fftSize / 2;
for (int i = 0; i < fftSize; ++i)
firData[i] = fftData[(i + halfSize) % fftSize];
// Apply window to reduce truncation artifacts
juce::dsp::WindowingFunction<float> window (fftSize, juce::dsp::WindowingFunction<float>::blackmanHarris);
window.multiplyWithWindowingTable (firData, fftSize);
return firBuffer;
}

46
Source/FIREngine.h Normal file
Fájl megtekintése

@@ -0,0 +1,46 @@
#pragma once
#include <JuceHeader.h>
#include "EQBand.h"
class FIREngine : private juce::Thread
{
public:
static constexpr int defaultFFTOrder = 13; // 8192 taps
static constexpr int maxBands = 8;
FIREngine();
~FIREngine() override;
void start (double sampleRate);
void stop();
// Called from GUI thread
void setBands (const std::vector<EQBand>& newBands);
void setFFTOrder (int order);
// Called from audio thread — returns new FIR if available, nullptr otherwise
std::unique_ptr<juce::AudioBuffer<float>> getNewFIR();
// Get magnitude response in dB for display (thread-safe copy)
std::vector<float> getMagnitudeResponseDb() const;
int getFIRLength() const { return 1 << fftOrder.load(); }
int getLatencySamples() const { return getFIRLength() / 2; }
private:
void run() override;
juce::AudioBuffer<float> generateFIR (const std::vector<EQBand>& bands, double sr, int order);
std::atomic<double> sampleRate { 44100.0 };
std::atomic<int> fftOrder { defaultFFTOrder };
std::atomic<bool> needsUpdate { true };
std::vector<EQBand> currentBands;
mutable juce::SpinLock bandLock;
std::unique_ptr<juce::AudioBuffer<float>> pendingFIR;
juce::SpinLock firLock;
std::vector<float> magnitudeDb;
mutable juce::SpinLock magLock;
};

337
Source/LookAndFeel.cpp Normal file
Fájl megtekintése

@@ -0,0 +1,337 @@
#include "LookAndFeel.h"
#include "BinaryData.h"
InstaLPEQLookAndFeel::InstaLPEQLookAndFeel()
{
typefaceRegular = juce::Typeface::createSystemTypefaceFor (
BinaryData::RajdhaniRegular_ttf, BinaryData::RajdhaniRegular_ttfSize);
typefaceMedium = juce::Typeface::createSystemTypefaceFor (
BinaryData::RajdhaniMedium_ttf, BinaryData::RajdhaniMedium_ttfSize);
typefaceBold = juce::Typeface::createSystemTypefaceFor (
BinaryData::RajdhaniBold_ttf, BinaryData::RajdhaniBold_ttfSize);
setColour (juce::ResizableWindow::backgroundColourId, bgDark);
setColour (juce::Label::textColourId, textPrimary);
setColour (juce::TextButton::buttonColourId, bgMedium);
setColour (juce::TextButton::textColourOffId, textPrimary);
generateNoiseTexture();
}
juce::Typeface::Ptr InstaLPEQLookAndFeel::getTypefaceForFont (const juce::Font& font)
{
if (font.isBold())
return typefaceBold;
return typefaceRegular;
}
juce::Font InstaLPEQLookAndFeel::getRegularFont (float height) const
{
return juce::Font (juce::FontOptions (typefaceRegular).withHeight (height));
}
juce::Font InstaLPEQLookAndFeel::getMediumFont (float height) const
{
return juce::Font (juce::FontOptions (typefaceMedium).withHeight (height));
}
juce::Font InstaLPEQLookAndFeel::getBoldFont (float height) const
{
return juce::Font (juce::FontOptions (typefaceBold).withHeight (height));
}
void InstaLPEQLookAndFeel::generateNoiseTexture()
{
const int texW = 256, texH = 256;
noiseTexture = juce::Image (juce::Image::ARGB, texW, texH, true);
juce::Random rng (42);
for (int y = 0; y < texH; ++y)
{
for (int x = 0; x < texW; ++x)
{
float noise = rng.nextFloat() * 0.06f;
bool crossA = ((x + y) % 4 == 0);
bool crossB = ((x - y + 256) % 4 == 0);
float pattern = (crossA || crossB) ? 0.03f : 0.0f;
float alpha = noise + pattern;
noiseTexture.setPixelAt (x, y, juce::Colour::fromFloatRGBA (1.0f, 1.0f, 1.0f, alpha));
}
}
}
void InstaLPEQLookAndFeel::drawBackgroundTexture (juce::Graphics& g, juce::Rectangle<int> area)
{
for (int y = area.getY(); y < area.getBottom(); y += noiseTexture.getHeight())
for (int x = area.getX(); x < area.getRight(); x += noiseTexture.getWidth())
g.drawImageAt (noiseTexture, x, y);
}
// ============================================================
// Rotary slider (3D metal knob)
// ============================================================
void InstaLPEQLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height,
float sliderPos, float rotaryStartAngle,
float rotaryEndAngle, juce::Slider& slider)
{
float knobSize = std::min ((float) width, (float) height);
float s = knobSize / 60.0f;
float margin = std::max (4.0f, 6.0f * s);
auto bounds = juce::Rectangle<int> (x, y, width, height).toFloat().reduced (margin);
auto radius = std::min (bounds.getWidth(), bounds.getHeight()) / 2.0f;
auto cx = bounds.getCentreX();
auto cy = bounds.getCentreY();
auto angle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle);
auto knobType = slider.getProperties() [knobTypeProperty].toString();
bool isDark = (knobType == "dark");
juce::Colour arcColour = isDark ? juce::Colour (0xff4488ff) : juce::Colour (0xffff8833);
juce::Colour arcBgColour = isDark ? juce::Colour (0xff1a2a44) : juce::Colour (0xff2a1a0a);
juce::Colour bodyTop = isDark ? juce::Colour (0xff3a3a4a) : juce::Colour (0xff5a4a3a);
juce::Colour bodyBottom = isDark ? juce::Colour (0xff1a1a2a) : juce::Colour (0xff2a1a0a);
juce::Colour rimColour = isDark ? juce::Colour (0xff555566) : juce::Colour (0xff886644);
juce::Colour highlightCol = isDark ? juce::Colour (0x33aabbff) : juce::Colour (0x44ffcc88);
juce::Colour pointerColour = isDark ? juce::Colour (0xff66aaff) : juce::Colour (0xffffaa44);
float arcW = std::max (1.5f, 2.5f * s);
float glowW1 = std::max (3.0f, 10.0f * s);
float hotW = std::max (0.8f, 1.2f * s);
float ptrW = std::max (1.2f, 2.0f * s);
float bodyRadius = radius * 0.72f;
// 1. Drop shadow
g.setColour (juce::Colours::black.withAlpha (0.35f));
g.fillEllipse (cx - bodyRadius + 1, cy - bodyRadius + 2, bodyRadius * 2, bodyRadius * 2);
// 2. Outer arc track
{
juce::Path arcBg;
arcBg.addCentredArc (cx, cy, radius - 1, radius - 1, 0.0f,
rotaryStartAngle, rotaryEndAngle, true);
g.setColour (arcBgColour);
g.strokePath (arcBg, juce::PathStrokeType (arcW, juce::PathStrokeType::curved,
juce::PathStrokeType::rounded));
}
// 3. Outer arc value with smooth multi-layer glow
if (sliderPos > 0.01f)
{
juce::Path arcVal;
arcVal.addCentredArc (cx, cy, radius - 1, radius - 1, 0.0f,
rotaryStartAngle, angle, true);
const int numGlowLayers = 8;
for (int i = 0; i < numGlowLayers; ++i)
{
float t = (float) i / (float) (numGlowLayers - 1);
float layerWidth = glowW1 * (1.0f - t * 0.7f);
float layerAlpha = 0.03f + t * t * 0.35f;
g.setColour (arcColour.withAlpha (layerAlpha));
g.strokePath (arcVal, juce::PathStrokeType (layerWidth, juce::PathStrokeType::curved,
juce::PathStrokeType::rounded));
}
g.setColour (arcColour);
g.strokePath (arcVal, juce::PathStrokeType (arcW, juce::PathStrokeType::curved,
juce::PathStrokeType::rounded));
g.setColour (arcColour.brighter (0.6f).withAlpha (0.5f));
g.strokePath (arcVal, juce::PathStrokeType (hotW, juce::PathStrokeType::curved,
juce::PathStrokeType::rounded));
}
// 4. Knob body
{
juce::ColourGradient bodyGrad (bodyTop, cx, cy - bodyRadius * 0.5f,
bodyBottom, cx, cy + bodyRadius, true);
g.setGradientFill (bodyGrad);
g.fillEllipse (cx - bodyRadius, cy - bodyRadius, bodyRadius * 2, bodyRadius * 2);
}
// 5. Rim
g.setColour (rimColour.withAlpha (0.6f));
g.drawEllipse (cx - bodyRadius, cy - bodyRadius, bodyRadius * 2, bodyRadius * 2, std::max (0.8f, 1.2f * s));
// 6. Inner shadow
{
float innerR = bodyRadius * 0.85f;
juce::ColourGradient innerGrad (juce::Colours::black.withAlpha (0.15f), cx, cy - innerR * 0.3f,
juce::Colours::transparentBlack, cx, cy + innerR, true);
g.setGradientFill (innerGrad);
g.fillEllipse (cx - innerR, cy - innerR, innerR * 2, innerR * 2);
}
// 7. Top highlight
{
float hlRadius = bodyRadius * 0.55f;
float hlY = cy - bodyRadius * 0.35f;
juce::ColourGradient hlGrad (highlightCol, cx, hlY - hlRadius * 0.5f,
juce::Colours::transparentBlack, cx, hlY + hlRadius, true);
g.setGradientFill (hlGrad);
g.fillEllipse (cx - hlRadius, hlY - hlRadius * 0.6f, hlRadius * 2, hlRadius * 1.2f);
}
// 8. Pointer with subtle glow
{
float pointerLen = bodyRadius * 0.75f;
for (int i = 0; i < 4; ++i)
{
float t = (float) i / 3.0f;
float gw = ptrW * (2.0f - t * 1.5f);
float alpha = 0.02f + t * t * 0.15f;
juce::Path glowLayer;
glowLayer.addRoundedRectangle (-gw, -pointerLen, gw * 2, pointerLen * 0.55f, gw * 0.5f);
glowLayer.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy));
g.setColour (pointerColour.withAlpha (alpha));
g.fillPath (glowLayer);
}
{
juce::Path pointer;
pointer.addRoundedRectangle (-ptrW * 0.5f, -pointerLen, ptrW, pointerLen * 0.55f, ptrW * 0.5f);
pointer.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy));
g.setColour (pointerColour);
g.fillPath (pointer);
}
{
juce::Path hotCenter;
float hw = ptrW * 0.3f;
hotCenter.addRoundedRectangle (-hw, -pointerLen, hw * 2, pointerLen * 0.5f, hw);
hotCenter.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy));
g.setColour (pointerColour.brighter (0.7f).withAlpha (0.6f));
g.fillPath (hotCenter);
}
}
// 9. Center cap
{
float capR = bodyRadius * 0.18f;
juce::ColourGradient capGrad (rimColour.brighter (0.3f), cx, cy - capR,
bodyBottom, cx, cy + capR, false);
g.setGradientFill (capGrad);
g.fillEllipse (cx - capR, cy - capR, capR * 2, capR * 2);
}
}
// ============================================================
// Button style
// ============================================================
void InstaLPEQLookAndFeel::drawButtonBackground (juce::Graphics& g, juce::Button& button,
const juce::Colour& backgroundColour,
bool shouldDrawButtonAsHighlighted,
bool shouldDrawButtonAsDown)
{
auto bounds = button.getLocalBounds().toFloat().reduced (0.5f);
auto baseColour = backgroundColour;
if (shouldDrawButtonAsDown)
baseColour = baseColour.brighter (0.2f);
else if (shouldDrawButtonAsHighlighted)
baseColour = baseColour.brighter (0.1f);
juce::ColourGradient grad (baseColour.brighter (0.05f), 0, bounds.getY(),
baseColour.darker (0.1f), 0, bounds.getBottom(), false);
g.setGradientFill (grad);
g.fillRoundedRectangle (bounds, 4.0f);
g.setColour (bgLight.withAlpha (shouldDrawButtonAsHighlighted ? 0.8f : 0.5f));
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
}
// ============================================================
// Toggle button — glowing switch
// ============================================================
void InstaLPEQLookAndFeel::drawToggleButton (juce::Graphics& g, juce::ToggleButton& button,
bool shouldDrawButtonAsHighlighted,
bool /*shouldDrawButtonAsDown*/)
{
auto bounds = button.getLocalBounds().toFloat();
float h = std::min (bounds.getHeight() * 0.6f, 14.0f);
float w = h * 1.8f;
float trackR = h * 0.5f;
float sx = bounds.getX() + (bounds.getWidth() - w) * 0.5f;
float sy = bounds.getCentreY() - h * 0.5f;
auto trackBounds = juce::Rectangle<float> (sx, sy, w, h);
bool isOn = button.getToggleState();
auto onColour = accent;
auto offColour = bgLight;
float targetPos = isOn ? 1.0f : 0.0f;
float animPos = (float) button.getProperties().getWithDefault ("animPos", targetPos);
animPos += (targetPos - animPos) * 0.25f;
if (std::abs (animPos - targetPos) < 0.01f) animPos = targetPos;
button.getProperties().set ("animPos", animPos);
if (std::abs (animPos - targetPos) > 0.005f)
button.repaint();
float thumbR = h * 0.4f;
float thumbX = sx + trackR + animPos * (w - trackR * 2);
float thumbY = sy + h * 0.5f;
float glowIntensity = animPos;
if (glowIntensity > 0.01f)
{
for (int i = 0; i < 3; ++i)
{
float t = (float) i / 2.0f;
float expand = (1.0f - t) * 1.5f;
float alpha = (0.04f + t * t * 0.1f) * glowIntensity;
g.setColour (onColour.withAlpha (alpha));
g.fillRoundedRectangle (trackBounds.expanded (expand), trackR + expand);
}
}
{
juce::Colour offCol = offColour.withAlpha (0.3f);
juce::Colour onCol = onColour.withAlpha (0.35f);
juce::Colour trackCol = offCol.interpolatedWith (onCol, glowIntensity);
if (shouldDrawButtonAsHighlighted)
trackCol = trackCol.brighter (0.15f);
g.setColour (trackCol);
g.fillRoundedRectangle (trackBounds, trackR);
g.setColour (offColour.withAlpha (0.4f).interpolatedWith (onColour.withAlpha (0.5f), glowIntensity));
g.drawRoundedRectangle (trackBounds, trackR, 0.8f);
}
if (glowIntensity > 0.01f)
{
for (int i = 0; i < 3; ++i)
{
float t = (float) i / 2.0f;
float r = thumbR * (1.5f - t * 0.5f);
float alpha = (0.05f + t * t * 0.12f) * glowIntensity;
g.setColour (onColour.withAlpha (alpha));
g.fillEllipse (thumbX - r, thumbY - r, r * 2, r * 2);
}
}
{
juce::Colour thumbTopOff (0xff555566), thumbBotOff (0xff333344);
juce::Colour thumbTopOn = onColour.brighter (0.3f), thumbBotOn = onColour.darker (0.2f);
juce::ColourGradient thumbGrad (
thumbTopOff.interpolatedWith (thumbTopOn, glowIntensity), thumbX, thumbY - thumbR,
thumbBotOff.interpolatedWith (thumbBotOn, glowIntensity), thumbX, thumbY + thumbR, false);
g.setGradientFill (thumbGrad);
g.fillEllipse (thumbX - thumbR, thumbY - thumbR, thumbR * 2, thumbR * 2);
g.setColour (juce::Colour (0xff666677).withAlpha (0.5f).interpolatedWith (onColour.withAlpha (0.6f), glowIntensity));
g.drawEllipse (thumbX - thumbR, thumbY - thumbR, thumbR * 2, thumbR * 2, 0.8f);
float hlR = thumbR * 0.4f;
g.setColour (juce::Colours::white.withAlpha (0.1f + 0.15f * glowIntensity));
g.fillEllipse (thumbX - hlR, thumbY - thumbR * 0.6f - hlR * 0.3f, hlR * 2, hlR * 1.2f);
}
}

50
Source/LookAndFeel.h Normal file
Fájl megtekintése

@@ -0,0 +1,50 @@
#pragma once
#include <JuceHeader.h>
class InstaLPEQLookAndFeel : public juce::LookAndFeel_V4
{
public:
// Colour palette
static inline const juce::Colour bgDark { 0xff1a1a2e };
static inline const juce::Colour bgMedium { 0xff16213e };
static inline const juce::Colour bgLight { 0xff0f3460 };
static inline const juce::Colour textPrimary { 0xffe0e0e0 };
static inline const juce::Colour textSecondary { 0xff888899 };
static inline const juce::Colour accent { 0xff00ff88 };
// Knob type property key
static constexpr const char* knobTypeProperty = "knobType";
InstaLPEQLookAndFeel();
void drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height,
float sliderPosProportional, float rotaryStartAngle,
float rotaryEndAngle, juce::Slider& slider) override;
void drawButtonBackground (juce::Graphics& g, juce::Button& button,
const juce::Colour& backgroundColour,
bool shouldDrawButtonAsHighlighted,
bool shouldDrawButtonAsDown) override;
void drawToggleButton (juce::Graphics& g, juce::ToggleButton& button,
bool shouldDrawButtonAsHighlighted,
bool shouldDrawButtonAsDown) override;
// Custom fonts
juce::Font getRegularFont (float height) const;
juce::Font getMediumFont (float height) const;
juce::Font getBoldFont (float height) const;
// Background texture
void drawBackgroundTexture (juce::Graphics& g, juce::Rectangle<int> area);
juce::Typeface::Ptr getTypefaceForFont (const juce::Font& font) override;
private:
juce::Typeface::Ptr typefaceRegular;
juce::Typeface::Ptr typefaceMedium;
juce::Typeface::Ptr typefaceBold;
juce::Image noiseTexture;
void generateNoiseTexture();
};

Fájl megtekintése

@@ -0,0 +1,145 @@
#include "NodeParameterPanel.h"
#include "LookAndFeel.h"
NodeParameterPanel::NodeParameterPanel()
{
setupSlider (freqSlider, freqLabel, 20.0, 20000.0, 1.0, " Hz");
freqSlider.setSkewFactorFromMidPoint (1000.0);
setupSlider (gainSlider, gainLabel, -24.0, 24.0, 0.1, " dB");
setupSlider (qSlider, qLabel, 0.1, 18.0, 0.01, "");
qSlider.setSkewFactorFromMidPoint (1.0);
qSlider.getProperties().set (InstaLPEQLookAndFeel::knobTypeProperty, "dark");
typeSelector.addItem ("Peak", 1);
typeSelector.addItem ("Low Shelf", 2);
typeSelector.addItem ("High Shelf", 3);
typeSelector.setSelectedId (1, juce::dontSendNotification);
typeSelector.addListener (this);
addAndMakeVisible (typeSelector);
deleteButton.addListener (this);
addAndMakeVisible (deleteButton);
addAndMakeVisible (bandInfoLabel);
bandInfoLabel.setJustificationType (juce::Justification::centredLeft);
setSelectedBand (-1, nullptr);
}
void NodeParameterPanel::setupSlider (juce::Slider& s, juce::Label& l, double min, double max, double step, const char* suffix)
{
s.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
s.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 60, 16);
s.setRange (min, max, step);
s.setTextValueSuffix (suffix);
s.addListener (this);
addAndMakeVisible (s);
l.setJustificationType (juce::Justification::centred);
addAndMakeVisible (l);
}
void NodeParameterPanel::setSelectedBand (int index, const EQBand* band)
{
selectedIndex = index;
updatingFromExternal = true;
bool hasBand = (index >= 0 && band != nullptr);
freqSlider.setEnabled (hasBand);
gainSlider.setEnabled (hasBand);
qSlider.setEnabled (hasBand);
typeSelector.setEnabled (hasBand);
deleteButton.setEnabled (hasBand);
if (hasBand)
{
currentBand = *band;
freqSlider.setValue (band->frequency, juce::dontSendNotification);
gainSlider.setValue (band->gainDb, juce::dontSendNotification);
qSlider.setValue (band->q, juce::dontSendNotification);
typeSelector.setSelectedId ((int) band->type + 1, juce::dontSendNotification);
bandInfoLabel.setText ("Band " + juce::String (index + 1), juce::dontSendNotification);
}
else
{
bandInfoLabel.setText ("No band selected", juce::dontSendNotification);
}
updatingFromExternal = false;
repaint();
}
void NodeParameterPanel::paint (juce::Graphics& g)
{
auto bounds = getLocalBounds().toFloat();
g.setColour (InstaLPEQLookAndFeel::bgMedium.darker (0.2f));
g.fillRoundedRectangle (bounds, 4.0f);
g.setColour (InstaLPEQLookAndFeel::bgLight.withAlpha (0.3f));
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
}
void NodeParameterPanel::resized()
{
auto bounds = getLocalBounds().reduced (6);
float scale = (float) getHeight() / 90.0f;
auto left = bounds.removeFromLeft (100);
bandInfoLabel.setBounds (left.removeFromTop ((int) (20 * scale)));
auto typeBounds = left.removeFromTop ((int) (22 * scale));
typeSelector.setBounds (typeBounds.reduced (2));
auto delBounds = left.removeFromTop ((int) (22 * scale));
deleteButton.setBounds (delBounds.reduced (2));
// Knobs take the rest
int knobW = bounds.getWidth() / 3;
int labelH = (int) std::max (14.0f, 16.0f * scale);
auto freqArea = bounds.removeFromLeft (knobW);
freqLabel.setBounds (freqArea.removeFromTop (labelH));
freqSlider.setBounds (freqArea);
auto gainArea = bounds.removeFromLeft (knobW);
gainLabel.setBounds (gainArea.removeFromTop (labelH));
gainSlider.setBounds (gainArea);
auto qArea = bounds;
qLabel.setBounds (qArea.removeFromTop (labelH));
qSlider.setBounds (qArea);
}
void NodeParameterPanel::sliderValueChanged (juce::Slider* slider)
{
if (updatingFromExternal || selectedIndex < 0 || listener == nullptr)
return;
if (slider == &freqSlider)
currentBand.frequency = (float) freqSlider.getValue();
else if (slider == &gainSlider)
currentBand.gainDb = (float) gainSlider.getValue();
else if (slider == &qSlider)
currentBand.q = (float) qSlider.getValue();
listener->nodeParameterChanged (selectedIndex, currentBand);
}
void NodeParameterPanel::comboBoxChanged (juce::ComboBox* box)
{
if (updatingFromExternal || selectedIndex < 0 || listener == nullptr)
return;
if (box == &typeSelector)
{
currentBand.type = static_cast<EQBand::Type> (typeSelector.getSelectedId() - 1);
listener->nodeParameterChanged (selectedIndex, currentBand);
}
}
void NodeParameterPanel::buttonClicked (juce::Button* button)
{
if (button == &deleteButton && selectedIndex >= 0 && listener != nullptr)
listener->nodeDeleteRequested (selectedIndex);
}

Fájl megtekintése

@@ -0,0 +1,49 @@
#pragma once
#include <JuceHeader.h>
#include "EQBand.h"
class NodeParameterPanel : public juce::Component,
private juce::Slider::Listener,
private juce::ComboBox::Listener,
private juce::Button::Listener
{
public:
struct Listener
{
virtual ~Listener() = default;
virtual void nodeParameterChanged (int bandIndex, const EQBand& band) = 0;
virtual void nodeDeleteRequested (int bandIndex) = 0;
};
NodeParameterPanel();
void setListener (Listener* l) { listener = l; }
void setSelectedBand (int index, const EQBand* band);
int getSelectedIndex() const { return selectedIndex; }
void resized() override;
void paint (juce::Graphics& g) override;
private:
void sliderValueChanged (juce::Slider* slider) override;
void comboBoxChanged (juce::ComboBox* box) override;
void buttonClicked (juce::Button* button) override;
int selectedIndex = -1;
EQBand currentBand;
bool updatingFromExternal = false;
juce::Slider freqSlider, gainSlider, qSlider;
juce::Label freqLabel { {}, "FREQ" };
juce::Label gainLabel { {}, "GAIN" };
juce::Label qLabel { {}, "Q" };
juce::Label bandInfoLabel { {}, "No band selected" };
juce::ComboBox typeSelector;
juce::TextButton deleteButton { "DELETE" };
Listener* listener = nullptr;
void setupSlider (juce::Slider& s, juce::Label& l, double min, double max, double step, const char* suffix);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NodeParameterPanel)
};

211
Source/PluginEditor.cpp Normal file
Fájl megtekintése

@@ -0,0 +1,211 @@
#include "PluginEditor.h"
InstaLPEQEditor::InstaLPEQEditor (InstaLPEQProcessor& p)
: AudioProcessorEditor (p), processor (p)
{
setLookAndFeel (&customLookAndFeel);
juce::LookAndFeel::setDefaultLookAndFeel (&customLookAndFeel);
// Title
titleLabel.setFont (customLookAndFeel.getBoldFont (26.0f));
titleLabel.setColour (juce::Label::textColourId, InstaLPEQLookAndFeel::accent);
addAndMakeVisible (titleLabel);
versionLabel.setFont (customLookAndFeel.getRegularFont (13.0f));
versionLabel.setColour (juce::Label::textColourId, InstaLPEQLookAndFeel::textSecondary);
versionLabel.setJustificationType (juce::Justification::centredRight);
addAndMakeVisible (versionLabel);
// Bypass
bypassToggle.setToggleState (processor.bypassed.load(), juce::dontSendNotification);
addAndMakeVisible (bypassToggle);
bypassLabel.setFont (customLookAndFeel.getMediumFont (13.0f));
bypassLabel.setColour (juce::Label::textColourId, InstaLPEQLookAndFeel::textSecondary);
addAndMakeVisible (bypassLabel);
// EQ curve
curveDisplay.setListener (this);
addAndMakeVisible (curveDisplay);
// Node panel
nodePanel.setListener (this);
addAndMakeVisible (nodePanel);
// Master gain
masterGainSlider.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
masterGainSlider.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 60, 16);
masterGainSlider.setRange (-24.0, 24.0, 0.1);
masterGainSlider.setValue (0.0);
masterGainSlider.setTextValueSuffix (" dB");
addAndMakeVisible (masterGainSlider);
masterGainLabel.setFont (customLookAndFeel.getMediumFont (13.0f));
masterGainLabel.setJustificationType (juce::Justification::centred);
addAndMakeVisible (masterGainLabel);
// Sizing
constrainer.setMinimumSize (700, 450);
constrainer.setMaximumSize (1920, 1080);
setConstrainer (&constrainer);
setResizable (true, true);
setSize (900, 650);
startTimerHz (30);
syncDisplayFromProcessor();
}
InstaLPEQEditor::~InstaLPEQEditor()
{
setLookAndFeel (nullptr);
}
void InstaLPEQEditor::paint (juce::Graphics& g)
{
auto bounds = getLocalBounds().toFloat();
// Background gradient
juce::ColourGradient bgGrad (InstaLPEQLookAndFeel::bgDark, 0, 0,
InstaLPEQLookAndFeel::bgDark.darker (0.3f), 0, bounds.getBottom(), false);
g.setGradientFill (bgGrad);
g.fillAll();
// Noise texture
customLookAndFeel.drawBackgroundTexture (g, getLocalBounds());
// Top header bar
float scale = (float) getHeight() / 650.0f;
int headerH = (int) std::max (28.0f, 36.0f * scale);
g.setColour (InstaLPEQLookAndFeel::bgDark.darker (0.4f));
g.fillRect (0, 0, getWidth(), headerH);
g.setColour (InstaLPEQLookAndFeel::bgLight.withAlpha (0.3f));
g.drawHorizontalLine (headerH, 0.0f, (float) getWidth());
}
void InstaLPEQEditor::resized()
{
auto bounds = getLocalBounds();
float scale = (float) getHeight() / 650.0f;
// Top bar
int headerH = (int) std::max (28.0f, 36.0f * scale);
auto header = bounds.removeFromTop (headerH);
int pad = (int) (8 * scale);
header.reduce (pad, 2);
titleLabel.setFont (customLookAndFeel.getBoldFont (std::max (18.0f, 26.0f * scale)));
titleLabel.setBounds (header.removeFromLeft (200));
versionLabel.setFont (customLookAndFeel.getRegularFont (std::max (10.0f, 13.0f * scale)));
versionLabel.setBounds (header.removeFromRight (60));
auto bypassArea = header.removeFromRight (80);
bypassLabel.setBounds (bypassArea.removeFromLeft (50));
bypassToggle.setBounds (bypassArea);
// Bottom master row
int masterH = (int) std::max (50.0f, 65.0f * scale);
auto masterArea = bounds.removeFromBottom (masterH).reduced (pad, 2);
// Divider above master
// (painted in paint())
masterGainLabel.setFont (customLookAndFeel.getMediumFont (std::max (11.0f, 14.0f * scale)));
auto labelArea = masterArea.removeFromLeft (60);
masterGainLabel.setBounds (labelArea);
masterGainSlider.setBounds (masterArea.removeFromLeft (masterH));
// Node parameter panel (15% of remaining height)
int nodePanelH = (int) (bounds.getHeight() * 0.18f);
auto nodePanelArea = bounds.removeFromBottom (nodePanelH).reduced (pad, 2);
nodePanel.setBounds (nodePanelArea);
// EQ curve display (rest)
auto curveArea = bounds.reduced (pad, 2);
curveDisplay.setBounds (curveArea);
}
void InstaLPEQEditor::timerCallback()
{
// Sync bypass
processor.bypassed.store (bypassToggle.getToggleState());
processor.masterGainDb.store ((float) masterGainSlider.getValue());
// Update display with latest magnitude response
auto magDb = processor.getFIREngine().getMagnitudeResponseDb();
if (! magDb.empty())
{
curveDisplay.setMagnitudeResponse (magDb, processor.getCurrentSampleRate(),
processor.getFIREngine().getFIRLength());
}
// Sync bands from processor (in case of state restore)
auto currentBands = processor.getBands();
curveDisplay.setBands (currentBands);
// Update node panel if selected
int sel = curveDisplay.getSelectedBandIndex();
if (sel >= 0 && sel < (int) currentBands.size())
nodePanel.setSelectedBand (sel, &currentBands[sel]);
}
// ============================================================
// EQCurveDisplay::Listener
// ============================================================
void InstaLPEQEditor::bandAdded (int /*index*/, float freq, float gainDb)
{
processor.addBand (freq, gainDb);
syncDisplayFromProcessor();
curveDisplay.setSelectedBand (processor.getNumBands() - 1);
}
void InstaLPEQEditor::bandRemoved (int index)
{
processor.removeBand (index);
curveDisplay.setSelectedBand (-1);
syncDisplayFromProcessor();
}
void InstaLPEQEditor::bandChanged (int index, const EQBand& band)
{
processor.setBand (index, band);
// Don't call syncDisplayFromProcessor here to avoid flicker during drag
auto currentBands = processor.getBands();
curveDisplay.setBands (currentBands);
if (index == nodePanel.getSelectedIndex() && index < (int) currentBands.size())
nodePanel.setSelectedBand (index, &currentBands[index]);
}
void InstaLPEQEditor::selectedBandChanged (int index)
{
auto currentBands = processor.getBands();
if (index >= 0 && index < (int) currentBands.size())
nodePanel.setSelectedBand (index, &currentBands[index]);
else
nodePanel.setSelectedBand (-1, nullptr);
}
// ============================================================
// NodeParameterPanel::Listener
// ============================================================
void InstaLPEQEditor::nodeParameterChanged (int bandIndex, const EQBand& band)
{
processor.setBand (bandIndex, band);
syncDisplayFromProcessor();
}
void InstaLPEQEditor::nodeDeleteRequested (int bandIndex)
{
processor.removeBand (bandIndex);
curveDisplay.setSelectedBand (-1);
nodePanel.setSelectedBand (-1, nullptr);
syncDisplayFromProcessor();
}
void InstaLPEQEditor::syncDisplayFromProcessor()
{
auto currentBands = processor.getBands();
curveDisplay.setBands (currentBands);
}

52
Source/PluginEditor.h Normal file
Fájl megtekintése

@@ -0,0 +1,52 @@
#pragma once
#include <JuceHeader.h>
#include "PluginProcessor.h"
#include "LookAndFeel.h"
#include "EQCurveDisplay.h"
#include "NodeParameterPanel.h"
class InstaLPEQEditor : public juce::AudioProcessorEditor,
private juce::Timer,
private EQCurveDisplay::Listener,
private NodeParameterPanel::Listener
{
public:
explicit InstaLPEQEditor (InstaLPEQProcessor& p);
~InstaLPEQEditor() override;
void paint (juce::Graphics& g) override;
void resized() override;
private:
void timerCallback() override;
// EQCurveDisplay::Listener
void bandAdded (int index, float freq, float gainDb) override;
void bandRemoved (int index) override;
void bandChanged (int index, const EQBand& band) override;
void selectedBandChanged (int index) override;
// NodeParameterPanel::Listener
void nodeParameterChanged (int bandIndex, const EQBand& band) override;
void nodeDeleteRequested (int bandIndex) override;
void syncDisplayFromProcessor();
InstaLPEQProcessor& processor;
InstaLPEQLookAndFeel customLookAndFeel;
EQCurveDisplay curveDisplay;
NodeParameterPanel nodePanel;
juce::Label titleLabel { {}, "INSTALPEQ" };
juce::Label versionLabel { {}, "v1.0" };
juce::ToggleButton bypassToggle;
juce::Label bypassLabel { {}, "BYPASS" };
juce::Slider masterGainSlider;
juce::Label masterGainLabel { {}, "MASTER" };
juce::ComponentBoundsConstrainer constrainer;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InstaLPEQEditor)
};

196
Source/PluginProcessor.cpp Normal file
Fájl megtekintése

@@ -0,0 +1,196 @@
#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);
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 master gain
float gain = juce::Decibels::decibelsToGain (masterGainDb.load());
if (std::abs (gain - 1.0f) > 0.001f)
buffer.applyGain (gain);
}
// ============================================================
// 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();
}
void InstaLPEQProcessor::updateFIR()
{
auto currentBands = getBands();
firEngine.setBands (currentBands);
}
// ============================================================
// State save/restore
// ============================================================
void InstaLPEQProcessor::getStateInformation (juce::MemoryBlock& destData)
{
juce::XmlElement xml ("InstaLPEQ");
xml.setAttribute ("bypass", bypassed.load());
xml.setAttribute ("masterGain", (double) masterGainDb.load());
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));
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();
}

64
Source/PluginProcessor.h Normal file
Fájl megtekintése

@@ -0,0 +1,64 @@
#pragma once
#include <JuceHeader.h>
#include "EQBand.h"
#include "FIREngine.h"
class InstaLPEQProcessor : public juce::AudioProcessor
{
public:
static constexpr int maxBands = 8;
InstaLPEQProcessor();
~InstaLPEQProcessor() override;
void prepareToPlay (double sampleRate, int samplesPerBlock) override;
void releaseResources() override;
void processBlock (juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override { return true; }
const juce::String getName() const override { return JucePlugin_Name; }
bool acceptsMidi() const override { return false; }
bool producesMidi() const override { return false; }
bool isBusesLayoutSupported (const BusesLayout& layouts) const override;
double getTailLengthSeconds() const override { return 0.0; }
int getNumPrograms() override { return 1; }
int getCurrentProgram() override { return 0; }
void setCurrentProgram (int) override {}
const juce::String getProgramName (int) override { return {}; }
void changeProgramName (int, const juce::String&) override {}
void getStateInformation (juce::MemoryBlock& destData) override;
void setStateInformation (const void* data, int sizeInBytes) override;
// Band management (called from GUI thread)
std::vector<EQBand> getBands() const;
void setBand (int index, const EQBand& band);
void addBand (float freq, float gainDb);
void removeBand (int index);
int getNumBands() const;
// Settings
std::atomic<bool> bypassed { false };
std::atomic<float> masterGainDb { 0.0f };
FIREngine& getFIREngine() { return firEngine; }
double getCurrentSampleRate() const { return currentSampleRate; }
private:
std::vector<EQBand> bands;
juce::SpinLock bandLock;
FIREngine firEngine;
juce::dsp::Convolution convolution;
double currentSampleRate = 44100.0;
int currentBlockSize = 512;
bool firLoaded = false;
void updateFIR();
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InstaLPEQProcessor)
};