Add compressor plugin source with build files

This commit is contained in:
Armin 2026-09-09 12:35:16 +02:00
commit ef5739546f
10 changed files with 925 additions and 0 deletions

80
Source/Compressor.cpp Normal file
View file

@ -0,0 +1,80 @@
#include "Compressor.h"
namespace pascal
{
void GlueCompressor::prepare (const juce::dsp::ProcessSpec& spec)
{
sampleRate = spec.sampleRate > 0.0 ? spec.sampleRate : 44100.0;
reset();
}
void GlueCompressor::reset()
{
detectorLevel = 0.0f;
}
void GlueCompressor::processBlock (juce::AudioBuffer<float>& buffer)
{
const auto numChannels = buffer.getNumChannels();
const auto numSamples = buffer.getNumSamples();
if (numChannels == 0 || numSamples == 0)
return;
const float inputGain = juce::Decibels::decibelsToGain (juce::jlimit (-60.0f, 60.0f, inputDb));
const float makeupGain = juce::Decibels::decibelsToGain (juce::jlimit (-60.0f, 60.0f, makeupDb));
const float fs = static_cast<float> (sampleRate);
const float attackCoef = std::exp (-1.0f / (juce::jmax (0.2f, attackMs) * 0.001f * fs));
const float releaseCoef = std::exp (-1.0f / (juce::jmax (10.0f, releaseMs) * 0.001f * fs));
const float wet = juce::jlimit (0.0f, 1.0f, dryWet);
const float dry = 1.0f - wet;
const float knee = 6.0f;
const float ratioV = juce::jmax (1.0f, ratio);
juce::AudioBuffer<float> input;
input.makeCopyOf (buffer, true);
const auto* inChannels = input.getArrayOfReadPointers();
auto* outChannels = buffer.getArrayOfWritePointers();
for (int n = 0; n < numSamples; ++n)
{
float detect = 0.0f;
for (int c = 0; c < numChannels; ++c)
detect += inChannels[c][n];
detect = std::fabs (detect / static_cast<float> (numChannels)) * inputGain;
const float coeff = detect > detectorLevel ? (1.0f - attackCoef)
: (1.0f - releaseCoef);
detectorLevel += coeff * (detect - detectorLevel);
const float overDb = juce::Decibels::gainToDecibels (detectorLevel + 1.0e-9f) - thresholdDb;
float grDb;
if (overDb <= -knee * 0.5f)
{
grDb = 0.0f;
}
else if (overDb >= knee * 0.5f)
{
grDb = -overDb * (1.0f - 1.0f / ratioV);
}
else
{
const float q = overDb + knee * 0.5f;
grDb = -(q * q / (2.0f * knee)) * (1.0f - 1.0f / ratioV);
}
const float gain = juce::Decibels::decibelsToGain (juce::jlimit (-80.0f, 24.0f, grDb)) * makeupGain;
for (int c = 0; c < numChannels; ++c)
{
const float in = inChannels[c][n] * inputGain;
const float wetSample = in * gain;
outChannels[c][n] = c < 2 ? in * dry + wetSample * wet : wetSample;
}
}
}
}

37
Source/Compressor.h Normal file
View file

@ -0,0 +1,37 @@
#pragma once
#include <juce_audio_utils/juce_audio_utils.h>
#include <juce_dsp/juce_dsp.h>
namespace pascal
{
class GlueCompressor final
{
public:
void prepare (const juce::dsp::ProcessSpec& spec);
void reset();
void setInputDb (float v) { inputDb = v; }
void setThresholdDb (float v) { thresholdDb = v; }
void setRatio (float v) { ratio = v; }
void setAttackMs (float v) { attackMs = v; }
void setReleaseMs (float v) { releaseMs = v; }
void setMakeupDb (float v) { makeupDb = v; }
void setDryWet (float v) { dryWet = v; }
void processBlock (juce::AudioBuffer<float>& buffer);
private:
float inputDb = 0.0f;
float thresholdDb = -18.0f;
float ratio = 2.0f;
float attackMs = 10.0f;
float releaseMs = 250.0f;
float makeupDb = 0.0f;
float dryWet = 1.0f;
double sampleRate = 44100.0;
float detectorLevel = 0.0f;
};
}

230
Source/PluginEditor.cpp Normal file
View file

@ -0,0 +1,230 @@
#include "PluginEditor.h"
#include "BuildInfo.h"
namespace pascal
{
void MinimalLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height,
float sliderPos, float rotaryStartAngle, float rotaryEndAngle,
juce::Slider& slider)
{
const auto centre = juce::Point<float> (x + width * 0.5f, y + height * 0.5f);
const auto radius = juce::jmin (width, height) * 0.5f - 7.0f;
const auto angle = rotaryStartAngle + (rotaryEndAngle - rotaryStartAngle) * sliderPos;
juce::Path trackPath;
trackPath.addArc (centre.x - radius, centre.y - radius, radius * 2.0f, radius * 2.0f,
rotaryStartAngle, rotaryEndAngle, true);
juce::Path fillPath;
fillPath.addArc (centre.x - radius, centre.y - radius, radius * 2.0f, radius * 2.0f,
rotaryStartAngle, angle, true);
const auto thickness = juce::jmax (1.0f, radius * 0.10f);
juce::PathStrokeType stroke (thickness, juce::PathStrokeType::curved, juce::PathStrokeType::rounded);
g.setColour (track);
g.strokePath (trackPath, stroke);
g.setColour (slider.isMouseOverOrDragging() ? juce::Colours::white : accent);
g.strokePath (fillPath, stroke);
const auto indicator = centre + juce::Point<float> (std::cos (angle - juce::MathConstants<float>::halfPi),
std::sin (angle - juce::MathConstants<float>::halfPi)) * radius;
g.fillEllipse (indicator.x - 2.0f, indicator.y - 2.0f, 4.0f, 4.0f);
}
KnobComponent::KnobComponent (juce::AudioProcessorValueTreeState& apvts,
const juce::String& paramId,
const juce::String& displayName,
float defaultValue,
juce::LookAndFeel& lnf,
std::function<juce::String (double)> fmt)
: format (std::move (fmt))
{
slider.setSliderStyle (juce::Slider::RotaryVerticalDrag);
slider.setRotaryParameters (juce::MathConstants<float>::pi * 1.25f,
juce::MathConstants<float>::pi * 2.75f,
true);
slider.setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0);
slider.setLookAndFeel (&lnf);
slider.setDoubleClickReturnValue (true, defaultValue);
slider.setWantsKeyboardFocus (true);
slider.addListener (this);
addAndMakeVisible (slider);
nameLabel.setText (displayName, juce::dontSendNotification);
nameLabel.setJustificationType (juce::Justification::centred);
nameLabel.setColour (juce::Label::textColourId, labelColour);
nameLabel.setColour (juce::Label::backgroundColourId, juce::Colours::transparentBlack);
nameLabel.setColour (juce::Label::outlineColourId, juce::Colours::transparentBlack);
addAndMakeVisible (nameLabel);
valueLabel.setJustificationType (juce::Justification::centred);
valueLabel.setColour (juce::Label::textColourId, valueColour);
valueLabel.setColour (juce::Label::backgroundColourId, juce::Colours::transparentBlack);
valueLabel.setColour (juce::Label::outlineColourId, juce::Colours::transparentBlack);
addAndMakeVisible (valueLabel);
attachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (
apvts, paramId, slider);
valueLabel.setText (format (slider.getValue()), juce::dontSendNotification);
}
void KnobComponent::resized()
{
auto bounds = getLocalBounds();
auto valueArea = bounds.removeFromBottom (18);
auto nameArea = bounds.removeFromTop (16);
const auto square = juce::jmin (bounds.getWidth(), bounds.getHeight());
nameLabel.setBounds (nameArea);
slider.setBounds (bounds.withSizeKeepingCentre (square, square));
valueLabel.setBounds (valueArea);
}
void KnobComponent::sliderValueChanged (juce::Slider* slider)
{
valueLabel.setText (format (slider->getValue()), juce::dontSendNotification);
}
namespace
{
juce::String buildArch()
{
#if defined (__arm64__) || defined (_M_ARM64) || defined (__aarch64__)
return "arm64";
#elif defined (__x86_64__) || defined (_M_X64)
return "x64";
#else
return "x86";
#endif
}
}
FooterBar::FooterBar (PascalcompressorAudioProcessor& p)
: processor (p)
{
leftText = juce::String ("Pascalcompressor 1.0.0")
+ " \u00B7 " + buildArch()
+ " \u00B7 VST3 + AU"
+ " \u00B7 built " + juce::String (PASCAL_BUILD_TIME) + " (local)";
startTimerHz (2);
}
void FooterBar::timerCallback()
{
const auto newRight = juce::String (juce::roundToInt (processor.getSampleRate())) + " Hz"
+ " \u00B7 " + juce::String (processor.getBlockSize()) + " smp";
if (newRight != rightText)
{
rightText = newRight;
repaint();
}
}
void FooterBar::paint (juce::Graphics& g)
{
g.fillAll (background);
g.setColour (ruleColour);
g.fillRect (0, 0, getWidth(), 1);
g.setColour (footerColour);
g.setFont (juce::FontOptions (11.0f));
auto bounds = getLocalBounds().reduced (14, 0);
g.drawText (leftText, bounds, juce::Justification::centredLeft);
g.drawText (rightText, bounds, juce::Justification::centredRight);
}
PascalcompressorAudioProcessorEditor::PascalcompressorAudioProcessorEditor (PascalcompressorAudioProcessor& p)
: AudioProcessorEditor (&p),
processorRef (p),
footer (p)
{
setOpaque (true);
setResizeLimits (500, 340, 1100, 860);
setResizable (true, true);
struct Spec
{
const char* id;
const char* name;
float defaultValue;
std::function<juce::String (double)> format;
};
const Spec specs[] =
{
{ "input", "Input", 0.0f,
[] (double v) { return juce::String::formatted ("%+.1f dB", v); } },
{ "threshold", "Threshold", -18.0f,
[] (double v) { return juce::String::formatted ("%.1f dB", v); } },
{ "ratio", "Ratio", 2.0f,
[] (double v) { return juce::String::formatted ("%.2f : 1", v); } },
{ "attack", "Attack", 10.0f,
[] (double v) { return juce::String::formatted ("%.1f ms", v); } },
{ "release", "Release", 250.0f,
[] (double v) { return juce::String::formatted ("%.0f ms", v); } },
{ "makeup", "Makeup", 0.0f,
[] (double v) { return juce::String::formatted ("%+.1f dB", v); } },
{ "drywet", "Dry/Wet", 100.0f,
[] (double v) { return juce::String (juce::roundToInt (v)) + " %"; } },
};
for (const auto& spec : specs)
{
auto* knob = new KnobComponent (processorRef.apvts, spec.id, spec.name,
spec.defaultValue, lookAndFeel, spec.format);
knobs.add (knob);
addAndMakeVisible (knob);
}
addAndMakeVisible (footer);
ready = true;
setSize (620, 420);
}
void PascalcompressorAudioProcessorEditor::paint (juce::Graphics& g)
{
jassert (ready);
g.fillAll (background);
}
void PascalcompressorAudioProcessorEditor::resized()
{
if (! ready)
return;
auto bounds = getLocalBounds();
footer.setBounds (bounds.removeFromBottom (24));
bounds.reduce (24, 16);
const auto knobSize = juce::jmin (bounds.getWidth() / 4 - 42,
(bounds.getHeight() - 20) / 2 - 40,
120);
const auto knobHeight = knobSize + 40;
const auto rowGap = 20;
const auto rowsTotal = knobHeight * 2 + rowGap;
const auto rowsTop = bounds.getY() + (bounds.getHeight() - rowsTotal) / 2;
const auto xStepTop = bounds.getWidth() / 4;
for (int i = 0; i < 4; ++i)
{
const auto x = bounds.getX() + xStepTop * i + (xStepTop - knobSize) / 2;
knobs.getUnchecked (i)->setBounds (x, rowsTop, knobSize, knobHeight);
}
const auto xStepBottom = bounds.getWidth() / 3;
const auto rowY = rowsTop + knobHeight + rowGap;
for (int i = 4; i < 7; ++i)
{
const auto x = bounds.getX() + xStepBottom * (i - 4) + (xStepBottom - knobSize) / 2;
knobs.getUnchecked (i)->setBounds (x, rowY, knobSize, knobHeight);
}
}
}

