From a1cd68b6b3ab0db7ebdb8f387cf05bd637fc7b31 Mon Sep 17 00:00:00 2001 From: Armin Date: Thu, 13 Aug 2026 14:15:41 +0200 Subject: [PATCH] Fix filter env: restore unipolar modulation so all env knobs are responsive Reverts the sustain-relative modulation that made F.ENV SUS and the cutoff knob feel dead while a note was held. The envelope now modulates up from the base cutoff; at env amount 0 the cutoff equals the knob, and lowering env amount darkens the held tone as expected. Also extends the filter to 24/48 dB modes (extra TPT stages), adds the matching combobox options, gives knobs a real blurred drop shadow, and updates the AGENTS.md filterType choice count. --- AGENTS.md | 2 +- Source/DSP/Filter.h | 25 ++++++++++++++++++++++--- Source/DSP/Voice.h | 15 ++++++--------- Source/PluginEditor.cpp | 11 +++++++---- Source/PluginProcessor.cpp | 3 ++- 5 files changed, 38 insertions(+), 18 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4ed07b7..522887c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,7 +101,7 @@ Osc1 + Osc2 → Amplitude Envelope × Velocity → Filter (cutoff modulated by F | osc2Fine | OSC2 Fine | -100..100 | 7 | | osc2Level | OSC2 Level | 0..1 | 0.7 | | phaseOffset | Phase Offset | 0..1 | 0.5 | -| filterType | Filter Type | Choice(5) | 0 (LP12)| +| filterType | Filter Type | Choice(9) | 0 (LP12)| | filterCutoff | Filter Cutoff | 20..20000 (log) | 8000 | | filterRes | Filter Res | 0..1 | 0.3 | | filterEnvAmt | Filter Env Amt | 0..1 | 0 | diff --git a/Source/DSP/Filter.h b/Source/DSP/Filter.h index bbbb138..03885a4 100644 --- a/Source/DSP/Filter.h +++ b/Source/DSP/Filter.h @@ -2,7 +2,8 @@ #include #include -enum class FilterType { LowPass12 = 0, LowPass24, BandPass, HighPass, Notch, NumTypes }; +enum class FilterType { LowPass12 = 0, LowPass24, BandPass, HighPass, Notch, + BandPass24, HighPass24, Notch24, LowPass48, NumTypes }; class Filter { public: @@ -12,7 +13,7 @@ public: } void reset() { - for (int i = 0; i < 4; ++i) + for (int i = 0; i < 8; ++i) stage[i] = 0.0f; gCoeff = 0.0f; gTarget = 0.0f; @@ -52,12 +53,30 @@ public: stage[2] = g * hp2 + bp2; stage[3] = g * bp2 + lp2; + // Stage 3 (fed by the stage-2 lowpass) — 36 dB + float hp3 = (lp2 - (k + g) * stage[4] - stage[5]) / denom; + float bp3 = g * hp3 + stage[4]; + float lp3 = g * bp3 + stage[5]; + stage[4] = g * hp3 + bp3; + stage[5] = g * bp3 + lp3; + + // Stage 4 (fed by the stage-3 lowpass) — 48 dB + float hp4 = (lp3 - (k + g) * stage[6] - stage[7]) / denom; + float bp4 = g * hp4 + stage[6]; + float lp4 = g * bp4 + stage[7]; + stage[6] = g * hp4 + bp4; + stage[7] = g * bp4 + lp4; + switch (filterType) { case FilterType::LowPass12: return lp; case FilterType::LowPass24: return lp2; case FilterType::BandPass: return bp; case FilterType::HighPass: return hp; case FilterType::Notch: return input - bp; + case FilterType::BandPass24: return bp2; + case FilterType::HighPass24: return hp2; + case FilterType::Notch24: return input - bp2; + case FilterType::LowPass48: return lp4; case FilterType::NumTypes: return input; } return lp; @@ -66,7 +85,7 @@ public: private: static constexpr float pi = 3.14159265358979323846f; double sampleRate = 44100.0; - float stage[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + float stage[8] = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; float gCoeff = 0.0f, gTarget = 0.0f; float kCoeff = 2.0f, kTarget = 2.0f; diff --git a/Source/DSP/Voice.h b/Source/DSP/Voice.h index 8b0db89..c0e6462 100644 --- a/Source/DSP/Voice.h +++ b/Source/DSP/Voice.h @@ -129,13 +129,12 @@ public: right *= noteVelocity * envSample; // Filter cutoff with env + keytrack. - // Modulate RELATIVE to the envelope's sustain level so the held - // (sustain) cutoff always equals the base cutoff knob regardless of - // env amount — the envelope only opens above (attack) and closes - // below (release) that centre. This keeps the cutoff knob audible - // at any env amount instead of collapsing when env amt is lowered. - float modCutoff = filterCutoff * std::pow(2.0f, - (fEnvSample - filterEnvSustain) * filterEnvAmount * 4.0f); + // Unipolar modulation UP from the base cutoff: the envelope (0..1) + // multiplied by env amount adds brightness on top of the cutoff + // knob, never going below it. Every env knob (attack/decay/ + // sustain/release) shapes the response, and at env amount 0 the + // cutoff equals the knob. + float modCutoff = filterCutoff * std::pow(2.0f, fEnvSample * filterEnvAmount * 4.0f); float keyTrackFactor = std::pow(2.0f, (static_cast(currentMidiNote) - 60.0f) / 12.0f * keyTrack); modCutoff *= keyTrackFactor; @@ -197,7 +196,6 @@ public: filterResonance = res; filterType = ft; filterEnvAmount = fEnvAmt; - filterEnvSustain = fSus; keyTrack = kTrack; pan = p; drive = std::max(drv, 1.001f); @@ -252,7 +250,6 @@ private: float filterResonance = 0.7f; FilterType filterType = FilterType::LowPass12; float filterEnvAmount = 0.0f; - float filterEnvSustain = 0.5f; float keyTrack = 0.5f; float pan = 0.0f; float drive = 1.5f; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 4610ace..a6e85ea 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -47,9 +47,11 @@ void KnobLookAndFeel::drawRotarySlider(juce::Graphics& g, int x, int y, int widt auto rw = radius * 2.0f; auto angle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle); - // Drop shadow - g.setColour(juce::Colour(0x40000000)); - g.fillEllipse(rx + 2.0f, ry + 3.0f, rw, rw); + // Blurred drop shadow + juce::DropShadow knobShadow(juce::Colour(0x55000000), 10, juce::Point(0, 5)); + juce::Path shadowPath; + shadowPath.addEllipse(juce::Rectangle(rx, ry, rw, rw)); + knobShadow.drawForPath(g, shadowPath); // Outer bevel (dark bottom-right, light top-left) auto bevelPath = juce::Path(); @@ -620,7 +622,8 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p) setupParam(phaseOffsetKnob, phaseOffsetAttach, "phaseOffset", "PHASE"); setupLabel(filterLabel, "FILTER"); - setupCB(filterTypeBox, filterTypeAttach, "filterType", {"LP 12dB", "LP 24dB", "Band Pass", "High Pass", "Notch"}); + setupCB(filterTypeBox, filterTypeAttach, "filterType", {"LP 12dB", "LP 24dB", "Band Pass", "High Pass", "Notch", + "Band Pass 24", "High Pass 24", "Notch 24", "LP 48dB"}); setupParam(filterCutoffKnob, filterCutoffAttach, "filterCutoff", "CUTOFF"); setupParam(filterResKnob, filterResAttach, "filterRes", "RES"); setupParam(filterEnvAmtKnob, filterEnvAmtAttach, "filterEnvAmt", "ENV AMT"); diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 0519140..253d717 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -91,7 +91,8 @@ juce::AudioProcessorValueTreeState::ParameterLayout ChromaFlockProcessor::create // FILTER layout.add(std::make_unique( juce::ParameterID{"filterType", 1}, "Filter Type", - juce::StringArray{"LP 12dB", "LP 24dB", "Band Pass", "High Pass", "Notch"}, 0)); + juce::StringArray{"LP 12dB", "LP 24dB", "Band Pass", "High Pass", "Notch", + "Band Pass 24", "High Pass 24", "Notch 24", "LP 48dB"}, 0)); layout.add(std::make_unique( juce::ParameterID{"filterCutoff", 1}, "Filter Cutoff", juce::NormalisableRange(