mirror of
https://codeberg.org/armin/saftpumpe.git
synced 2026-09-01 04:10:46 +02:00
667 lines
29 KiB
C++
667 lines
29 KiB
C++
#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::Preset>& SaftpumpeEditor::getPresets() {
|
|
static const std::vector<Preset> 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<SliderAttachment>(processorRef.parameters, "threshold", thresholdSlider);
|
|
|
|
setupKnob(ratioSlider, ratioLabel, "RATIO", "ratio", 1.0, 20.0, 4.0);
|
|
ratioAttachment = std::make_unique<SliderAttachment>(processorRef.parameters, "ratio", ratioSlider);
|
|
|
|
setupKnob(attackSlider, attackLabel, "ATTACK", "attack", 0.001, 0.1, 0.03);
|
|
attackAttachment = std::make_unique<SliderAttachment>(processorRef.parameters, "attack", attackSlider);
|
|
|
|
setupKnob(releaseSlider, releaseLabel, "RELEASE", "release", 0.05, 1.5, 0.3);
|
|
releaseAttachment = std::make_unique<SliderAttachment>(processorRef.parameters, "release", releaseSlider);
|
|
|
|
setupKnob(kneeSlider, kneeLabel, "KNEE", "knee", 0.0, 12.0, 6.0);
|
|
kneeAttachment = std::make_unique<SliderAttachment>(processorRef.parameters, "knee", kneeSlider);
|
|
|
|
setupKnob(makeupSlider, makeupLabel, "MAKEUP", "makeup", 0.0, 24.0, 0.0);
|
|
makeupAttachment = std::make_unique<SliderAttachment>(processorRef.parameters, "makeup", makeupSlider);
|
|
|
|
setupKnob(mixSlider, mixLabel, "MIX", "mix", 0.0, 1.0, 1.0);
|
|
mixAttachment = std::make_unique<SliderAttachment>(processorRef.parameters, "mix", mixSlider);
|
|
|
|
setupKnob(autoReleaseSlider, autoReleaseLabel, "AUTO REL", "autoRelease", 0.0, 1.0, 1.0);
|
|
autoReleaseAttachment = std::make_unique<SliderAttachment>(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<int>(700 * uiScale), static_cast<int>(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<int>(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<float>(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<float>(gridLeft, contentTop, gridW, rowH);
|
|
rowBounds[1] = juce::Rectangle<float>(gridLeft, contentTop + rowH, gridW, rowH);
|
|
|
|
// Compute knob positions within each row
|
|
int knobsPerRow = 4;
|
|
float colW = gridW / static_cast<float>(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<float>(col) + colW / 2.0f;
|
|
float cy = rowBounds[row].getY() + rowH * 0.40f;
|
|
knobCentre[idx] = {cx, cy};
|
|
knobBounds[idx] = juce::Rectangle<float>(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<float>(rightX, bottomY, ctrlW, bottomH);
|
|
zoomArea = juce::Rectangle<float>(rightX, bottomY + bottomH * 0.48f, ctrlW, bottomH * 0.48f);
|
|
scopeArea = juce::Rectangle<float>(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<float>(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<int>(
|
|
static_cast<int>(knobCentre[idx].getX() - knobRadius),
|
|
static_cast<int>(knobCentre[idx].getY() - knobRadius - 10.0f * s - 4.0f * s - labelH),
|
|
static_cast<int>(knobDiam),
|
|
static_cast<int>(labelH));
|
|
label.setBounds(lb);
|
|
|
|
// LCD below knob
|
|
auto lcdB = juce::Rectangle<int>(
|
|
static_cast<int>(knobCentre[idx].getX() - knobRadius * 0.85f),
|
|
static_cast<int>(knobCentre[idx].getY() + knobRadius + 3.0f * s),
|
|
static_cast<int>(knobRadius * 1.7f),
|
|
static_cast<int>(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<int>(presetArea.getX()), static_cast<int>(presetArea.getY()),
|
|
static_cast<int>(presetArea.getWidth()), static_cast<int>(14.0f * s));
|
|
presetBox.setBounds(static_cast<int>(presetArea.getX()), static_cast<int>(presetArea.getY() + 14.0f * s),
|
|
static_cast<int>(presetArea.getWidth()), static_cast<int>(22.0f * s));
|
|
|
|
zoomLabel.setBounds(static_cast<int>(zoomArea.getX()), static_cast<int>(zoomArea.getY()),
|
|
static_cast<int>(zoomArea.getWidth()), static_cast<int>(14.0f * s));
|
|
zoomBox.setBounds(static_cast<int>(zoomArea.getX()), static_cast<int>(zoomArea.getY() + 14.0f * s),
|
|
static_cast<int>(zoomArea.getWidth()), static_cast<int>(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<float>(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<float>(sliders[i]->valueToProportionOfLength(sliders[i]->getValue()));
|
|
drawKnob(g, knobBounds[i], norm, norm);
|
|
}
|
|
|
|
// Meters
|
|
float grH = meterArea.getHeight() * 0.42f;
|
|
auto grBounds = juce::Rectangle<float>(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<float>(meterArea.getX(), ioY, ioW, ioH);
|
|
auto outB = juce::Rectangle<float>(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<float> 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<float>(i) / static_cast<float>(numTeeth)) * juce::MathConstants<float>::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<float> 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<float> 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<float> 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<float> 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<float>(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<float>(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<float>(thresholdSlider.getValue())));
|
|
updateLCD(ratioLCD, formatValue(1, static_cast<float>(ratioSlider.getValue())));
|
|
updateLCD(attackLCD, formatValue(2, static_cast<float>(attackSlider.getValue())));
|
|
updateLCD(releaseLCD, formatValue(3, static_cast<float>(releaseSlider.getValue())));
|
|
updateLCD(kneeLCD, formatValue(4, static_cast<float>(kneeSlider.getValue())));
|
|
updateLCD(makeupLCD, formatValue(5, static_cast<float>(makeupSlider.getValue())));
|
|
updateLCD(mixLCD, formatValue(6, static_cast<float>(mixSlider.getValue())));
|
|
updateLCD(autoReleaseLCD, formatValue(7, static_cast<float>(autoReleaseSlider.getValue())));
|
|
|
|
repaint();
|
|
}
|