#include "PluginEditor.h" namespace gelyk { static juce::Font monoFont(float height) { return juce::Font(juce::FontOptions(juce::Font::getDefaultMonospacedFontName(), height, juce::Font::plain)); } // --------------------------------------------------------------------------- // NeonLookAndFeel // --------------------------------------------------------------------------- NeonLookAndFeel::NeonLookAndFeel() { 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, 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, 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)); } // --------------------------------------------------------------------------- // GelykEQAudioProcessorEditor // --------------------------------------------------------------------------- GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor& p) : AudioProcessorEditor(&p), processor(p) { setLookAndFeel(&lookAndFeel); setOpaque(true); titleLabel = std::make_unique("title", "GELYKEQ"); 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); // 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); masterAttachment = std::make_unique( processor.getAPVTS(), "master", *master); // 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(juce::Slider::LinearVertical, juce::Slider::NoTextBox); w.gain->setLookAndFeel(&lookAndFeel); w.gain->setSliderStyle(juce::Slider::LinearVertical); w.gain->setRange(-24.0, 24.0, 0.01); 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::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); }; 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); gainAttachments[i] = std::make_unique( processor.getAPVTS(), "gain" + juce::String(i), *w.gain); 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", "GELYKEQ v" + juce::String(ProjectInfo::versionString) + " | ANALYSER"); 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); 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::scaleUI(float percent) { const float s = percent / 100.0f; scaling.store(s); setSize((int) (1180 * s), (int) (640 * s)); resized(); } 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))); // 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]; juce::Rectangle col(x0 + i * (colW + gap), y0, colW, height); const int captionH = (int) (18 * s); const int valueH = (int) (15 * s); const int freqH = (int) (20 * 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 - knobStackH - (int)(6*s))); w.freqLabel->setBounds(col.removeFromTop(freqH)); // Bottom control row: FREQ and Q knobs side by side. juce::Rectangle knobRow = col; int rowW = knobRow.getWidth(); int halfW = rowW / 2; // FREQ (left half). juce::Rectangle 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. juce::Rectangle msub = masterArea.reduced((int)(14*s), (int)(6*s)); masterLabel->setBounds(msub.removeFromTop((int) (20 * 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)); const int topPad = (int) (20 * s); const int botPad = (int) (28 * s); const float topY = (float) (mainArea.getY() + topPad); const float botY = (float) (mainArea.getY() + mainArea.getHeight() - botPad); // 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) { 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(-72.0f, 12.0f, spectrum[i]); float norm = (db + 72.0f) / 84.0f; // 0..1 up float xx = 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), (float) mainArea.getX(), topY, lookAndFeel.magenta.withAlpha(0.05f), (float) mainArea.getX(), botY, false)); g.fillPath(filled); 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