Add per-band frequency control and static EQ curve overlay

This commit is contained in:
Gelyk 2026-08-29 15:42:21 +02:00
commit 8e9eb733cb
5 changed files with 172 additions and 11 deletions

View file

@ -231,10 +231,42 @@ GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor&
bands[i].qValue->setText(juce::String(bands[i].q->getValue(), 2), juce::dontSendNotification);
};
w.freqCaption = std::make_unique<juce::Label>("fcap", "FREQ");
w.freqCaption->setFont(monoFont(10.0f));
w.freqCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim);
w.freqCaption->setJustificationType(juce::Justification::centred);
w.freqValue = std::make_unique<juce::Label>("fval", "160");
w.freqValue->setFont(monoFont(10.0f));
w.freqValue->setColour(juce::Label::textColourId, lookAndFeel.cyan);
w.freqValue->setJustificationType(juce::Justification::centred);
w.freq = std::make_unique<juce::Slider>(juce::Slider::RotaryVerticalDrag, juce::Slider::NoTextBox);
w.freq->setLookAndFeel(&lookAndFeel);
w.freq->setSliderStyle(juce::Slider::RotaryVerticalDrag);
w.freq->setRotaryParameters(2.4f, 6.9f, true); // ~270° sweep
auto freqRange = juce::NormalisableRange<double>(20.0, 20000.0, 0.5);
freqRange.setSkewForCentre(1000.0);
w.freq->setNormalisableRange(freqRange);
w.freq->setDoubleClickReturnValue(true, 1000.0f);
w.freq->onValueChange = [this, i]
{
float f = bands[i].freq->getValue();
juce::String txt;
if (f >= 1000.0f)
txt = juce::String(f / 1000.0f, 1) + "K";
else
txt = juce::String((int) f);
bands[i].freqValue->setText(txt, juce::dontSendNotification);
};
addAndMakeVisible(*w.freqLabel);
addAndMakeVisible(*w.gainCaption);
addAndMakeVisible(*w.gainValue);
addAndMakeVisible(*w.gain);
addAndMakeVisible(*w.freqCaption);
addAndMakeVisible(*w.freqValue);
addAndMakeVisible(*w.freq);
addAndMakeVisible(*w.qCaption);
addAndMakeVisible(*w.q);
addAndMakeVisible(*w.qValue);
@ -243,6 +275,8 @@ GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor&
processor.getAPVTS(), "gain" + juce::String(i), *w.gain);
qAttachments[i] = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processor.getAPVTS(), "q" + juce::String(i), *w.q);
freqAttachments[i] = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processor.getAPVTS(), "freq" + juce::String(i), *w.freq);
}
// Status bar.
@ -261,6 +295,7 @@ GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor&
spectrum.ensureStorageAllocated(128);
freqBins.ensureStorageAllocated(128);
eqCurve.resize(128);
startTimerHz(30);
scaleUI(100.0f);
@ -275,6 +310,21 @@ GelykEQAudioProcessorEditor::~GelykEQAudioProcessorEditor()
void GelykEQAudioProcessorEditor::timerCallback()
{
processor.getAnalyser().copySpectrum(spectrum, freqBins, 128);
const int n = 128;
FilterBand::Type types[numBands];
float gains[numBands], freqs[numBands], qs[numBands];
auto& params = processor.getAPVTS();
for (int i = 0; i < numBands; ++i)
{
types[i] = (FilterBand::Type)(i == 0 ? 0 : (i == numBands - 1 ? 2 : 1));
gains[i] = params.getRawParameterValue("gain" + juce::String(i))->load();
freqs[i] = params.getRawParameterValue("freq" + juce::String(i))->load();
qs[i] = params.getRawParameterValue("q" + juce::String(i))->load();
}
computeEQResponse(types, gains, freqs, qs, numBands,
(double) processor.getSampleRate(), eqCurve.getRawDataPointer(), n);
repaint();
}
@ -330,24 +380,31 @@ void GelykEQAudioProcessorEditor::resized()
const int captionH = (int) (18 * s);
const int valueH = (int) (15 * s);
const int freqH = (int) (20 * s);
const int qValueH = (int) (14 * s);
const int qKnobH = (int) (58 * s);
const int qCapH = (int) (14 * s);
const int knobStackH = (int) (14 * s) + (int)(58 * s) + (int)(12 * s); // value + knob + caption
w.gainCaption->setBounds(col.removeFromTop(captionH));
w.gainValue->setBounds(col.removeFromTop(valueH));
// The gain fader fills the large analyser area (spectrum drawn behind it).
w.gain->setBounds(col.removeFromTop(col.getHeight()
- freqH - qValueH - qKnobH - qCapH - (int)(6*s)));
w.gain->setBounds(col.removeFromTop(col.getHeight() - freqH - knobStackH - (int)(6*s)));
w.freqLabel->setBounds(col.removeFromTop(freqH));
// Q rotary control: value label, knob, caption.
w.qValue->setBounds(col.removeFromTop(qValueH));
juce::Rectangle<int> knobArea = col.removeFromTop(qKnobH);
w.q->setBounds(knobArea.reduced((int)(colW * 0.22f), 0));
w.qCaption->setBounds(col.removeFromTop(qCapH));
// Bottom control row: FREQ and Q knobs side by side.
juce::Rectangle<int> knobRow = col;
int rowW = knobRow.getWidth();
int halfW = rowW / 2;
// FREQ (left half).
juce::Rectangle<int> freqCol = knobRow.removeFromLeft(halfW);
w.freqValue->setBounds(freqCol.removeFromTop((int)(14*s)));
w.freq->setBounds(freqCol.removeFromTop((int)(58*s)).reduced((int)(colW*0.08f), 0));
w.freqCaption->setBounds(freqCol.removeFromTop((int)(12*s)));
// Q (right half).
w.qValue->setBounds(knobRow.removeFromTop((int)(14*s)));
w.q->setBounds(knobRow.removeFromTop((int)(58*s)).reduced((int)(colW*0.08f), 0));
w.qCaption->setBounds(knobRow.removeFromTop((int)(12*s)));
}
// Master column.
@ -418,6 +475,32 @@ void GelykEQAudioProcessorEditor::paint(juce::Graphics& g)
g.setColour(lookAndFeel.cyan.withAlpha(0.9f));
g.strokePath(path, juce::PathStrokeType(1.6f * s));
}
// Static EQ curve overlay (the actual summed filter response).
if (eqCurve.size() > 2)
{
const float x0 = (float) mainArea.getX();
const float width = (float) mainArea.getWidth();
const float centreY = topY + (botY - topY) * 0.5f;
const float halfH = (botY - topY) * 0.42f; // ±24 dB maps here
juce::Path curve;
for (int i = 0; i < eqCurve.size(); ++i)
{
float db = juce::jlimit(-24.0f, 24.0f, eqCurve[i]);
float yy = centreY - (db / 24.0f) * halfH;
float xx = x0 + (float) i / (float) (eqCurve.size() - 1) * width;
if (i == 0) curve.startNewSubPath(xx, yy);
else curve.lineTo(xx, yy);
}
g.setColour(lookAndFeel.violet.withAlpha(0.20f));
g.strokePath(curve, juce::PathStrokeType(7.0f * s, juce::PathStrokeType::curved,
juce::PathStrokeType::rounded));
g.setColour(lookAndFeel.violet);
g.strokePath(curve, juce::PathStrokeType(1.8f * s, juce::PathStrokeType::curved,
juce::PathStrokeType::rounded));
}
}
} // namespace gelyk