#include "PluginEditor.h" static juce::String formatTime(float seconds) { if (seconds >= 1.0f) return juce::String(seconds, 2) + "s"; if (seconds >= 0.001f) return juce::String(seconds * 1000.0f, 1) + "ms"; return juce::String(seconds * 1000.0f, 1) + "ms"; } const std::vector& SaftpumpeEditor::getPresets() { static const std::vector presets = { {"-- None --", 0.0f, 1.0f, 0.030f, 0.30f, 6.0f, 0.0f, 1.0f, 1.0f}, {"Mastering 0dB", -2.0f, 2.0f, 0.020f, 0.20f, 6.0f, 0.0f, 1.0f, 1.0f}, {"Mastering -1dB", -6.0f, 2.5f, 0.015f, 0.18f, 6.0f, 1.0f, 1.0f, 1.0f}, {"Mastering -2dB", -10.0f, 3.0f, 0.010f, 0.15f, 8.0f, 1.5f, 1.0f, 1.0f}, {"Mastering -3dB", -14.0f, 4.0f, 0.010f, 0.12f, 8.0f, 2.0f, 1.0f, 1.0f}, {"Kick Punch", -18.0f, 6.0f, 0.003f, 0.10f, 10.0f, 3.0f, 0.8f, 0.0f}, }; return presets; } void SaftpumpeEditor::applyPreset(int index) { auto& presets = getPresets(); if (index < 0 || index >= (int)presets.size()) return; auto& p = presets[index]; thresholdSlider.setValue(p.threshold, juce::sendNotificationSync); ratioSlider.setValue(p.ratio, juce::sendNotificationSync); attackSlider.setValue(p.attack, juce::sendNotificationSync); releaseSlider.setValue(p.release, juce::sendNotificationSync); kneeSlider.setValue(p.knee, juce::sendNotificationSync); makeupSlider.setValue(p.makeup, juce::sendNotificationSync); mixSlider.setValue(p.mix, juce::sendNotificationSync); autoReleaseSlider.setValue(p.autoRelease, juce::sendNotificationSync); } SaftpumpeEditor::SaftpumpeEditor(SaftpumpeProcessor& p) : AudioProcessorEditor(&p), processorRef(p) { setSize(700, 500); setResizable(true, this); setResizeLimits(650, 480, 1100, 800); startTimerHz(30); auto setupKnob = [this](juce::Slider& s, juce::Label& l, const juce::String& text, const juce::String& pid, float min, float max, float def) { s.setLookAndFeel(&lnf); s.setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag); s.setTextBoxStyle(juce::Slider::NoTextBox, true, 0, 0); s.setRange(min, max, 0.001); s.setValue(def, juce::dontSendNotification); s.setPopupDisplayEnabled(true, true, this); addAndMakeVisible(s); l.setText(text, juce::dontSendNotification); l.setJustificationType(juce::Justification::centred); l.setColour(juce::Label::textColourId, textColour); l.setFont(juce::Font(juce::FontOptions(11.0f))); addAndMakeVisible(l); }; setupKnob(thresholdSlider, thresholdLabel, "THRESHOLD", "threshold", -48.0, 0.0, -20.0); thresholdAttachment = std::make_unique(processorRef.parameters, "threshold", thresholdSlider); setupKnob(ratioSlider, ratioLabel, "RATIO", "ratio", 1.0, 20.0, 4.0); ratioAttachment = std::make_unique(processorRef.parameters, "ratio", ratioSlider); setupKnob(attackSlider, attackLabel, "ATTACK", "attack", 0.001, 0.1, 0.03); attackAttachment = std::make_unique(processorRef.parameters, "attack", attackSlider); setupKnob(releaseSlider, releaseLabel, "RELEASE", "release", 0.05, 1.5, 0.3); releaseAttachment = std::make_unique(processorRef.parameters, "release", releaseSlider); setupKnob(kneeSlider, kneeLabel, "KNEE", "knee", 0.0, 12.0, 6.0); kneeAttachment = std::make_unique(processorRef.parameters, "knee", kneeSlider); setupKnob(makeupSlider, makeupLabel, "MAKEUP", "makeup", 0.0, 24.0, 0.0); makeupAttachment = std::make_unique(processorRef.parameters, "makeup", makeupSlider); setupKnob(mixSlider, mixLabel, "MIX", "mix", 0.0, 1.0, 1.0); mixAttachment = std::make_unique(processorRef.parameters, "mix", mixSlider); setupKnob(autoReleaseSlider, autoReleaseLabel, "AUTO REL", "autoRelease", 0.0, 1.0, 1.0); autoReleaseAttachment = std::make_unique(processorRef.parameters, "autoRelease", autoReleaseSlider); // LCD displays auto setupLCD = [this](juce::Label& lcd) { lcd.setJustificationType(juce::Justification::centred); lcd.setColour(juce::Label::textColourId, lcdText); lcd.setColour(juce::Label::backgroundColourId, lcdBg); lcd.setFont(juce::Font(juce::FontOptions(12.0f * uiScale).withStyle("Bold"))); addAndMakeVisible(lcd); }; setupLCD(thresholdLCD); setupLCD(ratioLCD); setupLCD(attackLCD); setupLCD(releaseLCD); setupLCD(kneeLCD); setupLCD(makeupLCD); setupLCD(mixLCD); setupLCD(autoReleaseLCD); // Zoom dropdown zoomLabel.setText("ZOOM", juce::dontSendNotification); zoomLabel.setJustificationType(juce::Justification::centredRight); zoomLabel.setColour(juce::Label::textColourId, textDim); zoomLabel.setFont(juce::Font(juce::FontOptions(10.0f))); addAndMakeVisible(zoomLabel); zoomBox.addItem("1.0x", 1); zoomBox.addItem("1.5x", 2); zoomBox.addItem("2.0x", 3); zoomBox.addItem("2.5x", 4); zoomBox.addItem("3.0x", 5); zoomBox.setSelectedId(2, juce::dontSendNotification); zoomBox.setColour(juce::ComboBox::backgroundColourId, grooveDark); zoomBox.setColour(juce::ComboBox::textColourId, textColour); zoomBox.setColour(juce::ComboBox::arrowColourId, textDim); zoomBox.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff333338)); zoomBox.addListener(this); addAndMakeVisible(zoomBox); // Preset dropdown presetLabel.setText("PRESET", juce::dontSendNotification); presetLabel.setJustificationType(juce::Justification::centredRight); presetLabel.setColour(juce::Label::textColourId, textDim); presetLabel.setFont(juce::Font(juce::FontOptions(10.0f))); addAndMakeVisible(presetLabel); auto& presets = getPresets(); for (int i = 0; i < (int)presets.size(); ++i) presetBox.addItem(presets[i].name, i + 1); presetBox.setSelectedId(1, juce::dontSendNotification); presetBox.setColour(juce::ComboBox::backgroundColourId, grooveDark); presetBox.setColour(juce::ComboBox::textColourId, textColour); presetBox.setColour(juce::ComboBox::arrowColourId, textDim); presetBox.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff333338)); presetBox.addListener(this); addAndMakeVisible(presetBox); } SaftpumpeEditor::~SaftpumpeEditor() { thresholdSlider.setLookAndFeel(nullptr); ratioSlider.setLookAndFeel(nullptr); attackSlider.setLookAndFeel(nullptr); releaseSlider.setLookAndFeel(nullptr); kneeSlider.setLookAndFeel(nullptr); makeupSlider.setLookAndFeel(nullptr); mixSlider.setLookAndFeel(nullptr); autoReleaseSlider.setLookAndFeel(nullptr); zoomBox.removeListener(this); presetBox.removeListener(this); stopTimer(); } void SaftpumpeEditor::comboBoxChanged(juce::ComboBox* box) { if (box == &zoomBox) { static const float scales[] = { 1.0f, 1.5f, 2.0f, 2.5f, 3.0f }; int id = box->getSelectedId(); if (id >= 1 && id <= 5) { uiScale = scales[id - 1]; setSize(static_cast(700 * uiScale), static_cast(500 * uiScale)); } } else if (box == &presetBox) { int id = box->getSelectedId() - 1; applyPreset(id); } } juce::String SaftpumpeEditor::formatValue(int idx, float val) { switch (idx) { case 0: return juce::String(val, 1) + " dB"; case 1: return juce::String(val, 1) + ":1"; case 2: return formatTime(val); case 3: return formatTime(val); case 4: return juce::String(val, 1) + " dB"; case 5: return juce::String(val, 1) + " dB"; case 6: return juce::String(static_cast(val * 100)) + "%"; case 7: return val > 0.5f ? "ON" : "OFF"; default: return {}; } } void SaftpumpeEditor::computeLayout() { float s = uiScale; auto bounds = getLocalBounds().toFloat(); auto faceBounds = bounds.reduced(8.0f * s); // Header float headerH = 28.0f * s + 16.0f * s + 8.0f * s; float headerY = faceBounds.getY() + 6.0f * s; float contentTop = headerY + headerH + 4.0f * s; // Meters — right column float meterW = 56.0f * s; meterArea = juce::Rectangle(faceBounds.getRight() - meterW - 14.0f * s, contentTop, meterW, faceBounds.getBottom() - contentTop - 48.0f * s); // Knob grid area — 2 rows x 4 cols float gridLeft = faceBounds.getX() + 14.0f * s; float gridW = meterArea.getX() - gridLeft - 14.0f * s; float gridH = meterArea.getHeight(); float rowH = gridH / 2.0f; rowBounds[0] = juce::Rectangle(gridLeft, contentTop, gridW, rowH); rowBounds[1] = juce::Rectangle(gridLeft, contentTop + rowH, gridW, rowH); // Compute knob positions within each row int knobsPerRow = 4; float colW = gridW / static_cast(knobsPerRow); knobRadius = juce::jmin(colW * 0.30f, rowH * 0.28f); for (int row = 0; row < 2; ++row) { for (int col = 0; col < knobsPerRow; ++col) { int idx = row * knobsPerRow + col; float cx = rowBounds[row].getX() + colW * static_cast(col) + colW / 2.0f; float cy = rowBounds[row].getY() + rowH * 0.40f; knobCentre[idx] = {cx, cy}; knobBounds[idx] = juce::Rectangle(cx - knobRadius, cy - knobRadius, knobRadius * 2.0f, knobRadius * 2.0f); } } // Bottom: scope + zoom + preset float bottomY = meterArea.getBottom() + 8.0f * s; float bottomH = faceBounds.getBottom() - bottomY - 10.0f * s; float ctrlW = 110.0f * s; float ctrlGap = 6.0f * s; float rightX = faceBounds.getRight() - ctrlW - 14.0f * s; presetArea = juce::Rectangle(rightX, bottomY, ctrlW, bottomH); zoomArea = juce::Rectangle(rightX, bottomY + bottomH * 0.48f, ctrlW, bottomH * 0.48f); scopeArea = juce::Rectangle(faceBounds.getX() + 14.0f * s, bottomY, presetArea.getX() - faceBounds.getX() - 24.0f * s, bottomH); } void SaftpumpeEditor::resized() { computeLayout(); float s = uiScale; float labelH = 14.0f * s; float lcdH = 18.0f * s; float knobDiam = knobRadius * 2.0f; float arcMargin = 14.0f * s; auto setKnobBounds = [&](juce::Slider& slider, juce::Label& label, juce::Label& lcd, int idx) { // Slider gets arc area (larger than knob body for arc meter) float arcR = knobRadius + arcMargin; auto arcB = juce::Rectangle(knobCentre[idx].getX() - arcR, knobCentre[idx].getY() - arcR, arcR * 2.0f, arcR * 2.0f); slider.setBounds(arcB.toNearestInt()); // Label above arc auto lb = juce::Rectangle( static_cast(knobCentre[idx].getX() - knobRadius), static_cast(knobCentre[idx].getY() - knobRadius - 10.0f * s - 4.0f * s - labelH), static_cast(knobDiam), static_cast(labelH)); label.setBounds(lb); // LCD below knob auto lcdB = juce::Rectangle( static_cast(knobCentre[idx].getX() - knobRadius * 0.85f), static_cast(knobCentre[idx].getY() + knobRadius + 3.0f * s), static_cast(knobRadius * 1.7f), static_cast(lcdH)); lcd.setBounds(lcdB); }; setKnobBounds(thresholdSlider, thresholdLabel, thresholdLCD, 0); setKnobBounds(ratioSlider, ratioLabel, ratioLCD, 1); setKnobBounds(attackSlider, attackLabel, attackLCD, 2); setKnobBounds(releaseSlider, releaseLabel, releaseLCD, 3); setKnobBounds(kneeSlider, kneeLabel, kneeLCD, 4); setKnobBounds(makeupSlider, makeupLabel, makeupLCD, 5); setKnobBounds(mixSlider, mixLabel, mixLCD, 6); setKnobBounds(autoReleaseSlider, autoReleaseLabel, autoReleaseLCD, 7); presetLabel.setBounds(static_cast(presetArea.getX()), static_cast(presetArea.getY()), static_cast(presetArea.getWidth()), static_cast(14.0f * s)); presetBox.setBounds(static_cast(presetArea.getX()), static_cast(presetArea.getY() + 14.0f * s), static_cast(presetArea.getWidth()), static_cast(22.0f * s)); zoomLabel.setBounds(static_cast(zoomArea.getX()), static_cast(zoomArea.getY()), static_cast(zoomArea.getWidth()), static_cast(14.0f * s)); zoomBox.setBounds(static_cast(zoomArea.getX()), static_cast(zoomArea.getY() + 14.0f * s), static_cast(zoomArea.getWidth()), static_cast(22.0f * s)); } void SaftpumpeEditor::paint(juce::Graphics& g) { if (rowBounds[0].getWidth() <= 0) computeLayout(); auto bounds = getLocalBounds().toFloat(); float s = uiScale; auto faceBounds = bounds.reduced(8.0f * s); // Dark bezel g.setColour(bezelColour); g.fillRect(bounds); // Faceplate juce::ColourGradient faceGrad(faceplateTop, faceBounds.getX(), faceBounds.getY(), faceplateBot, faceBounds.getX(), faceBounds.getBottom(), false); g.setGradientFill(faceGrad); g.fillRoundedRectangle(faceBounds, 6.0f * s); // Brushed texture g.setColour(juce::Colours::white.withAlpha(0.02f)); for (float y = faceBounds.getY(); y < faceBounds.getBottom(); y += 2.0f * s) g.drawLine(faceBounds.getX(), y, faceBounds.getRight(), y, 0.4f); // Edge g.setColour(juce::Colours::white.withAlpha(0.06f)); g.drawLine(faceBounds.getX() + 4, faceBounds.getY() + 1, faceBounds.getRight() - 4, faceBounds.getY() + 1, 1.0f); g.setColour(juce::Colours::black.withAlpha(0.4f)); g.drawLine(faceBounds.getX() + 4, faceBounds.getBottom() - 1, faceBounds.getRight() - 4, faceBounds.getBottom() - 1, 1.0f); // Screws float screwR = 3.5f * s; float screwInset = 14.0f * s; drawScrew(g, faceBounds.getX() + screwInset, faceBounds.getY() + screwInset, screwR); drawScrew(g, faceBounds.getRight() - screwInset, faceBounds.getY() + screwInset, screwR); drawScrew(g, faceBounds.getX() + screwInset, faceBounds.getBottom() - screwInset, screwR); drawScrew(g, faceBounds.getRight() - screwInset, faceBounds.getBottom() - screwInset, screwR); // Title auto titleArea = juce::Rectangle(faceBounds.getX() + 30.0f * s, faceBounds.getY() + 6.0f * s, faceBounds.getWidth() - 60.0f * s, 28.0f * s); g.setColour(textBright); g.setFont(juce::Font(juce::FontOptions(22.0f * s).withStyle("Bold"))); g.drawText("SAFTPUMPE", titleArea, juce::Justification::centred); g.setColour(textDim.withAlpha(0.2f)); g.drawText("SAFTPUMPE", titleArea.translated(0, 1.0f), juce::Justification::centred); // Subtitle auto subArea = titleArea.translated(0, 22.0f * s).withHeight(16.0f * s); g.setColour(textDim); g.setFont(juce::Font(juce::FontOptions(11.0f * s))); g.drawText("BUS COMPRESSOR", subArea, juce::Justification::centred); // Accent line float lineY = subArea.getBottom() + 4.0f * s; g.setColour(accentLine.withAlpha(0.25f)); g.drawLine(faceBounds.getX() + 20.0f * s, lineY, faceBounds.getRight() - 20.0f * s, lineY, 1.0f); // Draw knobs juce::Slider* sliders[] = { &thresholdSlider, &ratioSlider, &attackSlider, &releaseSlider, &kneeSlider, &makeupSlider, &mixSlider, &autoReleaseSlider }; juce::Label* lcds[] = { &thresholdLCD, &ratioLCD, &attackLCD, &releaseLCD, &kneeLCD, &makeupLCD, &mixLCD, &autoReleaseLCD }; const char* labels[] = { "THRESHOLD", "RATIO", "ATTACK", "RELEASE", "KNEE", "MAKEUP", "MIX", "AUTO REL" }; const juce::StringArray marks[] = { {"0", "-12", "-24", "-36", "-48"}, {"20:1", "10:1", "6:1", "4:1", "2:1", "1:1"}, {"100m", "30m", "10m", "3m", "1m"}, {"1.5s", "500m", "100m", "50m"}, {"12", "6", "0"}, {"24", "12", "0"}, {"100", "50", "0"}, {"OFF", "ON"} }; for (int i = 0; i < 8; ++i) { float norm = static_cast(sliders[i]->valueToProportionOfLength(sliders[i]->getValue())); drawKnob(g, knobBounds[i], norm, norm); } // Meters float grH = meterArea.getHeight() * 0.42f; auto grBounds = juce::Rectangle(meterArea.getX(), meterArea.getY(), meterArea.getWidth(), grH); float gr = processorRef.getGainReductionMeter(); drawMeter(g, grBounds, gr, 0.0f, 24.0f, true); drawPeakHold(g, grBounds, grPeak.peak, true); g.setColour(textDim); g.setFont(juce::Font(juce::FontOptions(10.0f * s))); g.drawText("GR", grBounds.translated(0, grBounds.getHeight() + 2.0f * s), juce::Justification::centredTop); float ioY = grBounds.getBottom() + 16.0f * s; float ioH = meterArea.getBottom() - ioY - 14.0f * s; float ioW = (meterArea.getWidth() - 3.0f * s) / 2.0f; auto inB = juce::Rectangle(meterArea.getX(), ioY, ioW, ioH); auto outB = juce::Rectangle(meterArea.getX() + ioW + 3.0f * s, ioY, ioW, ioH); drawMeter(g, inB, processorRef.getInputLevel(), -60.0f, 0.0f, false); drawMeter(g, outB, processorRef.getOutputLevel(), -60.0f, 0.0f, false); drawPeakHold(g, inB, inPeak.peak); drawPeakHold(g, outB, outPeak.peak); g.drawText("IN", inB.translated(0, ioH + 2.0f * s), juce::Justification::centredTop); g.drawText("OUT", outB.translated(0, ioH + 2.0f * s), juce::Justification::centredTop); // Scope drawScope(g, scopeArea); // Zoom + Preset area borders g.setColour(juce::Colour(0xff333338)); g.drawRoundedRectangle(zoomArea.reduced(2.0f * s), 2.0f * s, 0.5f); g.drawRoundedRectangle(presetArea.reduced(2.0f * s), 2.0f * s, 0.5f); } void SaftpumpeEditor::drawKnob(juce::Graphics& g, juce::Rectangle bounds, float normalizedValue, float arcFillFrac) { float s = uiScale; float cx = bounds.getCentreX(); float cy = bounds.getCentreY(); float r = bounds.getWidth() / 2.0f; // Arc meter around the knob float arcR = r + 10.0f * s; float arcThickness = 3.0f * s; auto buildArc = [&](float fromFrac, float toFrac) { juce::Path p; float a0 = startAngle + fromFrac * (endAngle - startAngle); float a1 = startAngle + toFrac * (endAngle - startAngle); int steps = juce::jmax(2, (int) std::ceil(std::abs(a1 - a0) / 0.15f)); float da = (a1 - a0) / (float) steps; p.startNewSubPath(cx + std::cos(a0) * arcR, cy + std::sin(a0) * arcR); for (int i = 1; i <= steps; ++i) { float a = a0 + da * (float) i; p.lineTo(cx + std::cos(a) * arcR, cy + std::sin(a) * arcR); } return p; }; // Background arc track g.setColour(juce::Colour(0xff3a3a42)); g.strokePath(buildArc(0.0f, 1.0f), juce::PathStrokeType(arcThickness)); // Filled arc — colored by parameter zone if (arcFillFrac > 0.001f) { juce::Colour arcCol; if (arcFillFrac < 0.5f) arcCol = greenLed.interpolatedWith(amberLed, arcFillFrac * 2.0f); else arcCol = amberLed.interpolatedWith(redLed, (arcFillFrac - 0.5f) * 2.0f); g.setColour(arcCol.withAlpha(0.9f)); g.strokePath(buildArc(0.0f, arcFillFrac), juce::PathStrokeType(arcThickness)); } // Knob shadow — soft outer + hard inner g.setColour(juce::Colours::black.withAlpha(0.3f)); g.fillEllipse(cx - r - 3.0f * s, cy - r + 3.0f * s, r * 2.0f + 6.0f * s, r * 2.0f + 6.0f * s); g.setColour(juce::Colours::black.withAlpha(0.6f)); g.fillEllipse(cx - r - 1.0f, cy - r + 2.0f * s, r * 2.0f + 2.0f * s, r * 2.0f + 2.0f * s); // Outer bevel ring — catches light on top, shadow on bottom float bevelR = r + 1.0f * s; { juce::ColourGradient bevelGrad(juce::Colour(0xff5a5a60), cx - bevelR, cy - bevelR, juce::Colour(0xff2a2a2e), cx + bevelR, cy + bevelR, true); g.setGradientFill(bevelGrad); g.fillEllipse(cx - bevelR, cy - bevelR, bevelR * 2.0f, bevelR * 2.0f); } // Knob body — radial gradient for 3D dome effect { juce::ColourGradient bodyGrad(juce::Colour(0xff9a9aa0), cx, cy - r * 0.3f, juce::Colour(0xff3a3a40), cx, cy + r, true); bodyGrad.addColour(0.3, juce::Colour(0xff808086)); bodyGrad.addColour(0.7, juce::Colour(0xff505056)); g.setGradientFill(bodyGrad); g.fillEllipse(bounds); } // Knurling — fine radial lines around the edge { float knurlR = r - 1.0f * s; float knurlInner = r - 4.0f * s; int numTeeth = 40; g.setColour(juce::Colours::black.withAlpha(0.12f)); for (int i = 0; i < numTeeth; ++i) { float a = (static_cast(i) / static_cast(numTeeth)) * juce::MathConstants::twoPi; float ca = std::cos(a); float sa = std::sin(a); g.drawLine(cx + ca * knurlInner, cy + sa * knurlInner, cx + ca * knurlR, cy + sa * knurlR, 0.6f); } } // Inner chamfer ring g.setColour(juce::Colours::white.withAlpha(0.06f)); g.drawEllipse(bounds.reduced(3.0f * s), 0.8f); g.setColour(juce::Colours::black.withAlpha(0.15f)); g.drawEllipse(bounds.reduced(1.5f * s), 0.8f); // Specular highlight — top-left crescent { juce::ColourGradient specGrad(juce::Colours::white.withAlpha(0.18f), cx - r * 0.2f, cy - r * 0.5f, juce::Colours::white.withAlpha(0.0f), cx + r * 0.3f, cy + r * 0.1f, true); g.setGradientFill(specGrad); g.fillEllipse(cx - r * 0.6f, cy - r * 0.8f, r * 1.0f, r * 0.55f); } // Bottom rim shadow inside the body g.setColour(juce::Colours::black.withAlpha(0.12f)); g.fillEllipse(cx - r * 0.7f, cy + r * 0.3f, r * 1.4f, r * 0.6f); // Pointer line — wider with subtle glow float angle = startAngle + normalizedValue * (endAngle - startAngle); float pIn = r * 0.22f; float pOut = r * 0.72f; float px1 = cx + std::cos(angle) * pIn; float py1 = cy + std::sin(angle) * pIn; float px2 = cx + std::cos(angle) * pOut; float py2 = cy + std::sin(angle) * pOut; // Glow behind pointer g.setColour(juce::Colours::white.withAlpha(0.12f)); g.drawLine(px1, py1, px2, py2, 4.0f * s); // Pointer itself g.setColour(juce::Colours::white.withAlpha(0.85f)); g.drawLine(px1, py1, px2, py2, 1.8f * s); // Dark edge for definition g.setColour(juce::Colours::black.withAlpha(0.2f)); g.drawLine(px1, py1, px2, py2, 0.4f); } void SaftpumpeEditor::drawLCD(juce::Graphics& g, juce::Rectangle bounds, const juce::String& text) { float s = uiScale; g.setColour(lcdBg); g.fillRoundedRectangle(bounds, 2.0f * s); g.setColour(juce::Colour(0xff1a2a1a)); g.drawRoundedRectangle(bounds.expanded(0.5f), 2.0f * s, 1.0f); g.setColour(lcdText); g.setFont(juce::Font(juce::FontOptions(12.0f * s).withStyle("Bold"))); g.drawText(text, bounds, juce::Justification::centred); } void SaftpumpeEditor::drawMeter(juce::Graphics& g, juce::Rectangle bounds, float level, float minDb, float maxDb, bool isGainReduction) { float s = uiScale; g.setColour(grooveDark); g.fillRoundedRectangle(bounds, 2.0f * s); g.setColour(juce::Colours::black.withAlpha(0.5f)); g.drawRoundedRectangle(bounds.expanded(0.5f), 2.0f * s, 1.0f); float norm = juce::jlimit(0.0f, 1.0f, (level - minDb) / (maxDb - minDb)); if (norm > 0.005f) { float h = bounds.getHeight() * norm; float y = isGainReduction ? bounds.getY() : bounds.getBottom() - h; juce::ColourGradient grad; if (isGainReduction) { grad = juce::ColourGradient(greenLed, 0.0f, y, redLed, 0.0f, y + h, false); } else { grad = juce::ColourGradient(greenLed, 0.0f, bounds.getBottom(), amberLed, 0.0f, y, false); if (norm > 0.8f) grad.addColour(1.0, redLed); } g.setGradientFill(grad); g.fillRect(bounds.getX() + 1.0f, y, bounds.getWidth() - 2.0f, h); } } void SaftpumpeEditor::drawPeakHold(juce::Graphics& g, juce::Rectangle bounds, float peakNorm, bool fromTop) { float s = uiScale; if (peakNorm < 0.005f) return; float y = fromTop ? bounds.getY() + bounds.getHeight() * peakNorm : bounds.getBottom() - bounds.getHeight() * peakNorm; g.setColour(juce::Colours::white.withAlpha(0.85f)); g.fillRect(bounds.getX() + 1.0f, y - 1.0f, bounds.getWidth() - 2.0f, 2.0f); } void SaftpumpeEditor::drawScope(juce::Graphics& g, juce::Rectangle bounds) { float s = uiScale; g.setColour(grooveDark); g.fillRoundedRectangle(bounds, 3.0f * s); g.setColour(juce::Colours::black.withAlpha(0.4f)); g.drawRoundedRectangle(bounds.expanded(0.5f), 3.0f * s, 1.0f); g.setColour(accentLine.withAlpha(0.06f)); float centerY = bounds.getCentreY(); g.drawLine(bounds.getX() + 2.0f, centerY, bounds.getRight() - 2.0f, centerY, 0.5f); for (int i = 1; i <= 4; ++i) { float gx = bounds.getX() + bounds.getWidth() * (static_cast(i) / 4.0f); g.drawLine(gx, bounds.getY() + 2.0f, gx, bounds.getBottom() - 2.0f, 0.3f); } auto& inData = processorRef.inputScope; auto& outData = processorRef.outputScope; int numPoints = processorRef.scopeSize; if (numPoints < 2) return; float width = bounds.getWidth() - 4.0f; float halfH = bounds.getHeight() / 2.0f - 2.0f; float xStep = width / static_cast(numPoints - 1); float ox = bounds.getX() + 2.0f; { juce::Path path; float x = ox; path.startNewSubPath(x, centerY - inData[0] * halfH); for (int i = 1; i < numPoints; ++i) { x += xStep; path.lineTo(x, centerY - inData[i] * halfH); } g.setColour(redLed.withAlpha(0.15f)); g.strokePath(path, juce::PathStrokeType(4.0f)); g.setColour(redLed.withAlpha(0.8f)); g.strokePath(path, juce::PathStrokeType(1.5f)); } { juce::Path path; float x = ox; path.startNewSubPath(x, centerY - outData[0] * halfH); for (int i = 1; i < numPoints; ++i) { x += xStep; path.lineTo(x, centerY - outData[i] * halfH); } g.setColour(greenLed.withAlpha(0.12f)); g.strokePath(path, juce::PathStrokeType(3.0f)); g.setColour(greenLed.withAlpha(0.65f)); g.strokePath(path, juce::PathStrokeType(1.0f)); } float legendY = bounds.getBottom() - 12.0f * s; float legendX = bounds.getX() + 6.0f * s; g.setFont(juce::Font(juce::FontOptions(9.0f * s))); g.setColour(redLed.withAlpha(0.8f)); g.drawText("IN", legendX, legendY, 20.0f * s, 10.0f * s, juce::Justification::centredLeft); g.setColour(greenLed.withAlpha(0.65f)); g.drawText("OUT", legendX + 24.0f * s, legendY, 26.0f * s, 10.0f * s, juce::Justification::centredLeft); } void SaftpumpeEditor::drawScrew(juce::Graphics& g, float cx, float cy, float radius) { juce::ColourGradient grad(juce::Colour(0xff3a3a40), cx - radius, cy - radius, juce::Colour(0xff1a1a1e), cx + radius, cy + radius, true); g.setGradientFill(grad); g.fillEllipse(cx - radius, cy - radius, radius * 2.0f, radius * 2.0f); g.setColour(juce::Colour(0xff555558)); float cs = radius * 0.6f; g.drawLine(cx - cs, cy, cx + cs, cy, 0.8f); g.drawLine(cx, cy - cs, cx, cy + cs, 0.8f); g.setColour(juce::Colours::white.withAlpha(0.15f)); g.fillEllipse(cx - radius * 0.3f, cy - radius * 0.5f, radius * 0.4f, radius * 0.25f); } void SaftpumpeEditor::timerCallback() { double now = juce::Time::getMillisecondCounterHiRes(); auto updatePeak = [&](PeakHold& pk, float level) { if (level > pk.peak || (now - pk.peakTime) > PeakHold::holdMs) { pk.peak = level; pk.peakTime = now; } }; float grNorm = juce::jlimit(0.0f, 1.0f, processorRef.getGainReductionMeter() / 24.0f); float inNorm = juce::jlimit(0.0f, 1.0f, (processorRef.getInputLevel() - (-60.0f)) / (0.0f - (-60.0f))); float outNorm = juce::jlimit(0.0f, 1.0f, (processorRef.getOutputLevel() - (-60.0f)) / (0.0f - (-60.0f))); updatePeak(grPeak, grNorm); updatePeak(inPeak, inNorm); updatePeak(outPeak, outNorm); // Update LCD displays auto updateLCD = [](juce::Label& lcd, const juce::String& text) { if (lcd.getText() != text) lcd.setText(text, juce::dontSendNotification); }; updateLCD(thresholdLCD, formatValue(0, static_cast(thresholdSlider.getValue()))); updateLCD(ratioLCD, formatValue(1, static_cast(ratioSlider.getValue()))); updateLCD(attackLCD, formatValue(2, static_cast(attackSlider.getValue()))); updateLCD(releaseLCD, formatValue(3, static_cast(releaseSlider.getValue()))); updateLCD(kneeLCD, formatValue(4, static_cast(kneeSlider.getValue()))); updateLCD(makeupLCD, formatValue(5, static_cast(makeupSlider.getValue()))); updateLCD(mixLCD, formatValue(6, static_cast(mixSlider.getValue()))); updateLCD(autoReleaseLCD, formatValue(7, static_cast(autoReleaseSlider.getValue()))); repaint(); }