mirror of
https://codeberg.org/armin/chromaflock.git
synced 2026-09-01 04:10:47 +02:00
Add per-note velocity routing to filter cutoff and resonance
This commit is contained in:
parent
9e84707f93
commit
9ef77e04ab
6 changed files with 63 additions and 20 deletions
|
|
@ -106,6 +106,8 @@ Osc1 + Osc2 → Amplitude Envelope × Velocity → Filter (cutoff modulated by F
|
||||||
| filterRes | Filter Res | 0..1 | 0.3 |
|
| filterRes | Filter Res | 0..1 | 0.3 |
|
||||||
| filterEnvAmt | Filter Env Amt | 0..1 | 0 |
|
| filterEnvAmt | Filter Env Amt | 0..1 | 0 |
|
||||||
| keyTrack | Key Tracking | 0..1 | 0.5 |
|
| keyTrack | Key Tracking | 0..1 | 0.5 |
|
||||||
|
| velToCut | Vel → Cutoff | 0..1 | 0 |
|
||||||
|
| velToRes | Vel → Resonance | 0..1 | 0 |
|
||||||
| fEnvAttack | Filter Attack | 0.001..5 (log) | 0.01 |
|
| fEnvAttack | Filter Attack | 0.001..5 (log) | 0.01 |
|
||||||
| fEnvDecay | Filter Decay | 0.001..5 (log) | 0.3 |
|
| fEnvDecay | Filter Decay | 0.001..5 (log) | 0.3 |
|
||||||
| fEnvSustain | Filter Sustain | 0..1 | 0.5 |
|
| fEnvSustain | Filter Sustain | 0..1 | 0.5 |
|
||||||
|
|
@ -198,6 +200,12 @@ ChromaFlockEditor (juce::AudioProcessorEditor)
|
||||||
### Filter Crash Fix
|
### Filter Crash Fix
|
||||||
`SubtractiveVoice` now has separate `filter` (L) and `filterR` (R) instances; `startNote()` prepares both. Resolves the state-sharing crash when filter env amount > 0.
|
`SubtractiveVoice` now has separate `filter` (L) and `filterR` (R) instances; `startNote()` prepares both. Resolves the state-sharing crash when filter env amount > 0.
|
||||||
|
|
||||||
|
### Per-Note Filter Velocity Routing
|
||||||
|
- `VEL CUT` (`velToCut`) and `VEL RES` (`velToRes`) knobs map each note's velocity to filter cutoff / resonance. Velocity is the only per-note MIDI data a DAW sequencer sends, so cutoff & resonance become drawable per note in the DAW's piano-roll velocity lane — no built-in step sequencer added.
|
||||||
|
- Cutoff path is envelope-independent: `modCutoff *= 2^(velToCut * noteVelocity * 4)` (up to +4 octaves at full depth/velocity). Resonance: `res * (1 + velToRes * noteVelocity)`, clamped to 0..1.
|
||||||
|
- Works through the arpeggiator (held-note velocities are forwarded) and the plugin's own piano roll (sends 0.8).
|
||||||
|
- FILTER section layout: type dropdown moved into the header row, knobs now a 3x2 grid (CUTOFF/RES/ENV AMT over KEY TRK/VEL CUT/VEL RES).
|
||||||
|
|
||||||
### Arpeggiator
|
### Arpeggiator
|
||||||
- New class `DSP/Arpeggiator.h` — tempo-synced, pure note-logic (no audio). Tracks held notes (press order for the As Played pattern), builds a pitch pool from held notes × octave range (1–3, clamped to MIDI range), and sequences it per pattern.
|
- New class `DSP/Arpeggiator.h` — tempo-synced, pure note-logic (no audio). Tracks held notes (press order for the As Played pattern), builds a pitch pool from held notes × octave range (1–3, clamped to MIDI range), and sequences it per pattern.
|
||||||
- Patterns: Up, Down, UpDown, DownUp, Random, As Played, Chord, Up&Down X, Down&Up X, Random Once, Octave Up, Octave Down, Pinky Up, Pinky Down. X variants are endpoint-exclusive bounces; Random Once shuffles per cycle (no repeat until full pass); Octave Up/Down play each root followed by its ±12 copies spanning the selected OCTAVES count; Pinky Up/Down alternate extremes low/high. Up arpeggiate a minor third + fifth (root, +3, +7) and the Up major variant a major third + fifth (root, +4, +7); UpDown/DownUp arpeggiate major (root, +4, +7) plus an UpDown minor variant (root, +3, +7), each as a separate step. Dropdown labels: "Up / Minor", "Down / Minor", "Up & Down / Major", "Down & Up / Major", "Up / Major", "Down / Major", "Up & Down / Minor". "Down" patterns always descend from each held note through the chord tones below it (root, fifth below, third below, next-octave root — e.g. C3, G2, E2, C2 for major), never playing above the held note; any harmony pattern does the same when DIR is set to Down. C3/C4 1/2/3 anchor to the most recently held key and play fixed 8-step loops alternating it and its octave above (3-3-4-3-4-3-3-4, 4-3-3-3-3-3-3-3, 3-3-3-4-3-3-3-3).
|
- Patterns: Up, Down, UpDown, DownUp, Random, As Played, Chord, Up&Down X, Down&Up X, Random Once, Octave Up, Octave Down, Pinky Up, Pinky Down. X variants are endpoint-exclusive bounces; Random Once shuffles per cycle (no repeat until full pass); Octave Up/Down play each root followed by its ±12 copies spanning the selected OCTAVES count; Pinky Up/Down alternate extremes low/high. Up arpeggiate a minor third + fifth (root, +3, +7) and the Up major variant a major third + fifth (root, +4, +7); UpDown/DownUp arpeggiate major (root, +4, +7) plus an UpDown minor variant (root, +3, +7), each as a separate step. Dropdown labels: "Up / Minor", "Down / Minor", "Up & Down / Major", "Down & Up / Major", "Up / Major", "Down / Major", "Up & Down / Minor". "Down" patterns always descend from each held note through the chord tones below it (root, fifth below, third below, next-octave root — e.g. C3, G2, E2, C2 for major), never playing above the held note; any harmony pattern does the same when DIR is set to Down. C3/C4 1/2/3 anchor to the most recently held key and play fixed 8-step loops alternating it and its octave above (3-3-4-3-4-3-3-4, 4-3-3-3-3-3-3-3, 3-3-3-4-3-3-3-3).
|
||||||
|
|
|
||||||
|
|
@ -128,13 +128,19 @@ public:
|
||||||
left *= noteVelocity * envSample;
|
left *= noteVelocity * envSample;
|
||||||
right *= 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)
|
// Unipolar modulation UP from the base cutoff: the envelope (0..1)
|
||||||
// multiplied by env amount adds brightness on top of the cutoff
|
// multiplied by env amount adds brightness on top of the cutoff
|
||||||
// knob, never going below it. Every env knob (attack/decay/
|
// knob, never going below it. The env amount is also scaled by
|
||||||
// sustain/release) shapes the response, and at env amount 0 the
|
// note velocity, so harder hits open the filter more. Every env
|
||||||
// cutoff equals the knob.
|
// knob (attack/decay/sustain/release) shapes the response, and at
|
||||||
float modCutoff = filterCutoff * std::pow(2.0f, fEnvSample * filterEnvAmount * 4.0f);
|
// 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);
|
float keyTrackFactor = std::pow(2.0f, (static_cast<float>(currentMidiNote) - 60.0f) / 12.0f * keyTrack);
|
||||||
modCutoff *= keyTrackFactor;
|
modCutoff *= keyTrackFactor;
|
||||||
|
|
||||||
|
|
@ -146,8 +152,10 @@ public:
|
||||||
|
|
||||||
modCutoff = std::clamp(modCutoff, 20.0f, static_cast<float>(getSampleRate() * 0.49));
|
modCutoff = std::clamp(modCutoff, 20.0f, static_cast<float>(getSampleRate() * 0.49));
|
||||||
|
|
||||||
filter.setCoefficients(modCutoff, filterResonance, filterType);
|
float noteRes = std::clamp(filterResonance * (1.0f + velToResonance * noteVelocity), 0.0f, 1.0f);
|
||||||
filterR.setCoefficients(modCutoff, filterResonance, filterType);
|
|
||||||
|
filter.setCoefficients(modCutoff, noteRes, filterType);
|
||||||
|
filterR.setCoefficients(modCutoff, noteRes, filterType);
|
||||||
|
|
||||||
left = filter.process(left);
|
left = filter.process(left);
|
||||||
right = filterR.process(right);
|
right = filterR.process(right);
|
||||||
|
|
@ -180,7 +188,8 @@ public:
|
||||||
float fAtt, float fDec, float fSus, float fRel,
|
float fAtt, float fDec, float fSus, float fRel,
|
||||||
float p, float drv, float outLvl,
|
float p, float drv, float outLvl,
|
||||||
float l1Rate, float l1Depth, int l1Shape, int l1Dest,
|
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;
|
waveform1 = wf1;
|
||||||
waveform2 = wf2;
|
waveform2 = wf2;
|
||||||
octave1 = oct1;
|
octave1 = oct1;
|
||||||
|
|
@ -197,6 +206,8 @@ public:
|
||||||
filterType = ft;
|
filterType = ft;
|
||||||
filterEnvAmount = fEnvAmt;
|
filterEnvAmount = fEnvAmt;
|
||||||
keyTrack = kTrack;
|
keyTrack = kTrack;
|
||||||
|
velToCutoff = velCut;
|
||||||
|
velToResonance = velRes;
|
||||||
pan = p;
|
pan = p;
|
||||||
drive = std::max(drv, 1.001f);
|
drive = std::max(drv, 1.001f);
|
||||||
outputLevel = outLvl * outLvl;
|
outputLevel = outLvl * outLvl;
|
||||||
|
|
@ -251,6 +262,8 @@ private:
|
||||||
FilterType filterType = FilterType::LowPass12;
|
FilterType filterType = FilterType::LowPass12;
|
||||||
float filterEnvAmount = 0.0f;
|
float filterEnvAmount = 0.0f;
|
||||||
float keyTrack = 0.5f;
|
float keyTrack = 0.5f;
|
||||||
|
float velToCutoff = 0.0f;
|
||||||
|
float velToResonance = 0.0f;
|
||||||
float pan = 0.0f;
|
float pan = 0.0f;
|
||||||
float drive = 1.5f;
|
float drive = 1.5f;
|
||||||
float outputLevel = 0.8f;
|
float outputLevel = 0.8f;
|
||||||
|
|
|
||||||
|
|
@ -640,6 +640,8 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p)
|
||||||
setupParam(filterResKnob, filterResAttach, "filterRes", "RES");
|
setupParam(filterResKnob, filterResAttach, "filterRes", "RES");
|
||||||
setupParam(filterEnvAmtKnob, filterEnvAmtAttach, "filterEnvAmt", "ENV AMT");
|
setupParam(filterEnvAmtKnob, filterEnvAmtAttach, "filterEnvAmt", "ENV AMT");
|
||||||
setupParam(keyTrackKnob, keyTrackAttach, "keyTrack", "KEY TRK");
|
setupParam(keyTrackKnob, keyTrackAttach, "keyTrack", "KEY TRK");
|
||||||
|
setupParam(velToCutKnob, velToCutAttach, "velToCut", "VEL CUT");
|
||||||
|
setupParam(velToResKnob, velToResAttach, "velToRes", "VEL RES");
|
||||||
|
|
||||||
setupLabel(envLabel, "AMP ENV");
|
setupLabel(envLabel, "AMP ENV");
|
||||||
setupParam(envAttackKnob, envAttackAttach, "envAttack", "ATTACK");
|
setupParam(envAttackKnob, envAttackAttach, "envAttack", "ATTACK");
|
||||||
|
|
@ -1247,12 +1249,23 @@ void MainContentComponent::resized() {
|
||||||
phaseOffsetKnob.setBounds(528 + (knobSize + knobSpacing) * 4, knobY2, knobSize, knobSize);
|
phaseOffsetKnob.setBounds(528 + (knobSize + knobSpacing) * 4, knobY2, knobSize, knobSize);
|
||||||
|
|
||||||
filterLabel.setBounds(1152, sectionY, 498, labelH);
|
filterLabel.setBounds(1152, sectionY, 498, labelH);
|
||||||
filterTypeBox.setBounds(1162, sectionY + labelH + 4, 200, 28);
|
filterTypeBox.setBounds(1430, 88, 220, 28);
|
||||||
int knobYF = sectionY + labelH + comboH + 8;
|
// FILTER holds 6 knobs (incl. per-note VEL CUT / VEL RES), so the type
|
||||||
filterCutoffKnob.setBounds(1162, knobYF, knobSize, knobSize);
|
// dropdown sits in the header row and the knobs form a 3x2 grid.
|
||||||
filterResKnob.setBounds(1162 + knobSize + knobSpacing, knobYF, knobSize, knobSize);
|
{
|
||||||
filterEnvAmtKnob.setBounds(1162 + (knobSize + knobSpacing) * 2, knobYF, knobSize, knobSize);
|
int fSize = 78;
|
||||||
keyTrackKnob.setBounds(1162 + (knobSize + knobSpacing) * 3, knobYF, knobSize, knobSize);
|
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);
|
envLabel.setBounds(10, 302, 563, labelH);
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,8 @@ private:
|
||||||
KnobSlider osc2OctKnob, osc2SemiKnob, osc2FineKnob, osc2LevelKnob, phaseOffsetKnob;
|
KnobSlider osc2OctKnob, osc2SemiKnob, osc2FineKnob, osc2LevelKnob, phaseOffsetKnob;
|
||||||
|
|
||||||
juce::ComboBox filterTypeBox;
|
juce::ComboBox filterTypeBox;
|
||||||
KnobSlider filterCutoffKnob, filterResKnob, filterEnvAmtKnob, keyTrackKnob;
|
KnobSlider filterCutoffKnob, filterResKnob, filterEnvAmtKnob, keyTrackKnob,
|
||||||
|
velToCutKnob, velToResKnob;
|
||||||
|
|
||||||
KnobSlider envAttackKnob, envDecayKnob, envSustainKnob, envReleaseKnob;
|
KnobSlider envAttackKnob, envDecayKnob, envSustainKnob, envReleaseKnob;
|
||||||
KnobSlider fEnvAttackKnob, fEnvDecayKnob, fEnvSustainKnob, fEnvReleaseKnob;
|
KnobSlider fEnvAttackKnob, fEnvDecayKnob, fEnvSustainKnob, fEnvReleaseKnob;
|
||||||
|
|
@ -270,7 +271,7 @@ private:
|
||||||
std::unique_ptr<ComboBoxAttachment> osc1WaveAttach, osc2WaveAttach;
|
std::unique_ptr<ComboBoxAttachment> osc1WaveAttach, osc2WaveAttach;
|
||||||
std::unique_ptr<IndexedComboBoxAttachment> filterTypeAttach;
|
std::unique_ptr<IndexedComboBoxAttachment> filterTypeAttach;
|
||||||
std::unique_ptr<ComboBoxAttachment> octaveTransposeAttach, semitoneTransposeAttach;
|
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> envAttackAttach, envDecayAttach, envSustainAttach, envReleaseAttach;
|
||||||
std::unique_ptr<SliderAttachment> fEnvAttackAttach, fEnvDecayAttach, fEnvSustainAttach, fEnvReleaseAttach;
|
std::unique_ptr<SliderAttachment> fEnvAttackAttach, fEnvDecayAttach, fEnvSustainAttach, fEnvReleaseAttach;
|
||||||
std::unique_ptr<SliderAttachment> panAttach, driveAttach, masterAttach, pbRangeAttach;
|
std::unique_ptr<SliderAttachment> panAttach, driveAttach, masterAttach, pbRangeAttach;
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,10 @@ juce::AudioProcessorValueTreeState::ParameterLayout ChromaFlockProcessor::create
|
||||||
juce::ParameterID{"filterEnvAmt", 1}, "Filter Env Amount", zeroOne, 0.0f));
|
juce::ParameterID{"filterEnvAmt", 1}, "Filter Env Amount", zeroOne, 0.0f));
|
||||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||||
juce::ParameterID{"keyTrack", 1}, "Key Tracking", zeroOne, 0.5f));
|
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
|
// FILTER ENVELOPE
|
||||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||||
|
|
@ -391,7 +395,9 @@ void ChromaFlockProcessor::updateVoiceParameters() {
|
||||||
l2Rate,
|
l2Rate,
|
||||||
getParam("lfo2Depth"),
|
getParam("lfo2Depth"),
|
||||||
static_cast<int>(getParam("lfo2Shape")),
|
static_cast<int>(getParam("lfo2Shape")),
|
||||||
static_cast<int>(getParam("lfo2Dest"))
|
static_cast<int>(getParam("lfo2Dest")),
|
||||||
|
getParam("velToCut"),
|
||||||
|
getParam("velToRes")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,9 @@ void renderPreset(const Preset& preset, Result& out) {
|
||||||
param(preset, "lfo2Rate", 2.0f),
|
param(preset, "lfo2Rate", 2.0f),
|
||||||
param(preset, "lfo2Depth", 0.0f),
|
param(preset, "lfo2Depth", 0.0f),
|
||||||
static_cast<int>(paramInt(preset, "lfo2Shape", 0)),
|
static_cast<int>(paramInt(preset, "lfo2Shape", 0)),
|
||||||
static_cast<int>(paramInt(preset, "lfo2Dest", 0)));
|
static_cast<int>(paramInt(preset, "lfo2Dest", 0)),
|
||||||
|
param(preset, "velToCut", 0.0f),
|
||||||
|
param(preset, "velToRes", 0.0f));
|
||||||
|
|
||||||
Distortion distortion;
|
Distortion distortion;
|
||||||
Compression compression;
|
Compression compression;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue