gelyk/Source/PluginEditor.cpp
2026-08-29 22:55:17 +02:00

881 lines
36 KiB
C++

#include "PluginEditor.h"
namespace gelyk
{
static constexpr float fontScale = 1.4f;
static juce::Font monoFont(float height)
{
return juce::Font(juce::FontOptions(juce::Font::getDefaultMonospacedFontName(), height * fontScale,
juce::Font::plain));
}
// ---------------------------------------------------------------------------
// NeonLookAndFeel
// ---------------------------------------------------------------------------
NeonLookAndFeel::NeonLookAndFeel()
{
refreshPalette();
}
void NeonLookAndFeel::refreshPalette()
{
setColour(juce::Slider::textBoxTextColourId, cyan());
setColour(juce::Slider::textBoxOutlineColourId, juce::Colours::transparentBlack);
setColour(juce::Slider::textBoxBackgroundColourId, panel());
setColour(juce::ComboBox::backgroundColourId, panel());
setColour(juce::ComboBox::outlineColourId, line());
setColour(juce::ComboBox::textColourId, cyan());
setColour(juce::ComboBox::arrowColourId, cyan());
}
void NeonLookAndFeel::drawLinearSlider(juce::Graphics& g, int x, int y, int width, int height,
float sliderPos, float, float,
const juce::Slider::SliderStyle style, juce::Slider& slider)
{
const bool vertical = (style == juce::Slider::LinearVertical);
const float cx = x + width * 0.5f;
// Track
if (vertical)
{
const float tw = 2.0f;
juce::Path track;
track.addRoundedRectangle(cx - tw * 0.5f, (float) y, tw, (float) height, 1.0f);
g.setColour(line());
g.fillPath(track);
// Filled region from centre (0 dB) down to the handle, glowing.
const float midY = y + height * 0.5f;
const float top = juce::jmin(midY, sliderPos);
const float bottom = juce::jmax(midY, sliderPos);
juce::Path fill;
fill.addRoundedRectangle(cx - 3.0f, top, 6.0f, juce::jmax(1.0f, bottom - top), 3.0f);
g.setColour(cyan());
g.fillPath(fill);
// Handle.
g.setColour(slider.isMouseOverOrDragging(true) ? violet() : cyan());
g.fillEllipse(cx - 5.0f, sliderPos - 5.0f, 10.0f, 10.0f);
g.setColour(bg());
g.fillEllipse(cx - 2.0f, sliderPos - 2.0f, 4.0f, 4.0f);
}
else
{
const float th = 2.0f;
juce::Path track;
track.addRoundedRectangle((float) x, y + height * 0.5f - th * 0.5f, (float) width, th, 1.0f);
g.setColour(line());
g.fillPath(track);
g.setColour(cyan());
g.drawHorizontalLine((int) (y + height * 0.5f), sliderPos - 5.0f, sliderPos + 5.0f);
}
}
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 + juce::MathConstants<float>::halfPi,
toAngle + juce::MathConstants<float>::halfPi, 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() : cyan());
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,
int, int, int, int, juce::ComboBox& box)
{
auto corner = 4.0f;
auto bounds = box.getLocalBounds().toFloat();
g.setColour(panel());
g.fillRoundedRectangle(bounds, corner);
g.setColour(cyan());
g.drawRoundedRectangle(bounds, corner, 1.0f);
juce::Rectangle<int> arrowZone(width - 20, 0, 20, height);
juce::Path p;
p.startNewSubPath((float) arrowZone.getX() + 4.0f, (float) arrowZone.getCentreY() - 3.0f);
p.lineTo((float) arrowZone.getCentreX(), (float) arrowZone.getCentreY() + 3.0f);
p.lineTo((float) arrowZone.getX() + 12.0f, (float) arrowZone.getCentreY() - 3.0f);
g.setColour(cyan());
g.strokePath(p, juce::PathStrokeType(1.5f));
}
void NeonLookAndFeel::positionComboBoxText(juce::ComboBox& box, juce::Label& label)
{
label.setBounds(2, 1, box.getWidth() - 22, box.getHeight() - 2);
label.setFont(monoFont(13.0f * uiScale));
}
// ---------------------------------------------------------------------------
// BandFader
// ---------------------------------------------------------------------------
void BandFader::paint(juce::Graphics& g)
{
const float cx = getWidth() * 0.5f;
const float y = 0.0f;
const float height = (float) getHeight();
const float value = getGain();
const auto start = (float) gainP->getNormalisableRange().start;
const auto end = (float) gainP->getNormalisableRange().end;
const float sliderPos = y + (end - value) / (end - start) * height;
// Track.
const float tw = 2.0f;
g.setColour(lf.line());
g.fillRect(cx - tw * 0.5f, y, tw, height);
// Filled region from centre (0 dB) down to the handle.
const float midY = y + height * 0.5f;
const float top = juce::jmin(midY, sliderPos);
const float bottom = juce::jmax(midY, sliderPos);
g.setColour(lf.cyan());
g.fillRect(cx - 3.0f, top, 6.0f, juce::jmax(1.0f, bottom - top));
// Handle.
g.setColour(hovered || dragging ? lf.violet() : lf.cyan());
g.fillEllipse(cx - 5.0f, sliderPos - 5.0f, 10.0f, 10.0f);
g.setColour(lf.bg());
g.fillEllipse(cx - 2.0f, sliderPos - 2.0f, 4.0f, 4.0f);
}
void BandFader::mouseDown(const juce::MouseEvent& e)
{
if (gainP == nullptr || freqP == nullptr)
return;
if (e.getNumberOfClicks() > 1)
{
*gainP = 0.0f;
repaint();
return;
}
dragging = true;
startGain = (float) gainP->get();
startFreq = (float) freqP->get();
startX = e.getScreenX();
startY = e.getScreenY();
repaint();
}
void BandFader::mouseDrag(const juce::MouseEvent& e)
{
if (!dragging || gainP == nullptr || freqP == nullptr)
return;
const float h = (float) juce::jmax(1, getHeight());
const float dy = (float) (startY - e.getScreenY());
const float span = (float) gainP->getNormalisableRange().end - (float) gainP->getNormalisableRange().start;
const float newGain = juce::jlimit((float) gainP->getNormalisableRange().start,
(float) gainP->getNormalisableRange().end,
startGain + dy / h * span);
if (gainP->get() != newGain)
*gainP = newGain;
const float w = dragWidth > 0.0f ? dragWidth : (float) juce::jmax(1, getWidth());
const float dx = (float) (e.getScreenX() - startX);
// Track 1:1 with the cursor over the full visualizer width: the fader's screen
// position moves by exactly the same number of pixels as the mouse.
const float startXpos = bandFreqToX(startFreq, dragX0, w);
float newFreq = bandXToFreq(startXpos + dx, dragX0, w);
newFreq = juce::jlimit(20.0f, 20000.0f, newFreq);
// Keep the band between its neighbours so they stay ordered left-to-right.
if (onGetFreqBounds != nullptr)
{
const auto [lo, hi] = onGetFreqBounds();
newFreq = juce::jlimit(lo, hi, newFreq);
}
if (std::abs(newFreq - (float) freqP->get()) > 0.5f)
{
*freqP = newFreq;
if (onFreqChange != nullptr)
onFreqChange();
}
repaint();
}
void BandFader::mouseUp(const juce::MouseEvent&)
{
dragging = false;
repaint();
}
void BandFader::mouseEnter(const juce::MouseEvent&)
{
hovered = true;
repaint();
}
void BandFader::mouseExit(const juce::MouseEvent&)
{
hovered = false;
repaint();
}
void BandFader::mouseWheelMove(const juce::MouseEvent&, const juce::MouseWheelDetails& wheel)
{
if (qP == nullptr)
return;
const float delta = (float) (wheel.deltaY * 0.3);
const float lo = (float) qP->getNormalisableRange().start;
const float hi = (float) qP->getNormalisableRange().end;
*qP = juce::jlimit(lo, hi, (float) qP->get() + delta);
}
// ---------------------------------------------------------------------------
// GelykEQAudioProcessorEditor
// ---------------------------------------------------------------------------
GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor& p)
: AudioProcessorEditor(&p),
processor(p)
{
setLookAndFeel(&lookAndFeel);
setOpaque(true);
titleLabel = std::make_unique<juce::Label>("title", "GELYK");
titleLabel->setFont(monoFont(26.0f));
titleLabel->setColour(juce::Label::textColourId, lookAndFeel.cyan());
titleLabel->setJustificationType(juce::Justification::centredLeft);
addAndMakeVisible(*titleLabel);
scaleBox = std::make_unique<juce::ComboBox>("scale");
scaleBox->addItem("100%", 100);
scaleBox->addItem("125%", 125);
scaleBox->addItem("150%", 150);
scaleBox->addItem("175%", 175);
scaleBox->addItem("200%", 200);
scaleBox->setSelectedId(100, juce::dontSendNotification);
scaleBox->setTooltip("UI scaling");
scaleBox->onChange = [this] { scaleUI((float) scaleBox->getSelectedId()); };
addAndMakeVisible(*scaleBox);
const struct ColorControl
{
std::unique_ptr<juce::Label>* label;
std::unique_ptr<juce::Slider>* slider;
const char* text;
double min, max, def;
const char* tip;
} colorControls[] = {
{ &waveSensLabel, &waveSensSlider, "SENS", -12.0, 12.0, 0.0, "Live waveform sensitivity (dB)" },
{ &colorHueLabel, &colorHueSlider, "HUE", -0.5, 0.5, 0.0, "Master colour hue shift" },
{ &colorSatLabel, &colorSatSlider, "SAT", 0.0, 1.0, 1.0, "Master colour saturation" },
{ &colorBriLabel, &colorBriSlider, "BRI", 0.5, 1.5, 1.0, "Master colour brightness" },
{ &waveLabel, &waveSlider, "WAVE", 0.0, 1.0, 0.2, "Live waveform opacity" },
};
for (const auto& cc : colorControls)
{
*cc.label = std::make_unique<juce::Label>("ccap", cc.text);
(*cc.label)->setFont(monoFont(8.0f));
(*cc.label)->setColour(juce::Label::textColourId, lookAndFeel.textDim());
(*cc.label)->setJustificationType(juce::Justification::centred);
addAndMakeVisible(**cc.label);
*cc.slider = std::make_unique<juce::Slider>(juce::Slider::LinearHorizontal, juce::Slider::NoTextBox);
(*cc.slider)->setLookAndFeel(&lookAndFeel);
(*cc.slider)->setSliderStyle(juce::Slider::LinearHorizontal);
(*cc.slider)->setRange(cc.min, cc.max, 0.01);
(*cc.slider)->setDoubleClickReturnValue(true, cc.def);
(*cc.slider)->setValue(cc.def, juce::dontSendNotification);
(*cc.slider)->setTooltip(cc.tip);
addAndMakeVisible(**cc.slider);
}
colorHueSlider->onValueChange = [this]
{
lookAndFeel.hueShift = (float) colorHueSlider->getValue();
refreshColors();
};
colorSatSlider->onValueChange = [this]
{
lookAndFeel.saturation = (float) colorSatSlider->getValue();
refreshColors();
};
colorBriSlider->onValueChange = [this]
{
lookAndFeel.brightness = (float) colorBriSlider->getValue();
refreshColors();
};
waveSlider->onValueChange = [this]
{
lookAndFeel.waveformOpacity = (float) waveSlider->getValue();
repaint();
};
waveSensSlider->onValueChange = [this]
{
lookAndFeel.waveSensitivity = (float) waveSensSlider->getValue();
repaint();
};
// Master gain (vertical fader, far right).
masterLabel = std::make_unique<juce::Label>("mlab", "MASTER");
masterLabel->setFont(monoFont(11.0f));
masterLabel->setColour(juce::Label::textColourId, lookAndFeel.magenta());
masterLabel->setJustificationType(juce::Justification::centred);
master = std::make_unique<juce::Slider>(juce::Slider::LinearVertical, juce::Slider::TextBoxBelow);
master->setLookAndFeel(&lookAndFeel);
master->setSliderStyle(juce::Slider::LinearVertical);
master->setTextBoxStyle(juce::Slider::TextBoxBelow, false, 40, 14);
master->setRange(-24.0, 24.0, 0.01);
master->setDoubleClickReturnValue(true, 0.0);
addAndMakeVisible(*masterLabel);
addAndMakeVisible(*master);
masterPercent = std::make_unique<juce::Label>("mper", "100%");
masterPercent->setFont(monoFont(10.0f));
masterPercent->setColour(juce::Label::textColourId, lookAndFeel.cyan());
masterPercent->setJustificationType(juce::Justification::centred);
addAndMakeVisible(*masterPercent);
updateMasterPercent();
masterAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processor.getAPVTS(), "master", *master);
master->onValueChange = [this] { updateMasterPercent(); };
// Band faders.
const char* const names[numBands] = { "LOW SHELF", "LOW", "LOW-MID", "MID", "HIGH-MID", "HIGH", "HIGH SHELF" };
const char* const freqs[numBands] = { "60", "160", "400", "800", "1.6K", "4K", "10K" };
for (int i = 0; i < numBands; ++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->setFont(monoFont(14.0f).boldened());
w.freqLabel->setColour(juce::Label::textColourId, accent);
w.freqLabel->setJustificationType(juce::Justification::centred);
w.gainCaption = std::make_unique<juce::Label>("gname", names[i]);
w.gainCaption->setFont(monoFont(10.0f));
w.gainCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim());
w.gainCaption->setJustificationType(juce::Justification::centred);
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<BandFader>(processor.getAPVTS(),
"gain" + juce::String(i),
"freq" + juce::String(i),
"q" + juce::String(i),
lookAndFeel);
w.gain->onFreqChange = [this]
{
resized();
};
// Keep bands in left-to-right frequency order: each band can only go up
// to its right-hand neighbour and down to its left-hand neighbour.
w.gain->onGetFreqBounds = [this, i]() -> std::pair<float, float>
{
auto& apvts = processor.getAPVTS();
float lo = 20.0f;
float hi = 20000.0f;
if (i > 0)
lo = apvts.getRawParameterValue("freq" + juce::String(i - 1))->load() * 1.01f;
if (i < numBands - 1)
hi = apvts.getRawParameterValue("freq" + juce::String(i + 1))->load() * 0.99f;
return { juce::jmax(20.0f, lo), juce::jmin(20000.0f, hi) };
};
w.levelValue = std::make_unique<juce::Label>("lval", "0.0");
w.levelValue->setFont(monoFont(10.0f));
w.levelValue->setColour(juce::Label::textColourId, accent);
w.levelValue->setJustificationType(juce::Justification::centred);
w.levelCaption = std::make_unique<juce::Label>("lcap", "LEVEL");
w.levelCaption->setFont(monoFont(10.0f));
w.levelCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim());
w.levelCaption->setJustificationType(juce::Justification::centred);
w.level = std::make_unique<juce::Slider>(juce::Slider::RotaryVerticalDrag, juce::Slider::NoTextBox);
w.level->setLookAndFeel(&lookAndFeel);
w.level->setSliderStyle(juce::Slider::RotaryVerticalDrag);
w.level->setRotaryParameters(2.4f, 6.9f, true); // ~270° sweep
w.level->setRange(-24.0, 24.0, 0.01);
w.level->setDoubleClickReturnValue(true, 0.0);
w.level->setValue(0.0, juce::dontSendNotification);
w.level->setTooltip("Gain (level), also adjustable via the fader");
w.level->onValueChange = [this, i]
{
updateGainValue(i);
};
w.q = std::make_unique<juce::Slider>(juce::Slider::RotaryVerticalDrag, juce::Slider::NoTextBox);
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->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);
};
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);
bands[i].freqLabel->setText(txt, juce::dontSendNotification);
};
addAndMakeVisible(*w.freqLabel);
addAndMakeVisible(*w.gainCaption);
addAndMakeVisible(*w.gainValue);
addAndMakeVisible(*w.gain);
addAndMakeVisible(*w.levelValue);
addAndMakeVisible(*w.levelCaption);
addAndMakeVisible(*w.level);
addAndMakeVisible(*w.freqCaption);
addAndMakeVisible(*w.freqValue);
addAndMakeVisible(*w.freq);
addAndMakeVisible(*w.qCaption);
addAndMakeVisible(*w.q);
addAndMakeVisible(*w.qValue);
levelAttachments[i] = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processor.getAPVTS(), "gain" + juce::String(i), *w.level);
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.
statusGit = std::make_unique<juce::Label>("git", processor.getGitInfo());
statusGit->setFont(monoFont(11.0f));
statusGit->setColour(juce::Label::textColourId, lookAndFeel.textDim());
statusGit->setJustificationType(juce::Justification::centredRight);
statusBar = std::make_unique<juce::Label>("status",
"GELYK v" + juce::String(ProjectInfo::versionString) + "");
statusBar->setFont(monoFont(11.0f));
statusBar->setColour(juce::Label::textColourId, lookAndFeel.cyan().withAlpha(0.8f));
statusBar->setJustificationType(juce::Justification::centredLeft);
addAndMakeVisible(*statusBar);
addAndMakeVisible(*statusGit);
spectrum.ensureStorageAllocated(128);
freqBins.ensureStorageAllocated(128);
eqCurve.resize(128);
startTimerHz(30);
refreshColors();
scaleUI(100.0f);
}
GelykEQAudioProcessorEditor::~GelykEQAudioProcessorEditor()
{
stopTimer();
setLookAndFeel(nullptr);
}
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();
}
void GelykEQAudioProcessorEditor::rebuildFaders()
{
}
void GelykEQAudioProcessorEditor::updateGainValue(int i)
{
const juce::String txt(juce::String(bands[i].level->getValue(), 1));
bands[i].gainValue->setText(txt, juce::dontSendNotification);
bands[i].levelValue->setText(txt, juce::dontSendNotification);
}
void GelykEQAudioProcessorEditor::updateMasterPercent()
{
const float db = (float) master->getValue();
const float percent = std::pow(10.0f, db / 20.0f) * 100.0f;
masterPercent->setText(juce::String(percent, 0) + "%", juce::dontSendNotification);
}
void GelykEQAudioProcessorEditor::refreshColors()
{
lookAndFeel.refreshPalette();
titleLabel->setColour(juce::Label::textColourId, lookAndFeel.cyan());
masterLabel->setColour(juce::Label::textColourId, lookAndFeel.magenta());
masterPercent->setColour(juce::Label::textColourId, lookAndFeel.cyan());
// The scale dropdown and the master value readout read their text colour from
// the LookAndFeel at construction, so they must be re-applied explicitly here
// for the saturation (and hue/brightness) faders to affect them.
scaleBox->setColour(juce::ComboBox::textColourId, lookAndFeel.cyan());
master->setColour(juce::Slider::textBoxTextColourId, lookAndFeel.cyan());
master->setColour(juce::Slider::textBoxBackgroundColourId, lookAndFeel.panel());
statusBar->setColour(juce::Label::textColourId, lookAndFeel.cyan().withAlpha(0.8f));
statusGit->setColour(juce::Label::textColourId, lookAndFeel.textDim());
colorHueLabel->setColour(juce::Label::textColourId, lookAndFeel.cyan());
colorSatLabel->setColour(juce::Label::textColourId, lookAndFeel.magenta());
colorBriLabel->setColour(juce::Label::textColourId, lookAndFeel.violet());
waveLabel->setColour(juce::Label::textColourId, lookAndFeel.textDim());
waveSensLabel->setColour(juce::Label::textColourId, lookAndFeel.textDim());
for (int i = 0; i < numBands; ++i)
{
auto& w = bands[i];
const juce::Colour accent = (i % 2 == 0) ? lookAndFeel.cyan() : lookAndFeel.magenta();
w.freqLabel->setColour(juce::Label::textColourId, accent);
w.gainCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim());
w.gainValue->setColour(juce::Label::textColourId, accent);
w.levelCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim());
w.levelValue->setColour(juce::Label::textColourId, accent);
w.qCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim());
w.qValue->setColour(juce::Label::textColourId, lookAndFeel.violet());
w.freqCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim());
w.freqValue->setColour(juce::Label::textColourId, lookAndFeel.cyan());
}
repaint();
}
void GelykEQAudioProcessorEditor::scaleUI(float percent)
{
const float s = percent / 100.0f;
scaling.store(s);
lookAndFeel.uiScale = s;
setSize((int) (1180 * s), (int) (640 * s));
resized();
refreshFonts(s);
}
void GelykEQAudioProcessorEditor::refreshFonts(float s)
{
titleLabel->setFont(monoFont(26.0f * s));
masterLabel->setFont(monoFont(11.0f * s));
statusGit->setFont(monoFont(11.0f * s));
statusBar->setFont(monoFont(11.0f * s));
colorHueLabel->setFont(monoFont(8.0f * s));
colorSatLabel->setFont(monoFont(8.0f * s));
colorBriLabel->setFont(monoFont(8.0f * s));
waveLabel->setFont(monoFont(8.0f * s));
waveSensLabel->setFont(monoFont(8.0f * s));
for (int i = 0; i < numBands; ++i)
{
auto& w = bands[i];
w.freqLabel->setFont(monoFont(14.0f * s).boldened());
w.gainCaption->setFont(monoFont(10.0f * s));
w.gainValue->setFont(monoFont(11.0f * s));
w.levelCaption->setFont(monoFont(10.0f * s));
w.levelValue->setFont(monoFont(10.0f * s));
w.qCaption->setFont(monoFont(10.0f * s));
w.qValue->setFont(monoFont(10.0f * s));
w.freqCaption->setFont(monoFont(10.0f * s));
w.freqValue->setFont(monoFont(10.0f * s));
}
repaint();
}
void GelykEQAudioProcessorEditor::resized()
{
juce::Rectangle<int> area = getLocalBounds();
const float s = scaling.load();
const int headerH = (int) (48 * s);
const int footerH = (int) (26 * s);
const int pad = (int) (12 * s);
// Header.
juce::Rectangle<int> header = area.removeFromTop(headerH);
titleLabel->setBounds(header.removeFromLeft((int) (180 * s)).reduced(pad, 0));
scaleBox->setBounds(header.removeFromRight((int) (90 * s)).reduced((int)(6*s), (int)(9*s)));
// Colour controls, just left of the scaling dropdown.
juce::Rectangle<int> colorArea = header.removeFromRight((int) (360 * s)).reduced((int)(2*s), (int)(9*s));
std::unique_ptr<juce::Slider>* colorSliders[] = { &waveSensSlider, &colorHueSlider, &colorSatSlider, &colorBriSlider, &waveSlider };
std::unique_ptr<juce::Label>* colorLabels[] = { &waveSensLabel, &colorHueLabel, &colorSatLabel, &colorBriLabel, &waveLabel };
int cellW = colorArea.getWidth() / 5;
for (int i = 0; i < 5; ++i)
{
juce::Rectangle<int> cell = colorArea.removeFromLeft(cellW);
(*colorLabels[i])->setBounds(cell.removeFromTop((int)(13*s)));
(*colorSliders[i])->setBounds(cell);
}
// Footer.
juce::Rectangle<int> footer = area.removeFromBottom(footerH);
footer.reduce(pad, 0);
statusBar->setBounds(footer.removeFromLeft(footer.getWidth() / 2));
statusGit->setBounds(footer);
// Master column (right).
const int masterW = (int) (64 * s);
juce::Rectangle<int> masterArea = area.removeFromRight(masterW);
const int x0 = area.getX();
const int y0 = area.getY();
const int totalW = area.getWidth();
const int height = area.getHeight();
const int gap = (int) (12 * s);
const int colW = (totalW - gap * (numBands - 1)) / numBands;
for (int i = 0; i < numBands; ++i)
{
auto& w = bands[i];
const int captionH = (int) (20 * s);
const int valueH = (int) (17 * s);
const int freqH = (int) (23 * s);
const int knobStackH = (int) (16 * s) + (int)(48 * s) + (int)(12 * s); // value + knob + caption
// Fixed, evenly spaced column for all labels and the knob row.
juce::Rectangle<int> col(x0 + i * (colW + gap), y0, colW, height);
w.gainCaption->setBounds(col.removeFromTop(captionH));
w.gainValue->setBounds(col.removeFromTop(valueH));
// Only the fader slides horizontally to the band's frequency on the shared
// log axis; everything below stays anchored to the fixed column.
juce::Rectangle<int> body = col;
const int faderH = body.getHeight() - freqH - knobStackH - (int)(6 * s);
const float f = processor.getAPVTS().getRawParameterValue("freq" + juce::String(i))->load();
const int cx = (int) bandFreqToX(f, (float) x0, (float) totalW);
const int faderX = juce::jlimit(x0, x0 + totalW - colW, cx - colW / 2);
w.gain->setBounds(faderX, body.getY(), colW, faderH);
w.gain->setDragSpace((float) x0, (float) totalW);
juce::Rectangle<int> rest = col;
rest.removeFromTop(faderH + (int)(6 * s));
w.freqLabel->setBounds(rest.removeFromTop(freqH));
// Bottom control row: LEVEL, FREQ and Q knobs side by side.
juce::Rectangle<int> knobRow = rest;
bandSections[i] = knobRow.toFloat();
int rowW = knobRow.getWidth();
int thirdW = rowW / 3;
// LEVEL (left third).
juce::Rectangle<int> levelCol = knobRow.removeFromLeft(thirdW);
w.levelValue->setBounds(levelCol.removeFromTop((int)(16*s)));
w.level->setBounds(levelCol.removeFromTop((int)(48*s)).reduced((int)(colW*0.08f), 0));
w.levelCaption->setBounds(levelCol.removeFromTop((int)(12*s)));
// FREQ (middle third).
juce::Rectangle<int> freqCol = knobRow.removeFromLeft(thirdW);
w.freqValue->setBounds(freqCol.removeFromTop((int)(16*s)));
w.freq->setBounds(freqCol.removeFromTop((int)(48*s)).reduced((int)(colW*0.08f), 0));
w.freqCaption->setBounds(freqCol.removeFromTop((int)(12*s)));
// Q (right third).
w.qValue->setBounds(knobRow.removeFromTop((int)(16*s)));
w.q->setBounds(knobRow.removeFromTop((int)(48*s)).reduced((int)(colW*0.08f), 0));
w.qCaption->setBounds(knobRow.removeFromTop((int)(12*s)));
}
// Master column.
juce::Rectangle<int> msub = masterArea.reduced((int)(14*s), (int)(6*s));
masterLabel->setBounds(msub.removeFromTop((int) (22 * s)));
masterPercent->setBounds(msub.removeFromTop((int) (16 * s)));
master->setBounds(msub);
}
void GelykEQAudioProcessorEditor::paint(juce::Graphics& g)
{
const float s = scaling.load();
g.fillAll(lookAndFeel.bg());
juce::Rectangle<int> area = getLocalBounds();
const int headerH = (int) (46 * s);
const int footerH = (int) (26 * s);
juce::Rectangle<int> mainArea = area;
mainArea.removeFromTop(headerH);
mainArea.removeFromBottom(footerH);
mainArea.removeFromRight((int) (64 * s));
// The visualizer is the gain-fader field, bounded above by the header controls
// and below by the LEVEL/FREQ/Q knob row. Keep the waveform/spectrum and grid
// strictly inside it so they don't spill over the knobs.
const float captionH = 20.0f * s;
const float valueH = 17.0f * s;
const float freqH = 23.0f * s;
const float knobStackH = (16.0f + 48.0f + 12.0f) * s;
const float topY = (float) mainArea.getY() + captionH + valueH;
const float botY = (float) mainArea.getBottom() - freqH - knobStackH - 6.0f * s;
// Neon grid lines.
g.setColour(lookAndFeel.line());
for (int i = 1; i <= 4; ++i)
{
if (i == 3) continue; // skip the 0 dB centre (drawn separately)
float yy = topY + (botY - topY) * (i / 5.0f);
g.drawHorizontalLine((int) yy, (float) mainArea.getX(), (float) mainArea.getRight());
}
// 0 dB centre line (glowing cyan).
const float centreLineY = topY + (botY - topY) * 0.5f;
g.setColour(lookAndFeel.cyan().withAlpha(0.55f));
g.drawHorizontalLine((int) centreLineY, (float) mainArea.getX(), (float) mainArea.getRight());
// Spectrum overlay.
if (spectrum.size() > 2)
{
const float waveAlpha = lookAndFeel.waveformOpacity;
const float sensFactor = std::pow(10.0f, lookAndFeel.waveSensitivity / 20.0f);
juce::Path path;
const float x0 = (float) mainArea.getX();
const float width = (float) mainArea.getWidth();
for (int i = 0; i < spectrum.size(); ++i)
{
float db = juce::Decibels::gainToDecibels(
juce::Decibels::decibelsToGain(spectrum[i]) * sensFactor);
db = juce::jlimit(-72.0f, 12.0f, db);
float norm = (db + 72.0f) / 84.0f; // 0..1 up
float xx = (i < freqBins.size())
? bandFreqToX(freqBins[i], x0, width)
: x0 + (float) i / (float) (spectrum.size() - 1) * width;
float yy = botY - norm * (botY - topY);
if (i == 0) path.startNewSubPath(xx, yy);
else path.lineTo(xx, yy);
}
juce::Path filled = path;
filled.lineTo((float) mainArea.getRight(), botY);
filled.lineTo((float) mainArea.getX(), botY);
filled.closeSubPath();
g.setGradientFill(juce::ColourGradient(lookAndFeel.magenta().withAlpha(0.30f * waveAlpha),
(float) mainArea.getX(), topY,
lookAndFeel.magenta().withAlpha(0.05f * waveAlpha),
(float) mainArea.getX(), botY, false));
g.fillPath(filled);
g.setColour(lookAndFeel.cyan().withAlpha(0.9f * waveAlpha));
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;
// eqCurve bins are log-spaced from 20 Hz to 20 kHz; use the same
// log-frequency mapping as the columns so peaks line up with bands.
const float f = 20.0f * std::pow(1000.0f, (float) i / (float) (eqCurve.size() - 1));
float xx = bandFreqToX(f, x0, 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));
}
// Very light rounded outline around each band's knob group (LEVEL/FREQ/Q).
g.setColour(lookAndFeel.line());
const float corner = 6.0f * s;
const float inset = 2.0f * s;
for (int i = 0; i < numBands; ++i)
{
juce::Rectangle<float> r = bandSections[i].reduced(inset);
g.drawRoundedRectangle(r, corner, 1.0f * s);
}
}
} // namespace gelyk