This commit is contained in:
Armin 2026-07-12 14:18:24 +02:00
commit 6391b75d15
9 changed files with 1230 additions and 0 deletions

119
Source/CompressorDSP.h Normal file
View file

@ -0,0 +1,119 @@
#pragma once
#include <cmath>
#include <algorithm>
class CompressorDSP {
public:
CompressorDSP() = default;
void prepare(double sampleRate, int samplesPerBlock) {
this->sampleRate = sampleRate;
envelope = 0.0;
gainReduction = 0.0;
}
void reset() {
envelope = 0.0;
gainReduction = 0.0;
}
struct Params {
float threshold = -20.0f; // dB
float ratio = 4.0f; // 2:1 to 8:1
float attack = 0.03f; // seconds
float release = 0.3f; // seconds (auto mode uses program-dependent)
float knee = 6.0f; // dB soft knee
float makeup = 0.0f; // dB
bool autoRelease = true;
float mix = 1.0f; // dry/wet
};
void setParams(const Params& p) {
params = p;
// Convert times to coefficients with analog-style smoothing
attackCoeff = std::exp(-1.0 / (sampleRate * std::max(0.001f, params.attack)));
// Auto release: program-dependent (faster for transients, slower for sustained)
if (params.autoRelease) {
releaseCoeff = std::exp(-1.0 / (sampleRate * 0.4));
} else {
releaseCoeff = std::exp(-1.0 / (sampleRate * std::max(0.01f, params.release)));
}
}
// Process a single sample (mono)
float processSample(float input) {
float absInput = std::abs(input);
// Input level in dB
float inputDb = (absInput > 1e-10f) ? 20.0f * std::log10(absInput) : -100.0f;
// Calculate desired gain reduction with soft knee
float gainDb = 0.0f;
float overThreshold = inputDb - params.threshold;
if (overThreshold > -params.knee / 2.0f) {
if (overThreshold < params.knee / 2.0f) {
// Soft knee region - smooth transition
float x = overThreshold + params.knee / 2.0f;
gainDb = (1.0f / params.ratio - 1.0f) * (x * x) / (2.0f * params.knee);
} else {
// Above knee - full compression
gainDb = overThreshold * (1.0f / params.ratio - 1.0f);
}
}
// Smooth envelope follower (analog-style ballistics)
float coeff = (gainDb < envelope) ? attackCoeff : releaseCoeff;
// Auto release: faster release for small reductions, slower for big hits
if (params.autoRelease && gainDb < envelope) {
float reductionAmount = std::abs(envelope);
float autoReleaseAdj = std::clamp(reductionAmount / 12.0f, 0.2f, 1.0f);
coeff = std::exp(-1.0 / (sampleRate * 0.15 * autoReleaseAdj));
}
envelope = coeff * envelope + (1.0f - coeff) * gainDb;
// Apply makeup gain and convert to linear
float totalGainDb = envelope + params.makeup;
float gain = std::pow(10.0f, totalGainDb / 20.0f);
// Store for metering
gainReduction = envelope;
// Apply compression with analog saturation
float compressed = input * gain;
// Subtle analog saturation (soft clipping like VCA bus compressor)
compressed = softClip(compressed);
// Dry/wet mix
float output = input * (1.0f - params.mix) + compressed * params.mix;
return output;
}
float getGainReduction() const { return gainReduction; }
private:
double sampleRate = 44100.0;
Params params;
float attackCoeff = 0.0f;
float releaseCoeff = 0.0f;
float envelope = 0.0f;
float gainReduction = 0.0f;
// Analog-style soft clipping (tanh-like)
float softClip(float x) {
// Gentle saturation that adds warmth
float drive = 1.2f; // Subtle drive
x *= drive;
// Tanh saturation for musical distortion
float out = std::tanh(x);
return out;
}
};

667
Source/PluginEditor.cpp Normal file
View file

@ -0,0 +1,667 @@
#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();
}

105
Source/PluginEditor.h Normal file
View file

@ -0,0 +1,105 @@
#pragma once
#include "PluginProcessor.h"
#include <juce_audio_processors/juce_audio_processors.h>
class SaftpumpeEditor : public juce::AudioProcessorEditor,
public juce::Timer,
public juce::ComboBox::Listener {
public:
SaftpumpeEditor(SaftpumpeProcessor&);
~SaftpumpeEditor() override;
void paint(juce::Graphics&) override;
void resized() override;
void timerCallback() override;
void comboBoxChanged(juce::ComboBox* box) override;
private:
SaftpumpeProcessor& processorRef;
float uiScale = 1.5f;
// Dark industrial palette
juce::Colour bezelColour{0xff0a0a0c};
juce::Colour faceplateTop{0xff222226};
juce::Colour faceplateBot{0xff161618};
juce::Colour knobLight{0xff72727a};
juce::Colour knobDark{0xff3a3a40};
juce::Colour grooveDark{0xff111114};
juce::Colour greenLed{0xff00e676};
juce::Colour redLed{0xffe63333};
juce::Colour amberLed{0xffe6a600};
juce::Colour textColour{0xffb0b0b6};
juce::Colour textDim{0xff6a6a70};
juce::Colour textBright{0xffe0e0e4};
juce::Colour accentLine{0xff00e676};
juce::Colour lcdBg{0xff080c08};
juce::Colour lcdText{0xff00cc66};
juce::Slider thresholdSlider, ratioSlider, attackSlider, releaseSlider;
juce::Slider kneeSlider, makeupSlider, mixSlider, autoReleaseSlider;
juce::Label thresholdLabel, ratioLabel, attackLabel, releaseLabel;
juce::Label kneeLabel, makeupLabel, mixLabel, autoReleaseLabel;
// LCD value displays
juce::Label thresholdLCD, ratioLCD, attackLCD, releaseLCD;
juce::Label kneeLCD, makeupLCD, mixLCD, autoReleaseLCD;
juce::ComboBox zoomBox;
juce::Label zoomLabel;
juce::ComboBox presetBox;
juce::Label presetLabel;
using SliderAttachment = juce::AudioProcessorValueTreeState::SliderAttachment;
std::unique_ptr<SliderAttachment> thresholdAttachment, ratioAttachment, attackAttachment;
std::unique_ptr<SliderAttachment> releaseAttachment, kneeAttachment, makeupAttachment;
std::unique_ptr<SliderAttachment> mixAttachment, autoReleaseAttachment;
juce::Rectangle<float> meterArea;
juce::Rectangle<float> scopeArea;
juce::Rectangle<float> zoomArea;
juce::Rectangle<float> rowBounds[2];
juce::Rectangle<float> knobBounds[8];
juce::Point<float> knobCentre[8];
float knobRadius = 0.0f;
struct PeakHold {
float peak = 0.0f;
double peakTime = 0.0;
static constexpr double holdMs = 1000.0;
} grPeak, inPeak, outPeak;
void computeLayout();
void drawKnob(juce::Graphics& g, juce::Rectangle<float> bounds, float normalizedValue,
float arcFillFrac);
void drawLCD(juce::Graphics& g, juce::Rectangle<float> bounds, const juce::String& text);
void drawMeter(juce::Graphics& g, juce::Rectangle<float> bounds,
float level, float minDb, float maxDb, bool isGainReduction);
void drawPeakHold(juce::Graphics& g, juce::Rectangle<float> bounds,
float peakNorm, bool fromTop = false);
void drawScope(juce::Graphics& g, juce::Rectangle<float> bounds);
void drawScrew(juce::Graphics& g, float cx, float cy, float radius);
static constexpr float startAngle = juce::MathConstants<float>::pi * 0.75f;
static constexpr float endAngle = juce::MathConstants<float>::pi * 2.25f;
juce::String formatValue(int idx, float val);
struct Preset {
const char* name;
float threshold, ratio, attack, release, knee, makeup, mix, autoRelease;
};
static const std::vector<Preset>& getPresets();
void applyPreset(int index);
juce::Rectangle<float> presetArea;
struct NoDotLookAndFeel : juce::LookAndFeel_V4 {
void drawRotarySlider(juce::Graphics&, int, int, int, int,
float, float, float, juce::Slider&) override {}
} lnf;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SaftpumpeEditor)
};

