#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, 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; // The held set changed, so the pitch pool changed too. Restart the // sequence from the top so the pattern always starts from a known // note instead of continuing at stepIndex % newPoolSize. stepIndex = 0; randomOncePool.clear(); randomOnceOrder.clear(); // 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); stepIndex = 0; randomOncePool.clear(); randomOnceOrder.clear(); } } 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; randomOncePool.clear(); randomOnceOrder.clear(); } // 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) { 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(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& 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 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 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()); 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: { 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 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 heldNotes; float velocities[128] = {}; std::vector currentNotes; int stepIndex = 0; bool needsImmediateStep = false; uint32_t randomState = 0x12345678u; std::vector randomOncePool; std::vector randomOnceOrder; };