mirror of
https://codeberg.org/armin/chromaflock.git
synced 2026-09-01 04:10:47 +02:00
Add brickwall limiter FX section
This commit is contained in:
parent
fcdb1939ee
commit
6cd7d4f3ba
4 changed files with 161 additions and 49 deletions
44
Source/DSP/Limiter.h
Normal file
44
Source/DSP/Limiter.h
Normal 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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue