Add FX chain: per-pad filter, master compressor/EQ/distortion/reverb, VU meter
- Per-pad low-pass filter (cutoff + resonance) applied in DrumPad render - Master bus FX chain in PluginProcessor::applyMasterFx(): - Distortion (tanh waveshaper with drive/mix) - 3-band EQ (low shelf 200Hz, peak 1kHz, high shelf 5kHz) - Compressor (juce::dsp::Compressor with threshold/ratio) - Reverb (juce::dsp::Reverb with size/decay) - Master volume + pan with constant-power pan law - VU meter: RMS level measurement with exponential smoothing - All FX panel and master panel knobs now connected to audio processing - SampleEditorPanel cutoff/reso knobs linked to DrumPad filter params Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,9 +3,17 @@
|
||||
DrumPad::DrumPad() {}
|
||||
DrumPad::~DrumPad() {}
|
||||
|
||||
void DrumPad::prepareToPlay (double sr, int /*samplesPerBlock*/)
|
||||
void DrumPad::prepareToPlay (double sr, int samplesPerBlock)
|
||||
{
|
||||
sampleRate = sr;
|
||||
|
||||
// Prepare per-pad filter
|
||||
juce::dsp::ProcessSpec spec { sr, (juce::uint32) samplesPerBlock, 1 };
|
||||
filterL.prepare (spec);
|
||||
filterR.prepare (spec);
|
||||
filterL.reset();
|
||||
filterR.reset();
|
||||
lastCutoff = filterCutoff;
|
||||
}
|
||||
|
||||
void DrumPad::releaseResources()
|
||||
@@ -339,6 +347,18 @@ void DrumPad::renderNextBlock (juce::AudioBuffer<float>& outputBuffer, int start
|
||||
float leftGain = std::cos (panPos * juce::MathConstants<float>::halfPi);
|
||||
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)
|
||||
{
|
||||
float clampedCutoff = juce::jlimit (20.0f, (float) (sampleRate * 0.49), filterCutoff);
|
||||
auto coeffs = juce::dsp::IIR::Coefficients<float>::makeLowPass (sampleRate, clampedCutoff, filterReso);
|
||||
*filterL.coefficients = *coeffs;
|
||||
*filterR.coefficients = *coeffs;
|
||||
lastCutoff = filterCutoff;
|
||||
}
|
||||
|
||||
bool useFilter = filterCutoff < 19900.0f; // Skip filter if fully open
|
||||
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
if (! playing) break;
|
||||
@@ -375,6 +395,11 @@ void DrumPad::renderNextBlock (juce::AudioBuffer<float>& outputBuffer, int start
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -64,6 +64,10 @@ public:
|
||||
float sustain = 1.0f;
|
||||
float release = 0.05f;
|
||||
|
||||
// Per-pad filter
|
||||
float filterCutoff = 20000.0f; // Hz
|
||||
float filterReso = 0.707f; // Q
|
||||
|
||||
// State
|
||||
bool isPlaying() const { return playing; }
|
||||
juce::String getLoadedFileName() const { return loadedFileName; }
|
||||
@@ -87,6 +91,10 @@ private:
|
||||
bool playing = false;
|
||||
float currentVelocity = 1.0f;
|
||||
|
||||
// Per-pad low-pass filter (stereo)
|
||||
juce::dsp::IIR::Filter<float> filterL, filterR;
|
||||
float lastCutoff = 20000.0f;
|
||||
|
||||
// ADSR state
|
||||
enum class EnvelopeStage { Idle, Attack, Decay, Sustain, Release };
|
||||
EnvelopeStage envStage = EnvelopeStage::Idle;
|
||||
|
||||
@@ -211,7 +211,22 @@ void InstaDrumsEditor::timerCallback()
|
||||
for (auto* pc : padComponents)
|
||||
pc->repaint();
|
||||
|
||||
// Update VU meter from processor output levels
|
||||
// (simplified: just repaint for now)
|
||||
masterPanel.getVuMeter().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 master panel -> processor
|
||||
processor.masterVolume.store (masterPanel.getMasterVolume());
|
||||
processor.masterPan.store (masterPanel.getMasterPan());
|
||||
processor.masterTune.store (masterPanel.getMasterTune());
|
||||
|
||||
// Update VU meter from processor
|
||||
masterPanel.getVuMeter().setLevel (processor.vuLevelL.load(), processor.vuLevelR.load());
|
||||
}
|
||||
|
||||
@@ -40,8 +40,27 @@ void InstaDrumsProcessor::initializeDefaults()
|
||||
|
||||
void InstaDrumsProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
|
||||
{
|
||||
currentSampleRate = sampleRate;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
void InstaDrumsProcessor::releaseResources()
|
||||
@@ -87,6 +106,122 @@ void InstaDrumsProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::
|
||||
// Render audio from all pads
|
||||
for (int i = 0; i < numActivePads; ++i)
|
||||
pads[i].renderNextBlock (buffer, 0, buffer.getNumSamples());
|
||||
|
||||
// Apply master FX chain
|
||||
applyMasterFx (buffer);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// --- Master Volume + Pan ---
|
||||
float mVol = masterVolume.load();
|
||||
float mPan = masterPan.load();
|
||||
float panPos = (mPan + 1.0f) * 0.5f;
|
||||
float mLGain = std::cos (panPos * juce::MathConstants<float>::halfPi) * mVol;
|
||||
float mRGain = std::sin (panPos * juce::MathConstants<float>::halfPi) * mVol;
|
||||
|
||||
if (buffer.getNumChannels() >= 2)
|
||||
{
|
||||
buffer.applyGain (0, 0, numSamples, mLGain);
|
||||
buffer.applyGain (1, 0, numSamples, mRGain);
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.applyGain (mVol);
|
||||
}
|
||||
|
||||
// --- VU Meter ---
|
||||
float rmsL = 0.0f, rmsR = 0.0f;
|
||||
if (buffer.getNumChannels() >= 1)
|
||||
rmsL = buffer.getRMSLevel (0, 0, numSamples);
|
||||
if (buffer.getNumChannels() >= 2)
|
||||
rmsR = buffer.getRMSLevel (1, 0, numSamples);
|
||||
|
||||
// Smooth VU (simple exponential)
|
||||
vuLevelL.store (vuLevelL.load() * 0.8f + rmsL * 0.2f);
|
||||
vuLevelR.store (vuLevelR.load() * 0.8f + rmsR * 0.2f);
|
||||
}
|
||||
|
||||
DrumPad* InstaDrumsProcessor::findPadForNote (int midiNote)
|
||||
|
||||
@@ -40,7 +40,6 @@ public:
|
||||
|
||||
juce::AudioFormatManager& getFormatManager() { return formatManager; }
|
||||
|
||||
// Find pad by MIDI note
|
||||
DrumPad* findPadForNote (int midiNote);
|
||||
|
||||
// Kit management
|
||||
@@ -48,13 +47,42 @@ public:
|
||||
void saveKitPreset (const juce::File& file);
|
||||
void loadKitPreset (const juce::File& file);
|
||||
|
||||
// Master FX parameters (read by editor from FxPanel/MasterPanel)
|
||||
std::atomic<float> masterVolume { 1.0f };
|
||||
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 };
|
||||
|
||||
// VU meter levels (written by audio thread, read by GUI)
|
||||
std::atomic<float> vuLevelL { 0.0f };
|
||||
std::atomic<float> vuLevelR { 0.0f };
|
||||
|
||||
private:
|
||||
std::array<DrumPad, maxPads> pads;
|
||||
int numActivePads = defaultNumPads;
|
||||
juce::AudioFormatManager formatManager;
|
||||
|
||||
// Default MIDI mapping (GM drum map)
|
||||
// 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();
|
||||
void applyMasterFx (juce::AudioBuffer<float>& buffer);
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InstaDrumsProcessor)
|
||||
};
|
||||
|
||||
@@ -61,6 +61,8 @@ void SampleEditorPanel::updateFromPad()
|
||||
releaseSlider.setValue (currentPad->release, juce::dontSendNotification);
|
||||
pitchSlider.setValue (currentPad->pitch, juce::dontSendNotification);
|
||||
panSlider.setValue (currentPad->pan, juce::dontSendNotification);
|
||||
cutoffSlider.setValue (currentPad->filterCutoff, juce::dontSendNotification);
|
||||
resoSlider.setValue (currentPad->filterReso, juce::dontSendNotification);
|
||||
|
||||
auto& buf = currentPad->getSampleBuffer();
|
||||
waveform.setBuffer (&buf);
|
||||
@@ -78,8 +80,10 @@ void SampleEditorPanel::sliderValueChanged (juce::Slider* slider)
|
||||
else if (slider == &decaySlider) currentPad->decay = (float) slider->getValue();
|
||||
else if (slider == &sustainSlider) currentPad->sustain = (float) slider->getValue();
|
||||
else if (slider == &releaseSlider) currentPad->release = (float) slider->getValue();
|
||||
else if (slider == &pitchSlider) currentPad->pitch = (float) slider->getValue();
|
||||
else if (slider == &panSlider) currentPad->pan = (float) slider->getValue();
|
||||
else if (slider == &pitchSlider) currentPad->pitch = (float) slider->getValue();
|
||||
else if (slider == &panSlider) currentPad->pan = (float) slider->getValue();
|
||||
else if (slider == &cutoffSlider) currentPad->filterCutoff = (float) slider->getValue();
|
||||
else if (slider == &resoSlider) currentPad->filterReso = (float) slider->getValue();
|
||||
|
||||
// Update ADSR overlay
|
||||
waveform.setADSR (currentPad->attack, currentPad->decay, currentPad->sustain, currentPad->release);
|
||||
|
||||
Reference in New Issue
Block a user