Add brickwall limiter FX section

This commit is contained in:
Armin 2026-07-15 23:49:51 +02:00
commit 6cd7d4f3ba
4 changed files with 161 additions and 49 deletions

44
Source/DSP/Limiter.h Normal file
View file

@ -0,0 +1,44 @@
#pragma once
#include <cmath>
#include <algorithm>
class Limiter {
public:
void prepare(double sr) {
sampleRate = sr;
envelope = 0.0f;
}
void setParameters(float threshold, float ceiling, float releaseMs) {
thresholdGain = threshold;
ceilingGain = ceiling;
releaseCoeff = std::exp(-1.0f / (std::max(releaseMs, 1.0f) * 0.001f * static_cast<float>(sampleRate)));
dryWet = 1.0f;
}
float process(float input) {
float absIn = std::abs(input);
if (absIn > envelope)
envelope = absIn;
else
envelope = releaseCoeff * envelope + (1.0f - releaseCoeff) * absIn;
float target = std::max(thresholdGain, ceilingGain);
float gain = 1.0f;
if (envelope > target && envelope > 1e-6f) {
float desired = target / envelope;
gain = std::min(1.0f, desired);
}
float wet = input * gain;
return input + (wet - input) * dryWet;
}
private:
double sampleRate = 44100.0;
float thresholdGain = 1.0f;
float ceilingGain = 1.0f;
float releaseCoeff = 0.0f;
float dryWet = 1.0f;
float envelope = 0.0f;
};

View file