126
Source/PluginProcessor.cpp Normal file
View file

@ -0,0 +1,126 @@
#include "PluginProcessor.h"
#include "PluginEditor.h"
SaftpumpeProcessor::SaftpumpeProcessor()
: AudioProcessor(BusesProperties()
.withInput("Input", juce::AudioChannelSet::stereo(), true)
.withOutput("Output", juce::AudioChannelSet::stereo(), true)),
parameters(*this, nullptr, "Parameters", {
std::make_unique<juce::AudioParameterFloat>(
thresholdID, "Threshold", juce::NormalisableRange<float>(-48.0f, 0.0f, 0.1f), -20.0f),
std::make_unique<juce::AudioParameterFloat>(
ratioID, "Ratio", juce::NormalisableRange<float>(1.0f, 20.0f, 0.1f, 0.5f), 4.0f),
std::make_unique<juce::AudioParameterFloat>(
attackID, "Attack", juce::NormalisableRange<float>(0.001f, 0.1f, 0.001f, 0.5f), 0.03f),
std::make_unique<juce::AudioParameterFloat>(
releaseID, "Release", juce::NormalisableRange<float>(0.05f, 1.5f, 0.01f, 0.5f), 0.3f),
std::make_unique<juce::AudioParameterFloat>(
kneeID, "Knee", juce::NormalisableRange<float>(0.0f, 12.0f, 0.5f), 6.0f),
std::make_unique<juce::AudioParameterFloat>(
makeupID, "Makeup", juce::NormalisableRange<float>(0.0f, 24.0f, 0.1f), 0.0f),
std::make_unique<juce::AudioParameterFloat>(
mixID, "Mix", juce::NormalisableRange<float>(0.0f, 1.0f, 0.01f), 1.0f),
std::make_unique<juce::AudioParameterFloat>(
autoReleaseID, "Auto Release", juce::NormalisableRange<float>(0.0f, 1.0f, 0.01f), 1.0f)
})
{
inputScope.fill(0.0f);
outputScope.fill(0.0f);
}
SaftpumpeProcessor::~SaftpumpeProcessor() {}
void SaftpumpeProcessor::prepareToPlay(double sampleRate, int samplesPerBlock) {
compressorL.prepare(sampleRate, samplesPerBlock);
compressorR.prepare(sampleRate, samplesPerBlock);
compressorL.reset();
compressorR.reset();
scopeIndex = 0;
}
void SaftpumpeProcessor::releaseResources() {}
void SaftpumpeProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer&) {
juce::ScopedNoDenormals noDenormals;
auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels();
for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
buffer.clear(i, 0, buffer.getNumSamples());
updateCompressorParams();
float maxInput = 0.0f;
float maxOutput = 0.0f;
float maxGR = 0.0f;
int numSamples = buffer.getNumSamples();
float* channelDataL = buffer.getWritePointer(0);
float* channelDataR = (totalNumInputChannels > 1) ? buffer.getWritePointer(1) : nullptr;
for (int sample = 0; sample < numSamples; ++sample) {
float inL = channelDataL[sample];
float inR = channelDataR ? channelDataR[sample] : inL;
float absIn = std::max(std::abs(inL), std::abs(inR));
if (absIn > maxInput) maxInput = absIn;
inputScope[scopeIndex] = inL;
float outL = compressorL.processSample(inL);
float outR = compressorR.processSample(inR);
channelDataL[sample] = outL;
if (channelDataR) channelDataR[sample] = outR;
float absOut = std::max(std::abs(outL), std::abs(outR));
if (absOut > maxOutput) maxOutput = absOut;
float gr = std::abs(compressorL.getGainReduction());
if (gr > maxGR) maxGR = gr;
outputScope[scopeIndex] = outL;
scopeIndex = (scopeIndex + 1) % scopeSize;
}
currentInputLevel = currentInputLevel * 0.8f + (maxInput > 0.0001f ? 20.0f * std::log10(maxInput) : -100.0f) * 0.2f;
currentOutputLevel = currentOutputLevel * 0.8f + (maxOutput > 0.0001f ? 20.0f * std::log10(maxOutput) : -100.0f) * 0.2f;
currentGainReduction = currentGainReduction * 0.7f + maxGR * 0.3f;
}
void SaftpumpeProcessor::updateCompressorParams() {
CompressorDSP::Params p;
p.threshold = parameters.getRawParameterValue(thresholdID)->load();
p.ratio = parameters.getRawParameterValue(ratioID)->load();
p.attack = parameters.getRawParameterValue(attackID)->load();
p.release = parameters.getRawParameterValue(releaseID)->load();
p.knee = parameters.getRawParameterValue(kneeID)->load();
p.makeup = parameters.getRawParameterValue(makeupID)->load();
p.mix = parameters.getRawParameterValue(mixID)->load();
p.autoRelease = parameters.getRawParameterValue(autoReleaseID)->load() > 0.5f;
compressorL.setParams(p);
compressorR.setParams(p);
}
juce::AudioProcessorEditor* SaftpumpeProcessor::createEditor() {
return new SaftpumpeEditor(*this);
}
void SaftpumpeProcessor::getStateInformation(juce::MemoryBlock& destData) {
auto state = parameters.copyState();
std::unique_ptr<juce::XmlElement> xml(state.createXml());
copyXmlToBinary(*xml, destData);
}
void SaftpumpeProcessor::setStateInformation(const void* data, int sizeInBytes) {
std::unique_ptr<juce::XmlElement> xml(getXmlFromBinary(data, sizeInBytes));
if (xml && xml->hasTagName(parameters.state.getType())) {
parameters.replaceState(juce::ValueTree::fromXml(*xml));
}
}
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() {
return new SaftpumpeProcessor();
}

