#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, 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: 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 regardless of the // octave direction; As Played keeps the key-press order. 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: for (int i = n - 1; i >= 0; --i) push(i); break; case UpDown: for (int i = 0; i < n; ++i) push(i); for (int i = n - 2; i >= 1; --i) push(i); break; case DownUp: for (int i = n - 1; i >= 0; --i) push(i); for (int i = 1; i < n - 1; ++i) push(i); break; case Random: 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 AsPlayed: order = pool; break; case Chord: case Up: default: for (int i = 0; i < n; ++i) push(i); break; } 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; };