77
Source/PluginEditor.h Normal file
View file

@ -0,0 +1,77 @@
#pragma once
#include <juce_audio_utils/juce_audio_utils.h>
#include <functional>
#include "PluginProcessor.h"
namespace pascal
{
inline const juce::Colour background (0xFF101014);
inline const juce::Colour track (0xFF2A2A30);
inline const juce::Colour accent (0xFFEDEDF2);
inline const juce::Colour labelColour (0xFF9A9AA2);
inline const juce::Colour valueColour (0xFFC8C8D0);
inline const juce::Colour footerColour (0xFF6A6A72);
inline const juce::Colour ruleColour (0xFF1E1E23);
class MinimalLookAndFeel final : public juce::LookAndFeel_V4
{
public:
void drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height,
float sliderPos, float rotaryStartAngle, float rotaryEndAngle,
juce::Slider& slider) override;
};
class KnobComponent final : public juce::Component, private juce::Slider::Listener
{
public:
KnobComponent (juce::AudioProcessorValueTreeState& apvts, const juce::String& paramId,
const juce::String& displayName, float defaultValue,
juce::LookAndFeel& lnf, std::function<juce::String (double)> format);
void resized() override;
private:
void sliderValueChanged (juce::Slider*) override;
juce::Slider slider;
juce::Label nameLabel;
juce::Label valueLabel;
std::function<juce::String (double)> format;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> attachment;
};
class FooterBar final : public juce::Component, private juce::Timer
{
public:
explicit FooterBar (PascalcompressorAudioProcessor& p);
void paint (juce::Graphics&) override;
private:
void timerCallback() override;
PascalcompressorAudioProcessor& processor;
juce::String leftText;
juce::String rightText;
};
class PascalcompressorAudioProcessorEditor final : public juce::AudioProcessorEditor
{
public:
explicit PascalcompressorAudioProcessorEditor (PascalcompressorAudioProcessor&);
~PascalcompressorAudioProcessorEditor() override = default;
void paint (juce::Graphics&) override;
void resized() override;
private:
PascalcompressorAudioProcessor& processorRef;
MinimalLookAndFeel lookAndFeel;
juce::OwnedArray<KnobComponent> knobs;
FooterBar footer;
bool ready = false;
};
}

