From 6cd7d4f3ba8aa36546eedf76735ee4c33a77a41e Mon Sep 17 00:00:00 2001 From: Armin Date: Wed, 15 Jul 2026 23:49:51 +0200 Subject: [PATCH] Add brickwall limiter FX section --- Source/DSP/Limiter.h | 44 ++++++++++++++ Source/PluginEditor.cpp | 119 +++++++++++++++++++++++++------------ Source/PluginEditor.h | 14 +++-- Source/PluginProcessor.cpp | 27 ++++++++- 4 files changed, 158 insertions(+), 46 deletions(-) create mode 100644 Source/DSP/Limiter.h diff --git a/Source/DSP/Limiter.h b/Source/DSP/Limiter.h new file mode 100644 index 0000000..fa4f6da --- /dev/null +++ b/Source/DSP/Limiter.h @@ -0,0 +1,44 @@ +#pragma once +#include +#include + +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(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; +}; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 17afe68..535c1b9 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -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); - 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); + 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(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 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); + { + int rowW = 4 * knobSize + 3 * knobSpacing; + int sx = 583 + (502 - rowW) / 2; + int knobYFE = 352 + labelH + 6; + 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); - 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); + 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(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; diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 5e61217..19b1452 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -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 distTypeAttach; - std::unique_ptr distAmountAttach, distMixAttach; + std::unique_ptr distAmountAttach, distSymmetryAttach, distToneAttach; std::unique_ptr compThresholdAttach, compRatioAttach, compAttackAttach; - std::unique_ptr compReleaseAttach, compMakeupAttach, compMixAttach; - std::unique_ptr autoPanRateAttach, autoPanDepthAttach; + std::unique_ptr compReleaseAttach, compMakeupAttach; + std::unique_ptr autoPanRateAttach, autoPanDepthAttach, autoPanPhaseAttach; + std::unique_ptr limiterThresholdAttach, limiterCeilingAttach, limiterReleaseAttach; // LFO attachments std::unique_ptr lfo1RateAttach, lfo1DepthAttach, lfo1BeatAttach; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 95486d5..ac028ef 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -180,6 +180,19 @@ juce::AudioProcessorValueTreeState::ParameterLayout ChromaFlockProcessor::create juce::NormalisableRange(0.05f, 20.0f, 0.01f, 0.4f), 2.0f)); layout.add(std::make_unique( juce::ParameterID{"autoPanDepth", 1}, "Pan Depth", zeroOne, 0.0f)); + layout.add(std::make_unique( + juce::ParameterID{"autoPanPhase", 1}, "Pan Phase", zeroOne, 0.0f)); + + // LIMITER + layout.add(std::make_unique( + juce::ParameterID{"limiterThreshold", 1}, "Limiter Threshold", + juce::NormalisableRange(0.0f, 1.0f, 0.001f, 0.3f), 1.0f)); + layout.add(std::make_unique( + juce::ParameterID{"limiterCeiling", 1}, "Limiter Ceiling", + juce::NormalisableRange(0.0f, 1.0f, 0.001f, 0.3f), 0.9f)); + layout.add(std::make_unique( + juce::ParameterID{"limiterRelease", 1}, "Limiter Release", + juce::NormalisableRange(1.0f, 500.0f, 0.1f), 50.0f)); // LFO 1 layout.add(std::make_unique( @@ -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& 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& 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;