#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::halfPi, toAngle + juce::MathConstants::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 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("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("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* label; std::unique_ptr* slider; const char* text; double min, max, def; const char* tip; } colorControls[] = { { &waveSensLabel, &waveSensSlider, "SENS", -48.0, 24.0, 0.0, "Live waveform sensitivity (dB offset)" }, { &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("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::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("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::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("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( 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("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("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("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("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("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(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 { 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("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("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::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::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("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("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::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(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( processor.getAPVTS(), "gain" + juce::String(i), *w.level); qAttachments[i] = std::make_unique( processor.getAPVTS(), "q" + juce::String(i), *w.q); freqAttachments[i] = std::make_unique( processor.getAPVTS(), "freq" + juce::String(i), *w.freq); } // Status bar. statusGit = std::make_unique("git", processor.getGitInfo()); statusGit->setFont(monoFont(11.0f)); statusGit->setColour(juce::Label::textColourId, lookAndFeel.textDim()); statusGit->setJustificationType(juce::Justification::centredRight); statusBar = std::make_unique("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 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 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 colorArea = header.removeFromRight((int) (360 * s)).reduced((int)(2*s), (int)(9*s)); std::unique_ptr* colorSliders[] = { &waveSensSlider, &colorHueSlider, &colorSatSlider, &colorBriSlider, &waveSlider }; std::unique_ptr* colorLabels[] = { &waveSensLabel, &colorHueLabel, &colorSatLabel, &colorBriLabel, &waveLabel }; int cellW = colorArea.getWidth() / 5; for (int i = 0; i < 5; ++i) { juce::Rectangle cell = colorArea.removeFromLeft(cellW); (*colorLabels[i])->setBounds(cell.removeFromTop((int)(13*s))); (*colorSliders[i])->setBounds(cell); } // Footer. juce::Rectangle 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 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 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 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 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 knobRow = rest; bandSections[i] = knobRow.toFloat(); int rowW = knobRow.getWidth(); int thirdW = rowW / 3; // LEVEL (left third). juce::Rectangle 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 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 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 area = getLocalBounds(); const int headerH = (int) (46 * s); const int footerH = (int) (26 * s); juce::Rectangle 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. The live waveform uses a fixed range tuned to real // per-FFT-bin levels: a single loud bin reads ~0 dB at the top edge, typical // content (~-20 dB) sits mid-screen, and -40 dB is the floor. SENS shifts the // whole trace up/down as a dB offset. if (spectrum.size() > 2) { const float waveAlpha = lookAndFeel.waveformOpacity; const float sensDb = lookAndFeel.waveSensitivity; // dB offset applied to the trace const float dbMax = 0.0f; // full-scale bin → top of the plot const float dbMin = -40.0f; // silence floor → bottom of the plot 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::jlimit(dbMin, dbMax, spectrum[i] + sensDb); float norm = (db - dbMin) / (dbMax - dbMin); // 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 r = bandSections[i].reduced(inset); g.drawRoundedRectangle(r, corner, 1.0f * s); } } } // namespace gelyk