109
Source/PluginProcessor.cpp Normal file
View file

@ -0,0 +1,109 @@
#include "PluginProcessor.h"
#include "PluginEditor.h"
PascalcompressorAudioProcessor::PascalcompressorAudioProcessor()
: AudioProcessor (BusesProperties()
.withInput ("Input", juce::AudioChannelSet::stereo(), true)
.withOutput ("Output", juce::AudioChannelSet::stereo(), true)),
apvts (*this, nullptr, "PARAMS", createParameterLayout())
{
}
PascalcompressorAudioProcessor::~PascalcompressorAudioProcessor() = default;
juce::AudioProcessorValueTreeState::ParameterLayout PascalcompressorAudioProcessor::createParameterLayout()
{
juce::AudioProcessorValueTreeState::ParameterLayout layout;
layout.add (std::make_unique<juce::AudioParameterFloat> ("input",
"Input", juce::NormalisableRange<float> (-24.0f, 24.0f, 0.1f, 1.0f), 0.0f));
layout.add (std::make_unique<juce::AudioParameterFloat> ("threshold",
"Threshold", juce::NormalisableRange<float> (-60.0f, 0.0f, 0.1f, 1.0f), -18.0f));
layout.add (std::make_unique<juce::AudioParameterFloat> ("ratio",
"Ratio", juce::NormalisableRange<float> (1.0f, 20.0f, 0.05f, 0.4f), 2.0f));
layout.add (std::make_unique<juce::AudioParameterFloat> ("attack",
"Attack", juce::NormalisableRange<float> (0.1f, 200.0f, 0.1f, 0.35f), 10.0f));
layout.add (std::make_unique<juce::AudioParameterFloat> ("release",
"Release", juce::NormalisableRange<float> (10.0f, 3000.0f, 1.0f, 0.35f), 250.0f));
layout.add (std::make_unique<juce::AudioParameterFloat> ("makeup",
"Makeup", juce::NormalisableRange<float> (0.0f, 24.0f, 0.1f, 1.0f), 0.0f));
layout.add (std::make_unique<juce::AudioParameterFloat> ("drywet",
"Dry/Wet", juce::NormalisableRange<float> (0.0f, 100.0f, 1.0f, 1.0f), 100.0f));
return layout;
}
void PascalcompressorAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
juce::dsp::ProcessSpec spec;
spec.sampleRate = sampleRate;
spec.maximumBlockSize = static_cast<juce::uint32> (samplesPerBlock);
spec.numChannels = static_cast<juce::uint32> (juce::jmax (1, getTotalNumInputChannels()));
compressor.prepare (spec);
}
void PascalcompressorAudioProcessor::releaseResources()
{
}
void PascalcompressorAudioProcessor::updateParameters()
{
compressor.setInputDb (*apvts.getRawParameterValue ("input"));
compressor.setThresholdDb (*apvts.getRawParameterValue ("threshold"));
compressor.setRatio (*apvts.getRawParameterValue ("ratio"));
compressor.setAttackMs (*apvts.getRawParameterValue ("attack"));
compressor.setReleaseMs (*apvts.getRawParameterValue ("release"));
compressor.setMakeupDb (*apvts.getRawParameterValue ("makeup"));
compressor.setDryWet (*apvts.getRawParameterValue ("drywet") * 0.01f);
}
void PascalcompressorAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer,
juce::MidiBuffer&)
{
updateParameters();
if (getTotalNumInputChannels() > 0)
compressor.processBlock (buffer);
}
bool PascalcompressorAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono()
&& layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
return false;
if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet())
return false;
return true;
}
juce::AudioProcessorEditor* PascalcompressorAudioProcessor::createEditor()
{
return new pascal::PascalcompressorAudioProcessorEditor (*this);
}
void PascalcompressorAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
{
auto state = apvts.copyState();
std::unique_ptr<juce::XmlElement> xml (state.createXml());
copyXmlToBinary (*xml, destData);
}
void PascalcompressorAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
std::unique_ptr<juce::XmlElement> xmlState (getXmlFromBinary (data, sizeInBytes));
if (xmlState != nullptr && xmlState->hasTagName (apvts.state.getType()))
apvts.replaceState (juce::ValueTree::fromXml (*xmlState));
}
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
return new PascalcompressorAudioProcessor();
}

47
Source/PluginProcessor.h Normal file
View file

@ -0,0 +1,47 @@
#pragma once
#include <juce_audio_utils/juce_audio_utils.h>
#include <juce_dsp/juce_dsp.h>
#include "Compressor.h"
class PascalcompressorAudioProcessor final : public juce::AudioProcessor
{
public:
PascalcompressorAudioProcessor();
~PascalcompressorAudioProcessor() 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 "Pascalcompressor"; }
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&) override;
void setStateInformation (const void*, int) override;
bool isBusesLayoutSupported (const BusesLayout&) const override;
juce::AudioProcessorValueTreeState apvts;
static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
private:
void updateParameters();
pascal::GlueCompressor compressor;
};