mirror of
https://codeberg.org/armin/saftpumpe.git
synced 2026-09-01 04:10:46 +02:00
init
This commit is contained in:
parent
10573a6c67
commit
6391b75d15
9 changed files with 1230 additions and 0 deletions
119
Source/CompressorDSP.h
Normal file
119
Source/CompressorDSP.h
Normal 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
667
Source/PluginEditor.cpp
Normal 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
105
Source/PluginEditor.h
Normal 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
126
Source/PluginProcessor.cpp
Normal 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
65
Source/PluginProcessor.h
Normal 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)
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue