Commitok összehasonlítása
3 Commit-ok
| Szerző | SHA1 | Dátum | |
|---|---|---|---|
|
|
9c5b5a3957 | ||
|
|
72c7958d98 | ||
|
|
2c440d8deb |
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
project(InstaLPEQ VERSION 1.1.0)
|
||||
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
|
||||
|
||||
@@ -17,6 +17,13 @@ void EQCurveDisplay::setMagnitudeResponse (const std::vector<float>& 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())
|
||||
|
||||
@@ -19,6 +19,7 @@ public:
|
||||
void setListener (Listener* l) { listener = l; }
|
||||
void setBands (const std::vector<EQBand>& bands);
|
||||
void setMagnitudeResponse (const std::vector<float>& 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<EQBand> bands;
|
||||
std::vector<float> magnitudeResponseDb;
|
||||
std::vector<float> 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);
|
||||
|
||||
@@ -33,7 +33,7 @@ void FIREngine::setBands (const std::vector<EQBand>& newBands)
|
||||
|
||||
void FIREngine::setFFTOrder (int order)
|
||||
{
|
||||
fftOrder.store (juce::jlimit (12, 14, order));
|
||||
fftOrder.store (juce::jlimit (9, 14, order));
|
||||
needsUpdate.store (true);
|
||||
notify();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
class FIREngine : private juce::Thread
|
||||
{
|
||||
public:
|
||||
static constexpr int defaultFFTOrder = 13; // 8192 taps
|
||||
static constexpr int defaultFFTOrder = 11; // 2048 taps
|
||||
static constexpr int maxBands = 8;
|
||||
|
||||
FIREngine();
|
||||
|
||||
@@ -5,11 +5,14 @@ NodeParameterPanel::NodeParameterPanel()
|
||||
{
|
||||
setupSlider (freqSlider, freqLabel, 20.0, 20000.0, 1.0, " Hz");
|
||||
freqSlider.setSkewFactorFromMidPoint (1000.0);
|
||||
freqSlider.setDoubleClickReturnValue (true, 1000.0);
|
||||
|
||||
setupSlider (gainSlider, gainLabel, -24.0, 24.0, 0.1, " dB");
|
||||
gainSlider.setDoubleClickReturnValue (true, 0.0);
|
||||
|
||||
setupSlider (qSlider, qLabel, 0.1, 18.0, 0.01, "");
|
||||
qSlider.setSkewFactorFromMidPoint (1.0);
|
||||
qSlider.setDoubleClickReturnValue (true, 1.0);
|
||||
qSlider.getProperties().set (InstaLPEQLookAndFeel::knobTypeProperty, "dark");
|
||||
|
||||
typeSelector.addItem ("Peak", 1);
|
||||
|
||||
@@ -36,21 +36,34 @@ InstaLPEQEditor::InstaLPEQEditor (InstaLPEQProcessor& p)
|
||||
addAndMakeVisible (newBandButton);
|
||||
|
||||
// Quality selector (FIR latency)
|
||||
qualitySelector.addItem ("4096 (~46ms)", 1);
|
||||
qualitySelector.addItem ("8192 (~93ms)", 2);
|
||||
qualitySelector.addItem ("16384 (~186ms)", 3);
|
||||
qualitySelector.setSelectedId (2, juce::dontSendNotification); // default 8192
|
||||
qualitySelector.addItem ("512 (~6ms)", 1);
|
||||
qualitySelector.addItem ("1024 (~12ms)", 2);
|
||||
qualitySelector.addItem ("2048 (~23ms)", 3);
|
||||
qualitySelector.addItem ("4096 (~46ms)", 4);
|
||||
qualitySelector.addItem ("8192 (~93ms)", 5);
|
||||
qualitySelector.addItem ("16384 (~186ms)", 6);
|
||||
qualitySelector.setSelectedId (3, juce::dontSendNotification); // default 2048
|
||||
qualitySelector.onChange = [this]
|
||||
{
|
||||
int sel = qualitySelector.getSelectedId();
|
||||
int order = (sel == 1) ? 12 : (sel == 2) ? 13 : 14;
|
||||
int order = sel + 8; // 1->9, 2->10, 3->11, 4->12, 5->13, 6->14
|
||||
processor.setQuality (order);
|
||||
|
||||
if (sel <= 2) // 512 or 1024
|
||||
qualityWarning.setText ("Low freq accuracy reduced", juce::dontSendNotification);
|
||||
else
|
||||
qualityWarning.setText ("", juce::dontSendNotification);
|
||||
};
|
||||
addAndMakeVisible (qualitySelector);
|
||||
qualityLabel.setFont (customLookAndFeel.getMediumFont (13.0f));
|
||||
qualityLabel.setJustificationType (juce::Justification::centredRight);
|
||||
addAndMakeVisible (qualityLabel);
|
||||
|
||||
qualityWarning.setFont (customLookAndFeel.getRegularFont (11.0f));
|
||||
qualityWarning.setColour (juce::Label::textColourId, juce::Colour (0xffff6644));
|
||||
qualityWarning.setJustificationType (juce::Justification::centredRight);
|
||||
addAndMakeVisible (qualityWarning);
|
||||
|
||||
// EQ curve
|
||||
curveDisplay.setListener (this);
|
||||
addAndMakeVisible (curveDisplay);
|
||||
@@ -65,11 +78,37 @@ InstaLPEQEditor::InstaLPEQEditor (InstaLPEQProcessor& p)
|
||||
masterGainSlider.setRange (-24.0, 24.0, 0.1);
|
||||
masterGainSlider.setValue (0.0);
|
||||
masterGainSlider.setTextValueSuffix (" dB");
|
||||
masterGainSlider.setDoubleClickReturnValue (true, 0.0);
|
||||
addAndMakeVisible (masterGainSlider);
|
||||
masterGainLabel.setFont (customLookAndFeel.getMediumFont (13.0f));
|
||||
masterGainLabel.setJustificationType (juce::Justification::centred);
|
||||
addAndMakeVisible (masterGainLabel);
|
||||
|
||||
// Limiter toggle
|
||||
limiterToggle.setToggleState (processor.limiterEnabled.load(), juce::dontSendNotification);
|
||||
addAndMakeVisible (limiterToggle);
|
||||
limiterLabel.setFont (customLookAndFeel.getMediumFont (13.0f));
|
||||
limiterLabel.setColour (juce::Label::textColourId, InstaLPEQLookAndFeel::textSecondary);
|
||||
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);
|
||||
@@ -132,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);
|
||||
|
||||
@@ -141,11 +184,23 @@ void InstaLPEQEditor::resized()
|
||||
masterGainLabel.setBounds (labelArea);
|
||||
masterGainSlider.setBounds (masterArea.removeFromLeft (masterH));
|
||||
|
||||
// Limiter toggle next to master gain
|
||||
limiterLabel.setFont (customLookAndFeel.getMediumFont (std::max (11.0f, 14.0f * scale)));
|
||||
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);
|
||||
qualityLabel.setBounds (qLabelArea);
|
||||
qualitySelector.setBounds (masterArea.removeFromRight ((int) (130 * scale)).reduced (2, (masterH - 24) / 2));
|
||||
qualityWarning.setFont (customLookAndFeel.getRegularFont (std::max (9.0f, 11.0f * scale)));
|
||||
qualityWarning.setBounds (masterArea.removeFromRight ((int) (170 * scale)));
|
||||
|
||||
// Node parameter panel (15% of remaining height)
|
||||
int nodePanelH = (int) (bounds.getHeight() * 0.18f);
|
||||
@@ -159,9 +214,19 @@ void InstaLPEQEditor::resized()
|
||||
|
||||
void InstaLPEQEditor::timerCallback()
|
||||
{
|
||||
// Sync bypass
|
||||
// Sync bypass & limiter
|
||||
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<float, 1024> 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();
|
||||
@@ -237,6 +302,11 @@ void InstaLPEQEditor::nodeDeleteRequested (int bandIndex)
|
||||
syncDisplayFromProcessor();
|
||||
}
|
||||
|
||||
void InstaLPEQEditor::chainOrderChanged (const std::array<InstaLPEQProcessor::ChainStage, InstaLPEQProcessor::numChainStages>& order)
|
||||
{
|
||||
processor.setChainOrder (order);
|
||||
}
|
||||
|
||||
void InstaLPEQEditor::syncDisplayFromProcessor()
|
||||
{
|
||||
auto currentBands = processor.getBands();
|
||||
|
||||
@@ -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<InstaLPEQProcessor::ChainStage, InstaLPEQProcessor::numChainStages>& order) override;
|
||||
|
||||
void syncDisplayFromProcessor();
|
||||
|
||||
InstaLPEQProcessor& processor;
|
||||
@@ -39,16 +44,23 @@ private:
|
||||
NodeParameterPanel nodePanel;
|
||||
|
||||
juce::Label titleLabel { {}, "INSTALPEQ" };
|
||||
juce::Label versionLabel { {}, "v1.1" };
|
||||
juce::Label versionLabel { {}, "v1.2.2" };
|
||||
juce::ToggleButton bypassToggle;
|
||||
juce::Label bypassLabel { {}, "BYPASS" };
|
||||
|
||||
juce::TextButton newBandButton { "NEW BAND" };
|
||||
juce::ComboBox qualitySelector;
|
||||
juce::Label qualityLabel { {}, "FIR" };
|
||||
juce::Label qualityWarning { {}, "" };
|
||||
|
||||
juce::Slider masterGainSlider;
|
||||
juce::Label masterGainLabel { {}, "MASTER" };
|
||||
juce::ToggleButton limiterToggle;
|
||||
juce::Label limiterLabel { {}, "LIMITER" };
|
||||
juce::Slider makeupGainSlider;
|
||||
juce::Label makeupGainLabel { {}, "MAKEUP" };
|
||||
|
||||
SignalChainPanel chainPanel;
|
||||
|
||||
juce::ComponentBoundsConstrainer constrainer;
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ void InstaLPEQProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
|
||||
|
||||
juce::dsp::ProcessSpec spec { sampleRate, (juce::uint32) samplesPerBlock, 2 };
|
||||
convolution.prepare (spec);
|
||||
limiter.prepare (spec);
|
||||
limiter.setThreshold (0.0f);
|
||||
limiter.setRelease (50.0f);
|
||||
|
||||
firEngine.start (sampleRate);
|
||||
updateFIR();
|
||||
@@ -63,10 +66,81 @@ void InstaLPEQProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::M
|
||||
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);
|
||||
// Apply chain in configured order
|
||||
std::array<ChainStage, numChainStages> order;
|
||||
{
|
||||
const juce::SpinLock::ScopedTryLockType lock (chainLock);
|
||||
if (lock.isLocked())
|
||||
order = chainOrder;
|
||||
else
|
||||
order = { MasterGain, Limiter, MakeupGain };
|
||||
}
|
||||
|
||||
for (auto stage : order)
|
||||
{
|
||||
switch (stage)
|
||||
{
|
||||
case MasterGain:
|
||||
{
|
||||
float gain = juce::Decibels::decibelsToGain (masterGainDb.load());
|
||||
if (std::abs (gain - 1.0f) > 0.001f)
|
||||
buffer.applyGain (gain);
|
||||
break;
|
||||
}
|
||||
case Limiter:
|
||||
{
|
||||
if (limiterEnabled.load())
|
||||
{
|
||||
juce::dsp::AudioBlock<float> limBlock (buffer);
|
||||
juce::dsp::ProcessContextReplacing<float> limContext (limBlock);
|
||||
limiter.process (limContext);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MakeupGain:
|
||||
{
|
||||
float mkGain = juce::Decibels::decibelsToGain (makeupGainDb.load());
|
||||
if (std::abs (mkGain - 1.0f) > 0.001f)
|
||||
buffer.applyGain (mkGain);
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
// Feed spectrum analyzer (mono mix of output)
|
||||
const int numSamples = buffer.getNumSamples();
|
||||
const int numChannels = buffer.getNumChannels();
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
float sample = 0.0f;
|
||||
for (int ch = 0; ch < numChannels; ++ch)
|
||||
sample += buffer.getSample (ch, i);
|
||||
sample /= (float) numChannels;
|
||||
|
||||
fifoBuffer[fifoIndex++] = sample;
|
||||
|
||||
if (fifoIndex >= spectrumFFTSize)
|
||||
{
|
||||
fifoIndex = 0;
|
||||
std::copy (fifoBuffer.begin(), fifoBuffer.end(), fftData.begin());
|
||||
std::fill (fftData.begin() + spectrumFFTSize, fftData.end(), 0.0f);
|
||||
spectrumWindow.multiplyWithWindowingTable (fftData.data(), spectrumFFTSize);
|
||||
spectrumFFT.performFrequencyOnlyForwardTransform (fftData.data());
|
||||
|
||||
{
|
||||
const juce::SpinLock::ScopedLockType lock (spectrumLock);
|
||||
for (int b = 0; b < spectrumFFTSize / 2; ++b)
|
||||
{
|
||||
float mag = fftData[b] / (float) spectrumFFTSize;
|
||||
float dbVal = juce::Decibels::gainToDecibels (mag, -100.0f);
|
||||
// Smooth: 70% old + 30% new
|
||||
spectrumMagnitude[b] = spectrumMagnitude[b] * 0.7f + dbVal * 0.3f;
|
||||
}
|
||||
}
|
||||
spectrumReady.store (true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -119,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::ChainStage, InstaLPEQProcessor::numChainStages> InstaLPEQProcessor::getChainOrder() const
|
||||
{
|
||||
const juce::SpinLock::ScopedLockType lock (chainLock);
|
||||
return chainOrder;
|
||||
}
|
||||
|
||||
void InstaLPEQProcessor::setChainOrder (const std::array<ChainStage, numChainStages>& order)
|
||||
{
|
||||
const juce::SpinLock::ScopedLockType lock (chainLock);
|
||||
chainOrder = order;
|
||||
}
|
||||
|
||||
void InstaLPEQProcessor::updateFIR()
|
||||
{
|
||||
auto currentBands = getBands();
|
||||
@@ -141,6 +241,17 @@ void InstaLPEQProcessor::getStateInformation (juce::MemoryBlock& destData)
|
||||
juce::XmlElement xml ("InstaLPEQ");
|
||||
xml.setAttribute ("bypass", bypassed.load());
|
||||
xml.setAttribute ("masterGain", (double) masterGainDb.load());
|
||||
xml.setAttribute ("limiter", limiterEnabled.load());
|
||||
xml.setAttribute ("makeupGain", (double) makeupGainDb.load());
|
||||
|
||||
auto order = getChainOrder();
|
||||
juce::String chainStr;
|
||||
for (int i = 0; i < numChainStages; ++i)
|
||||
{
|
||||
if (i > 0) chainStr += ",";
|
||||
chainStr += juce::String ((int) order[i]);
|
||||
}
|
||||
xml.setAttribute ("chainOrder", chainStr);
|
||||
|
||||
auto currentBands = getBands();
|
||||
for (int i = 0; i < (int) currentBands.size(); ++i)
|
||||
@@ -164,6 +275,18 @@ 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<ChainStage, numChainStages> order;
|
||||
for (int i = 0; i < numChainStages; ++i)
|
||||
order[i] = static_cast<ChainStage> (tokens[i].getIntValue());
|
||||
setChainOrder (order);
|
||||
}
|
||||
|
||||
std::vector<EQBand> loadedBands;
|
||||
for (auto* bandXml : xml->getChildIterator())
|
||||
|
||||
@@ -40,9 +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<bool> bypassed { false };
|
||||
std::atomic<float> masterGainDb { 0.0f };
|
||||
std::atomic<bool> limiterEnabled { true };
|
||||
std::atomic<float> makeupGainDb { 0.0f }; // -24 to +24 dB
|
||||
|
||||
// Chain order (read/write from GUI, read from audio thread)
|
||||
std::array<ChainStage, numChainStages> getChainOrder() const;
|
||||
void setChainOrder (const std::array<ChainStage, numChainStages>& order);
|
||||
|
||||
void setQuality (int fftOrder);
|
||||
|
||||
@@ -55,11 +65,30 @@ private:
|
||||
|
||||
FIREngine firEngine;
|
||||
juce::dsp::Convolution convolution;
|
||||
juce::dsp::Limiter<float> 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<float> spectrumWindow { spectrumFFTSize, juce::dsp::WindowingFunction<float>::hann };
|
||||
std::array<float, spectrumFFTSize> fifoBuffer {};
|
||||
int fifoIndex = 0;
|
||||
std::array<float, spectrumFFTSize * 2> fftData {};
|
||||
std::array<float, spectrumFFTSize / 2> spectrumMagnitude {};
|
||||
juce::SpinLock spectrumLock;
|
||||
std::atomic<bool> spectrumReady { false };
|
||||
|
||||
public:
|
||||
bool getSpectrum (float* dest, int maxBins) const;
|
||||
|
||||
double currentSampleRate = 44100.0;
|
||||
int currentBlockSize = 512;
|
||||
bool firLoaded = false;
|
||||
|
||||
std::array<ChainStage, numChainStages> chainOrder { MasterGain, Limiter, MakeupGain };
|
||||
juce::SpinLock chainLock;
|
||||
|
||||
void updateFIR();
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InstaLPEQProcessor)
|
||||
|
||||
165
Source/SignalChainPanel.cpp
Normal file
165
Source/SignalChainPanel.cpp
Normal file
@@ -0,0 +1,165 @@
|
||||
#include "SignalChainPanel.h"
|
||||
#include "LookAndFeel.h"
|
||||
|
||||
SignalChainPanel::SignalChainPanel()
|
||||
{
|
||||
setMouseCursor (juce::MouseCursor::DraggingHandCursor);
|
||||
}
|
||||
|
||||
void SignalChainPanel::setOrder (const std::array<InstaLPEQProcessor::ChainStage, numBlocks>& 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<float> 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<InstaLPEQLookAndFeel*> (&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();
|
||||
}
|
||||
46
Source/SignalChainPanel.h
Normal file
46
Source/SignalChainPanel.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
#include "PluginProcessor.h"
|
||||
|
||||
class SignalChainPanel : public juce::Component
|
||||
{
|
||||
public:
|
||||
struct Listener
|
||||
{
|
||||
virtual ~Listener() = default;
|
||||
virtual void chainOrderChanged (const std::array<InstaLPEQProcessor::ChainStage, InstaLPEQProcessor::numChainStages>& order) = 0;
|
||||
};
|
||||
|
||||
SignalChainPanel();
|
||||
|
||||
void setListener (Listener* l) { listener = l; }
|
||||
void setOrder (const std::array<InstaLPEQProcessor::ChainStage, InstaLPEQProcessor::numChainStages>& order);
|
||||
std::array<InstaLPEQProcessor::ChainStage, InstaLPEQProcessor::numChainStages> 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<InstaLPEQProcessor::ChainStage, numBlocks> currentOrder {
|
||||
InstaLPEQProcessor::MasterGain,
|
||||
InstaLPEQProcessor::Limiter,
|
||||
InstaLPEQProcessor::MakeupGain
|
||||
};
|
||||
|
||||
int draggedIndex = -1;
|
||||
float dragOffsetX = 0.0f;
|
||||
float dragCurrentX = 0.0f;
|
||||
|
||||
Listener* listener = nullptr;
|
||||
|
||||
juce::Rectangle<float> 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)
|
||||
};
|
||||
Reference in New Issue
Block a user