mirror of
https://codeberg.org/armin/chromaflock.git
synced 2026-09-01 04:10:47 +02:00
re-work appegiator patterns
This commit is contained in:
parent
278a14053a
commit
56fc80c405
4 changed files with 122 additions and 8 deletions
|
|
@ -118,7 +118,7 @@ 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(7) | 0 (Up) |
|
| arpPattern | Arp Pattern | Choice(17) | 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(6) 1/16..2 | 1 (1/8) |
|
||||||
|
|
@ -200,8 +200,8 @@ 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.
|
- 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".
|
||||||
- `arpDirection` (Up/Down) controls whether octave copies extend above or below the root; sequential patterns always order by ascending pitch.
|
- `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.
|
||||||
- `arpRate` step divisions (beats): 1/16, 1/8, 1/4, 1/2, 1, 2 — tick period = `rateBeats * 60 / bpm`.
|
- `arpRate` step divisions (beats): 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).
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,16 @@ public:
|
||||||
Random,
|
Random,
|
||||||
AsPlayed,
|
AsPlayed,
|
||||||
Chord,
|
Chord,
|
||||||
|
UpDownX,
|
||||||
|
DownUpX,
|
||||||
|
RandomOnce,
|
||||||
|
OctaveUp,
|
||||||
|
OctaveDown,
|
||||||
|
PinkyUp,
|
||||||
|
PinkyDown,
|
||||||
|
UpMajor,
|
||||||
|
DownMajor,
|
||||||
|
UpDownMinor,
|
||||||
numPatterns
|
numPatterns
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -137,6 +147,19 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Harmony patterns arpeggiate a third + fifth after each root: minor
|
||||||
|
// (root, +3, +7) or major (root, +4, +7). Each is a separate step.
|
||||||
|
bool hasHarmony() const {
|
||||||
|
return pattern == Up || pattern == Down || pattern == UpDown || pattern == DownUp
|
||||||
|
|| pattern == UpMajor || pattern == DownMajor || pattern == UpDownMinor;
|
||||||
|
}
|
||||||
|
|
||||||
|
int harmonyThird() const {
|
||||||
|
switch (pattern) {
|
||||||
|
case Up: case Down: case UpDownMinor: return 3;
|
||||||
|
default: return 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
void buildPool(std::vector<NotePitch>& pool) const {
|
void buildPool(std::vector<NotePitch>& pool) const {
|
||||||
bool used[128] = {};
|
bool used[128] = {};
|
||||||
auto addPitch = [&](int pitch, float velocity) {
|
auto addPitch = [&](int pitch, float velocity) {
|
||||||
|
|
@ -166,8 +189,9 @@ 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 regardless of the
|
// Sequential patterns play by ascending pitch; Direction Down inverts
|
||||||
// octave direction; As Played keeps the key-press order.
|
// 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; });
|
||||||
|
|
@ -178,17 +202,43 @@ private:
|
||||||
|
|
||||||
switch (pattern) {
|
switch (pattern) {
|
||||||
case Down:
|
case Down:
|
||||||
|
case DownMajor:
|
||||||
for (int i = n - 1; i >= 0; --i) push(i);
|
for (int i = n - 1; i >= 0; --i) push(i);
|
||||||
break;
|
break;
|
||||||
case UpDown:
|
case UpDown:
|
||||||
|
case UpDownMinor:
|
||||||
|
for (int i = 0; i < n; ++i) push(i);
|
||||||
|
for (int i = n - 2; i >= 0; --i) push(i);
|
||||||
|
break;
|
||||||
|
case DownUp:
|
||||||
|
for (int i = n - 1; i >= 0; --i) push(i);
|
||||||
|
for (int i = 1; i < n; ++i) push(i);
|
||||||
|
break;
|
||||||
|
case UpDownX:
|
||||||
for (int i = 0; i < n; ++i) push(i);
|
for (int i = 0; i < n; ++i) push(i);
|
||||||
for (int i = n - 2; i >= 1; --i) push(i);
|
for (int i = n - 2; i >= 1; --i) push(i);
|
||||||
break;
|
break;
|
||||||
case DownUp:
|
case DownUpX:
|
||||||
for (int i = n - 1; i >= 0; --i) push(i);
|
for (int i = n - 1; i >= 0; --i) push(i);
|
||||||
for (int i = 1; i < n - 1; ++i) push(i);
|
for (int i = 1; i < n - 1; ++i) push(i);
|
||||||
break;
|
break;
|
||||||
case Random:
|
case Random:
|
||||||
|
case RandomOnce: {
|
||||||
|
if (pattern == RandomOnce) {
|
||||||
|
std::vector<int> keys;
|
||||||
|
for (const auto& np : seq) keys.push_back(np.note);
|
||||||
|
if (keys != randomOncePool || stepIndex % n == 0) {
|
||||||
|
randomOncePool = keys;
|
||||||
|
randomOnceOrder = seq;
|
||||||
|
for (int i = n - 1; i > 0; --i) {
|
||||||
|
int j = static_cast<int>(randomState % static_cast<uint32_t>(i + 1));
|
||||||
|
std::swap(randomOnceOrder[i], randomOnceOrder[j]);
|
||||||
|
randomState = randomState * 1664525u + 1013904223u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
order = randomOnceOrder;
|
||||||
|
break;
|
||||||
|
}
|
||||||
order = seq;
|
order = seq;
|
||||||
for (int i = n - 1; i > 0; --i) {
|
for (int i = n - 1; i > 0; --i) {
|
||||||
int j = static_cast<int>(randomState % static_cast<uint32_t>(i + 1));
|
int j = static_cast<int>(randomState % static_cast<uint32_t>(i + 1));
|
||||||
|
|
@ -196,15 +246,71 @@ private:
|
||||||
randomState = randomState * 1664525u + 1013904223u;
|
randomState = randomState * 1664525u + 1013904223u;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
case OctaveUp:
|
||||||
|
case OctaveDown:
|
||||||
|
for (const auto& np : seq) {
|
||||||
|
bool isRoot = true;
|
||||||
|
for (const auto& other : seq) {
|
||||||
|
if (other.note == np.note - 12) { isRoot = false; break; }
|
||||||
|
}
|
||||||
|
if (!isRoot) continue;
|
||||||
|
int step = (pattern == OctaveUp ? 12 : -12);
|
||||||
|
for (int k = 0; k < octaves; ++k) {
|
||||||
|
int pitch = np.note + step * k;
|
||||||
|
pitch = pitch < 0 ? 0 : (pitch > 127 ? 127 : pitch);
|
||||||
|
auto it = std::find_if(seq.begin(), seq.end(),
|
||||||
|
[&](const NotePitch& o) { return o.note == pitch; });
|
||||||
|
if (it != seq.end())
|
||||||
|
order.push_back(*it);
|
||||||
|
else
|
||||||
|
order.push_back({pitch, np.velocity});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PinkyUp:
|
||||||
|
case PinkyDown:
|
||||||
|
for (int hi = n - 1, lo = 0; hi >= lo; --hi, ++lo) {
|
||||||
|
if (pattern == PinkyUp) {
|
||||||
|
push(hi);
|
||||||
|
if (lo < hi) push(lo);
|
||||||
|
} else {
|
||||||
|
push(lo);
|
||||||
|
if (lo < hi) push(hi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
case AsPlayed:
|
case AsPlayed:
|
||||||
order = pool;
|
order = pool;
|
||||||
break;
|
break;
|
||||||
case Chord:
|
case Chord:
|
||||||
case Up:
|
case Up:
|
||||||
|
case UpMajor:
|
||||||
default:
|
default:
|
||||||
for (int i = 0; i < n; ++i) push(i);
|
for (int i = 0; i < n; ++i) push(i);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (direction == DirectionDown && pattern != Random && pattern != RandomOnce
|
||||||
|
&& pattern != AsPlayed)
|
||||||
|
std::reverse(order.begin(), order.end());
|
||||||
|
|
||||||
|
if (hasHarmony()) {
|
||||||
|
std::vector<NotePitch> expanded;
|
||||||
|
expanded.reserve(order.size() * 3);
|
||||||
|
int third = harmonyThird();
|
||||||
|
for (const auto& np : order) {
|
||||||
|
expanded.push_back(np);
|
||||||
|
auto pushClamped = [&](int pitch) {
|
||||||
|
pitch = pitch < 0 ? 0 : (pitch > 127 ? 127 : pitch);
|
||||||
|
expanded.push_back({pitch, np.velocity});
|
||||||
|
};
|
||||||
|
pushClamped(np.note + third);
|
||||||
|
pushClamped(np.note + 7);
|
||||||
|
}
|
||||||
|
order.swap(expanded);
|
||||||
|
}
|
||||||
|
|
||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,4 +328,6 @@ private:
|
||||||
bool needsImmediateStep = false;
|
bool needsImmediateStep = false;
|
||||||
|
|
||||||
uint32_t randomState = 0x12345678u;
|
uint32_t randomState = 0x12345678u;
|
||||||
|
std::vector<int> randomOncePool;
|
||||||
|
std::vector<NotePitch> randomOnceOrder;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -699,7 +699,10 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p)
|
||||||
|
|
||||||
setupCB(arpEnabledBox, arpEnabledAttach, "arpEnabled", {"Off", "On"});
|
setupCB(arpEnabledBox, arpEnabledAttach, "arpEnabled", {"Off", "On"});
|
||||||
setupCB(arpPatternBox, arpPatternAttach, "arpPattern",
|
setupCB(arpPatternBox, arpPatternAttach, "arpPattern",
|
||||||
{"Up", "Down", "UpDown", "DownUp", "Random", "As Played", "Chord"});
|
{"Up / Minor", "Down / Minor", "Up & Down / Major", "Down & Up / Major", "Random",
|
||||||
|
"As Played", "Chord", "Up & Down X", "Down & Up X", "Random Once", "Octave Up",
|
||||||
|
"Octave Down", "Pinky Up", "Pinky Down", "Up / Major", "Down / Major",
|
||||||
|
"Up & Down / Minor"});
|
||||||
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/16", "1/8", "1/4", "1/2", "1", "2"});
|
||||||
|
|
|
||||||
|
|
@ -280,7 +280,10 @@ juce::AudioProcessorValueTreeState::ParameterLayout ChromaFlockProcessor::create
|
||||||
juce::StringArray{"Off", "On"}, 0));
|
juce::StringArray{"Off", "On"}, 0));
|
||||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||||
juce::ParameterID{"arpPattern", 1}, "Arp Pattern",
|
juce::ParameterID{"arpPattern", 1}, "Arp Pattern",
|
||||||
juce::StringArray{"Up", "Down", "UpDown", "DownUp", "Random", "As Played", "Chord"}, 0));
|
juce::StringArray{"Up / Minor", "Down / Minor", "Up & Down / Major",
|
||||||
|
"Down & Up / Major", "Random", "As Played", "Chord", "Up & Down X",
|
||||||
|
"Down & Up X", "Random Once", "Octave Up", "Octave Down", "Pinky Up",
|
||||||
|
"Pinky Down", "Up / Major", "Down / Major", "Up & Down / Minor"}, 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));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue