diff --git a/Source/DrumPad.cpp b/Source/DrumPad.cpp index a009345..3c534bc 100644 --- a/Source/DrumPad.cpp +++ b/Source/DrumPad.cpp @@ -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& 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& outputBuffer, int start 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) + if (std::abs (filterCutoff - lastCutoff) > 1.0f) { float clampedCutoff = juce::jlimit (20.0f, (float) (sampleRate * 0.49), filterCutoff); auto coeffs = juce::dsp::IIR::Coefficients::makeLowPass (sampleRate, clampedCutoff, filterReso); @@ -357,8 +378,9 @@ void DrumPad::renderNextBlock (juce::AudioBuffer& 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& 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& 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::makeLowShelf (sampleRate, 200.0, 0.707f, juce::Decibels::decibelsToGain (fxEqLo)); + auto midC = juce::dsp::IIR::Coefficients::makePeakFilter (sampleRate, 1000.0, 1.0f, juce::Decibels::decibelsToGain (fxEqMid)); + auto hiC = juce::dsp::IIR::Coefficients::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 block (buf); + juce::dsp::ProcessContextReplacing 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 block (buf); + juce::dsp::ProcessContextReplacing ctx (block); + padReverb.process (ctx); + } } diff --git a/Source/DrumPad.h b/Source/DrumPad.h index 70be39a..f2c3e4a 100644 --- a/Source/DrumPad.h +++ b/Source/DrumPad.h @@ -4,7 +4,6 @@ class DrumPad { public: - // A single sample with its audio data and source sample rate struct Sample { juce::AudioBuffer 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 samples; // round-robin variations + juce::OwnedArray 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 compGainReduction { 0.0f }; // State bool isPlaying() const { return playing; } @@ -77,24 +92,26 @@ public: const juce::AudioBuffer& getSampleBuffer() const; private: - // Velocity layers (sorted by velocity range) juce::OwnedArray layers; - - // Currently playing sample reference Sample* activeSample = nullptr; - - // Fallback empty buffer for getSampleBuffer when nothing loaded juce::AudioBuffer emptyBuffer; + juce::AudioBuffer 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 filterL, filterR; float lastCutoff = 20000.0f; + // Per-pad FX processors + juce::dsp::Compressor padCompressor; + juce::dsp::IIR::Filter 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& 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); diff --git a/Source/FxPanel.cpp b/Source/FxPanel.cpp index 6ce7ecc..88073ca 100644 --- a/Source/FxPanel.cpp +++ b/Source/FxPanel.cpp @@ -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 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); +} diff --git a/Source/FxPanel.h b/Source/FxPanel.h index 10c5956..2fde920 100644 --- a/Source/FxPanel.h +++ b/Source/FxPanel.h @@ -1,5 +1,6 @@ #pragma once #include +#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 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) }; diff --git a/Source/LookAndFeel.cpp b/Source/LookAndFeel.cpp index 323fffc..d90d56e 100644 --- a/Source/LookAndFeel.cpp +++ b/Source/LookAndFeel.cpp @@ -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, - juce::PathStrokeType::rounded)); + // 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 - g.setColour (pointerColour); - g.fillPath (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 (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); + } +} diff --git a/Source/LookAndFeel.h b/Source/LookAndFeel.h index ea2eeb8..01eff90 100644 --- a/Source/LookAndFeel.h +++ b/Source/LookAndFeel.h @@ -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; diff --git a/Source/MasterPanel.cpp b/Source/MasterPanel.cpp index 26b7de3..0ac2278 100644 --- a/Source/MasterPanel.cpp +++ b/Source/MasterPanel.cpp @@ -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 }; diff --git a/Source/MasterPanel.h b/Source/MasterPanel.h index 0ce77c6..8d7fd38 100644 --- a/Source/MasterPanel.h +++ b/Source/MasterPanel.h @@ -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, diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 6013b33..fbed390 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -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.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()); } diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 07ff84b..c14d596 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -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& 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); - } + // 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& 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) diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 8e55620..7b8d111 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -52,16 +52,8 @@ public: 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 }; + // Master bus + std::atomic outputLimiterEnabled { true }; // VU meter levels (written by audio thread, read by GUI) std::atomic vuLevelL { 0.0f }; @@ -72,13 +64,6 @@ private: int numActivePads = defaultNumPads; juce::AudioFormatManager formatManager; - // 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();