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

14
.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
build/
build_juce/
JUCE/
*.DS_Store
*.swp
*.swo
*~
.cache/
.vscode/
.idea/
*.xcworkspace
*.xcodeproj
DerivedData/

64
CMakeLists.txt Normal file
View file

@ -0,0 +1,64 @@
cmake_minimum_required(VERSION 3.22)
project(Saftpumpe VERSION 1.0.0 LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(JUCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/JUCE")
add_subdirectory("${JUCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/juce_build" EXCLUDE_FROM_ALL)
juce_add_plugin(Saftpumpe
COMPANY_NAME "Saftpumpe"
PLUGIN_MANUFACTURER_CODE SfPm
PLUGIN_CODE SaFp
FORMATS VST3 Standalone AU
PRODUCT_NAME "Saftpumpe"
IS_SYNTH FALSE
NEEDS_MIDI_INPUT FALSE
NEEDS_MIDI_OUTPUT FALSE
IS_MIDI_EFFECT FALSE
EDITOR_WANTS_KEYBOARD_FOCUS FALSE
COPY_PLUGIN_AFTER_BUILD TRUE
PLUGIN_AU_TYPE_PREFIX "aufx"
VST3_CATEGORIES "Fx" "Dynamics"
)
target_sources(Saftpumpe PRIVATE
Source/PluginProcessor.cpp
Source/PluginEditor.cpp
)
target_compile_definitions(Saftpumpe PUBLIC
JUCE_WEB_BROWSER=0
JUCE_USE_CURL=0
JUCE_VST3_CAN_REPLACE_VST2=0
JUCE_DISPLAY_SPLASH_SCREEN=0
JUCE_MODAL_LOOPS_PERMITTED=0
)
target_link_libraries(Saftpumpe
PRIVATE
juce::juce_audio_basics
juce::juce_audio_devices
juce::juce_audio_formats
juce::juce_audio_plugin_client
juce::juce_audio_processors
juce::juce_audio_utils
juce::juce_core
juce::juce_data_structures
juce::juce_events
juce::juce_graphics
juce::juce_gui_basics
juce::juce_gui_extra
PUBLIC
juce::juce_recommended_config_flags
)
set_target_properties(Saftpumpe PROPERTIES
XCODE_ATTRIBUTE_CLANG_CXX_LIBRARY "libc++"
XCODE_ATTRIBUTE_CLANG_CXX_LANGUAGE_STANDARD "c++17"
XCODE_ATTRIBUTE_ARCHS "arm64"
XCODE_ATTRIBUTE_VALID_ARCHS "arm64"
XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH "YES"
)

23
LICENSE Normal file
View file

@ -0,0 +1,23 @@
MIT License
Copyright (c) 2026 Armin Jenewein
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

47
Makefile Normal file
View file

@ -0,0 +1,47 @@
.PHONY: all build clean install uninstall rebuild run
BUILD_DIR := build
BUILD_TYPE := Release
JOBS := $(shell sysctl -n hw.ncpu)
all: build
$(BUILD_DIR)/Makefile:
@mkdir -p $(BUILD_DIR)
cd $(BUILD_DIR) && cmake .. -DCMAKE_BUILD_TYPE=$(BUILD_TYPE)
build: $(BUILD_DIR)/Makefile
cmake --build $(BUILD_DIR) -j $(JOBS)
clean:
rm -rf $(BUILD_DIR)
reinstall: clean build
uninstall:
rm -rf ~/Library/Audio/Plug-Ins/VST3/Saftpumpe.vst3
rm -rf ~/Library/Audio/Plug-Ins/Components/Saftpumpe.component
install: build
run: build
open "$(BUILD_DIR)/Saftpumpe_artefacts/Release/Standalone/Saftpumpe.app"
debug:
cmake --build $(BUILD_DIR) -j $(JOBS) 2>&1 | head -100
help:
@echo "Saftpumpe - Bus Compressor Plugin"
@echo ""
@echo "Targets:"
@echo " make - Build the plugin"
@echo " make clean - Remove build directory"
@echo " make reinstall - Clean and rebuild"
@echo " make uninstall - Remove installed plugins"
@echo " make run - Launch standalone app"
@echo " make debug - Build with verbose output"
@echo ""
@echo "Formats: VST3, AU, Standalone"
@echo "Install paths:"
@echo " VST3: ~/Library/Audio/Plug-Ins/VST3/"
@echo " AU: ~/Library/Audio/Plug-Ins/Components/"

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)
};