mirror of
https://codeberg.org/armin/chromaflock.git
synced 2026-09-01 04:10:47 +02:00
rework arpeggiator patterns
This commit is contained in:
parent
56fc80c405
commit
7c444ad849
4 changed files with 71 additions and 18 deletions
10
AGENTS.md
10
AGENTS.md
|
|
@ -118,10 +118,10 @@ Osc1 + Osc2 → Amplitude Envelope × Velocity → Filter (cutoff modulated by F
|
||||||
| drive | Drive | 1..5 (log) | 1.5 |
|
| drive | Drive | 1..5 (log) | 1.5 |
|
||||||
| masterLevel | Master Level | 0..1 | 0.8 |
|
| masterLevel | Master Level | 0..1 | 0.8 |
|
||||||
| arpEnabled | Arp On | Choice(2) Off/On | 0 (Off) |
|
| arpEnabled | Arp On | Choice(2) Off/On | 0 (Off) |
|
||||||
| arpPattern | Arp Pattern | Choice(17) | 0 (Up) |
|
| arpPattern | Arp Pattern | Choice(20) | 0 (Up) |
|
||||||
| arpOctaves | Arp Octaves | Choice(3) 1/2/3 | 1 (2) |
|
| arpOctaves | Arp Octaves | Choice(3) 1/2/3 | 1 (2) |
|
||||||
| arpDirection | Arp Direction | Choice(2) Up/Down | 0 (Up) |
|
| arpDirection | Arp Direction | Choice(2) Up/Down | 0 (Up) |
|
||||||
| arpRate | Arp Rate | Choice(6) 1/16..2 | 1 (1/8) |
|
| arpRate | Arp Rate | Choice(7) 1/32..2 | 1 (1/16)|
|
||||||
|
|
||||||
## UI Architecture
|
## UI Architecture
|
||||||
|
|
||||||
|
|
@ -200,9 +200,9 @@ ChromaFlockEditor (juce::AudioProcessorEditor)
|
||||||
|
|
||||||
### 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/Down arpeggiate a minor third + fifth (root, +3, +7) and Up/Down major variants 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".
|
- 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).
|
||||||
- `arpDirection` (Up/Down) controls whether octave copies extend above or below the root AND inverts the traversal for sequential patterns (Up/Down/UpDown/DownUp) so Direction Down actually steps descending; Random and As Played ignore the inversion.
|
- `arpDirection` (Up/Down) controls whether octave copies extend above or below the root AND sorts sequential patterns descending so Direction Down actually steps downward; Random and As Played ignore the inversion. Harmony patterns with Down direction descend through chord tones below each root (see above).
|
||||||
- `arpRate` step divisions (beats): 1/16, 1/8, 1/4, 1/2, 1, 2 — tick period = `rateBeats * 60 / bpm`.
|
- `arpRate` step divisions (beats): 1/32, 1/16, 1/8, 1/4, 1/2, 1, 2 — tick period = `rateBeats * 60 / bpm`.
|
||||||
- Routing (`PluginProcessor.cpp::processBlock`): when `arpEnabled`, MIDI note events feed the arp instead of the synth; the arp drives `synth.noteOn/noteOff` (with transpose) on each tick via `arpSampleCount` accumulation. Piano-roll `noteOn/noteOff` route through the same logic. When disabled, `arp.reset()` stops any sounding arp note and notes play normally.
|
- Routing (`PluginProcessor.cpp::processBlock`): when `arpEnabled`, MIDI note events feed the arp instead of the synth; the arp drives `synth.noteOn/noteOff` (with transpose) on each tick via `arpSampleCount` accumulation. Piano-roll `noteOn/noteOff` route through the same logic. When disabled, `arp.reset()` stops any sounding arp note and notes play normally.
|
||||||
- UI: bottom-right section at (860, 746) titled ARPEGGIATOR with 5 combos (ON, PATTERN, OCTAVES, DIR, RATE).
|
- UI: bottom-right section at (860, 746) titled ARPEGGIATOR with 5 combos (ON, PATTERN, OCTAVES, DIR, RATE).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,9 @@ public:
|
||||||
UpMajor,
|
UpMajor,
|
||||||
DownMajor,
|
DownMajor,
|
||||||
UpDownMinor,
|
UpDownMinor,
|
||||||
|
C3C4A,
|
||||||
|
C3C4B,
|
||||||
|
C3C4C,
|
||||||
numPatterns
|
numPatterns
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -189,14 +192,42 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<NotePitch> buildOrder(const std::vector<NotePitch>& pool) {
|
std::vector<NotePitch> buildOrder(const std::vector<NotePitch>& pool) {
|
||||||
// Sequential patterns play by ascending pitch; Direction Down inverts
|
|
||||||
// the traversal so the arpeggio descends. As Played keeps the
|
|
||||||
// key-press order; Random is direction-agnostic.
|
|
||||||
std::vector<NotePitch> seq(pool);
|
std::vector<NotePitch> seq(pool);
|
||||||
std::sort(seq.begin(), seq.end(),
|
std::sort(seq.begin(), seq.end(),
|
||||||
[](const NotePitch& a, const NotePitch& b) { return a.note < b.note; });
|
[](const NotePitch& a, const NotePitch& b) { return a.note < b.note; });
|
||||||
int n = static_cast<int>(seq.size());
|
int n = static_cast<int>(seq.size());
|
||||||
|
|
||||||
|
// "Down" patterns (and any harmony pattern with Direction Down) always
|
||||||
|
// descend from each held note through the chord tones of the octave
|
||||||
|
// below it: root, fifth below, third below, then the next octave's
|
||||||
|
// root. Built from the held notes directly so nothing ever plays
|
||||||
|
// above them.
|
||||||
|
bool wantDownFigure = hasHarmony()
|
||||||
|
&& (direction == DirectionDown || pattern == Down || pattern == DownMajor);
|
||||||
|
if (wantDownFigure) {
|
||||||
|
std::vector<NotePitch> down;
|
||||||
|
int thirdBelow = 12 - harmonyThird();
|
||||||
|
std::vector<int> tops(heldNotes.begin(), heldNotes.end());
|
||||||
|
std::sort(tops.begin(), tops.end(), [](int a, int b) { return a > b; });
|
||||||
|
for (int root : tops) {
|
||||||
|
if (std::find(tops.begin(), tops.end(), root + 12) != tops.end())
|
||||||
|
continue;
|
||||||
|
float vel = velocities[root];
|
||||||
|
for (int k = 0; k < octaves; ++k) {
|
||||||
|
auto pushClamped = [&](int pitch) {
|
||||||
|
pitch = pitch < 0 ? 0 : (pitch > 127 ? 127 : pitch);
|
||||||
|
down.push_back({pitch, vel});
|
||||||
|
};
|
||||||
|
pushClamped(root - 12 * k);
|
||||||
|
if (k < octaves - 1) {
|
||||||
|
pushClamped(root - 12 * k - 5);
|
||||||
|
pushClamped(root - 12 * k - thirdBelow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return down;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<NotePitch> order;
|
std::vector<NotePitch> order;
|
||||||
auto push = [&](int i) { order.push_back(seq[i]); };
|
auto push = [&](int i) { order.push_back(seq[i]); };
|
||||||
|
|
||||||
|
|
@ -283,6 +314,26 @@ private:
|
||||||
case AsPlayed:
|
case AsPlayed:
|
||||||
order = pool;
|
order = pool;
|
||||||
break;
|
break;
|
||||||
|
case C3C4A:
|
||||||
|
case C3C4B:
|
||||||
|
case C3C4C: {
|
||||||
|
int anchor = heldNotes.empty() ? 48 : heldNotes.back();
|
||||||
|
int up = anchor + 12;
|
||||||
|
up = up > 127 ? 127 : up;
|
||||||
|
float vel = heldNotes.empty() ? 0.9f : velocities[heldNotes.back()];
|
||||||
|
order.clear();
|
||||||
|
if (pattern == C3C4A) {
|
||||||
|
order = {{anchor, vel}, {anchor, vel}, {up, vel}, {anchor, vel},
|
||||||
|
{up, vel}, {anchor, vel}, {anchor, vel}, {up, vel}};
|
||||||
|
} else if (pattern == C3C4B) {
|
||||||
|
order = {{up, vel}, {anchor, vel}, {anchor, vel}, {anchor, vel},
|
||||||
|
{anchor, vel}, {anchor, vel}, {anchor, vel}, {anchor, vel}};
|
||||||
|
} else {
|
||||||
|
order = {{anchor, vel}, {anchor, vel}, {anchor, vel}, {up, vel},
|
||||||
|
{anchor, vel}, {anchor, vel}, {anchor, vel}, {anchor, vel}};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case Chord:
|
case Chord:
|
||||||
case Up:
|
case Up:
|
||||||
case UpMajor:
|
case UpMajor:
|
||||||
|
|
@ -291,10 +342,6 @@ private:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (direction == DirectionDown && pattern != Random && pattern != RandomOnce
|
|
||||||
&& pattern != AsPlayed)
|
|
||||||
std::reverse(order.begin(), order.end());
|
|
||||||
|
|
||||||
if (hasHarmony()) {
|
if (hasHarmony()) {
|
||||||
std::vector<NotePitch> expanded;
|
std::vector<NotePitch> expanded;
|
||||||
expanded.reserve(order.size() * 3);
|
expanded.reserve(order.size() * 3);
|
||||||
|
|
@ -311,6 +358,11 @@ private:
|
||||||
order.swap(expanded);
|
order.swap(expanded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (direction == DirectionDown && pattern != Random && pattern != RandomOnce
|
||||||
|
&& pattern != AsPlayed && pattern != C3C4A && pattern != C3C4B && pattern != C3C4C)
|
||||||
|
std::sort(order.begin(), order.end(),
|
||||||
|
[](const NotePitch& a, const NotePitch& b) { return a.note > b.note; });
|
||||||
|
|
||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -702,10 +702,10 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p)
|
||||||
{"Up / Minor", "Down / Minor", "Up & Down / Major", "Down & Up / Major", "Random",
|
{"Up / Minor", "Down / Minor", "Up & Down / Major", "Down & Up / Major", "Random",
|
||||||
"As Played", "Chord", "Up & Down X", "Down & Up X", "Random Once", "Octave Up",
|
"As Played", "Chord", "Up & Down X", "Down & Up X", "Random Once", "Octave Up",
|
||||||
"Octave Down", "Pinky Up", "Pinky Down", "Up / Major", "Down / Major",
|
"Octave Down", "Pinky Up", "Pinky Down", "Up / Major", "Down / Major",
|
||||||
"Up & Down / Minor"});
|
"Up & Down / Minor", "C3/C4 1", "C3/C4 2", "C3/C4 3"});
|
||||||
setupCB(arpOctavesBox, arpOctavesAttach, "arpOctaves", {"1", "2", "3"});
|
setupCB(arpOctavesBox, arpOctavesAttach, "arpOctaves", {"1", "2", "3"});
|
||||||
setupCB(arpDirectionBox, arpDirectionAttach, "arpDirection", {"Up", "Down"});
|
setupCB(arpDirectionBox, arpDirectionAttach, "arpDirection", {"Up", "Down"});
|
||||||
setupCB(arpRateBox, arpRateAttach, "arpRate", {"1/16", "1/8", "1/4", "1/2", "1", "2"});
|
setupCB(arpRateBox, arpRateAttach, "arpRate", {"1/32", "1/16", "1/8", "1/4", "1/2", "1", "2"});
|
||||||
|
|
||||||
setupCombo(uiScaleBox);
|
setupCombo(uiScaleBox);
|
||||||
uiScaleBox.addItem("100%", 1);
|
uiScaleBox.addItem("100%", 1);
|
||||||
|
|
|
||||||
|
|
@ -283,7 +283,8 @@ juce::AudioProcessorValueTreeState::ParameterLayout ChromaFlockProcessor::create
|
||||||
juce::StringArray{"Up / Minor", "Down / Minor", "Up & Down / Major",
|
juce::StringArray{"Up / Minor", "Down / Minor", "Up & Down / Major",
|
||||||
"Down & Up / Major", "Random", "As Played", "Chord", "Up & Down X",
|
"Down & Up / Major", "Random", "As Played", "Chord", "Up & Down X",
|
||||||
"Down & Up X", "Random Once", "Octave Up", "Octave Down", "Pinky Up",
|
"Down & Up X", "Random Once", "Octave Up", "Octave Down", "Pinky Up",
|
||||||
"Pinky Down", "Up / Major", "Down / Major", "Up & Down / Minor"}, 0));
|
"Pinky Down", "Up / Major", "Down / Major", "Up & Down / Minor",
|
||||||
|
"C3/C4 1", "C3/C4 2", "C3/C4 3"}, 0));
|
||||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||||
juce::ParameterID{"arpOctaves", 1}, "Arp Octaves",
|
juce::ParameterID{"arpOctaves", 1}, "Arp Octaves",
|
||||||
juce::StringArray{"1", "2", "3"}, 1));
|
juce::StringArray{"1", "2", "3"}, 1));
|
||||||
|
|
@ -292,7 +293,7 @@ juce::AudioProcessorValueTreeState::ParameterLayout ChromaFlockProcessor::create
|
||||||
juce::StringArray{"Up", "Down"}, 0));
|
juce::StringArray{"Up", "Down"}, 0));
|
||||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||||
juce::ParameterID{"arpRate", 1}, "Arp Rate",
|
juce::ParameterID{"arpRate", 1}, "Arp Rate",
|
||||||
juce::StringArray{"1/16", "1/8", "1/4", "1/2", "1", "2"}, 1));
|
juce::StringArray{"1/32", "1/16", "1/8", "1/4", "1/2", "1", "2"}, 1));
|
||||||
|
|
||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
|
|
@ -413,8 +414,8 @@ void ChromaFlockProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::
|
||||||
|
|
||||||
bool arpOn = getRaw("arpEnabled") > 0.5f;
|
bool arpOn = getRaw("arpEnabled") > 0.5f;
|
||||||
if (arpOn) {
|
if (arpOn) {
|
||||||
static const double arpRateBeats[] = {0.25, 0.5, 1.0, 2.0, 4.0, 8.0};
|
static const double arpRateBeats[] = {0.125, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0};
|
||||||
int rateIdx = juce::jlimit(0, 5, juce::roundToInt(getRaw("arpRate")));
|
int rateIdx = juce::jlimit(0, 6, juce::roundToInt(getRaw("arpRate")));
|
||||||
arp.setParameters(true,
|
arp.setParameters(true,
|
||||||
juce::roundToInt(getRaw("arpPattern")),
|
juce::roundToInt(getRaw("arpPattern")),
|
||||||
juce::roundToInt(getRaw("arpOctaves")) + 1,
|
juce::roundToInt(getRaw("arpOctaves")) + 1,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue