#pragma once #include #include #include // 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& 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& notesToStop, std::vector& notesToPlay) { notesToStop.clear(); notesToPlay.clear(); if (!enabled) return; if (heldNotes.empty()) { notesToStop = currentNotes; currentNotes.clear(); stepIndex = 0; return; } std::vector 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(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& 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 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 buildOrder(const std::vector& pool) { std::vector seq(pool); std::sort(seq.begin(), seq.end(), [](const NotePitch& a, const NotePitch& b) { return a.note < b.note; }); int n = static_cast(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 down; int thirdBelow = 12 - harmonyThird(); std::vector 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 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 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(randomState % static_cast(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(randomState % static_cast(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 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 heldNotes; float velocities[128] = {}; std::vector currentNotes; int stepIndex = 0; bool needsImmediateStep = false; uint32_t randomState = 0x12345678u; std::vector randomOncePool; std::vector randomOnceOrder; };