Add per-note velocity routing to filter cutoff and resonance

This commit is contained in:
Armin 2026-08-30 00:01:53 +02:00
commit 9ef77e04ab
6 changed files with 63 additions and 20 deletions

View file

@ -128,13 +128,19 @@ public:
left *= noteVelocity * envSample;
right *= noteVelocity * envSample;
// Filter cutoff with env + keytrack.
// Filter cutoff with env + keytrack + velocity.
// 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);
// knob, never going below it. The env amount is also scaled by
// note velocity, so harder hits open the filter more. Every env
// knob (attack/decay/sustain/release) shapes the response, and at
// env amount 0 the cutoff equals the knob. The independent VEL CUT
// depth adds an envelope-free velocity path and VEL RES maps note
// velocity to resonance; together they let cutoff and resonance be
// drawn per note from the DAW sequencer (velocity is the only
// per-note MIDI data a DAW sends).
float modCutoff = filterCutoff * std::pow(2.0f, fEnvSample * filterEnvAmount * noteVelocity * 4.0f);
modCutoff *= std::pow(2.0f, velToCutoff * noteVelocity * 4.0f);
float keyTrackFactor = std::pow(2.0f, (static_cast<float>(currentMidiNote) - 60.0f) / 12.0f * keyTrack);
modCutoff *= keyTrackFactor;
@ -146,8 +152,10 @@ public:
modCutoff = std::clamp(modCutoff, 20.0f, static_cast<float>(getSampleRate() * 0.49));
filter.setCoefficients(modCutoff, filterResonance, filterType);
filterR.setCoefficients(modCutoff, filterResonance, filterType);
float noteRes = std::clamp(filterResonance * (1.0f + velToResonance * noteVelocity), 0.0f, 1.0f);
filter.setCoefficients(modCutoff, noteRes, filterType);
filterR.setCoefficients(modCutoff, noteRes, filterType);
left = filter.process(left);
right = filterR.process(right);
@ -180,7 +188,8 @@ public:
float fAtt, float fDec, float fSus, float fRel,
float p, float drv, float outLvl,
float l1Rate, float l1Depth, int l1Shape, int l1Dest,
float l2Rate, float l2Depth, int l2Shape, int l2Dest) {
float l2Rate, float l2Depth, int l2Shape, int l2Dest,
float velCut, float velRes) {
waveform1 = wf1;
waveform2 = wf2;
octave1 = oct1;
@ -197,6 +206,8 @@ public:
filterType = ft;
filterEnvAmount = fEnvAmt;
keyTrack = kTrack;
velToCutoff = velCut;
velToResonance = velRes;
pan = p;
drive = std::max(drv, 1.001f);
outputLevel = outLvl * outLvl;
@ -251,6 +262,8 @@ private:
FilterType filterType = FilterType::LowPass12;
float filterEnvAmount = 0.0f;
float keyTrack = 0.5f;
float velToCutoff = 0.0f;
float velToResonance = 0.0f;
float pan = 0.0f;
float drive = 1.5f;
float outputLevel = 0.8f;

View file

@ -640,6 +640,8 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p)
setupParam(filterResKnob, filterResAttach, "filterRes", "RES");
setupParam(filterEnvAmtKnob, filterEnvAmtAttach, "filterEnvAmt", "ENV AMT");
setupParam(keyTrackKnob, keyTrackAttach, "keyTrack", "KEY TRK");
setupParam(velToCutKnob, velToCutAttach, "velToCut", "VEL CUT");
setupParam(velToResKnob, velToResAttach, "velToRes", "VEL RES");
setupLabel(envLabel, "AMP ENV");
setupParam(envAttackKnob, envAttackAttach, "envAttack", "ATTACK");
@ -1247,12 +1249,23 @@ void MainContentComponent::resized() {
phaseOffsetKnob.setBounds(528 + (knobSize + knobSpacing) * 4, knobY2, knobSize, knobSize);
filterLabel.setBounds(1152, sectionY, 498, labelH);
filterTypeBox.setBounds(1162, sectionY + labelH + 4, 200, 28);
int knobYF = sectionY + labelH + comboH + 8;
filterCutoffKnob.setBounds(1162, knobYF, knobSize, knobSize);
filterResKnob.setBounds(1162 + knobSize + knobSpacing, knobYF, knobSize, knobSize);
filterEnvAmtKnob.setBounds(1162 + (knobSize + knobSpacing) * 2, knobYF, knobSize, knobSize);
keyTrackKnob.setBounds(1162 + (knobSize + knobSpacing) * 3, knobYF, knobSize, knobSize);
filterTypeBox.setBounds(1430, 88, 220, 28);
// FILTER holds 6 knobs (incl. per-note VEL CUT / VEL RES), so the type
// dropdown sits in the header row and the knobs form a 3x2 grid.
{
int fSize = 78;
int fGap = 6;
int gridW = 3 * fSize + 2 * fGap;
int fSx = 1152 + (498 - gridW) / 2;
int fRow1Y = 124;
int fRow2Y = fRow1Y + fSize + fGap;
filterCutoffKnob.setBounds(fSx, fRow1Y, fSize, fSize);
filterResKnob.setBounds(fSx + fSize + fGap, fRow1Y, fSize, fSize);
filterEnvAmtKnob.setBounds(fSx + (fSize + fGap) * 2, fRow1Y, fSize, fSize);
keyTrackKnob.setBounds(fSx, fRow2Y, fSize, fSize);
velToCutKnob.setBounds(fSx + fSize + fGap, fRow2Y, fSize, fSize);
velToResKnob.setBounds(fSx + (fSize + fGap) * 2, fRow2Y, fSize, fSize);
}
envLabel.setBounds(10, 302, 563, labelH);
{

View file

@ -168,7 +168,8 @@ private:
KnobSlider osc2OctKnob, osc2SemiKnob, osc2FineKnob, osc2LevelKnob, phaseOffsetKnob;
juce::ComboBox filterTypeBox;
KnobSlider filterCutoffKnob, filterResKnob, filterEnvAmtKnob, keyTrackKnob;
KnobSlider filterCutoffKnob, filterResKnob, filterEnvAmtKnob, keyTrackKnob,
velToCutKnob, velToResKnob;
KnobSlider envAttackKnob, envDecayKnob, envSustainKnob, envReleaseKnob;
KnobSlider fEnvAttackKnob, fEnvDecayKnob, fEnvSustainKnob, fEnvReleaseKnob;
@ -270,7 +271,7 @@ private:
std::unique_ptr<ComboBoxAttachment> osc1WaveAttach, osc2WaveAttach;
std::unique_ptr<IndexedComboBoxAttachment> filterTypeAttach;
std::unique_ptr<ComboBoxAttachment> octaveTransposeAttach, semitoneTransposeAttach;
std::unique_ptr<SliderAttachment> filterCutoffAttach, filterResAttach, filterEnvAmtAttach, keyTrackAttach;
std::unique_ptr<SliderAttachment> filterCutoffAttach, filterResAttach, filterEnvAmtAttach, keyTrackAttach, velToCutAttach, velToResAttach;
std::unique_ptr<SliderAttachment> envAttackAttach, envDecayAttach, envSustainAttach, envReleaseAttach;
std::unique_ptr<SliderAttachment> fEnvAttackAttach, fEnvDecayAttach, fEnvSustainAttach, fEnvReleaseAttach;
std::unique_ptr<SliderAttachment> panAttach, driveAttach, masterAttach, pbRangeAttach;

View file

@ -107,6 +107,10 @@ juce::AudioProcessorValueTreeState::ParameterLayout ChromaFlockProcessor::create
juce::ParameterID{"filterEnvAmt", 1}, "Filter Env Amount", zeroOne, 0.0f));
layout.add(std::make_unique<juce::AudioParameterFloat>(
juce::ParameterID{"keyTrack", 1}, "Key Tracking", zeroOne, 0.5f));
layout.add(std::make_unique<juce::AudioParameterFloat>(
juce::ParameterID{"velToCut", 1}, "Vel -> Cutoff", zeroOne, 0.0f));
layout.add(std::make_unique<juce::AudioParameterFloat>(
juce::ParameterID{"velToRes", 1}, "Vel -> Resonance", zeroOne, 0.0f));
// FILTER ENVELOPE
layout.add(std::make_unique<juce::AudioParameterFloat>(
@ -370,9 +374,9 @@ void ChromaFlockProcessor::updateVoiceParameters() {
getParam("phaseOffset"),
getParam("filterCutoff"),
getParam("filterRes"),
static_cast<FilterType>(static_cast<int>(getParam("filterType"))),
static_cast<FilterType>(static_cast<int>(getParam("filterType"))),
getParam("filterEnvAmt"),
getParam("keyTrack"),
getParam("keyTrack"),
getParam("envAttack"),
getParam("envDecay"),
getParam("envSustain"),
@ -391,7 +395,9 @@ void ChromaFlockProcessor::updateVoiceParameters() {
l2Rate,
getParam("lfo2Depth"),
static_cast<int>(getParam("lfo2Shape")),
static_cast<int>(getParam("lfo2Dest"))
static_cast<int>(getParam("lfo2Dest")),
getParam("velToCut"),
getParam("velToRes")
);
}
}