65
Source/PluginProcessor.h Normal file
View file

@ -0,0 +1,65 @@
#pragma once
#include <juce_audio_processors/juce_audio_processors.h>
#include "CompressorDSP.h"
class SaftpumpeProcessor : public juce::AudioProcessor {
public:
SaftpumpeProcessor();
~SaftpumpeProcessor() override;
void prepareToPlay(double sampleRate, int samplesPerBlock) override;
void releaseResources() override;
void processBlock(juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override { return true; }
const juce::String getName() const override { return JucePlugin_Name; }
bool acceptsMidi() const override { return false; }
bool producesMidi() const override { return false; }
bool isMidiEffect() const override { return false; }
double getTailLengthSeconds() const override { return 0.0; }
int getNumPrograms() override { return 1; }
int getCurrentProgram() override { return 0; }
void setCurrentProgram(int) override {}
const juce::String getProgramName(int) override { return {}; }
void changeProgramName(int, const juce::String&) override {}
void getStateInformation(juce::MemoryBlock& destData) override;
void setStateInformation(const void* data, int sizeInBytes) override;
static constexpr const char* thresholdID = "threshold";
static constexpr const char* ratioID = "ratio";
static constexpr const char* attackID = "attack";
static constexpr const char* releaseID = "release";
static constexpr const char* kneeID = "knee";
static constexpr const char* makeupID = "makeup";
static constexpr const char* mixID = "mix";
static constexpr const char* autoReleaseID = "autoRelease";
juce::AudioProcessorValueTreeState parameters;
float getGainReductionMeter() const { return currentGainReduction; }
float getInputLevel() const { return currentInputLevel; }
float getOutputLevel() const { return currentOutputLevel; }
static constexpr int scopeSize = 512;
std::array<float, scopeSize> inputScope{};
std::array<float, scopeSize> outputScope{};
int scopeIndex = 0;
private:
CompressorDSP compressorL;
CompressorDSP compressorR;
float currentGainReduction = 0.0f;
float currentInputLevel = 0.0f;
float currentOutputLevel = 0.0f;
void updateCompressorParams();
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SaftpumpeProcessor)
};