From 9c5b5a3957a05266c87bec566e4077d06a898e08 Mon Sep 17 00:00:00 2001 From: hariel1985 Date: Wed, 25 Mar 2026 11:44:27 +0100 Subject: [PATCH] v1.2.2: Live spectrum analyzer, makeup gain, drag-and-drop signal chain - Real-time FFT spectrum analyzer drawn behind EQ curves - Makeup gain knob (+/- 24 dB) after limiter - Draggable signal chain panel: reorder Master Gain / Limiter / Makeup Gain - Chain order saved/restored with DAW session - Scaled fonts in signal chain panel Co-Authored-By: Claude Opus 4.6 (1M context) --- CMakeLists.txt | 3 +- Source/EQCurveDisplay.cpp | 57 +++++++++++++ Source/EQCurveDisplay.h | 5 ++ Source/PluginEditor.cpp | 42 ++++++++- Source/PluginEditor.h | 13 ++- Source/PluginProcessor.cpp | 130 +++++++++++++++++++++++++--- Source/PluginProcessor.h | 27 ++++++ Source/SignalChainPanel.cpp | 165 ++++++++++++++++++++++++++++++++++++ Source/SignalChainPanel.h | 46 ++++++++++ 9 files changed, 474 insertions(+), 14 deletions(-) create mode 100644 Source/SignalChainPanel.cpp create mode 100644 Source/SignalChainPanel.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 948670e..0f97f26 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.22) -project(InstaLPEQ VERSION 1.1.3) +project(InstaLPEQ VERSION 1.2.2) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -35,6 +35,7 @@ target_sources(InstaLPEQ Source/EQCurveDisplay.cpp Source/FIREngine.cpp Source/NodeParameterPanel.cpp + Source/SignalChainPanel.cpp ) target_compile_definitions(InstaLPEQ diff --git a/Source/EQCurveDisplay.cpp b/Source/EQCurveDisplay.cpp index 267db6c..5c293f6 100644 --- a/Source/EQCurveDisplay.cpp +++ b/Source/EQCurveDisplay.cpp @@ -17,6 +17,13 @@ void EQCurveDisplay::setMagnitudeResponse (const std::vector& magnitudesD repaint(); } +void EQCurveDisplay::setSpectrum (const float* data, int numBins, double sampleRate, int fftSize) +{ + spectrumDb.assign (data, data + numBins); + spectrumSampleRate = sampleRate; + spectrumFftSize = fftSize; +} + void EQCurveDisplay::setSelectedBand (int index) { if (selectedBand != index) @@ -93,6 +100,7 @@ void EQCurveDisplay::paint (juce::Graphics& g) g.drawRoundedRectangle (bounds, 4.0f, 1.0f); drawGrid (g); + drawSpectrum (g); drawPerBandCurves (g); drawResponseCurve (g); drawNodes (g); @@ -140,6 +148,55 @@ void EQCurveDisplay::drawGrid (juce::Graphics& g) } } +void EQCurveDisplay::drawSpectrum (juce::Graphics& g) +{ + if (spectrumDb.empty()) + return; + + auto area = getPlotArea(); + int numBins = (int) spectrumDb.size(); + + juce::Path specPath; + specPath.startNewSubPath (area.getX(), area.getBottom()); + bool hasPoints = false; + + for (float px = area.getX(); px <= area.getRight(); px += 1.5f) + { + float freq = xToFreq (px); + if (freq < 1.0f || freq > spectrumSampleRate * 0.5) + continue; + + float binFloat = freq * (float) spectrumFftSize / (float) spectrumSampleRate; + int bin = (int) binFloat; + float frac = binFloat - (float) bin; + + if (bin < 0 || bin >= numBins - 1) + continue; + + float dbVal = spectrumDb[bin] * (1.0f - frac) + spectrumDb[bin + 1] * frac; + // Map dB range: -100 dB = bottom, 0 dB = top area + // Shift up so typical audio is visible + float mapped = juce::jmap (dbVal, -80.0f, 0.0f, minDb, maxDb); + mapped = juce::jlimit (minDb - 6.0f, maxDb, mapped); + float yPos = dbToY (mapped); + + specPath.lineTo (px, yPos); + hasPoints = true; + } + + if (! hasPoints) + return; + + specPath.lineTo (area.getRight(), area.getBottom()); + specPath.closeSubPath(); + + // Fill with subtle gradient + juce::ColourGradient specGrad (juce::Colour (0xff4488ff).withAlpha (0.12f), 0, area.getY(), + juce::Colour (0xff4488ff).withAlpha (0.03f), 0, area.getBottom(), false); + g.setGradientFill (specGrad); + g.fillPath (specPath); +} + void EQCurveDisplay::drawResponseCurve (juce::Graphics& g) { if (magnitudeResponseDb.empty()) diff --git a/Source/EQCurveDisplay.h b/Source/EQCurveDisplay.h index e1f49f1..337fbef 100644 --- a/Source/EQCurveDisplay.h +++ b/Source/EQCurveDisplay.h @@ -19,6 +19,7 @@ public: void setListener (Listener* l) { listener = l; } void setBands (const std::vector& bands); void setMagnitudeResponse (const std::vector& magnitudesDb, double sampleRate, int fftSize); + void setSpectrum (const float* data, int numBins, double sampleRate, int fftSize); int getSelectedBandIndex() const { return selectedBand; } void setSelectedBand (int index); @@ -32,6 +33,9 @@ public: private: std::vector bands; std::vector magnitudeResponseDb; + std::vector spectrumDb; + double spectrumSampleRate = 44100.0; + int spectrumFftSize = 2048; double responseSampleRate = 44100.0; int responseFftSize = 8192; int selectedBand = -1; @@ -62,6 +66,7 @@ private: float yToDb (float y) const; void drawGrid (juce::Graphics& g); + void drawSpectrum (juce::Graphics& g); void drawResponseCurve (juce::Graphics& g); void drawPerBandCurves (juce::Graphics& g); void drawNodes (juce::Graphics& g); diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 2b7d0df..ab6fb47 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -92,6 +92,23 @@ InstaLPEQEditor::InstaLPEQEditor (InstaLPEQProcessor& p) limiterLabel.setJustificationType (juce::Justification::centred); addAndMakeVisible (limiterLabel); + // Makeup gain + makeupGainSlider.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag); + makeupGainSlider.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 60, 16); + makeupGainSlider.setRange (-24.0, 24.0, 0.1); + makeupGainSlider.setValue (0.0); + makeupGainSlider.setTextValueSuffix (" dB"); + makeupGainSlider.setDoubleClickReturnValue (true, 0.0); + addAndMakeVisible (makeupGainSlider); + makeupGainLabel.setFont (customLookAndFeel.getMediumFont (13.0f)); + makeupGainLabel.setJustificationType (juce::Justification::centred); + addAndMakeVisible (makeupGainLabel); + + // Signal chain panel + chainPanel.setListener (this); + chainPanel.setOrder (processor.getChainOrder()); + addAndMakeVisible (chainPanel); + // Sizing constrainer.setMinimumSize (700, 450); constrainer.setMaximumSize (1920, 1080); @@ -154,7 +171,11 @@ void InstaLPEQEditor::resized() newBandButton.setBounds (header.removeFromRight ((int) (90 * scale)).reduced (2)); - // Bottom master row + // Signal chain panel (bottom-most) + int chainH = (int) std::max (28.0f, 36.0f * scale); + chainPanel.setBounds (bounds.removeFromBottom (chainH).reduced (pad, 2)); + + // Master controls row (above chain) int masterH = (int) std::max (50.0f, 65.0f * scale); auto masterArea = bounds.removeFromBottom (masterH).reduced (pad, 2); @@ -168,6 +189,11 @@ void InstaLPEQEditor::resized() limiterLabel.setBounds (masterArea.removeFromLeft (55)); limiterToggle.setBounds (masterArea.removeFromLeft (40)); + // Makeup gain knob + makeupGainLabel.setFont (customLookAndFeel.getMediumFont (std::max (11.0f, 14.0f * scale))); + makeupGainLabel.setBounds (masterArea.removeFromLeft (55)); + makeupGainSlider.setBounds (masterArea.removeFromLeft (masterH)); + // Quality selector on the right side of master row qualityLabel.setFont (customLookAndFeel.getMediumFont (std::max (11.0f, 14.0f * scale))); auto qLabelArea = masterArea.removeFromRight (30); @@ -192,6 +218,15 @@ void InstaLPEQEditor::timerCallback() processor.bypassed.store (bypassToggle.getToggleState()); processor.masterGainDb.store ((float) masterGainSlider.getValue()); processor.limiterEnabled.store (limiterToggle.getToggleState()); + processor.makeupGainDb.store ((float) makeupGainSlider.getValue()); + + // Update spectrum analyzer + { + std::array specData {}; + if (processor.getSpectrum (specData.data(), (int) specData.size())) + curveDisplay.setSpectrum (specData.data(), (int) specData.size(), + processor.getCurrentSampleRate(), 2048); + } // Update display with latest magnitude response auto magDb = processor.getFIREngine().getMagnitudeResponseDb(); @@ -267,6 +302,11 @@ void InstaLPEQEditor::nodeDeleteRequested (int bandIndex) syncDisplayFromProcessor(); } +void InstaLPEQEditor::chainOrderChanged (const std::array& order) +{ + processor.setChainOrder (order); +} + void InstaLPEQEditor::syncDisplayFromProcessor() { auto currentBands = processor.getBands(); diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 133b181..43ec222 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -4,11 +4,13 @@ #include "LookAndFeel.h" #include "EQCurveDisplay.h" #include "NodeParameterPanel.h" +#include "SignalChainPanel.h" class InstaLPEQEditor : public juce::AudioProcessorEditor, private juce::Timer, private EQCurveDisplay::Listener, - private NodeParameterPanel::Listener + private NodeParameterPanel::Listener, + private SignalChainPanel::Listener { public: explicit InstaLPEQEditor (InstaLPEQProcessor& p); @@ -30,6 +32,9 @@ private: void nodeParameterChanged (int bandIndex, const EQBand& band) override; void nodeDeleteRequested (int bandIndex) override; + // SignalChainPanel::Listener + void chainOrderChanged (const std::array& order) override; + void syncDisplayFromProcessor(); InstaLPEQProcessor& processor; @@ -39,7 +44,7 @@ private: NodeParameterPanel nodePanel; juce::Label titleLabel { {}, "INSTALPEQ" }; - juce::Label versionLabel { {}, "v1.1.3" }; + juce::Label versionLabel { {}, "v1.2.2" }; juce::ToggleButton bypassToggle; juce::Label bypassLabel { {}, "BYPASS" }; @@ -52,6 +57,10 @@ private: juce::Label masterGainLabel { {}, "MASTER" }; juce::ToggleButton limiterToggle; juce::Label limiterLabel { {}, "LIMITER" }; + juce::Slider makeupGainSlider; + juce::Label makeupGainLabel { {}, "MAKEUP" }; + + SignalChainPanel chainPanel; juce::ComponentBoundsConstrainer constrainer; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 2f8a198..eda53a4 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -66,17 +66,80 @@ void InstaLPEQProcessor::processBlock (juce::AudioBuffer& buffer, juce::M juce::dsp::ProcessContextReplacing 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); - - // Brickwall limiter (0 dB ceiling) - if (limiterEnabled.load()) + // Apply chain in configured order + std::array order; { - juce::dsp::AudioBlock limBlock (buffer); - juce::dsp::ProcessContextReplacing limContext (limBlock); - limiter.process (limContext); + const juce::SpinLock::ScopedTryLockType lock (chainLock); + if (lock.isLocked()) + order = chainOrder; + else + order = { MasterGain, Limiter, MakeupGain }; + } + + for (auto stage : order) + { + switch (stage) + { + case MasterGain: + { + float gain = juce::Decibels::decibelsToGain (masterGainDb.load()); + if (std::abs (gain - 1.0f) > 0.001f) + buffer.applyGain (gain); + break; + } + case Limiter: + { + if (limiterEnabled.load()) + { + juce::dsp::AudioBlock limBlock (buffer); + juce::dsp::ProcessContextReplacing limContext (limBlock); + limiter.process (limContext); + } + break; + } + case MakeupGain: + { + float mkGain = juce::Decibels::decibelsToGain (makeupGainDb.load()); + if (std::abs (mkGain - 1.0f) > 0.001f) + buffer.applyGain (mkGain); + break; + } + default: break; + } + } + + // Feed spectrum analyzer (mono mix of output) + const int numSamples = buffer.getNumSamples(); + const int numChannels = buffer.getNumChannels(); + for (int i = 0; i < numSamples; ++i) + { + float sample = 0.0f; + for (int ch = 0; ch < numChannels; ++ch) + sample += buffer.getSample (ch, i); + sample /= (float) numChannels; + + fifoBuffer[fifoIndex++] = sample; + + if (fifoIndex >= spectrumFFTSize) + { + fifoIndex = 0; + std::copy (fifoBuffer.begin(), fifoBuffer.end(), fftData.begin()); + std::fill (fftData.begin() + spectrumFFTSize, fftData.end(), 0.0f); + spectrumWindow.multiplyWithWindowingTable (fftData.data(), spectrumFFTSize); + spectrumFFT.performFrequencyOnlyForwardTransform (fftData.data()); + + { + const juce::SpinLock::ScopedLockType lock (spectrumLock); + for (int b = 0; b < spectrumFFTSize / 2; ++b) + { + float mag = fftData[b] / (float) spectrumFFTSize; + float dbVal = juce::Decibels::gainToDecibels (mag, -100.0f); + // Smooth: 70% old + 30% new + spectrumMagnitude[b] = spectrumMagnitude[b] * 0.7f + dbVal * 0.3f; + } + } + spectrumReady.store (true); + } } } @@ -130,6 +193,32 @@ int InstaLPEQProcessor::getNumBands() const return (int) bands.size(); } +bool InstaLPEQProcessor::getSpectrum (float* dest, int maxBins) const +{ + if (! spectrumReady.load()) + return false; + + const juce::SpinLock::ScopedTryLockType lock (spectrumLock); + if (! lock.isLocked()) + return false; + + int bins = std::min (maxBins, spectrumFFTSize / 2); + std::copy (spectrumMagnitude.begin(), spectrumMagnitude.begin() + bins, dest); + return true; +} + +std::array InstaLPEQProcessor::getChainOrder() const +{ + const juce::SpinLock::ScopedLockType lock (chainLock); + return chainOrder; +} + +void InstaLPEQProcessor::setChainOrder (const std::array& order) +{ + const juce::SpinLock::ScopedLockType lock (chainLock); + chainOrder = order; +} + void InstaLPEQProcessor::updateFIR() { auto currentBands = getBands(); @@ -153,6 +242,16 @@ void InstaLPEQProcessor::getStateInformation (juce::MemoryBlock& destData) xml.setAttribute ("bypass", bypassed.load()); xml.setAttribute ("masterGain", (double) masterGainDb.load()); xml.setAttribute ("limiter", limiterEnabled.load()); + xml.setAttribute ("makeupGain", (double) makeupGainDb.load()); + + auto order = getChainOrder(); + juce::String chainStr; + for (int i = 0; i < numChainStages; ++i) + { + if (i > 0) chainStr += ","; + chainStr += juce::String ((int) order[i]); + } + xml.setAttribute ("chainOrder", chainStr); auto currentBands = getBands(); for (int i = 0; i < (int) currentBands.size(); ++i) @@ -177,6 +276,17 @@ void InstaLPEQProcessor::setStateInformation (const void* data, int sizeInBytes) bypassed.store (xml->getBoolAttribute ("bypass", false)); masterGainDb.store ((float) xml->getDoubleAttribute ("masterGain", 0.0)); limiterEnabled.store (xml->getBoolAttribute ("limiter", true)); + makeupGainDb.store ((float) xml->getDoubleAttribute ("makeupGain", 0.0)); + + auto chainStr = xml->getStringAttribute ("chainOrder", "0,1,2"); + auto tokens = juce::StringArray::fromTokens (chainStr, ",", ""); + if (tokens.size() == numChainStages) + { + std::array order; + for (int i = 0; i < numChainStages; ++i) + order[i] = static_cast (tokens[i].getIntValue()); + setChainOrder (order); + } std::vector loadedBands; for (auto* bandXml : xml->getChildIterator()) diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index e8cdc97..fc3a7ff 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -40,10 +40,19 @@ public: void removeBand (int index); int getNumBands() const; + // Signal chain stages + enum ChainStage { MasterGain = 0, Limiter, MakeupGain, NumStages }; + static constexpr int numChainStages = (int) NumStages; + // Settings std::atomic bypassed { false }; std::atomic masterGainDb { 0.0f }; std::atomic limiterEnabled { true }; + std::atomic makeupGainDb { 0.0f }; // -24 to +24 dB + + // Chain order (read/write from GUI, read from audio thread) + std::array getChainOrder() const; + void setChainOrder (const std::array& order); void setQuality (int fftOrder); @@ -58,10 +67,28 @@ private: juce::dsp::Convolution convolution; juce::dsp::Limiter limiter; + // Spectrum analyzer + static constexpr int spectrumFFTOrder = 11; // 2048-point FFT + static constexpr int spectrumFFTSize = 1 << spectrumFFTOrder; + juce::dsp::FFT spectrumFFT { spectrumFFTOrder }; + juce::dsp::WindowingFunction spectrumWindow { spectrumFFTSize, juce::dsp::WindowingFunction::hann }; + std::array fifoBuffer {}; + int fifoIndex = 0; + std::array fftData {}; + std::array spectrumMagnitude {}; + juce::SpinLock spectrumLock; + std::atomic spectrumReady { false }; + +public: + bool getSpectrum (float* dest, int maxBins) const; + double currentSampleRate = 44100.0; int currentBlockSize = 512; bool firLoaded = false; + std::array chainOrder { MasterGain, Limiter, MakeupGain }; + juce::SpinLock chainLock; + void updateFIR(); JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InstaLPEQProcessor) diff --git a/Source/SignalChainPanel.cpp b/Source/SignalChainPanel.cpp new file mode 100644 index 0000000..f668dcc --- /dev/null +++ b/Source/SignalChainPanel.cpp @@ -0,0 +1,165 @@ +#include "SignalChainPanel.h" +#include "LookAndFeel.h" + +SignalChainPanel::SignalChainPanel() +{ + setMouseCursor (juce::MouseCursor::DraggingHandCursor); +} + +void SignalChainPanel::setOrder (const std::array& order) +{ + if (currentOrder != order) + { + currentOrder = order; + repaint(); + } +} + +juce::String SignalChainPanel::getStageName (InstaLPEQProcessor::ChainStage stage) const +{ + switch (stage) + { + case InstaLPEQProcessor::MasterGain: return "MASTER GAIN"; + case InstaLPEQProcessor::Limiter: return "LIMITER"; + case InstaLPEQProcessor::MakeupGain: return "MAKEUP GAIN"; + default: return "?"; + } +} + +juce::Colour SignalChainPanel::getStageColour (InstaLPEQProcessor::ChainStage stage) const +{ + switch (stage) + { + case InstaLPEQProcessor::MasterGain: return juce::Colour (0xffff8833); + case InstaLPEQProcessor::Limiter: return juce::Colour (0xffff4455); + case InstaLPEQProcessor::MakeupGain: return juce::Colour (0xff44bbff); + default: return InstaLPEQLookAndFeel::textSecondary; + } +} + +juce::Rectangle SignalChainPanel::getBlockRect (int index) const +{ + auto bounds = getLocalBounds().toFloat().reduced (2); + float gap = 6.0f; + float blockW = (bounds.getWidth() - gap * (numBlocks - 1)) / numBlocks; + float x = bounds.getX() + index * (blockW + gap); + return { x, bounds.getY(), blockW, bounds.getHeight() }; +} + +int SignalChainPanel::getBlockAtX (float x) const +{ + for (int i = 0; i < numBlocks; ++i) + { + if (getBlockRect (i).contains (x, getHeight() * 0.5f)) + return i; + } + return -1; +} + +void SignalChainPanel::paint (juce::Graphics& g) +{ + auto bounds = getLocalBounds().toFloat(); + + // Background + g.setColour (InstaLPEQLookAndFeel::bgDark.darker (0.3f)); + g.fillRoundedRectangle (bounds, 4.0f); + g.setColour (InstaLPEQLookAndFeel::bgLight.withAlpha (0.2f)); + g.drawRoundedRectangle (bounds, 4.0f, 1.0f); + + auto* lf = dynamic_cast (&getLookAndFeel()); + + // Draw arrows between blocks (scale with height) + float arrowScale = bounds.getHeight() / 30.0f; + for (int i = 0; i < numBlocks - 1; ++i) + { + auto r1 = getBlockRect (i); + auto r2 = getBlockRect (i + 1); + float arrowX = (r1.getRight() + r2.getX()) * 0.5f; + float arrowY = bounds.getCentreY(); + float aw = 5.0f * arrowScale; + float ah = 6.0f * arrowScale; + g.setColour (InstaLPEQLookAndFeel::textSecondary.withAlpha (0.5f)); + + juce::Path arrow; + arrow.addTriangle (arrowX - aw, arrowY - ah, arrowX - aw, arrowY + ah, arrowX + aw, arrowY); + g.fillPath (arrow); + } + + // Draw blocks + for (int i = 0; i < numBlocks; ++i) + { + bool isDragged = (i == draggedIndex); + auto rect = getBlockRect (i); + + // If this block is being dragged, offset it + if (isDragged) + { + float offset = dragCurrentX - dragOffsetX; + rect = rect.withX (rect.getX() + offset); + } + + auto colour = getStageColour (currentOrder[i]); + + // Block background + g.setColour (isDragged ? colour.withAlpha (0.25f) : colour.withAlpha (0.12f)); + g.fillRoundedRectangle (rect, 4.0f); + + // Block border + g.setColour (isDragged ? colour.withAlpha (0.8f) : colour.withAlpha (0.4f)); + g.drawRoundedRectangle (rect, 4.0f, isDragged ? 2.0f : 1.0f); + + // Label — scale with block height + juce::Font font = lf ? lf->getBoldFont (std::max (12.0f, rect.getHeight() * 0.45f)) + : juce::Font (juce::FontOptions (14.0f)); + g.setFont (font); + g.setColour (isDragged ? colour : colour.withAlpha (0.8f)); + g.drawText (getStageName (currentOrder[i]), rect.reduced (4), juce::Justification::centred, false); + } + + // "SIGNAL CHAIN" label on the left + if (lf) + { + g.setFont (lf->getRegularFont (10.0f)); + g.setColour (InstaLPEQLookAndFeel::textSecondary.withAlpha (0.5f)); + } +} + +void SignalChainPanel::resized() {} + +void SignalChainPanel::mouseDown (const juce::MouseEvent& e) +{ + draggedIndex = getBlockAtX (e.position.x); + if (draggedIndex >= 0) + { + dragOffsetX = e.position.x; + dragCurrentX = e.position.x; + } +} + +void SignalChainPanel::mouseDrag (const juce::MouseEvent& e) +{ + if (draggedIndex < 0) + return; + + dragCurrentX = e.position.x; + + // Check if we should swap with a neighbor + int targetIndex = getBlockAtX (e.position.x); + if (targetIndex >= 0 && targetIndex != draggedIndex) + { + std::swap (currentOrder[draggedIndex], currentOrder[targetIndex]); + draggedIndex = targetIndex; + dragOffsetX = e.position.x; + + if (listener) + listener->chainOrderChanged (currentOrder); + } + + repaint(); +} + +void SignalChainPanel::mouseUp (const juce::MouseEvent&) +{ + draggedIndex = -1; + repaint(); +} diff --git a/Source/SignalChainPanel.h b/Source/SignalChainPanel.h new file mode 100644 index 0000000..0f49d7a --- /dev/null +++ b/Source/SignalChainPanel.h @@ -0,0 +1,46 @@ +#pragma once +#include +#include "PluginProcessor.h" + +class SignalChainPanel : public juce::Component +{ +public: + struct Listener + { + virtual ~Listener() = default; + virtual void chainOrderChanged (const std::array& order) = 0; + }; + + SignalChainPanel(); + + void setListener (Listener* l) { listener = l; } + void setOrder (const std::array& order); + std::array getOrder() const { return currentOrder; } + + void paint (juce::Graphics& g) override; + void resized() override; + void mouseDown (const juce::MouseEvent& e) override; + void mouseDrag (const juce::MouseEvent& e) override; + void mouseUp (const juce::MouseEvent& e) override; + +private: + static constexpr int numBlocks = InstaLPEQProcessor::numChainStages; + std::array currentOrder { + InstaLPEQProcessor::MasterGain, + InstaLPEQProcessor::Limiter, + InstaLPEQProcessor::MakeupGain + }; + + int draggedIndex = -1; + float dragOffsetX = 0.0f; + float dragCurrentX = 0.0f; + + Listener* listener = nullptr; + + juce::Rectangle getBlockRect (int index) const; + int getBlockAtX (float x) const; + juce::String getStageName (InstaLPEQProcessor::ChainStage stage) const; + juce::Colour getStageColour (InstaLPEQProcessor::ChainStage stage) const; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SignalChainPanel) +};