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:
12
Source/EQBand.h
Normal file
12
Source/EQBand.h
Normal file
@@ -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
420
Source/EQCurveDisplay.cpp
Normal file
@@ -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
71
Source/EQCurveDisplay.h
Normal file
@@ -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
172
Source/FIREngine.cpp
Normal file
@@ -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
46
Source/FIREngine.h
Normal file
@@ -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
337
Source/LookAndFeel.cpp
Normal file
@@ -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
50
Source/LookAndFeel.h
Normal file
@@ -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();
|
||||
};
|
||||
145
Source/NodeParameterPanel.cpp
Normal file
145
Source/NodeParameterPanel.cpp
Normal file
@@ -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);
|
||||
}
|
||||
49
Source/NodeParameterPanel.h
Normal file
49
Source/NodeParameterPanel.h
Normal file
@@ -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
211
Source/PluginEditor.cpp
Normal file
@@ -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, ¤tBands[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, ¤tBands[index]);
|
||||
}
|
||||
|
||||
void InstaLPEQEditor::selectedBandChanged (int index)
|
||||
{
|
||||
auto currentBands = processor.getBands();
|
||||
if (index >= 0 && index < (int) currentBands.size())
|
||||
nodePanel.setSelectedBand (index, ¤tBands[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
52
Source/PluginEditor.h
Normal file
@@ -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
196
Source/PluginProcessor.cpp
Normal file
@@ -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
64
Source/PluginProcessor.h
Normal file
@@ -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)
|
||||
};
|
||||
Reference in New Issue
Block a user