@ -477,21 +477,25 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p)
// FX
setupCB(distTypeBox, distTypeAttach, "distType", {"Soft Clip", "Hard Clip", "Foldback", "Overdrive"});
setupParam(distAmountKnob, distAmountAttach, "distAmount", "AMOUNT");
setupParam(distMixKnob, distMixAttach, "distMix", "MIX");
setupParam(distSymmetryKnob, distSymmetryAttach, "distSymmetry", "SYMMETRY");
setupParam(distToneKnob, distToneAttach, "distTone", "TONE");
setupParam(compThresholdKnob, compThresholdAttach, "compThreshold", "THRESH");
setupParam(compRatioKnob, compRatioAttach, "compRatio", "RATIO");
setupParam(compAttackKnob, compAttackAttach, "compAttack", "ATTACK");
setupParam(compReleaseKnob, compReleaseAttach, "compRelease", "RELEASE");
setupParam(compMakeupKnob, compMakeupAttach, "compMakeup", "MAKEUP");
setupParam(compMixKnob, compMixAttach, "compMix", "MIX");
setupParam(autoPanRateKnob, autoPanRateAttach, "autoPanRate", "RATE");
setupParam(autoPanDepthKnob, autoPanDepthAttach, "autoPanDepth", "DEPTH");
setupParam(autoPanPhaseKnob, autoPanPhaseAttach, "autoPanPhase", "PHASE");
setupParam(limiterThresholdKnob, limiterThresholdAttach, "limiterThreshold", "THRESH");
setupParam(limiterCeilingKnob, limiterCeilingAttach, "limiterCeiling", "CEILING");
setupParam(limiterReleaseKnob, limiterReleaseAttach, "limiterRelease", "RELEASE");
// LFO
setupKnob(lfo1RateKnob, "RATE");
setupParam(lfo1DepthKnob, lfo1DepthAttach, "lfo1Depth", "DEPTH");
setupCB(lfo1ShapeBox, lfo1ShapeAttach, "lfo1Shape", {"Sine", "Triangle", "Saw", "Square"});
setupCB(lfo1DestBox, lfo1DestAttach, "lfo1Dest", {"Filter", "OSC1", "OSC2", "Both OSC"});
setupCB(lfo1DestBox, lfo1DestAttach, "lfo1Dest", {"Filter", "OSC1", "OSC2", "Both OSC", "OSC2 Phase"});
setupCB(lfo1SyncBox, lfo1SyncAttach, "lfo1Sync", {"Sync Off", "Sync On"});
lfo1SyncBox.onChange = [this]() {
setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncBox);
@ -938,9 +942,10 @@ void MainContentComponent::paint(juce::Graphics& g) {
g.drawText(title, x + 6, y + titlePadY, w - 12, 14, juce::Justification::centred);
};
drawSubSection(172, 508, 395, 140, "DISTORTION", 6);
drawSubSection(577, 508, 660, 140, "COMPRESSOR", 6);
drawSubSection(1247, 508, 240, 140, "AUTO-PAN", 6);
drawSubSection(10, 508, 512, 140, "DISTORTION", 6);
drawSubSection(528, 508, 470, 140, "COMPRESSOR", 6);
drawSubSection(1004, 508, 270, 140, "AUTO-PAN", 6);
drawSubSection(1280, 508, 370, 140, "LIMITER", 6);
// LFO sub-sections
drawSubSection(10, 658, 540, 150, "LFO 1", 4);
@ -976,58 +981,94 @@ void MainContentComponent::resized() {
phaseOffsetKnob.setBounds(528 + (knobSize + knobSpacing) * 4, knobY2, knobSize, knobSize);
filterLabel.setBounds(1152, sectionY, 498, labelH);
filterTypeBox.setBounds(1162, sectionY + labelH + 4, 200, 36);
filterTypeBox.setBounds(1162, sectionY + labelH + 4, 200, 28);
int knobYF = sectionY + labelH + comboH + 8;
filterCutoffKnob.setBounds(1162, knobYF, knobSize, knobSize);
filterResKnob.setBounds(1162 + knobSize + knobSpacing, knobYF, knobSize, knobSize);
filterEnvAmtKnob.setBounds(1162 + (knobSize + knobSpacing) * 2, knobYF, knobSize, knobSize);
keyTrackKnob.setBounds(1162 + (knobSize + knobSpacing) * 3, knobYF, knobSize, knobSize);
envLabel.setBounds(75, 342, 498, labelH);
envLabel.setBounds(10, 342, 563, labelH);
{
int rowW = 4 * knobSize + 3 * knobSpacing;
int sx = 10 + (563 - rowW) / 2;
int knobYA = 352 + labelH + 6;
envAttackKnob.setBounds(85, knobYA, knobSize, knobSize);
envDecayKnob.setBounds(85 + knobSize + knobSpacing, knobYA, knobSize, knobSize);
envSustainKnob.setBounds(85 + (knobSize + knobSpacing) * 2, knobYA, knobSize, knobSize);
envReleaseKnob.setBounds(85 + (knobSize + knobSpacing) * 3, knobYA, knobSize, knobSize);
envAttackKnob.setBounds(sx, knobYA, knobSize, knobSize);
envDecayKnob.setBounds(sx + (knobSize + knobSpacing), knobYA, knobSize, knobSize);
envSustainKnob.setBounds(sx + (knobSize + knobSpacing) * 2, knobYA, knobSize, knobSize);
envReleaseKnob.setBounds(sx + (knobSize + knobSpacing) * 3, knobYA, knobSize, knobSize);
}
fEnvLabel.setBounds(583, 342, 502, labelH);
{
int rowW = 4 * knobSize + 3 * knobSpacing;
int sx = 583 + (502 - rowW) / 2;
int knobYFE = 352 + labelH + 6;
fEnvAttackKnob.setBounds(593, knobYFE, knobSize, knobSize);
fEnvDecayKnob.setBounds(593 + knobSize + knobSpacing, knobYFE, knobSize, knobSize);
fEnvSustainKnob.setBounds(593 + (knobSize + knobSpacing) * 2, knobYFE, knobSize, knobSize);
fEnvReleaseKnob.setBounds(593 + (knobSize + knobSpacing) * 3, knobYFE, knobSize, knobSize);
fEnvAttackKnob.setBounds(sx, knobYFE, knobSize, knobSize);
fEnvDecayKnob.setBounds(sx + (knobSize + knobSpacing), knobYFE, knobSize, knobSize);
fEnvSustainKnob.setBounds(sx + (knobSize + knobSpacing) * 2, knobYFE, knobSize, knobSize);
fEnvReleaseKnob.setBounds(sx + (knobSize + knobSpacing) * 3, knobYFE, knobSize, knobSize);
}
globalLabel.setBounds(1095, 342, 490, labelH);
globalLabel.setBounds(1095, 342, 555, labelH);
{
int rowW = 4 * knobSize + 3 * knobSpacing;
int sx = 1095 + (555 - rowW) / 2;
int knobYM = 352 + labelH + 6;
panKnob.setBounds(1105, knobYM, knobSize, knobSize);
driveKnob.setBounds(1105 + knobSize + knobSpacing, knobYM, knobSize, knobSize);
masterKnob.setBounds(1105 + (knobSize + knobSpacing) * 2, knobYM, knobSize, knobSize);
pbRangeKnob.setBounds(1105 + (knobSize + knobSpacing) * 3, knobYM, knobSize, knobSize);
panKnob.setBounds(sx, knobYM, knobSize, knobSize);
driveKnob.setBounds(sx + (knobSize + knobSpacing), knobYM, knobSize, knobSize);
masterKnob.setBounds(sx + (knobSize + knobSpacing) * 2, knobYM, knobSize, knobSize);
pbRangeKnob.setBounds(sx + (knobSize + knobSpacing) * 3, knobYM, knobSize, knobSize);
}
// --- FX SECTION ---
int fxY = 480;
int fxKnobSize = 100;
int fxKnobSize = 80;
int fxLabelH = 16;
int fxKnobY = fxY + 54; // sub-section y(476) + titlePad(6) + titleH(14) + bottomPad(6) + 4
fxLabel.setBounds(10, fxY + 8, 1640, fxLabelH);
// Distortion sub-section (X=172, Y=476, W=395, H=140)
distTypeBox.setBounds(179, fxKnobY + 2, 140, 36);
distAmountKnob.setBounds(327, fxKnobY, fxKnobSize, fxKnobSize);
distMixKnob.setBounds(437, fxKnobY, fxKnobSize, fxKnobSize);
// Distortion sub-section (X=10, W=512) — dropdown fixed at x=17, 3 knobs fill remaining width
distTypeBox.setBounds(17, fxKnobY + (fxKnobSize - 28) / 2, 140, 28);
{
int availLeft = 17 + 140;
int availW = (10 + 512) - availLeft;
int rowW = 3 * fxKnobSize + 2 * 6;
int dx = availLeft + (availW - rowW) / 2;
distAmountKnob.setBounds(dx, fxKnobY, fxKnobSize, fxKnobSize);
distSymmetryKnob.setBounds(dx + (fxKnobSize + 6), fxKnobY, fxKnobSize, fxKnobSize);
distToneKnob.setBounds(dx + (fxKnobSize + 6) * 2, fxKnobY, fxKnobSize, fxKnobSize);
}
// Compressor sub-section (X=577, Y=476, W=660, H=140)
compThresholdKnob.setBounds(585, fxKnobY, fxKnobSize, fxKnobSize);
compRatioKnob.setBounds(695, fxKnobY, fxKnobSize, fxKnobSize);
compAttackKnob.setBounds(805, fxKnobY, fxKnobSize, fxKnobSize);
compReleaseKnob.setBounds(915, fxKnobY, fxKnobSize, fxKnobSize);
compMakeupKnob.setBounds(1025, fxKnobY, fxKnobSize, fxKnobSize);
compMixKnob.setBounds(1135, fxKnobY, fxKnobSize, fxKnobSize);
// Compressor sub-section (X=528, W=470)
{
int rowW = 5 * fxKnobSize + 4 * 6;
int sx = 528 + (470 - rowW) / 2;
compThresholdKnob.setBounds(sx, fxKnobY, fxKnobSize, fxKnobSize);
compRatioKnob.setBounds(sx + (fxKnobSize + 6), fxKnobY, fxKnobSize, fxKnobSize);
compAttackKnob.setBounds(sx + (fxKnobSize + 6) * 2, fxKnobY, fxKnobSize, fxKnobSize);
compReleaseKnob.setBounds(sx + (fxKnobSize + 6) * 3, fxKnobY, fxKnobSize, fxKnobSize);
compMakeupKnob.setBounds(sx + (fxKnobSize + 6) * 4, fxKnobY, fxKnobSize, fxKnobSize);
}
// Auto-Pan sub-section (X=1247, Y=476, W=240, H=140)
autoPanRateKnob.setBounds(1255, fxKnobY, fxKnobSize, fxKnobSize);
autoPanDepthKnob.setBounds(1255 + fxKnobSize + 8, fxKnobY, fxKnobSize, fxKnobSize);
// Auto-Pan sub-section (X=1004, W=270)
{
int rowW = 3 * fxKnobSize + 2 * 6;
int ax = 1004 + (270 - rowW) / 2;
autoPanRateKnob.setBounds(ax, fxKnobY, fxKnobSize, fxKnobSize);
autoPanDepthKnob.setBounds(ax + (fxKnobSize + 6), fxKnobY, fxKnobSize, fxKnobSize);
autoPanPhaseKnob.setBounds(ax + (fxKnobSize + 6) * 2, fxKnobY, fxKnobSize, fxKnobSize);
}
// Limiter sub-section (X=1280, W=370)
{
int rowW = 3 * fxKnobSize + 2 * 6;
int lx = 1280 + (370 - rowW) / 2;
limiterThresholdKnob.setBounds(lx, fxKnobY, fxKnobSize, fxKnobSize);
limiterCeilingKnob.setBounds(lx + (fxKnobSize + 6), fxKnobY, fxKnobSize, fxKnobSize);
limiterReleaseKnob.setBounds(lx + (fxKnobSize + 6) * 2, fxKnobY, fxKnobSize, fxKnobSize);
}
// --- LFO SECTION ---
int lfoY = 638;

View file

@ -143,9 +143,10 @@ private:
// FX controls
juce::ComboBox distTypeBox;
juce::Slider distAmountKnob, distMixKnob;
juce::Slider compThresholdKnob, compRatioKnob, compAttackKnob, compReleaseKnob, compMakeupKnob, compMixKnob;
juce::Slider autoPanRateKnob, autoPanDepthKnob;
juce::Slider distAmountKnob, distSymmetryKnob, distToneKnob;
juce::Slider compThresholdKnob, compRatioKnob, compAttackKnob, compReleaseKnob, compMakeupKnob;
juce::Slider autoPanRateKnob, autoPanDepthKnob, autoPanPhaseKnob;
juce::Slider limiterThresholdKnob, limiterCeilingKnob, limiterReleaseKnob;
// LFO controls
juce::Slider lfo1RateKnob, lfo1DepthKnob;
@ -181,10 +182,11 @@ private:
// FX attachments
std::unique_ptr<ComboBoxAttachment> distTypeAttach;
std::unique_ptr<SliderAttachment> distAmountAttach, distMixAttach;
std::unique_ptr<SliderAttachment> distAmountAttach, distSymmetryAttach, distToneAttach;
std::unique_ptr<SliderAttachment> compThresholdAttach, compRatioAttach, compAttackAttach;
std::unique_ptr<SliderAttachment> compReleaseAttach, compMakeupAttach, compMixAttach;
std::unique_ptr<SliderAttachment> autoPanRateAttach, autoPanDepthAttach;
std::unique_ptr<SliderAttachment> compReleaseAttach, compMakeupAttach;
std::unique_ptr<SliderAttachment> autoPanRateAttach, autoPanDepthAttach, autoPanPhaseAttach;
std::unique_ptr<SliderAttachment> limiterThresholdAttach, limiterCeilingAttach, limiterReleaseAttach;
// LFO attachments
std::unique_ptr<SliderAttachment> lfo1RateAttach, lfo1DepthAttach, lfo1BeatAttach;

View file

@ -180,6 +180,19 @@ juce::AudioProcessorValueTreeState::ParameterLayout ChromaFlockProcessor::create
juce::NormalisableRange<float>(0.05f, 20.0f, 0.01f, 0.4f), 2.0f));
layout.add(std::make_unique<juce::AudioParameterFloat>(
juce::ParameterID{"autoPanDepth", 1}, "Pan Depth", zeroOne, 0.0f));
layout.add(std::make_unique<juce::AudioParameterFloat>(
juce::ParameterID{"autoPanPhase", 1}, "Pan Phase", zeroOne, 0.0f));
// LIMITER
layout.add(std::make_unique<juce::AudioParameterFloat>(
juce::ParameterID{"limiterThreshold", 1}, "Limiter Threshold",
juce::NormalisableRange<float>(0.0f, 1.0f, 0.001f, 0.3f), 1.0f));
layout.add(std::make_unique<juce::AudioParameterFloat>(
juce::ParameterID{"limiterCeiling", 1}, "Limiter Ceiling",
juce::NormalisableRange<float>(0.0f, 1.0f, 0.001f, 0.3f), 0.9f));
layout.add(std::make_unique<juce::AudioParameterFloat>(
juce::ParameterID{"limiterRelease", 1}, "Limiter Release",
juce::NormalisableRange<float>(1.0f, 500.0f, 0.1f), 50.0f));
// LFO 1
layout.add(std::make_unique<juce::AudioParameterFloat>(
@ -256,6 +269,7 @@ void ChromaFlockProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
synth.setCurrentPlaybackSampleRate(sampleRate);
distortion.prepare(sampleRate);
compressor.prepare(sampleRate);
limiter.prepare(sampleRate);
delay.prepare(sampleRate);
reverbFX.prepare(sampleRate);
autoPanPhase = 0.0f;
@ -402,6 +416,13 @@ void ChromaFlockProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::
}
delay.setParameters(delayTimeMs, getParam("delayFeedback"), getParam("delayMix"));
reverbFX.setParameters(getParam("reverbSize"), getParam("reverbDamping"), 0.8f, getParam("reverbMix"));
limiter.setParameters(getParam("limiterThreshold"), getParam("limiterCeiling"),
getParam("limiterRelease"));
// Master fader applies a natural (perceptual) taper and is the final gain
// stage, so it always controls output level regardless of the limiter.
float masterValue = getParam("masterLevel");
float masterGain = masterValue * masterValue;
// FX processing: distortion → compression → auto-pan
int numSamples = buffer.getNumSamples();
@ -421,9 +442,13 @@ void ChromaFlockProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::
left = compressor.process(left);
right = compressor.process(right);
// Brickwall limiter (stereo)
left = limiter.process(left);
right = limiter.process(right);
// Auto-pan
if (panDepth > 0.001f) {
float panLfo = std::sin(autoPanPhase * twoPi);
float panLfo = std::sin((autoPanPhase + autoPanPhaseOffset) * twoPi);
float leftGain = 1.0f - panDepth * 0.5f * (1.0f + panLfo);
float rightGain = 1.0f - panDepth * 0.5f * (1.0f - panLfo);
left *= leftGain;