mirror of
https://codeberg.org/armin/chromaflock.git
synced 2026-09-01 12:20:47 +02:00
391 lines
14 KiB
C++
391 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,
|
|
C3C4D,
|
|
C3C4E,
|
|
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) {
|
|
static const bool chordBlanks[] =
|
|
{false, false, true, false, false, true, false, false,
|
|
true, false, false, true, false, true, false, true};
|
|
bool blank = chordBlanks[stepIndex % 16];
|
|
++stepIndex;
|
|
notesToStop = currentNotes;
|
|
currentNotes.clear();
|
|
if (blank)
|
|
return;
|
|
notesToPlay = pool;
|
|
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();
|
|
if (np.note != restNote) {
|
|
currentNotes.push_back(np.note);
|
|
notesToPlay.push_back(np);
|
|
}
|
|
}
|
|
|
|
private:
|
|
static constexpr int restNote = -1;
|
|
|
|
// 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) {
|
|
addPitch(n + 12 * k, 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) {
|
|
addPitch(n + 12 * k, 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());
|
|
|
|
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: {
|
|
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()];
|
|
NotePitch rest{restNote, vel};
|
|
int lo = (direction == DirectionDown) ? up : anchor;
|
|
int hi = (direction == DirectionDown) ? anchor : up;
|
|
order.clear();
|
|
if (pattern == PinkyUp) {
|
|
order = {{lo, vel}, rest, {hi, vel}, {lo, vel},
|
|
rest, {hi, vel}, {lo, vel}, {lo, vel}};
|
|
} else {
|
|
order = {{hi, vel}, rest, {lo, vel}, {hi, vel},
|
|
rest, {lo, vel}, {hi, vel}, {hi, vel}};
|
|
}
|
|
break;
|
|
}
|
|
case AsPlayed:
|
|
order = pool;
|
|
break;
|
|
case C3C4A:
|
|
case C3C4B:
|
|
case C3C4C:
|
|
case C3C4D:
|
|
case C3C4E: {
|
|
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()];
|
|
NotePitch rest{restNote, vel};
|
|
// Invert swaps the lower/upper notes (C3 <-> C4) so the Stab
|
|
// patterns play upside down when direction is Down.
|
|
int lo = (direction == DirectionDown) ? up : anchor;
|
|
int hi = (direction == DirectionDown) ? anchor : up;
|
|
order.clear();
|
|
if (pattern == C3C4A) {
|
|
order = {{lo, vel}, {lo, vel}, {hi, vel}, {lo, vel},
|
|
{hi, vel}, {lo, vel}, rest, {hi, vel}};
|
|
} else if (pattern == C3C4B) {
|
|
order = {{hi, vel}, {lo, vel}, rest, {lo, vel},
|
|
{lo, vel}, {lo, vel}, rest, {lo, vel}};
|
|
} else if (pattern == C3C4C) {
|
|
order = {{lo, vel}, {lo, vel}, rest, {hi, vel},
|
|
rest, {lo, vel}, rest, {lo, vel}};
|
|
} else if (pattern == C3C4D) {
|
|
order = {{lo, vel}, {lo, vel}, {hi, vel}, {lo, vel},
|
|
{lo, vel}, {hi, vel}, {lo, vel}, {lo, vel},
|
|
{hi, vel}, {lo, vel}, {lo, vel}, {hi, vel},
|
|
{lo, vel}, {lo, vel}, {lo, vel}, {lo, vel}};
|
|
} else {
|
|
order = {{lo, vel}, {lo, vel}, rest, {lo, vel},
|
|
{lo, vel}, rest, {lo, vel}, {lo, vel},
|
|
rest, {lo, vel}, {lo, vel}, rest,
|
|
{lo, vel}, {lo, vel}, {lo, vel}, {lo, 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 && pattern != C3C4D && pattern != C3C4E
|
|
&& pattern != PinkyUp && pattern != PinkyDown)
|
|
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;
|
|
};
|