Expand to 7 bands; add labeled rotary Q knobs and gain dB readouts

This commit is contained in:
Gelyk 2026-08-29 15:32:16 +02:00
commit 10feb7a7fe
5 changed files with 126 additions and 49 deletions

View file

@ -102,13 +102,6 @@ target_link_libraries(GelykEQ PRIVATE juce::juce_dsp)
target_compile_definitions(GelykEQ PRIVATE JUCE_MODULE_AVAILABLE_juce_dsp=1) target_compile_definitions(GelykEQ PRIVATE JUCE_MODULE_AVAILABLE_juce_dsp=1)
target_compile_definitions(GelykEQ PRIVATE
JucePlugin_Build_VST=1
JucePlugin_Build_AU=1
JucePlugin_Build_AAX=0
JucePlugin_Build_Standalone=0
)
target_compile_options(GelykEQ PRIVATE target_compile_options(GelykEQ PRIVATE
$<$<CXX_COMPILER_ID:AppleClang,Clang>:-Wall -Wextra> $<$<CXX_COMPILER_ID:AppleClang,Clang>:-Wall -Wextra>
) )

View file

@ -67,6 +67,46 @@ void NeonLookAndFeel::drawLinearSlider(juce::Graphics& g, int x, int y, int widt
} }
} }
void NeonLookAndFeel::drawRotarySlider(juce::Graphics& g, int x, int y, int width, int height,
float sliderPos, float startAngle, float endAngle, juce::Slider& slider)
{
const float cx = x + width * 0.5f;
const float cy = y + height * 0.5f;
const float radius = juce::jmin(width, height) * 0.5f;
const float toAngle = startAngle + sliderPos * (endAngle - startAngle);
const float lineW = juce::jmax(2.0f, radius * 0.10f);
const float arcRadius = radius - lineW * 0.5f;
// Background disc.
g.setColour(panel);
g.fillEllipse(cx - radius, cy - radius, radius * 2.0f, radius * 2.0f);
g.setColour(line);
g.drawEllipse(cx - radius, cy - radius, radius * 2.0f, radius * 2.0f, 1.0f);
// Filled arc (progress).
juce::Path filledArc;
filledArc.addCentredArc(cx, cy, arcRadius, arcRadius, 0.0f,
startAngle, toAngle, true);
juce::PathStrokeType stroke(lineW, juce::PathStrokeType::curved, juce::PathStrokeType::rounded);
g.setColour(slider.isMouseOverOrDragging(true) ? magenta : cyan);
g.strokePath(filledArc, stroke);
// Pointer line.
juce::Path pointer;
const float pointerLength = radius * 0.68f;
pointer.startNewSubPath(cx, cy);
pointer.lineTo(cx + pointerLength * std::cos(toAngle),
cy + pointerLength * std::sin(toAngle));
g.setColour(slider.isMouseOverOrDragging(true) ? violet : bg);
g.strokePath(pointer, juce::PathStrokeType(juce::jmax(1.5f, radius * 0.07f),
juce::PathStrokeType::curved,
juce::PathStrokeType::rounded));
// Centre hub.
g.setColour(slider.isMouseOverOrDragging(true) ? cyan : line);
g.fillEllipse(cx - 2.0f, cy - 2.0f, 4.0f, 4.0f);
}
void NeonLookAndFeel::drawComboBox(juce::Graphics& g, int width, int height, bool, void NeonLookAndFeel::drawComboBox(juce::Graphics& g, int width, int height, bool,
int, int, int, int, juce::ComboBox& box) int, int, int, int, juce::ComboBox& box)
{ {
@ -136,39 +176,68 @@ GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor&
masterAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>( masterAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processor.getAPVTS(), "master", *master); processor.getAPVTS(), "master", *master);
// Band faders. // Band faders.
const char* const names[numBands] = { "LOW", "LOW-MID", "MID", "HIGH-MID", "HIGH" }; const char* const names[numBands] = { "LOW SHELF", "LOW", "LOW-MID", "MID", "HIGH-MID", "HIGH", "HIGH SHELF" };
const char* const freqs[numBands] = { "60", "200", "1K", "4K", "10K" }; const char* const freqs[numBands] = { "60", "160", "400", "800", "1.6K", "4K", "10K" };
for (int i = 0; i < numBands; ++i) for (int i = 0; i < numBands; ++i)
{ {
auto& w = bands[i]; auto& w = bands[i];
const juce::Colour accent = (i % 2 == 0) ? lookAndFeel.cyan : lookAndFeel.magenta;
w.freqLabel = std::make_unique<juce::Label>("freq", freqs[i]); w.freqLabel = std::make_unique<juce::Label>("freq", freqs[i]);
w.freqLabel->setFont(monoFont(13.0f)); w.freqLabel->setFont(monoFont(14.0f).boldened());
w.freqLabel->setColour(juce::Label::textColourId, lookAndFeel.textDim); w.freqLabel->setColour(juce::Label::textColourId, accent);
w.freqLabel->setJustificationType(juce::Justification::centred); w.freqLabel->setJustificationType(juce::Justification::centred);
w.gainLabel = std::make_unique<juce::Label>("gname", names[i]); w.gainCaption = std::make_unique<juce::Label>("gname", names[i]);
w.gainLabel->setFont(monoFont(11.0f)); w.gainCaption->setFont(monoFont(10.0f));
w.gainLabel->setColour(juce::Label::textColourId, w.gainCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim);
i % 2 == 0 ? lookAndFeel.cyan : lookAndFeel.magenta); w.gainCaption->setJustificationType(juce::Justification::centred);
w.gainLabel->setJustificationType(juce::Justification::centred);
w.gain = std::make_unique<juce::Slider>(juce::Slider::LinearVertical, juce::Slider::TextBoxBelow); w.gainValue = std::make_unique<juce::Label>("gval", "0.0");
w.gainValue->setFont(monoFont(11.0f));
w.gainValue->setColour(juce::Label::textColourId, accent);
w.gainValue->setJustificationType(juce::Justification::centred);
w.qCaption = std::make_unique<juce::Label>("qcap", "Q");
w.qCaption->setFont(monoFont(10.0f));
w.qCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim);
w.qCaption->setJustificationType(juce::Justification::centred);
w.qValue = std::make_unique<juce::Label>("qval", "1.00");
w.qValue->setFont(monoFont(10.0f));
w.qValue->setColour(juce::Label::textColourId, lookAndFeel.violet);
w.qValue->setJustificationType(juce::Justification::centred);
w.gain = std::make_unique<juce::Slider>(juce::Slider::LinearVertical, juce::Slider::NoTextBox);
w.gain->setLookAndFeel(&lookAndFeel); w.gain->setLookAndFeel(&lookAndFeel);
w.gain->setSliderStyle(juce::Slider::LinearVertical); w.gain->setSliderStyle(juce::Slider::LinearVertical);
w.gain->setTextBoxStyle(juce::Slider::TextBoxBelow, false, 40, 14);
w.gain->setRange(-24.0, 24.0, 0.01); w.gain->setRange(-24.0, 24.0, 0.01);
w.gain->setDoubleClickReturnValue(true, 0.0); w.gain->setDoubleClickReturnValue(true, 0.0);
w.gain->setValue(0.0, juce::dontSendNotification);
w.gain->onValueChange = [this, i]
{
bands[i].gainValue->setText(juce::String(bands[i].gain->getValue(), 1), juce::dontSendNotification);
};
w.q = std::make_unique<juce::Slider>(juce::Slider::LinearHorizontal, juce::Slider::NoTextBox); w.q = std::make_unique<juce::Slider>(juce::Slider::RotaryVerticalDrag, juce::Slider::NoTextBox);
w.q->setLookAndFeel(&lookAndFeel); w.q->setLookAndFeel(&lookAndFeel);
w.q->setSliderStyle(juce::Slider::RotaryVerticalDrag);
w.q->setRotaryParameters(2.4f, 6.9f, true); // ~270° sweep
w.q->setRange(0.1, 6.0, 0.01); w.q->setRange(0.1, 6.0, 0.01);
w.q->setDoubleClickReturnValue(true, 1.0); w.q->setDoubleClickReturnValue(true, 1.0);
w.q->setValue(1.0, juce::dontSendNotification);
w.q->onValueChange = [this, i]
{
bands[i].qValue->setText(juce::String(bands[i].q->getValue(), 2), juce::dontSendNotification);
};
addAndMakeVisible(*w.freqLabel); addAndMakeVisible(*w.freqLabel);
addAndMakeVisible(*w.gainLabel); addAndMakeVisible(*w.gainCaption);
addAndMakeVisible(*w.gainValue);
addAndMakeVisible(*w.gain); addAndMakeVisible(*w.gain);
addAndMakeVisible(*w.qCaption);
addAndMakeVisible(*w.q); addAndMakeVisible(*w.q);
addAndMakeVisible(*w.qValue);
gainAttachments[i] = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>( gainAttachments[i] = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processor.getAPVTS(), "gain" + juce::String(i), *w.gain); processor.getAPVTS(), "gain" + juce::String(i), *w.gain);
@ -217,7 +286,7 @@ void GelykEQAudioProcessorEditor::scaleUI(float percent)
{ {
const float s = percent / 100.0f; const float s = percent / 100.0f;
scaling.store(s); scaling.store(s);
setSize((int) (920 * s), (int) (540 * s)); setSize((int) (1180 * s), (int) (640 * s));
resized(); resized();
} }
@ -226,13 +295,13 @@ void GelykEQAudioProcessorEditor::resized()
juce::Rectangle<int> area = getLocalBounds(); juce::Rectangle<int> area = getLocalBounds();
const float s = scaling.load(); const float s = scaling.load();
const int headerH = (int) (46 * s); const int headerH = (int) (48 * s);
const int footerH = (int) (26 * s); const int footerH = (int) (26 * s);
const int pad = (int) (12 * s); const int pad = (int) (12 * s);
// Header. // Header.
juce::Rectangle<int> header = area.removeFromTop(headerH); juce::Rectangle<int> header = area.removeFromTop(headerH);
titleLabel->setBounds(header.removeFromLeft((int) (170 * s)).reduced(pad, 0)); titleLabel->setBounds(header.removeFromLeft((int) (180 * s)).reduced(pad, 0));
scaleBox->setBounds(header.removeFromRight((int) (90 * s)).reduced((int)(6*s), (int)(9*s))); scaleBox->setBounds(header.removeFromRight((int) (90 * s)).reduced((int)(6*s), (int)(9*s)));
// Footer. // Footer.
@ -241,7 +310,7 @@ void GelykEQAudioProcessorEditor::resized()
statusBar->setBounds(footer.removeFromLeft(footer.getWidth() / 2)); statusBar->setBounds(footer.removeFromLeft(footer.getWidth() / 2));
statusGit->setBounds(footer); statusGit->setBounds(footer);
// Main grid. // Master column (right).
const int masterW = (int) (64 * s); const int masterW = (int) (64 * s);
juce::Rectangle<int> masterArea = area.removeFromRight(masterW); juce::Rectangle<int> masterArea = area.removeFromRight(masterW);
@ -250,7 +319,7 @@ void GelykEQAudioProcessorEditor::resized()
const int totalW = area.getWidth(); const int totalW = area.getWidth();
const int height = area.getHeight(); const int height = area.getHeight();
const int gap = (int) (10 * s); const int gap = (int) (12 * s);
const int colW = (totalW - gap * (numBands - 1)) / numBands; const int colW = (totalW - gap * (numBands - 1)) / numBands;
for (int i = 0; i < numBands; ++i) for (int i = 0; i < numBands; ++i)
@ -258,22 +327,27 @@ void GelykEQAudioProcessorEditor::resized()
auto& w = bands[i]; auto& w = bands[i];
juce::Rectangle<int> col(x0 + i * (colW + gap), y0, colW, height); juce::Rectangle<int> col(x0 + i * (colW + gap), y0, colW, height);
const int topLabelH = (int) (20 * s); const int captionH = (int) (18 * s);
const int freqH = (int) (16 * s); const int valueH = (int) (15 * s);
const int gainSliderH = (int) (58 * s); const int freqH = (int) (20 * s);
const int qH = (int) (18 * s); const int qValueH = (int) (14 * s);
const int qKnobH = (int) (58 * s);
const int qCapH = (int) (14 * s);
w.gainLabel->setBounds(col.removeFromTop(topLabelH)); w.gainCaption->setBounds(col.removeFromTop(captionH));
w.gainValue->setBounds(col.removeFromTop(valueH));
const int analyserH = col.getHeight() - freqH - gainSliderH - qH - (int)(8*s); // The gain fader fills the large analyser area (spectrum drawn behind it).
// reserve analyser space by subtracting it (faders sit on top of the spectrum) w.gain->setBounds(col.removeFromTop(col.getHeight()
col.removeFromTop(juce::jmax(0, analyserH)); - freqH - qValueH - qKnobH - qCapH - (int)(6*s)));
w.freqLabel->setBounds(col.removeFromTop(freqH)); w.freqLabel->setBounds(col.removeFromTop(freqH));
w.gain->setBounds(col.removeFromTop(gainSliderH));
juce::Rectangle<int> qArea = col.removeFromBottom(qH); // Q rotary control: value label, knob, caption.
w.q->setBounds(qArea.reduced((int)(12*s), 0)); 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));
} }
// Master column. // Master column.

