diff --git a/Source/DrumPad.cpp b/Source/DrumPad.cpp index dc9582f..a009345 100644 --- a/Source/DrumPad.cpp +++ b/Source/DrumPad.cpp @@ -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& outputBuffer, int start float leftGain = std::cos (panPos * juce::MathConstants::halfPi); float rightGain = std::sin (panPos * juce::MathConstants::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::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& 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); } diff --git a/Source/DrumPad.h b/Source/DrumPad.h index 12429a3..70be39a 100644 --- a/Source/DrumPad.h +++ b/Source/DrumPad.h @@ -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 filterL, filterR; + float lastCutoff = 20000.0f; + // ADSR state enum class EnvelopeStage { Idle, Attack, Decay, Sustain, Release }; EnvelopeStage envStage = EnvelopeStage::Idle; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index b7de05a..c73e39a 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -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()); } diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index fea63e4..07ff84b 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -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& 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& 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::makeLowShelf (currentSampleRate, 200.0, 0.707f, juce::Decibels::decibelsToGain (lo)); + auto midCoeffs = juce::dsp::IIR::Coefficients::makePeakFilter (currentSampleRate, 1000.0, 1.0f, juce::Decibels::decibelsToGain (mid)); + auto hiCoeffs = juce::dsp::IIR::Coefficients::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 block (buffer); + juce::dsp::ProcessContextReplacing 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 block (buffer); + juce::dsp::ProcessContextReplacing 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::halfPi) * mVol; + float mRGain = std::sin (panPos * juce::MathConstants::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) diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 8727797..8e55620 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -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 masterVolume { 1.0f }; + std::atomic masterPan { 0.0f }; + std::atomic masterTune { 0.0f }; + + // FX params + std::atomic compThreshold { -12.0f }; + std::atomic compRatio { 4.0f }; + std::atomic eqLo { 0.0f }; + std::atomic eqMid { 0.0f }; + std::atomic eqHi { 0.0f }; + std::atomic distDrive { 0.0f }; + std::atomic distMix { 0.0f }; + std::atomic reverbSize { 0.3f }; + std::atomic reverbDecay { 0.5f }; + + // VU meter levels (written by audio thread, read by GUI) + std::atomic vuLevelL { 0.0f }; + std::atomic vuLevelR { 0.0f }; + private: std::array pads; int numActivePads = defaultNumPads; juce::AudioFormatManager formatManager; - // Default MIDI mapping (GM drum map) + // Master FX chain + juce::dsp::Reverb reverb; + juce::dsp::Compressor compressor; + juce::dsp::IIR::Filter eqLoFilterL, eqLoFilterR; + juce::dsp::IIR::Filter eqMidFilterL, eqMidFilterR; + juce::dsp::IIR::Filter eqHiFilterL, eqHiFilterR; + + double currentSampleRate = 44100.0; + void initializeDefaults(); + void applyMasterFx (juce::AudioBuffer& buffer); JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InstaDrumsProcessor) }; diff --git a/Source/SampleEditorPanel.cpp b/Source/SampleEditorPanel.cpp index 9cb0913..8149c4c 100644 --- a/Source/SampleEditorPanel.cpp +++ b/Source/SampleEditorPanel.cpp @@ -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);