chromaflock/Source/DSP/Arpeggiator.h
2026-08-13 01:39:02 +02:00

385 lines
14 KiB
C++

#pragma once
#include <vector>
#include <algorithm>
#include <cstdint>
// Tempo-synced arpeggiator. Tracks held notes and, on each clock step,
// produces the notes to play for the selected pattern / octave range /
// direction. Pure note logic — timing is driven from outside (the audio
// thread) via step().
class Arpeggiator {
public:
enum Pattern {
Up = 0,
Down,
UpDown,
DownUp,
Random,
AsPlayed,
Chord,
UpDownX,
DownUpX,
RandomOnce,
OctaveUp,
OctaveDown,
PinkyUp,
PinkyDown,
UpMajor,
DownMajor,
UpDownMinor,
C3C4A,
C3C4B,
C3C4C,
numPatterns
};
enum Direction {
DirectionUp = 0,
DirectionDown
};
struct NotePitch {
int note;
float velocity;
};
void setParameters(bool enabled, int pattern, int octaves, int direction,
double rateBeats, double bpm) {
this->enabled = enabled;
this->pattern = pattern < 0 ? Up : (pattern >= numPatterns ? Chord : pattern);
this->octaves = octaves < 1 ? 1 : (octaves > 3 ? 3 : octaves);
this->direction = (direction == DirectionDown) ? DirectionDown : DirectionUp;
this->rateBeats = rateBeats;
this->bpm = bpm > 0.0 ? bpm : 120.0;
}
bool isEnabled() const { return enabled; }
double getTickSeconds() const {
return rateBeats * 60.0 / bpm;
}
void noteOn(int note, float velocity) {
if (note < 0 || note > 127) return;
if (std::find(heldNotes.begin(), heldNotes.end(), note) != heldNotes.end())
return;
bool wasIdle = heldNotes.empty();
heldNotes.push_back(note);
velocities[note] = velocity;
// A fresh key press should sound immediately instead of waiting for
// the next host-synced tick.
if (wasIdle)
needsImmediateStep = true;
}
void noteOff(int note) {
auto it = std::find(heldNotes.begin(), heldNotes.end(), note);
if (it != heldNotes.end())
heldNotes.erase(it);
}
bool hasHeldNotes() const { return !heldNotes.empty(); }
// Returns true if the next step should fire right now (a fresh key press
// requested an immediate trigger) and clears the request.
bool shouldTriggerNow() {
bool v = needsImmediateStep;
needsImmediateStep = false;
return v;
}
// Clears held notes and step state. Notes that were sounding when reset
// was called are returned so the caller can issue note-offs.
void reset(std::vector<int>& notesToStop) {
notesToStop.swap(currentNotes);
heldNotes.clear();
stepIndex = 0;
needsImmediateStep = false;
}
// Advances one step. Notes that should stop go into `notesToStop`
// (usually the note(s) from the previous step); notes that should start
// go into `notesToPlay`.
void step(std::vector<int>& notesToStop, std::vector<NotePitch>& notesToPlay) {
notesToStop.clear();
notesToPlay.clear();
if (!enabled) return;
if (heldNotes.empty()) {
notesToStop = currentNotes;
currentNotes.clear();
stepIndex = 0;
return;
}
std::vector<NotePitch> pool;
buildPool(pool);
if (pool.empty()) {
notesToStop = currentNotes;
currentNotes.clear();
return;
}
if (pattern == Chord) {
notesToStop = currentNotes;
notesToPlay = pool;
currentNotes.clear();
for (const auto& np : pool)
currentNotes.push_back(np.note);
return;
}
auto order = buildOrder(pool);
if (order.empty()) {
notesToStop = currentNotes;
currentNotes.clear();
return;
}
int idx = stepIndex % static_cast<int>(order.size());
++stepIndex;
const NotePitch& np = order[idx];
notesToStop = currentNotes;
currentNotes.clear();
currentNotes.push_back(np.note);
notesToPlay.push_back(np);
}
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 {
bool used[128] = {};
auto addPitch = [&](int pitch, float velocity) {
pitch = pitch < 0 ? 0 : (pitch > 127 ? 127 : pitch);
if (used[pitch]) return;
used[pitch] = true;
pool.push_back({pitch, velocity});
};
if (pattern == AsPlayed) {
for (int n : heldNotes) {
for (int k = 0; k < octaves; ++k) {
int off = (direction == DirectionUp ? 12 * k : -12 * k);
addPitch(n + off, velocities[n]);
}
}
} else {
std::vector<int> sorted(heldNotes.begin(), heldNotes.end());
std::sort(sorted.begin(), sorted.end());
for (int n : sorted) {
for (int k = 0; k < octaves; ++k) {
int off = (direction == DirectionUp ? 12 * k : -12 * k);
addPitch(n + off, velocities[n]);
}
}
}
}
std::vector<NotePitch> buildOrder(const std::vector<NotePitch>& pool) {
std::vector<NotePitch> seq(pool);
std::sort(seq.begin(), seq.end(),
[](const NotePitch& a, const NotePitch& b) { return a.note < b.note; });
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;
auto push = [&](int i) { order.push_back(seq[i]); };
switch (pattern) {
case Down:
case DownMajor:
for (int i = n - 1; i >= 0; --i) push(i);
break;
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 = n - 2; i >= 1; --i) push(i);
break;
case DownUpX:
for (int i = n - 1; i >= 0; --i) push(i);
for (int i = 1; i < n - 1; ++i) push(i);
break;
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;
for (int i = n - 1; i > 0; --i) {
int j = static_cast<int>(randomState % static_cast<uint32_t>(i + 1));
std::swap(order[i], order[j]);
randomState = randomState * 1664525u + 1013904223u;
}
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:
order = pool;
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 Up:
case UpMajor:
default:
for (int i = 0; i < n; ++i) push(i);
break;
}
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);
}
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;
}
bool enabled = false;
int pattern = Up;
int octaves = 1;
int direction = DirectionUp;
double rateBeats = 0.5;
double bpm = 120.0;
std::vector<int> heldNotes;
float velocities[128] = {};
std::vector<int> currentNotes;
int stepIndex = 0;
bool needsImmediateStep = false;
uint32_t randomState = 0x12345678u;
std::vector<int> randomOncePool;
std::vector<NotePitch> randomOnceOrder;
};