View file

@ -13,6 +13,9 @@ public:
void drawLinearSlider(juce::Graphics&, int x, int y, int width, int height, void drawLinearSlider(juce::Graphics&, int x, int y, int width, int height,
float sliderPos, float minSliderPos, float maxSliderPos, float sliderPos, float minSliderPos, float maxSliderPos,
const juce::Slider::SliderStyle, juce::Slider&) override; const juce::Slider::SliderStyle, juce::Slider&) override;
void drawRotarySlider(juce::Graphics&, int x, int y, int width, int height,
float sliderPosProportional, float rotaryStartAngle,
float rotaryEndAngle, juce::Slider&) override;
void drawComboBox(juce::Graphics&, int width, int height, bool isButtonDown, void drawComboBox(juce::Graphics&, int width, int height, bool isButtonDown,
int buttonX, int buttonY, int buttonW, int buttonH, int buttonX, int buttonY, int buttonW, int buttonH,
juce::ComboBox&) override; juce::ComboBox&) override;
@ -48,13 +51,16 @@ private:
NeonLookAndFeel lookAndFeel; NeonLookAndFeel lookAndFeel;
static constexpr int numBands = 5; static constexpr int numBands = 7;
struct BandWidget struct BandWidget
{ {
std::unique_ptr<juce::Slider> gain; std::unique_ptr<juce::Slider> gain;
std::unique_ptr<juce::Slider> q; std::unique_ptr<juce::Slider> q;
std::unique_ptr<juce::Label> freqLabel; std::unique_ptr<juce::Label> freqLabel;
std::unique_ptr<juce::Label> gainLabel; std::unique_ptr<juce::Label> gainCaption;
std::unique_ptr<juce::Label> gainValue;
std::unique_ptr<juce::Label> qCaption;
std::unique_ptr<juce::Label> qValue;
}; };
BandWidget bands[numBands]; BandWidget bands[numBands];

View file

@ -24,16 +24,20 @@ juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout()
}; };
addGain("gain0", "Low Shelf Gain", 0.0f); addGain("gain0", "Low Shelf Gain", 0.0f);
addGain("gain1", "Low-Mid Gain", 0.0f); addGain("gain1", "Low Gain", 0.0f);
addGain("gain2", "Mid Gain", 0.0f); addGain("gain2", "Low-Mid Gain", 0.0f);
addGain("gain3", "High-Mid Gain", 0.0f); addGain("gain3", "Mid Gain", 0.0f);
addGain("gain4", "High Shelf Gain", 0.0f); addGain("gain4", "High-Mid Gain", 0.0f);
addGain("gain5", "High Gain", 0.0f);
addGain("gain6", "High Shelf Gain", 0.0f);
addQ("q0", "Low Shelf Q", 0.71f); addQ("q0", "Low Shelf Q", 0.71f);
addQ("q1", "Low-Mid Q", 1.0f); addQ("q1", "Low Q", 1.0f);
addQ("q2", "Mid Q", 1.0f); addQ("q2", "Low-Mid Q", 1.0f);
addQ("q3", "High-Mid Q", 1.0f); addQ("q3", "Mid Q", 1.0f);
addQ("q4", "High Shelf Q", 0.71f); addQ("q4", "High-Mid Q", 1.0f);
addQ("q5", "High Q", 1.0f);
addQ("q6", "High Shelf Q", 0.71f);
layout.add(std::make_unique<juce::AudioParameterFloat>( layout.add(std::make_unique<juce::AudioParameterFloat>(
"master", "Master", juce::NormalisableRange<float>(-24.0f, 24.0f, 0.01f), 0.0f)); "master", "Master", juce::NormalisableRange<float>(-24.0f, 24.0f, 0.01f), 0.0f));
@ -44,8 +48,8 @@ juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout()
return layout; return layout;
} }
const char* const bandTypes[5] = { "lowShelf", "peak", "peak", "peak", "highShelf" }; const char* const bandTypes[7] = { "lowShelf", "peak", "peak", "peak", "peak", "peak", "highShelf" };
const float bandFreqs[5] = { 60.0f, 200.0f, 1000.0f, 4000.0f, 10000.0f }; const float bandFreqs[7] = { 60.0f, 160.0f, 400.0f, 800.0f, 1600.0f, 4000.0f, 10000.0f };
} // namespace } // namespace
GelykEQAudioProcessor::GelykEQAudioProcessor() GelykEQAudioProcessor::GelykEQAudioProcessor()

View file

@ -44,7 +44,7 @@ public:
private: private:
juce::AudioProcessorValueTreeState apvts; juce::AudioProcessorValueTreeState apvts;
static constexpr int numBands = 5; static constexpr int numBands = 7;
FilterBand bands[numBands]; FilterBand bands[numBands];
double currentSampleRate = 44100.0; double currentSampleRate = 44100.0;