#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, 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) { // 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 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()); 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 Chord: case Up: case UpMajor: default: for (int i = 0; i < n; ++i) push(i); break; } if (direction == DirectionDown && pattern != Random && pattern != RandomOnce && pattern != AsPlayed) std::reverse(order.begin(), order.end()); 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); } 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; };