Per-pad FX chain, animated toggles, GR meter, simplified master panel
- FX moved from master bus to per-pad processing: each pad has its own Filter, Distortion, EQ, Compressor, Reverb via DrumPad::applyPadFx() with temp buffer rendering - FxPanel now edits the selected pad's FX parameters - Animated toggle switches with smooth lerp transition and glow - Per-pad compressor GR meter connected to FxPanel display - Master panel simplified: Volume/Tune/Pan + Limiter toggle + VU meter - Master bus chain: Vol/Pan → Output Limiter (0dB brickwall) → VU - Pointer glow reduced to half intensity (4 layers, narrower spread) - Smooth 8-layer arc glow with exponential opacity falloff Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,14 +6,29 @@ DrumPad::~DrumPad() {}
|
||||
void DrumPad::prepareToPlay (double sr, int samplesPerBlock)
|
||||
{
|
||||
sampleRate = sr;
|
||||
blockSize = samplesPerBlock;
|
||||
|
||||
// Prepare per-pad filter
|
||||
juce::dsp::ProcessSpec spec { sr, (juce::uint32) samplesPerBlock, 1 };
|
||||
filterL.prepare (spec);
|
||||
filterR.prepare (spec);
|
||||
filterL.reset();
|
||||
filterR.reset();
|
||||
tempBuffer.setSize (2, samplesPerBlock);
|
||||
|
||||
// Per-pad filter
|
||||
juce::dsp::ProcessSpec monoSpec { sr, (juce::uint32) samplesPerBlock, 1 };
|
||||
filterL.prepare (monoSpec); filterR.prepare (monoSpec);
|
||||
filterL.reset(); filterR.reset();
|
||||
lastCutoff = filterCutoff;
|
||||
|
||||
// Per-pad FX
|
||||
juce::dsp::ProcessSpec stereoSpec { sr, (juce::uint32) samplesPerBlock, 2 };
|
||||
padCompressor.prepare (stereoSpec);
|
||||
padCompressor.reset();
|
||||
padReverb.prepare (stereoSpec);
|
||||
padReverb.reset();
|
||||
|
||||
padEqLoL.prepare (monoSpec); padEqLoR.prepare (monoSpec);
|
||||
padEqMidL.prepare (monoSpec); padEqMidR.prepare (monoSpec);
|
||||
padEqHiL.prepare (monoSpec); padEqHiR.prepare (monoSpec);
|
||||
padEqLoL.reset(); padEqLoR.reset();
|
||||
padEqMidL.reset(); padEqMidR.reset();
|
||||
padEqHiL.reset(); padEqHiR.reset();
|
||||
}
|
||||
|
||||
void DrumPad::releaseResources()
|
||||
@@ -335,6 +350,12 @@ void DrumPad::renderNextBlock (juce::AudioBuffer<float>& outputBuffer, int start
|
||||
if (! playing || activeSample == nullptr)
|
||||
return;
|
||||
|
||||
// Ensure temp buffer is large enough
|
||||
if (tempBuffer.getNumSamples() < numSamples)
|
||||
tempBuffer.setSize (2, numSamples, false, false, true);
|
||||
|
||||
tempBuffer.clear (0, numSamples);
|
||||
|
||||
const auto& sampleBuffer = activeSample->buffer;
|
||||
const int sampleLength = sampleBuffer.getNumSamples();
|
||||
const int srcChannels = sampleBuffer.getNumChannels();
|
||||
@@ -348,7 +369,7 @@ void DrumPad::renderNextBlock (juce::AudioBuffer<float>& outputBuffer, int start
|
||||
float rightGain = std::sin (panPos * juce::MathConstants<float>::halfPi);
|
||||
|
||||
// Update filter coefficients if cutoff changed
|
||||
if (std::abs (filterCutoff - lastCutoff) > 1.0f || std::abs (filterReso - filterL.coefficients->coefficients[0]) > 0.01f)
|
||||
if (std::abs (filterCutoff - lastCutoff) > 1.0f)
|
||||
{
|
||||
float clampedCutoff = juce::jlimit (20.0f, (float) (sampleRate * 0.49), filterCutoff);
|
||||
auto coeffs = juce::dsp::IIR::Coefficients<float>::makeLowPass (sampleRate, clampedCutoff, filterReso);
|
||||
@@ -357,8 +378,9 @@ void DrumPad::renderNextBlock (juce::AudioBuffer<float>& outputBuffer, int start
|
||||
lastCutoff = filterCutoff;
|
||||
}
|
||||
|
||||
bool useFilter = filterCutoff < 19900.0f; // Skip filter if fully open
|
||||
bool useFilter = filterCutoff < 19900.0f;
|
||||
|
||||
// Render into temp buffer
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
if (! playing) break;
|
||||
@@ -388,23 +410,119 @@ void DrumPad::renderNextBlock (juce::AudioBuffer<float>& outputBuffer, int start
|
||||
int pos1 = std::min (pos0 + 1, sampleLength - 1);
|
||||
float frac = (float) (readPosition - (double) pos0);
|
||||
|
||||
for (int ch = 0; ch < outputBuffer.getNumChannels(); ++ch)
|
||||
for (int ch = 0; ch < 2; ++ch)
|
||||
{
|
||||
int srcCh = std::min (ch, srcChannels - 1);
|
||||
float s0 = sampleBuffer.getSample (srcCh, pos0);
|
||||
float s1 = sampleBuffer.getSample (srcCh, pos1);
|
||||
float sampleVal = s0 + frac * (s1 - s0);
|
||||
|
||||
// Apply per-pad filter
|
||||
if (useFilter)
|
||||
sampleVal = (ch == 0) ? filterL.processSample (sampleVal)
|
||||
: filterR.processSample (sampleVal);
|
||||
|
||||
float channelGain = (ch == 0) ? leftGain : rightGain;
|
||||
outputBuffer.addSample (ch, startSample + i, sampleVal * gain * channelGain);
|
||||
tempBuffer.setSample (ch, i, sampleVal * gain * channelGain);
|
||||
}
|
||||
}
|
||||
|
||||
readPosition += pitchRatio;
|
||||
}
|
||||
|
||||
// Apply per-pad FX chain to temp buffer
|
||||
applyPadFx (tempBuffer, numSamples);
|
||||
|
||||
// Mix temp buffer into output
|
||||
for (int ch = 0; ch < outputBuffer.getNumChannels(); ++ch)
|
||||
outputBuffer.addFrom (ch, startSample, tempBuffer, std::min (ch, 1), 0, numSamples);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Per-pad FX chain
|
||||
// ============================================================
|
||||
|
||||
void DrumPad::applyPadFx (juce::AudioBuffer<float>& buf, int numSamples)
|
||||
{
|
||||
// --- Distortion ---
|
||||
if (fxDistEnabled && fxDistDrive > 0.001f && fxDistMix > 0.001f)
|
||||
{
|
||||
float driveGain = 1.0f + fxDistDrive * 20.0f;
|
||||
for (int ch = 0; ch < buf.getNumChannels(); ++ch)
|
||||
{
|
||||
float* data = buf.getWritePointer (ch);
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
float dry = data[i];
|
||||
float wet = std::tanh (dry * driveGain) / std::tanh (driveGain);
|
||||
data[i] = dry * (1.0f - fxDistMix) + wet * fxDistMix;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- EQ ---
|
||||
if (fxEqEnabled && (std::abs (fxEqLo) > 0.1f || std::abs (fxEqMid) > 0.1f || std::abs (fxEqHi) > 0.1f))
|
||||
{
|
||||
auto loC = juce::dsp::IIR::Coefficients<float>::makeLowShelf (sampleRate, 200.0, 0.707f, juce::Decibels::decibelsToGain (fxEqLo));
|
||||
auto midC = juce::dsp::IIR::Coefficients<float>::makePeakFilter (sampleRate, 1000.0, 1.0f, juce::Decibels::decibelsToGain (fxEqMid));
|
||||
auto hiC = juce::dsp::IIR::Coefficients<float>::makeHighShelf (sampleRate, 5000.0, 0.707f, juce::Decibels::decibelsToGain (fxEqHi));
|
||||
|
||||
*padEqLoL.coefficients = *loC; *padEqLoR.coefficients = *loC;
|
||||
*padEqMidL.coefficients = *midC; *padEqMidR.coefficients = *midC;
|
||||
*padEqHiL.coefficients = *hiC; *padEqHiR.coefficients = *hiC;
|
||||
|
||||
float* L = buf.getWritePointer (0);
|
||||
float* R = buf.getWritePointer (1);
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
L[i] = padEqHiL.processSample (padEqMidL.processSample (padEqLoL.processSample (L[i])));
|
||||
R[i] = padEqHiR.processSample (padEqMidR.processSample (padEqLoR.processSample (R[i])));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Compressor ---
|
||||
if (fxCompEnabled)
|
||||
{
|
||||
float peakLevel = 0.0f;
|
||||
for (int ch = 0; ch < buf.getNumChannels(); ++ch)
|
||||
peakLevel = std::max (peakLevel, buf.getMagnitude (ch, 0, numSamples));
|
||||
float inputDb = juce::Decibels::gainToDecibels (peakLevel, -80.0f);
|
||||
|
||||
float gr = 0.0f;
|
||||
if (inputDb > fxCompThreshold && fxCompRatio > 1.0f)
|
||||
gr = (inputDb - fxCompThreshold) * (1.0f - 1.0f / fxCompRatio);
|
||||
|
||||
float prevGr = std::abs (compGainReduction.load());
|
||||
if (gr > prevGr)
|
||||
compGainReduction.store (-(prevGr * 0.3f + gr * 0.7f));
|
||||
else
|
||||
compGainReduction.store (-(prevGr * 0.92f + gr * 0.08f));
|
||||
|
||||
padCompressor.setThreshold (fxCompThreshold);
|
||||
padCompressor.setRatio (fxCompRatio);
|
||||
padCompressor.setAttack (10.0f);
|
||||
padCompressor.setRelease (100.0f);
|
||||
juce::dsp::AudioBlock<float> block (buf);
|
||||
juce::dsp::ProcessContextReplacing<float> ctx (block);
|
||||
padCompressor.process (ctx);
|
||||
}
|
||||
else
|
||||
{
|
||||
float prev = std::abs (compGainReduction.load());
|
||||
compGainReduction.store (-(prev * 0.9f));
|
||||
}
|
||||
|
||||
// --- Reverb ---
|
||||
if (fxReverbEnabled && (fxReverbSize > 0.01f || fxReverbDecay > 0.01f))
|
||||
{
|
||||
juce::dsp::Reverb::Parameters rp;
|
||||
rp.roomSize = fxReverbSize;
|
||||
rp.damping = 1.0f - fxReverbDecay;
|
||||
rp.wetLevel = fxReverbSize * 0.5f;
|
||||
rp.dryLevel = 1.0f;
|
||||
rp.width = 1.0f;
|
||||
padReverb.setParameters (rp);
|
||||
juce::dsp::AudioBlock<float> block (buf);
|
||||
juce::dsp::ProcessContextReplacing<float> ctx (block);
|
||||
padReverb.process (ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
class DrumPad
|
||||
{
|
||||
public:
|
||||
// A single sample with its audio data and source sample rate
|
||||
struct Sample
|
||||
{
|
||||
juce::AudioBuffer<float> buffer;
|
||||
@@ -12,12 +11,11 @@ public:
|
||||
juce::File file;
|
||||
};
|
||||
|
||||
// A velocity layer: velocity range + multiple round-robin samples
|
||||
struct VelocityLayer
|
||||
{
|
||||
float velocityLow = 0.0f; // 0.0 - 1.0
|
||||
float velocityLow = 0.0f;
|
||||
float velocityHigh = 1.0f;
|
||||
juce::OwnedArray<Sample> samples; // round-robin variations
|
||||
juce::OwnedArray<Sample> samples;
|
||||
int nextRoundRobin = 0;
|
||||
|
||||
Sample* getNextSample()
|
||||
@@ -35,12 +33,8 @@ public:
|
||||
void prepareToPlay (double sampleRate, int samplesPerBlock);
|
||||
void releaseResources();
|
||||
|
||||
// Single sample loading (backwards compatible)
|
||||
void loadSample (const juce::File& file, juce::AudioFormatManager& formatManager);
|
||||
|
||||
// Velocity layer loading from a folder
|
||||
void loadLayersFromFolder (const juce::File& folder, juce::AudioFormatManager& formatManager);
|
||||
|
||||
bool hasSample() const;
|
||||
|
||||
void trigger (float velocity = 1.0f);
|
||||
@@ -65,8 +59,29 @@ public:
|
||||
float release = 0.05f;
|
||||
|
||||
// Per-pad filter
|
||||
float filterCutoff = 20000.0f; // Hz
|
||||
float filterReso = 0.707f; // Q
|
||||
float filterCutoff = 20000.0f;
|
||||
float filterReso = 0.707f;
|
||||
|
||||
// Per-pad FX parameters
|
||||
bool fxCompEnabled = false;
|
||||
float fxCompThreshold = -12.0f;
|
||||
float fxCompRatio = 4.0f;
|
||||
|
||||
bool fxEqEnabled = false;
|
||||
float fxEqLo = 0.0f;
|
||||
float fxEqMid = 0.0f;
|
||||
float fxEqHi = 0.0f;
|
||||
|
||||
bool fxDistEnabled = false;
|
||||
float fxDistDrive = 0.0f;
|
||||
float fxDistMix = 0.0f;
|
||||
|
||||
bool fxReverbEnabled = false;
|
||||
float fxReverbSize = 0.3f;
|
||||
float fxReverbDecay = 0.5f;
|
||||
|
||||
// Compressor GR readout (written by audio, read by GUI)
|
||||
std::atomic<float> compGainReduction { 0.0f };
|
||||
|
||||
// State
|
||||
bool isPlaying() const { return playing; }
|
||||
@@ -77,24 +92,26 @@ public:
|
||||
const juce::AudioBuffer<float>& getSampleBuffer() const;
|
||||
|
||||
private:
|
||||
// Velocity layers (sorted by velocity range)
|
||||
juce::OwnedArray<VelocityLayer> layers;
|
||||
|
||||
// Currently playing sample reference
|
||||
Sample* activeSample = nullptr;
|
||||
|
||||
// Fallback empty buffer for getSampleBuffer when nothing loaded
|
||||
juce::AudioBuffer<float> emptyBuffer;
|
||||
juce::AudioBuffer<float> tempBuffer; // for per-pad FX processing
|
||||
|
||||
double sampleRate = 44100.0;
|
||||
int blockSize = 512;
|
||||
double readPosition = 0.0;
|
||||
bool playing = false;
|
||||
float currentVelocity = 1.0f;
|
||||
|
||||
// Per-pad low-pass filter (stereo)
|
||||
// Per-pad filter (stereo)
|
||||
juce::dsp::IIR::Filter<float> filterL, filterR;
|
||||
float lastCutoff = 20000.0f;
|
||||
|
||||
// Per-pad FX processors
|
||||
juce::dsp::Compressor<float> padCompressor;
|
||||
juce::dsp::IIR::Filter<float> padEqLoL, padEqLoR, padEqMidL, padEqMidR, padEqHiL, padEqHiR;
|
||||
juce::dsp::Reverb padReverb;
|
||||
|
||||
// ADSR state
|
||||
enum class EnvelopeStage { Idle, Attack, Decay, Sustain, Release };
|
||||
EnvelopeStage envStage = EnvelopeStage::Idle;
|
||||
@@ -105,8 +122,8 @@ private:
|
||||
|
||||
void advanceEnvelope();
|
||||
VelocityLayer* findLayerForVelocity (float velocity);
|
||||
void applyPadFx (juce::AudioBuffer<float>& buf, int numSamples);
|
||||
|
||||
// Parse velocity tag from filename (e.g. "snare_OH_FF_1" -> FF)
|
||||
static float velocityTagToLow (const juce::String& tag);
|
||||
static float velocityTagToHigh (const juce::String& tag);
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@ FxPanel::FxPanel()
|
||||
setupKnob (distMixSlider, distMixLabel, "Mix", 0.0, 1.0, 0.0, 0.01);
|
||||
setupKnob (reverbSizeSlider, reverbSizeLabel, "Size", 0.0, 1.0, 0.3, 0.01);
|
||||
setupKnob (reverbDecaySlider, reverbDecayLabel, "Decay", 0.0, 1.0, 0.5, 0.01);
|
||||
|
||||
setupToggle (compToggle);
|
||||
setupToggle (eqToggle);
|
||||
setupToggle (distToggle);
|
||||
setupToggle (reverbToggle);
|
||||
}
|
||||
|
||||
void FxPanel::setupKnob (juce::Slider& s, juce::Label& l, const juce::String& name,
|
||||
@@ -44,6 +49,63 @@ void FxPanel::setupTitle (juce::Label& l, const juce::String& text)
|
||||
addAndMakeVisible (l);
|
||||
}
|
||||
|
||||
void FxPanel::setupToggle (juce::ToggleButton& t)
|
||||
{
|
||||
t.setToggleState (false, juce::dontSendNotification);
|
||||
t.setButtonText ("");
|
||||
addAndMakeVisible (t);
|
||||
}
|
||||
|
||||
void FxPanel::setPad (DrumPad* pad)
|
||||
{
|
||||
currentPad = pad;
|
||||
syncFromPad();
|
||||
}
|
||||
|
||||
void FxPanel::syncFromPad()
|
||||
{
|
||||
if (currentPad == nullptr) return;
|
||||
|
||||
compThreshSlider.setValue (currentPad->fxCompThreshold, juce::dontSendNotification);
|
||||
compRatioSlider.setValue (currentPad->fxCompRatio, juce::dontSendNotification);
|
||||
compToggle.setToggleState (currentPad->fxCompEnabled, juce::dontSendNotification);
|
||||
|
||||
eqLoSlider.setValue (currentPad->fxEqLo, juce::dontSendNotification);
|
||||
eqMidSlider.setValue (currentPad->fxEqMid, juce::dontSendNotification);
|
||||
eqHiSlider.setValue (currentPad->fxEqHi, juce::dontSendNotification);
|
||||
eqToggle.setToggleState (currentPad->fxEqEnabled, juce::dontSendNotification);
|
||||
|
||||
distDriveSlider.setValue (currentPad->fxDistDrive, juce::dontSendNotification);
|
||||
distMixSlider.setValue (currentPad->fxDistMix, juce::dontSendNotification);
|
||||
distToggle.setToggleState (currentPad->fxDistEnabled, juce::dontSendNotification);
|
||||
|
||||
reverbSizeSlider.setValue (currentPad->fxReverbSize, juce::dontSendNotification);
|
||||
reverbDecaySlider.setValue (currentPad->fxReverbDecay, juce::dontSendNotification);
|
||||
reverbToggle.setToggleState (currentPad->fxReverbEnabled, juce::dontSendNotification);
|
||||
}
|
||||
|
||||
void FxPanel::syncToPad()
|
||||
{
|
||||
if (currentPad == nullptr) return;
|
||||
|
||||
currentPad->fxCompThreshold = (float) compThreshSlider.getValue();
|
||||
currentPad->fxCompRatio = (float) compRatioSlider.getValue();
|
||||
currentPad->fxCompEnabled = compToggle.getToggleState();
|
||||
|
||||
currentPad->fxEqLo = (float) eqLoSlider.getValue();
|
||||
currentPad->fxEqMid = (float) eqMidSlider.getValue();
|
||||
currentPad->fxEqHi = (float) eqHiSlider.getValue();
|
||||
currentPad->fxEqEnabled = eqToggle.getToggleState();
|
||||
|
||||
currentPad->fxDistDrive = (float) distDriveSlider.getValue();
|
||||
currentPad->fxDistMix = (float) distMixSlider.getValue();
|
||||
currentPad->fxDistEnabled = distToggle.getToggleState();
|
||||
|
||||
currentPad->fxReverbSize = (float) reverbSizeSlider.getValue();
|
||||
currentPad->fxReverbDecay = (float) reverbDecaySlider.getValue();
|
||||
currentPad->fxReverbEnabled = reverbToggle.getToggleState();
|
||||
}
|
||||
|
||||
void FxPanel::paint (juce::Graphics& g)
|
||||
{
|
||||
auto bounds = getLocalBounds().toFloat();
|
||||
@@ -108,10 +170,18 @@ void FxPanel::resized()
|
||||
distTitle.setFont (juce::FontOptions (titleSize, juce::Font::bold));
|
||||
reverbTitle.setFont (juce::FontOptions (titleSize, juce::Font::bold));
|
||||
|
||||
int toggleW = std::max (28, (int) (36 * scale));
|
||||
int toggleH = std::max (16, (int) (20 * scale));
|
||||
int sectionTitleH = std::max (titleH, toggleH + 4);
|
||||
|
||||
auto layoutSection = [&] (juce::Rectangle<int> secArea, juce::Label& title,
|
||||
juce::ToggleButton& toggle,
|
||||
juce::Slider* sliders[], juce::Label* labels[], int count)
|
||||
{
|
||||
title.setBounds (secArea.removeFromTop (titleH).reduced (4, 0));
|
||||
auto titleRow = secArea.removeFromTop (sectionTitleH).reduced (2, 0);
|
||||
auto toggleArea = titleRow.removeFromLeft (toggleW + 4);
|
||||
toggle.setBounds (toggleArea.withSizeKeepingCentre (toggleW, toggleH));
|
||||
title.setBounds (titleRow);
|
||||
int kw = secArea.getWidth() / count;
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
@@ -122,12 +192,15 @@ void FxPanel::resized()
|
||||
}
|
||||
};
|
||||
|
||||
// Top-left: Compressor
|
||||
// Top-left: Compressor (with GR meter area on the right)
|
||||
{
|
||||
auto sec = area.removeFromTop (rowH).removeFromLeft (halfW).reduced (4, 2);
|
||||
auto compFullArea = area.removeFromTop (rowH).removeFromLeft (halfW).reduced (4, 2);
|
||||
int grMeterW = std::max (10, (int) (14 * scale));
|
||||
compGrArea = compFullArea.removeFromRight (grMeterW);
|
||||
auto sec = compFullArea;
|
||||
juce::Slider* s[] = { &compThreshSlider, &compRatioSlider };
|
||||
juce::Label* l[] = { &compThreshLabel, &compRatioLabel };
|
||||
layoutSection (sec, compTitle, s, l, 2);
|
||||
layoutSection (sec, compTitle, compToggle, s, l, 2);
|
||||
}
|
||||
|
||||
// Top-right: EQ (need to recalculate since we consumed area)
|
||||
@@ -139,7 +212,7 @@ void FxPanel::resized()
|
||||
{
|
||||
juce::Slider* s[] = { &eqLoSlider, &eqMidSlider, &eqHiSlider };
|
||||
juce::Label* l[] = { &eqLoLabel, &eqMidLabel, &eqHiLabel };
|
||||
layoutSection (rightTop, eqTitle, s, l, 3);
|
||||
layoutSection (rightTop, eqTitle, eqToggle, s, l, 3);
|
||||
}
|
||||
|
||||
// Bottom-left: Distortion
|
||||
@@ -149,7 +222,7 @@ void FxPanel::resized()
|
||||
auto sec = bottomArea.removeFromLeft (halfW).reduced (4, 2);
|
||||
juce::Slider* s[] = { &distDriveSlider, &distMixSlider };
|
||||
juce::Label* l[] = { &distDriveLabel, &distMixLabel };
|
||||
layoutSection (sec, distTitle, s, l, 2);
|
||||
layoutSection (sec, distTitle, distToggle, s, l, 2);
|
||||
}
|
||||
|
||||
// Bottom-right: Reverb
|
||||
@@ -157,6 +230,48 @@ void FxPanel::resized()
|
||||
auto sec = bottomArea.reduced (4, 2);
|
||||
juce::Slider* s[] = { &reverbSizeSlider, &reverbDecaySlider };
|
||||
juce::Label* l[] = { &reverbSizeLabel, &reverbDecayLabel };
|
||||
layoutSection (sec, reverbTitle, s, l, 2);
|
||||
layoutSection (sec, reverbTitle, reverbToggle, s, l, 2);
|
||||
}
|
||||
}
|
||||
|
||||
void FxPanel::paintOverChildren (juce::Graphics& g)
|
||||
{
|
||||
// Draw compressor GR meter
|
||||
if (compGrArea.isEmpty()) return;
|
||||
|
||||
auto bar = compGrArea.toFloat().reduced (2, 4);
|
||||
|
||||
// Background
|
||||
g.setColour (juce::Colour (0xff111122));
|
||||
g.fillRoundedRectangle (bar, 2.0f);
|
||||
|
||||
// GR bar (grows downward from top, since GR is negative)
|
||||
float grNorm = juce::jlimit (0.0f, 1.0f, std::abs (compGrDb) / 30.0f); // 30dB range
|
||||
float barH = bar.getHeight() * grNorm;
|
||||
|
||||
if (barH > 0.5f)
|
||||
{
|
||||
auto filled = bar.removeFromTop (barH);
|
||||
|
||||
// Colour: green for light GR, orange for medium, red for heavy
|
||||
juce::Colour grColour;
|
||||
if (grNorm < 0.3f)
|
||||
grColour = juce::Colour (0xff00cc44);
|
||||
else if (grNorm < 0.6f)
|
||||
grColour = juce::Colour (0xffccaa00);
|
||||
else
|
||||
grColour = juce::Colour (0xffff4422);
|
||||
|
||||
g.setColour (grColour);
|
||||
g.fillRoundedRectangle (filled, 2.0f);
|
||||
|
||||
// Glow
|
||||
g.setColour (grColour.withAlpha (0.2f));
|
||||
g.fillRoundedRectangle (filled.expanded (2, 0), 3.0f);
|
||||
}
|
||||
|
||||
// "GR" label
|
||||
g.setColour (InstaDrumsLookAndFeel::textSecondary.withAlpha (0.6f));
|
||||
g.setFont (juce::FontOptions (8.0f));
|
||||
g.drawText ("GR", compGrArea.toFloat(), juce::Justification::centredBottom);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
#include "DrumPad.h"
|
||||
|
||||
class FxPanel : public juce::Component
|
||||
{
|
||||
@@ -9,37 +10,47 @@ public:
|
||||
void paint (juce::Graphics& g) override;
|
||||
void resized() override;
|
||||
|
||||
// FX parameter getters (for processor to read)
|
||||
float getCompThreshold() const { return (float) compThreshSlider.getValue(); }
|
||||
float getCompRatio() const { return (float) compRatioSlider.getValue(); }
|
||||
float getEqLo() const { return (float) eqLoSlider.getValue(); }
|
||||
float getEqMid() const { return (float) eqMidSlider.getValue(); }
|
||||
float getEqHi() const { return (float) eqHiSlider.getValue(); }
|
||||
float getDistDrive() const { return (float) distDriveSlider.getValue(); }
|
||||
float getDistMix() const { return (float) distMixSlider.getValue(); }
|
||||
float getReverbSize() const { return (float) reverbSizeSlider.getValue(); }
|
||||
float getReverbDecay() const { return (float) reverbDecaySlider.getValue(); }
|
||||
// Connect to a pad's FX params
|
||||
void setPad (DrumPad* pad);
|
||||
void syncToPad(); // write GUI values to pad
|
||||
void syncFromPad(); // read pad values to GUI
|
||||
|
||||
// Compressor GR meter
|
||||
void setCompGainReduction (float grDb) { compGrDb = grDb; repaint (compGrArea); }
|
||||
|
||||
void paintOverChildren (juce::Graphics& g) override;
|
||||
|
||||
private:
|
||||
// Compressor
|
||||
juce::Slider compThreshSlider, compRatioSlider;
|
||||
juce::Label compThreshLabel, compRatioLabel, compTitle;
|
||||
juce::ToggleButton compToggle;
|
||||
|
||||
// EQ
|
||||
juce::Slider eqLoSlider, eqMidSlider, eqHiSlider;
|
||||
juce::Label eqLoLabel, eqMidLabel, eqHiLabel, eqTitle;
|
||||
juce::ToggleButton eqToggle;
|
||||
|
||||
// Distortion
|
||||
juce::Slider distDriveSlider, distMixSlider;
|
||||
juce::Label distDriveLabel, distMixLabel, distTitle;
|
||||
juce::ToggleButton distToggle;
|
||||
|
||||
// Reverb
|
||||
juce::Slider reverbSizeSlider, reverbDecaySlider;
|
||||
juce::Label reverbSizeLabel, reverbDecayLabel, reverbTitle;
|
||||
juce::ToggleButton reverbToggle;
|
||||
|
||||
DrumPad* currentPad = nullptr;
|
||||
|
||||
float compGrDb = 0.0f;
|
||||
bool repaintCompGr = false;
|
||||
juce::Rectangle<int> compGrArea; // stored from resized for paintOverChildren
|
||||
|
||||
void setupKnob (juce::Slider& s, juce::Label& l, const juce::String& name,
|
||||
double min, double max, double val, double step = 0.01);
|
||||
void setupTitle (juce::Label& l, const juce::String& text);
|
||||
void setupToggle (juce::ToggleButton& t);
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FxPanel)
|
||||
};
|
||||
|
||||
@@ -131,30 +131,31 @@ void InstaDrumsLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, i
|
||||
juce::PathStrokeType::rounded));
|
||||
}
|
||||
|
||||
// 3. Outer arc value with scaled glow
|
||||
// 3. Outer arc value with smooth multi-layer glow
|
||||
if (sliderPos > 0.01f)
|
||||
{
|
||||
juce::Path arcVal;
|
||||
arcVal.addCentredArc (cx, cy, radius - 1, radius - 1, 0.0f,
|
||||
rotaryStartAngle, angle, true);
|
||||
|
||||
// Glow layers (scale with knob size)
|
||||
g.setColour (arcColour.withAlpha (0.08f));
|
||||
g.strokePath (arcVal, juce::PathStrokeType (glowW1, juce::PathStrokeType::curved,
|
||||
juce::PathStrokeType::rounded));
|
||||
g.setColour (arcColour.withAlpha (0.15f));
|
||||
g.strokePath (arcVal, juce::PathStrokeType (glowW2, juce::PathStrokeType::curved,
|
||||
juce::PathStrokeType::rounded));
|
||||
g.setColour (arcColour.withAlpha (0.3f));
|
||||
g.strokePath (arcVal, juce::PathStrokeType (glowW3, juce::PathStrokeType::curved,
|
||||
// Smooth glow: 8 layers from wide/faint to narrow/bright
|
||||
const int numGlowLayers = 8;
|
||||
for (int i = 0; i < numGlowLayers; ++i)
|
||||
{
|
||||
float t = (float) i / (float) (numGlowLayers - 1); // 0.0 (outermost) to 1.0 (innermost)
|
||||
float layerWidth = glowW1 * (1.0f - t * 0.7f); // wide -> narrow
|
||||
float layerAlpha = 0.03f + t * t * 0.35f; // exponential: faint -> bright
|
||||
g.setColour (arcColour.withAlpha (layerAlpha));
|
||||
g.strokePath (arcVal, juce::PathStrokeType (layerWidth, juce::PathStrokeType::curved,
|
||||
juce::PathStrokeType::rounded));
|
||||
}
|
||||
|
||||
// Core arc
|
||||
// Core arc (full brightness)
|
||||
g.setColour (arcColour);
|
||||
g.strokePath (arcVal, juce::PathStrokeType (arcW, juce::PathStrokeType::curved,
|
||||
juce::PathStrokeType::rounded));
|
||||
|
||||
// Hot center
|
||||
// Hot center (white-ish)
|
||||
g.setColour (arcColour.brighter (0.6f).withAlpha (0.5f));
|
||||
g.strokePath (arcVal, juce::PathStrokeType (hotW, juce::PathStrokeType::curved,
|
||||
juce::PathStrokeType::rounded));
|
||||
@@ -191,37 +192,32 @@ void InstaDrumsLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, i
|
||||
g.fillEllipse (cx - hlRadius, hlY - hlRadius * 0.6f, hlRadius * 2, hlRadius * 1.2f);
|
||||
}
|
||||
|
||||
// 8. Pointer with scaled glow
|
||||
// 8. Pointer with subtle glow (half intensity)
|
||||
{
|
||||
juce::Path pointer;
|
||||
float pointerLen = bodyRadius * 0.75f;
|
||||
|
||||
pointer.addRoundedRectangle (-ptrW * 0.5f, -pointerLen, ptrW, pointerLen * 0.55f, ptrW * 0.5f);
|
||||
pointer.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy));
|
||||
|
||||
// Wide outer glow
|
||||
g.setColour (pointerColour.withAlpha (0.1f));
|
||||
// Smooth glow: 4 layers, half the width
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
juce::Path glow3;
|
||||
float gw = ptrW * 3.5f;
|
||||
glow3.addRoundedRectangle (-gw, -pointerLen, gw * 2, pointerLen * 0.55f, ptrW * 1.5f);
|
||||
glow3.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy));
|
||||
g.fillPath (glow3);
|
||||
}
|
||||
float t = (float) i / 3.0f;
|
||||
float gw = ptrW * (2.0f - t * 1.5f); // narrower spread
|
||||
float alpha = 0.02f + t * t * 0.15f; // lower opacity
|
||||
|
||||
// Medium glow
|
||||
g.setColour (pointerColour.withAlpha (0.25f));
|
||||
{
|
||||
juce::Path glow2;
|
||||
float gw = ptrW * 2.0f;
|
||||
glow2.addRoundedRectangle (-gw, -pointerLen, gw * 2, pointerLen * 0.55f, ptrW);
|
||||
glow2.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy));
|
||||
g.fillPath (glow2);
|
||||
juce::Path glowLayer;
|
||||
glowLayer.addRoundedRectangle (-gw, -pointerLen, gw * 2, pointerLen * 0.55f, gw * 0.5f);
|
||||
glowLayer.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy));
|
||||
g.setColour (pointerColour.withAlpha (alpha));
|
||||
g.fillPath (glowLayer);
|
||||
}
|
||||
|
||||
// Core pointer
|
||||
{
|
||||
juce::Path pointer;
|
||||
pointer.addRoundedRectangle (-ptrW * 0.5f, -pointerLen, ptrW, pointerLen * 0.55f, ptrW * 0.5f);
|
||||
pointer.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy));
|
||||
g.setColour (pointerColour);
|
||||
g.fillPath (pointer);
|
||||
}
|
||||
|
||||
// Hot center
|
||||
{
|
||||
@@ -269,3 +265,100 @@ void InstaDrumsLookAndFeel::drawButtonBackground (juce::Graphics& g, juce::Butto
|
||||
g.setColour (bgLight.withAlpha (shouldDrawButtonAsHighlighted ? 0.8f : 0.5f));
|
||||
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Toggle button — glowing radio dot
|
||||
// ============================================================
|
||||
|
||||
void InstaDrumsLookAndFeel::drawToggleButton (juce::Graphics& g, juce::ToggleButton& button,
|
||||
bool shouldDrawButtonAsHighlighted,
|
||||
bool /*shouldDrawButtonAsDown*/)
|
||||
{
|
||||
auto bounds = button.getLocalBounds().toFloat();
|
||||
float h = std::min (bounds.getHeight() * 0.6f, 14.0f);
|
||||
float w = h * 1.8f;
|
||||
float trackR = h * 0.5f;
|
||||
|
||||
// Center the switch in the component
|
||||
float sx = bounds.getX() + (bounds.getWidth() - w) * 0.5f;
|
||||
float sy = bounds.getCentreY() - h * 0.5f;
|
||||
auto trackBounds = juce::Rectangle<float> (sx, sy, w, h);
|
||||
|
||||
bool isOn = button.getToggleState();
|
||||
auto onColour = accent;
|
||||
auto offColour = bgLight;
|
||||
|
||||
// Animated thumb position (stored as component property, lerps each frame)
|
||||
float targetPos = isOn ? 1.0f : 0.0f;
|
||||
float animPos = (float) button.getProperties().getWithDefault ("animPos", targetPos);
|
||||
animPos += (targetPos - animPos) * 0.25f; // smooth lerp
|
||||
if (std::abs (animPos - targetPos) < 0.01f) animPos = targetPos;
|
||||
button.getProperties().set ("animPos", animPos);
|
||||
|
||||
// Trigger repaint if still animating
|
||||
if (std::abs (animPos - targetPos) > 0.005f)
|
||||
button.repaint();
|
||||
|
||||
float thumbR = h * 0.4f;
|
||||
float thumbX = sx + trackR + animPos * (w - trackR * 2);
|
||||
float thumbY = sy + h * 0.5f;
|
||||
float glowIntensity = animPos; // 0 = off, 1 = full glow
|
||||
|
||||
// Track glow (intensity follows animation)
|
||||
if (glowIntensity > 0.01f)
|
||||
{
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
float t = (float) i / 2.0f;
|
||||
float expand = (1.0f - t) * 1.5f;
|
||||
float alpha = (0.04f + t * t * 0.1f) * glowIntensity;
|
||||
g.setColour (onColour.withAlpha (alpha));
|
||||
g.fillRoundedRectangle (trackBounds.expanded (expand), trackR + expand);
|
||||
}
|
||||
}
|
||||
|
||||
// Track background (blend between off/on colours)
|
||||
{
|
||||
juce::Colour offCol = offColour.withAlpha (0.3f);
|
||||
juce::Colour onCol = onColour.withAlpha (0.35f);
|
||||
juce::Colour trackCol = offCol.interpolatedWith (onCol, glowIntensity);
|
||||
if (shouldDrawButtonAsHighlighted)
|
||||
trackCol = trackCol.brighter (0.15f);
|
||||
g.setColour (trackCol);
|
||||
g.fillRoundedRectangle (trackBounds, trackR);
|
||||
|
||||
g.setColour (offColour.withAlpha (0.4f).interpolatedWith (onColour.withAlpha (0.5f), glowIntensity));
|
||||
g.drawRoundedRectangle (trackBounds, trackR, 0.8f);
|
||||
}
|
||||
|
||||
// Thumb glow (intensity follows animation)
|
||||
if (glowIntensity > 0.01f)
|
||||
{
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
float t = (float) i / 2.0f;
|
||||
float r = thumbR * (1.5f - t * 0.5f);
|
||||
float alpha = (0.05f + t * t * 0.12f) * glowIntensity;
|
||||
g.setColour (onColour.withAlpha (alpha));
|
||||
g.fillEllipse (thumbX - r, thumbY - r, r * 2, r * 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Thumb circle (colour blends with animation)
|
||||
{
|
||||
juce::Colour thumbTopOff (0xff555566), thumbBotOff (0xff333344);
|
||||
juce::Colour thumbTopOn = onColour.brighter (0.3f), thumbBotOn = onColour.darker (0.2f);
|
||||
juce::ColourGradient thumbGrad (
|
||||
thumbTopOff.interpolatedWith (thumbTopOn, glowIntensity), thumbX, thumbY - thumbR,
|
||||
thumbBotOff.interpolatedWith (thumbBotOn, glowIntensity), thumbX, thumbY + thumbR, false);
|
||||
g.setGradientFill (thumbGrad);
|
||||
g.fillEllipse (thumbX - thumbR, thumbY - thumbR, thumbR * 2, thumbR * 2);
|
||||
|
||||
g.setColour (juce::Colour (0xff666677).withAlpha (0.5f).interpolatedWith (onColour.withAlpha (0.6f), glowIntensity));
|
||||
g.drawEllipse (thumbX - thumbR, thumbY - thumbR, thumbR * 2, thumbR * 2, 0.8f);
|
||||
|
||||
float hlR = thumbR * 0.4f;
|
||||
g.setColour (juce::Colours::white.withAlpha (0.1f + 0.15f * glowIntensity));
|
||||
g.fillEllipse (thumbX - hlR, thumbY - thumbR * 0.6f - hlR * 0.3f, hlR * 2, hlR * 1.2f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ public:
|
||||
bool shouldDrawButtonAsHighlighted,
|
||||
bool shouldDrawButtonAsDown) override;
|
||||
|
||||
void drawToggleButton (juce::Graphics& g, juce::ToggleButton& button,
|
||||
bool shouldDrawButtonAsHighlighted,
|
||||
bool shouldDrawButtonAsDown) override;
|
||||
|
||||
// Custom fonts
|
||||
juce::Font getRegularFont (float height) const;
|
||||
juce::Font getMediumFont (float height) const;
|
||||
|
||||
@@ -10,6 +10,13 @@ MasterPanel::MasterPanel()
|
||||
setupKnob (tuneSlider, tuneLabel, "Tune", -12.0, 12.0, 0.0, 0.1);
|
||||
setupKnob (panSlider, panLabel, "Pan", -1.0, 1.0, 0.0, 0.01);
|
||||
|
||||
limiterToggle.setToggleState (true, juce::dontSendNotification);
|
||||
limiterToggle.setButtonText ("");
|
||||
addAndMakeVisible (limiterToggle);
|
||||
limiterLabel.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::accent);
|
||||
limiterLabel.setJustificationType (juce::Justification::centredLeft);
|
||||
addAndMakeVisible (limiterLabel);
|
||||
|
||||
addAndMakeVisible (vuMeter);
|
||||
}
|
||||
|
||||
@@ -45,13 +52,29 @@ void MasterPanel::resized()
|
||||
float titleSize = std::max (12.0f, 16.0f * scale);
|
||||
float labelSize = std::max (9.0f, 12.0f * scale);
|
||||
int labelH = (int) (labelSize + 4);
|
||||
int toggleW = std::max (28, (int) (36 * scale));
|
||||
int toggleH = std::max (14, (int) (16 * scale));
|
||||
|
||||
masterTitle.setFont (juce::FontOptions (titleSize, juce::Font::bold));
|
||||
masterTitle.setBounds (area.removeFromLeft ((int) (65 * scale)).reduced (0, 2));
|
||||
|
||||
// VU meter on right
|
||||
vuMeter.setBounds (area.removeFromRight ((int) (28 * scale)).reduced (0, 2));
|
||||
area.removeFromRight (4);
|
||||
area.removeFromRight (6);
|
||||
|
||||
// Limiter toggle + label
|
||||
auto limArea = area.removeFromRight ((int) (90 * scale));
|
||||
{
|
||||
auto center = limArea.withSizeKeepingCentre (limArea.getWidth(), toggleH + 4);
|
||||
auto row = center;
|
||||
limiterToggle.setBounds (row.removeFromLeft (toggleW).withSizeKeepingCentre (toggleW, toggleH));
|
||||
limiterLabel.setFont (juce::FontOptions (std::max (10.0f, 13.0f * scale), juce::Font::bold));
|
||||
limiterLabel.setBounds (row);
|
||||
}
|
||||
|
||||
area.removeFromRight (6);
|
||||
|
||||
// Master knobs
|
||||
int knobW = area.getWidth() / 3;
|
||||
juce::Slider* sliders[] = { &volumeSlider, &tuneSlider, &panSlider };
|
||||
juce::Label* labels[] = { &volumeLabel, &tuneLabel, &panLabel };
|
||||
|
||||
@@ -13,6 +13,7 @@ public:
|
||||
float getMasterVolume() const { return (float) volumeSlider.getValue(); }
|
||||
float getMasterTune() const { return (float) tuneSlider.getValue(); }
|
||||
float getMasterPan() const { return (float) panSlider.getValue(); }
|
||||
bool isLimiterEnabled() const { return limiterToggle.getToggleState(); }
|
||||
|
||||
VuMeter& getVuMeter() { return vuMeter; }
|
||||
|
||||
@@ -20,6 +21,10 @@ private:
|
||||
juce::Slider volumeSlider, tuneSlider, panSlider;
|
||||
juce::Label volumeLabel, tuneLabel, panLabel;
|
||||
juce::Label masterTitle { {}, "MASTER" };
|
||||
|
||||
juce::ToggleButton limiterToggle;
|
||||
juce::Label limiterLabel { {}, "LIMITER" };
|
||||
|
||||
VuMeter vuMeter;
|
||||
|
||||
void setupKnob (juce::Slider& s, juce::Label& l, const juce::String& name,
|
||||
|
||||
@@ -142,7 +142,10 @@ void InstaDrumsEditor::selectPad (int index)
|
||||
padComponents[i]->setSelected (i == index);
|
||||
|
||||
if (index >= 0 && index < processor.getNumPads())
|
||||
{
|
||||
sampleEditor.setPad (&processor.getPad (index));
|
||||
fxPanel.setPad (&processor.getPad (index));
|
||||
}
|
||||
}
|
||||
|
||||
void InstaDrumsEditor::paint (juce::Graphics& g)
|
||||
@@ -167,7 +170,7 @@ void InstaDrumsEditor::paint (juce::Graphics& g)
|
||||
// Divider lines
|
||||
auto bounds = getLocalBounds();
|
||||
int rightPanelX = (int) (bounds.getWidth() * 0.52f);
|
||||
int masterH = std::max (44, (int) (60 * sc));
|
||||
int masterH = std::max (50, (int) (65 * sc));
|
||||
int bottomPanelY = bounds.getHeight() - masterH - 4;
|
||||
|
||||
g.setColour (InstaDrumsLookAndFeel::bgLight.withAlpha (0.4f));
|
||||
@@ -199,7 +202,7 @@ void InstaDrumsEditor::resized()
|
||||
loadSampleButton.setBounds (topBar.removeFromRight (btnW).reduced (2));
|
||||
|
||||
// Bottom master bar
|
||||
int masterH = std::max (44, (int) (60 * scale));
|
||||
int masterH = std::max (50, (int) (65 * scale));
|
||||
masterPanel.setBounds (area.removeFromBottom (masterH).reduced (4, 2));
|
||||
|
||||
// Left panel: pad grid (~52% width)
|
||||
@@ -242,22 +245,19 @@ void InstaDrumsEditor::timerCallback()
|
||||
for (auto* pc : padComponents)
|
||||
pc->repaint();
|
||||
|
||||
// Sync FX panel knobs -> processor atomic params
|
||||
processor.compThreshold.store (fxPanel.getCompThreshold());
|
||||
processor.compRatio.store (fxPanel.getCompRatio());
|
||||
processor.eqLo.store (fxPanel.getEqLo());
|
||||
processor.eqMid.store (fxPanel.getEqMid());
|
||||
processor.eqHi.store (fxPanel.getEqHi());
|
||||
processor.distDrive.store (fxPanel.getDistDrive());
|
||||
processor.distMix.store (fxPanel.getDistMix());
|
||||
processor.reverbSize.store (fxPanel.getReverbSize());
|
||||
processor.reverbDecay.store (fxPanel.getReverbDecay());
|
||||
// Sync FX panel knobs -> selected pad's FX params
|
||||
fxPanel.syncToPad();
|
||||
|
||||
// Update per-pad compressor GR meter
|
||||
if (selectedPadIndex >= 0 && selectedPadIndex < processor.getNumPads())
|
||||
fxPanel.setCompGainReduction (processor.getPad (selectedPadIndex).compGainReduction.load());
|
||||
|
||||
// Sync master panel -> processor
|
||||
processor.masterVolume.store (masterPanel.getMasterVolume());
|
||||
processor.masterPan.store (masterPanel.getMasterPan());
|
||||
processor.masterTune.store (masterPanel.getMasterTune());
|
||||
processor.outputLimiterEnabled.store (masterPanel.isLimiterEnabled());
|
||||
|
||||
// Update VU meter from processor
|
||||
// Update VU meter
|
||||
masterPanel.getVuMeter().setLevel (processor.vuLevelL.load(), processor.vuLevelR.load());
|
||||
}
|
||||
|
||||
@@ -45,22 +45,7 @@ void InstaDrumsProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
|
||||
for (int i = 0; i < numActivePads; ++i)
|
||||
pads[i].prepareToPlay (sampleRate, samplesPerBlock);
|
||||
|
||||
// Master FX chain
|
||||
juce::dsp::ProcessSpec spec { sampleRate, (juce::uint32) samplesPerBlock, 2 };
|
||||
|
||||
reverb.prepare (spec);
|
||||
compressor.prepare (spec);
|
||||
|
||||
juce::dsp::ProcessSpec monoSpec { sampleRate, (juce::uint32) samplesPerBlock, 1 };
|
||||
eqLoFilterL.prepare (monoSpec); eqLoFilterR.prepare (monoSpec);
|
||||
eqMidFilterL.prepare (monoSpec); eqMidFilterR.prepare (monoSpec);
|
||||
eqHiFilterL.prepare (monoSpec); eqHiFilterR.prepare (monoSpec);
|
||||
|
||||
reverb.reset();
|
||||
compressor.reset();
|
||||
eqLoFilterL.reset(); eqLoFilterR.reset();
|
||||
eqMidFilterL.reset(); eqMidFilterR.reset();
|
||||
eqHiFilterL.reset(); eqHiFilterR.reset();
|
||||
// Per-pad FX is prepared in DrumPad::prepareToPlay()
|
||||
}
|
||||
|
||||
void InstaDrumsProcessor::releaseResources()
|
||||
@@ -115,85 +100,8 @@ void InstaDrumsProcessor::applyMasterFx (juce::AudioBuffer<float>& buffer)
|
||||
{
|
||||
const int numSamples = buffer.getNumSamples();
|
||||
|
||||
// --- Distortion (pre-EQ) ---
|
||||
float drive = distDrive.load();
|
||||
float dMix = distMix.load();
|
||||
if (drive > 0.001f && dMix > 0.001f)
|
||||
{
|
||||
float driveGain = 1.0f + drive * 20.0f;
|
||||
for (int ch = 0; ch < buffer.getNumChannels(); ++ch)
|
||||
{
|
||||
float* data = buffer.getWritePointer (ch);
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
float dry = data[i];
|
||||
float wet = std::tanh (dry * driveGain) / std::tanh (driveGain);
|
||||
data[i] = dry * (1.0f - dMix) + wet * dMix;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3-band EQ ---
|
||||
float lo = eqLo.load();
|
||||
float mid = eqMid.load();
|
||||
float hi = eqHi.load();
|
||||
|
||||
if (std::abs (lo) > 0.1f || std::abs (mid) > 0.1f || std::abs (hi) > 0.1f)
|
||||
{
|
||||
auto loCoeffs = juce::dsp::IIR::Coefficients<float>::makeLowShelf (currentSampleRate, 200.0, 0.707f, juce::Decibels::decibelsToGain (lo));
|
||||
auto midCoeffs = juce::dsp::IIR::Coefficients<float>::makePeakFilter (currentSampleRate, 1000.0, 1.0f, juce::Decibels::decibelsToGain (mid));
|
||||
auto hiCoeffs = juce::dsp::IIR::Coefficients<float>::makeHighShelf (currentSampleRate, 5000.0, 0.707f, juce::Decibels::decibelsToGain (hi));
|
||||
|
||||
*eqLoFilterL.coefficients = *loCoeffs; *eqLoFilterR.coefficients = *loCoeffs;
|
||||
*eqMidFilterL.coefficients = *midCoeffs; *eqMidFilterR.coefficients = *midCoeffs;
|
||||
*eqHiFilterL.coefficients = *hiCoeffs; *eqHiFilterR.coefficients = *hiCoeffs;
|
||||
|
||||
if (buffer.getNumChannels() >= 2)
|
||||
{
|
||||
float* L = buffer.getWritePointer (0);
|
||||
float* R = buffer.getWritePointer (1);
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
L[i] = eqLoFilterL.processSample (L[i]);
|
||||
L[i] = eqMidFilterL.processSample (L[i]);
|
||||
L[i] = eqHiFilterL.processSample (L[i]);
|
||||
R[i] = eqLoFilterR.processSample (R[i]);
|
||||
R[i] = eqMidFilterR.processSample (R[i]);
|
||||
R[i] = eqHiFilterR.processSample (R[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Compressor ---
|
||||
float thresh = compThreshold.load();
|
||||
float ratio = compRatio.load();
|
||||
compressor.setThreshold (thresh);
|
||||
compressor.setRatio (ratio);
|
||||
compressor.setAttack (10.0f);
|
||||
compressor.setRelease (100.0f);
|
||||
{
|
||||
juce::dsp::AudioBlock<float> block (buffer);
|
||||
juce::dsp::ProcessContextReplacing<float> ctx (block);
|
||||
compressor.process (ctx);
|
||||
}
|
||||
|
||||
// --- Reverb ---
|
||||
float rSize = reverbSize.load();
|
||||
float rDecay = reverbDecay.load();
|
||||
if (rSize > 0.01f || rDecay > 0.01f)
|
||||
{
|
||||
juce::dsp::Reverb::Parameters rParams;
|
||||
rParams.roomSize = rSize;
|
||||
rParams.damping = 1.0f - rDecay;
|
||||
rParams.wetLevel = rSize * 0.5f;
|
||||
rParams.dryLevel = 1.0f;
|
||||
rParams.width = 1.0f;
|
||||
reverb.setParameters (rParams);
|
||||
|
||||
juce::dsp::AudioBlock<float> block (buffer);
|
||||
juce::dsp::ProcessContextReplacing<float> ctx (block);
|
||||
reverb.process (ctx);
|
||||
}
|
||||
// Per-pad FX is now in DrumPad::applyPadFx()
|
||||
// Master chain: just Volume + Pan + Output Limiter
|
||||
|
||||
// --- Master Volume + Pan ---
|
||||
float mVol = masterVolume.load();
|
||||
@@ -212,6 +120,17 @@ void InstaDrumsProcessor::applyMasterFx (juce::AudioBuffer<float>& buffer)
|
||||
buffer.applyGain (mVol);
|
||||
}
|
||||
|
||||
// --- Output Limiter (brickwall at 0dB) ---
|
||||
if (outputLimiterEnabled.load())
|
||||
{
|
||||
for (int ch = 0; ch < buffer.getNumChannels(); ++ch)
|
||||
{
|
||||
float* data = buffer.getWritePointer (ch);
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
data[i] = juce::jlimit (-1.0f, 1.0f, data[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// --- VU Meter ---
|
||||
float rmsL = 0.0f, rmsR = 0.0f;
|
||||
if (buffer.getNumChannels() >= 1)
|
||||
|
||||
@@ -52,16 +52,8 @@ public:
|
||||
std::atomic<float> masterPan { 0.0f };
|
||||
std::atomic<float> masterTune { 0.0f };
|
||||
|
||||
// FX params
|
||||
std::atomic<float> compThreshold { -12.0f };
|
||||
std::atomic<float> compRatio { 4.0f };
|
||||
std::atomic<float> eqLo { 0.0f };
|
||||
std::atomic<float> eqMid { 0.0f };
|
||||
std::atomic<float> eqHi { 0.0f };
|
||||
std::atomic<float> distDrive { 0.0f };
|
||||
std::atomic<float> distMix { 0.0f };
|
||||
std::atomic<float> reverbSize { 0.3f };
|
||||
std::atomic<float> reverbDecay { 0.5f };
|
||||
// Master bus
|
||||
std::atomic<bool> outputLimiterEnabled { true };
|
||||
|
||||
// VU meter levels (written by audio thread, read by GUI)
|
||||
std::atomic<float> vuLevelL { 0.0f };
|
||||
@@ -72,13 +64,6 @@ private:
|
||||
int numActivePads = defaultNumPads;
|
||||
juce::AudioFormatManager formatManager;
|
||||
|
||||
// Master FX chain
|
||||
juce::dsp::Reverb reverb;
|
||||
juce::dsp::Compressor<float> compressor;
|
||||
juce::dsp::IIR::Filter<float> eqLoFilterL, eqLoFilterR;
|
||||
juce::dsp::IIR::Filter<float> eqMidFilterL, eqMidFilterR;
|
||||
juce::dsp::IIR::Filter<float> eqHiFilterL, eqHiFilterR;
|
||||
|
||||
double currentSampleRate = 44100.0;
|
||||
|
||||
void initializeDefaults();
|
||||
|
||||
Reference in New Issue
Block a user