mirror of
https://codeberg.org/armin/chromaflock.git
synced 2026-09-01 04:10:47 +02:00
add arpeggiator, rename presets, move spectrum analyzer into waveform
This commit is contained in:
parent
a806e1a8b5
commit
3331b4cc16
9 changed files with 1126 additions and 395 deletions
225
Source/DSP/Arpeggiator.h
Normal file
225
Source/DSP/Arpeggiator.h
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
#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,
|
||||
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) {
|
||||
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<int>(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<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) {
|
||||
int off = (direction == DirectionUp ? 12 * k : -12 * k);
|
||||
addPitch(n + off, 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) {
|
||||
int off = (direction == DirectionUp ? 12 * k : -12 * k);
|
||||
addPitch(n + off, velocities[n]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<NotePitch> buildOrder(const std::vector<NotePitch>& pool) {
|
||||
// Sequential patterns play by ascending pitch regardless of the
|
||||
// octave direction; As Played keeps the key-press order.
|
||||
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:
|
||||
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<int>(randomState % static_cast<uint32_t>(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<int> heldNotes;
|
||||
float velocities[128] = {};
|
||||
std::vector<int> currentNotes;
|
||||
int stepIndex = 0;
|
||||
bool needsImmediateStep = false;
|
||||
|
||||
uint32_t randomState = 0x12345678u;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue