Compare commits

...

7 Commits

Author SHA1 Message Date
Dejvino
4ec306b8b9 Stability fix 2026-02-19 07:24:59 +01:00
Dejvino
de6775a7d7 Tall cursor backdrop 2026-02-19 00:10:10 +01:00
Dejvino
66c20de208 Refactor and locking cleanup 2026-02-18 23:51:43 +01:00
Dejvino
07c30d20ef More strategies 2026-02-18 19:31:38 +01:00
Dejvino
da30471f31 Play and Setup menu only and no Tracker 2026-02-18 19:08:04 +01:00
Dejvino
5ea0070283 Poly mode 2026-02-18 14:50:03 +01:00
Dejvino
4180bb6a12 Euclidean strategy 2026-02-17 15:31:43 +01:00
19 changed files with 1452 additions and 623 deletions

View File

@ -6,7 +6,7 @@
class ArpStrategy : public MelodyStrategy {
public:
void generate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes, int seed) override {
void generate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes, int seed) override {
randomSeed(seed);
if (numScaleNotes == 0) return;
@ -86,7 +86,7 @@ public:
// 3. Fill Sequence
for (int i = 0; i < numSteps; i++) {
sequence[i] = arpPattern[i % arpLength];
sequence[track][i] = arpPattern[i % arpLength];
}
randomSeed(micros());
}
@ -106,14 +106,14 @@ public:
sortArray(scaleNotes, numScaleNotes);
}
void mutate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes) override {
void mutate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes) override {
// Swap two notes
int s1 = random(numSteps);
int s2 = random(numSteps);
if (sequence[s1].note != -1 && sequence[s2].note != -1) {
int8_t temp = sequence[s1].note;
sequence[s1].note = sequence[s2].note;
sequence[s2].note = temp;
if (sequence[track][s1].note != -1 && sequence[track][s2].note != -1) {
int8_t temp = sequence[track][s1].note;
sequence[track][s1].note = sequence[track][s2].note;
sequence[track][s2].note = temp;
}
}

116
CellularAutomataStrategy.h Normal file
View File

@ -0,0 +1,116 @@
#ifndef CELLULAR_AUTOMATA_STRATEGY_H
#define CELLULAR_AUTOMATA_STRATEGY_H
#include "MelodyStrategy.h"
#include <Arduino.h>
class CellularAutomataStrategy : public MelodyStrategy {
public:
void generate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes, int seed) override {
randomSeed(seed);
if (numScaleNotes == 0) return;
// 1. Setup CA
// Pick a rule. Some rules are more musical (structured chaos) than others.
// Rule 30, 90, 110, 184 are classics. Random is fun too.
uint8_t rule = random(256);
bool cells[NUM_STEPS];
bool next_cells[NUM_STEPS];
// Init: 50% chance of single center seed, 50% random noise
if (random(2) == 0) {
for(int i=0; i<numSteps; i++) cells[i] = false;
cells[numSteps/2] = true;
} else {
for(int i=0; i<numSteps; i++) cells[i] = (random(100) < 30);
}
// Evolve for some generations to let patterns emerge
int generations = numSteps + random(16);
for (int g = 0; g < generations; g++) {
for (int i = 0; i < numSteps; i++) {
bool left = cells[(i - 1 + numSteps) % numSteps];
bool center = cells[i];
bool right = cells[(i + 1) % numSteps];
uint8_t pattern = (left ? 4 : 0) | (center ? 2 : 0) | (right ? 1 : 0);
next_cells[i] = (rule >> pattern) & 1;
}
for(int i=0; i<numSteps; i++) cells[i] = next_cells[i];
}
// Map to notes
for (int i = 0; i < numSteps; i++) {
if (cells[i]) {
int octave = 3 + random(3); // 3, 4, 5
sequence[track][i].note = 12 * octave + scaleNotes[random(numScaleNotes)];
sequence[track][i].accent = (random(100) < 30);
sequence[track][i].tie = (random(100) < 15);
} else {
sequence[track][i].note = -1;
sequence[track][i].accent = false;
sequence[track][i].tie = false;
}
}
randomSeed(micros());
}
void generateScale(int* scaleNotes, int& numScaleNotes) override {
numScaleNotes = random(5, 8);
for (int i = 0; i < 12; i++) {
scaleNotes[i] = i;
}
for (int i = 0; i < 12; i++) {
int j = random(12);
int temp = scaleNotes[i];
scaleNotes[i] = scaleNotes[j];
scaleNotes[j] = temp;
}
sortArray(scaleNotes, numScaleNotes);
}
void mutate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes) override {
// Evolve the current sequence by one generation
// Use a random rule for mutation to keep it dynamic
uint8_t rule = random(256);
Step next_seq[NUM_STEPS];
for (int i = 0; i < numSteps; i++) {
bool left = (sequence[track][(i - 1 + numSteps) % numSteps].note != -1);
bool center = (sequence[track][i].note != -1);
bool right = (sequence[track][(i + 1) % numSteps].note != -1);
uint8_t pattern = (left ? 4 : 0) | (center ? 2 : 0) | (right ? 1 : 0);
bool alive = (rule >> pattern) & 1;
if (alive) {
if (center) {
// Survived: Keep note
next_seq[i] = sequence[track][i];
} else {
// Born: New note
int octave = 3 + random(3);
next_seq[i].note = 12 * octave + scaleNotes[random(numScaleNotes)];
next_seq[i].accent = (random(100) < 30);
next_seq[i].tie = false;
}
} else {
// Died
next_seq[i].note = -1;
next_seq[i].accent = false;
next_seq[i].tie = false;
}
}
for(int i=0; i<numSteps; i++) sequence[track][i] = next_seq[i];
}
const char* getName() override {
return "Cellular";
}
};
#endif

87
EuclideanStrategy.h Normal file
View File

@ -0,0 +1,87 @@
#ifndef EUCLIDEAN_STRATEGY_H
#define EUCLIDEAN_STRATEGY_H
#include "MelodyStrategy.h"
#include <Arduino.h>
class EuclideanStrategy : public MelodyStrategy {
public:
void generate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes, int seed) override {
randomSeed(seed);
if (numScaleNotes == 0) return;
int pulses = random(1, numSteps + 1);
int offset = random(numSteps);
// Euclidean distribution (Bresenham)
int bucket = 0;
// Generate pattern
bool pattern[numSteps];
for(int i=0; i<numSteps; i++) {
bucket += pulses;
if (bucket >= numSteps) {
bucket -= numSteps;
pattern[i] = true;
} else {
pattern[i] = false;
}
}
for (int i = 0; i < numSteps; i++) {
// Apply offset
int stepIndex = (i + offset) % numSteps;
if (pattern[i]) {
int octave = random(3) + 3; // 3, 4, 5
sequence[track][stepIndex].note = 12 * octave + scaleNotes[random(numScaleNotes)];
sequence[track][stepIndex].accent = (random(100) < 30);
sequence[track][stepIndex].tie = (random(100) < 10);
} else {
sequence[track][stepIndex].note = -1;
sequence[track][stepIndex].accent = false;
sequence[track][stepIndex].tie = false;
}
}
randomSeed(micros());
}
void generateScale(int* scaleNotes, int& numScaleNotes) override {
numScaleNotes = random(3, 13); // 3 to 12 notes
for (int i = 0; i < 12; i++) {
scaleNotes[i] = i; // Fill with all notes
}
// Shuffle
for (int i = 0; i < 12; i++) {
int j = random(12);
int temp = scaleNotes[i];
scaleNotes[i] = scaleNotes[j];
scaleNotes[j] = temp;
}
sortArray(scaleNotes, numScaleNotes);
}
void mutate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes) override {
// Rotate sequence
if (random(2) == 0) {
Step last = sequence[track][numSteps - 1];
for (int i = numSteps - 1; i > 0; i--) {
sequence[track][i] = sequence[track][i - 1];
}
sequence[track][0] = last;
} else {
// Randomize a note
int s = random(numSteps);
if (sequence[track][s].note != -1) {
int octave = random(3) + 3;
sequence[track][s].note = 12 * octave + scaleNotes[random(numScaleNotes)];
}
}
}
const char* getName() override {
return "Euclid";
}
};
#endif

163
LSystemStrategy.h Normal file
View File

@ -0,0 +1,163 @@
#ifndef LSYSTEM_STRATEGY_H
#define LSYSTEM_STRATEGY_H
#include "MelodyStrategy.h"
#include <Arduino.h>
struct Rule {
char from;
const char* to;
};
struct RuleSet {
const char* axiom;
const Rule* rules;
uint8_t numRules;
};
// --- L-System Rule Definitions ---
const Rule rules1[] = { {'A', "B-A-B"}, {'B', "A+B+A"} };
const Rule rules2[] = { {'F', "F+F-F-F+F"} };
const Rule rules3[] = { {'X', "F+[[X]-X]-F[-FX]+X"}, {'F', "FF"} };
const RuleSet ruleSets[] = {
{"A", rules1, 2}, // Sierpinski triangle
{"F", rules2, 1}, // Koch curve
{"X", rules3, 2} // Fractal plant
};
const uint8_t numRuleSets = sizeof(ruleSets) / sizeof(RuleSet);
class LSystemStrategy : public MelodyStrategy {
public:
void generate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes, int seed) override {
randomSeed(seed);
if (numScaleNotes == 0) {
// Fill with silence if no scale
for (int i = 0; i < numSteps; i++) {
sequence[track][i].note = -1;
sequence[track][i].accent = false;
sequence[track][i].tie = false;
}
return;
}
// 1. Select a rule set and expand the axiom
const RuleSet& selectedRuleSet = ruleSets[random(numRuleSets)];
String currentString = selectedRuleSet.axiom;
int iterations = random(2, 5); // 2-4 iterations
for (int i = 0; i < iterations; i++) {
String nextString = "";
for (char c : currentString) {
bool replaced = false;
for (int j = 0; j < selectedRuleSet.numRules; j++) {
if (c == selectedRuleSet.rules[j].from) {
nextString += selectedRuleSet.rules[j].to;
replaced = true;
break;
}
}
if (!replaced) {
nextString += c;
}
if (nextString.length() > 256) break; // Memory safety
}
currentString = nextString;
if (currentString.length() > 256) break;
}
// 2. Interpret the string to create the sequence
int stepIndex = 0;
int noteIndex = random(numScaleNotes);
int octave = 4;
// Stack for branching rules
int note_stack[8];
int octave_stack[8];
int stack_ptr = 0;
for (char c : currentString) {
if (stepIndex >= numSteps) break;
switch(c) {
case 'F': case 'G': case 'A': case 'B': // Characters that draw notes
sequence[track][stepIndex].note = 12 * octave + scaleNotes[noteIndex];
sequence[track][stepIndex].accent = (random(100) < 25);
sequence[track][stepIndex].tie = (random(100) < 10);
stepIndex++;
break;
case '+': // Go up in scale
noteIndex = (noteIndex + 1) % numScaleNotes;
break;
case '-': // Go down in scale
noteIndex = (noteIndex - 1 + numScaleNotes) % numScaleNotes;
break;
case '[': // Push current state
if(stack_ptr < 8) {
note_stack[stack_ptr] = noteIndex;
octave_stack[stack_ptr] = octave;
stack_ptr++;
}
break;
case ']': // Pop state
if(stack_ptr > 0) {
stack_ptr--;
noteIndex = note_stack[stack_ptr];
octave = octave_stack[stack_ptr];
}
break;
// Any other characters (like 'X' in rule 3) are ignored for drawing
}
}
// 3. Fill any remaining steps with rests
for (int i = 0; i < numSteps; i++) {
if (i >= stepIndex) {
sequence[track][i].note = -1;
sequence[track][i].accent = false;
sequence[track][i].tie = false;
}
}
randomSeed(micros());
}
void generateScale(int* scaleNotes, int& numScaleNotes) override {
numScaleNotes = random(5, 8);
for (int i = 0; i < 12; i++) {
scaleNotes[i] = i;
}
for (int i = 0; i < 12; i++) {
int j = random(12);
int temp = scaleNotes[i];
scaleNotes[i] = scaleNotes[j];
scaleNotes[j] = temp;
}
sortArray(scaleNotes, numScaleNotes);
}
void mutate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes) override {
// Swap two non-rest steps to create a variation
int s1 = random(numSteps);
int s2 = random(numSteps);
int attempts = 0;
// Try to find two different, non-rest steps
while((sequence[track][s1].note == -1 || sequence[track][s2].note == -1 || s1 == s2) && attempts < 20) {
s1 = random(numSteps);
s2 = random(numSteps);
attempts++;
}
if (sequence[track][s1].note != -1 && sequence[track][s2].note != -1) {
Step temp = sequence[track][s1];
sequence[track][s1] = sequence[track][s2];
sequence[track][s2] = temp;
}
}
const char* getName() override {
return "L-System";
}
};
#endif

View File

@ -6,15 +6,15 @@
class LuckyStrategy : public MelodyStrategy {
public:
void generate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes, int seed) override {
void generate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes, int seed) override {
randomSeed(seed);
if (numScaleNotes == 0) return;
for (int i = 0; i < numSteps; i++) {
int octave = random(3) + 3; // 3, 4, 5 (Base is 4)
sequence[i].note = (random(100) < 50) ? (12 * octave + scaleNotes[random(numScaleNotes)]) : -1;
sequence[i].accent = (random(100) < 30);
sequence[i].tie = (random(100) < 20);
sequence[track][i].note = (random(100) < 50) ? (12 * octave + scaleNotes[random(numScaleNotes)]) : -1;
sequence[track][i].accent = (random(100) < 30);
sequence[track][i].tie = (random(100) < 20);
}
randomSeed(micros());
}
@ -34,17 +34,17 @@ public:
sortArray(scaleNotes, numScaleNotes);
}
void mutate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes) override {
void mutate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes) override {
// Mutate 1 or 2 steps
int count = random(1, 3);
for (int i = 0; i < count; i++) {
int s = random(numSteps);
if (sequence[s].note != -1) {
if (sequence[track][s].note != -1) {
int r = random(100);
if (r < 30) sequence[s].accent = !sequence[s].accent;
else if (r < 60) sequence[s].tie = !sequence[s].tie;
else if (r < 80) sequence[s].note += 12; // Up octave
else sequence[s].note -= 12; // Down octave
if (r < 30) sequence[track][s].accent = !sequence[track][s].accent;
else if (r < 60) sequence[track][s].tie = !sequence[track][s].tie;
else if (r < 80) sequence[track][s].note += 12; // Up octave
else sequence[track][s].note -= 12; // Down octave
}
}
}

126
MarkovStrategy.h Normal file
View File

@ -0,0 +1,126 @@
#ifndef MARKOV_STRATEGY_H
#define MARKOV_STRATEGY_H
#include "MelodyStrategy.h"
#include <Arduino.h>
class MarkovStrategy : public MelodyStrategy {
public:
void generate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes, int seed) override {
randomSeed(seed);
if (numScaleNotes == 0) return;
// Transition matrix: weights for moving from index i to index j
// Max scale size is 12
uint8_t matrix[12][12];
// Initialize matrix with weighted probabilities
for (int i = 0; i < numScaleNotes; i++) {
for (int j = 0; j < numScaleNotes; j++) {
// Base random weight
matrix[i][j] = random(1, 8);
// Musical heuristics: favor small intervals
int dist = abs(i - j);
if (dist == 0) matrix[i][j] += 8; // Repeat note
else if (dist == 1) matrix[i][j] += 12; // Stepwise motion
else if (dist == 2) matrix[i][j] += 6; // Thirds
// Large jumps remain low probability
}
}
int currentIdx = random(numScaleNotes);
for (int i = 0; i < numSteps; i++) {
// 20% chance of rest
if (random(100) < 20) {
sequence[track][i].note = -1;
sequence[track][i].accent = false;
sequence[track][i].tie = false;
continue;
}
// Select next note based on matrix
int totalWeight = 0;
for (int j = 0; j < numScaleNotes; j++) {
totalWeight += matrix[currentIdx][j];
}
int r = random(totalWeight);
int nextIdx = 0;
for (int j = 0; j < numScaleNotes; j++) {
r -= matrix[currentIdx][j];
if (r < 0) {
nextIdx = j;
break;
}
}
currentIdx = nextIdx;
// Determine Octave (weighted towards middle)
int octave = 4;
int rOct = random(100);
if (rOct < 20) octave = 3;
else if (rOct > 80) octave = 5;
sequence[track][i].note = 12 * octave + scaleNotes[currentIdx];
sequence[track][i].accent = (random(100) < 25);
sequence[track][i].tie = (random(100) < 15);
}
randomSeed(micros());
}
void generateScale(int* scaleNotes, int& numScaleNotes) override {
numScaleNotes = random(5, 8); // Pentatonic to Major
for (int i = 0; i < 12; i++) {
scaleNotes[i] = i; // Fill with all notes
}
// Shuffle
for (int i = 0; i < 12; i++) {
int j = random(12);
int temp = scaleNotes[i];
scaleNotes[i] = scaleNotes[j];
scaleNotes[j] = temp;
}
sortArray(scaleNotes, numScaleNotes);
}
void mutate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes) override {
// Drift mutation: pick a note and move it stepwise in the scale
int s = random(numSteps);
if (sequence[track][s].note != -1) {
int currentNoteVal = sequence[track][s].note % 12;
int idx = -1;
// Find index in scale
for(int i=0; i<numScaleNotes; i++) {
if (scaleNotes[i] == currentNoteVal) {
idx = i;
break;
}
}
if (idx != -1) {
// Move up or down 1 step in scale
if (random(2) == 0) idx = (idx + 1) % numScaleNotes;
else idx = (idx - 1 + numScaleNotes) % numScaleNotes;
int octave = sequence[track][s].note / 12;
sequence[track][s].note = 12 * octave + scaleNotes[idx];
}
} else {
// Chance to fill a rest
if (random(100) < 25) {
int octave = 3 + random(3);
sequence[track][s].note = 12 * octave + scaleNotes[random(numScaleNotes)];
sequence[track][s].accent = false;
sequence[track][s].tie = false;
}
}
}
const char* getName() override {
return "Markov";
}
};
#endif

View File

@ -1,13 +1,14 @@
#ifndef MELODY_STRATEGY_H
#define MELODY_STRATEGY_H
#include "config.h"
#include "TrackerTypes.h"
class MelodyStrategy {
public:
virtual void generate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes, int seed) = 0;
virtual void generate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes, int seed) = 0;
virtual void generateScale(int* scaleNotes, int& numScaleNotes) = 0;
virtual void mutate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes) = 0;
virtual void mutate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes) = 0;
virtual const char* getName() = 0;
virtual ~MelodyStrategy() {}
};

View File

@ -44,6 +44,6 @@ void MidiDriver::sendRealtime(uint8_t status) {
void MidiDriver::panic(uint8_t channel) {
uint8_t status = 0xB0 | (channel - 1);
Serial1.write(status);
Serial1.write(123); // All Notes Off
Serial1.write((uint8_t)123); // All Notes Off
Serial1.write((uint8_t)0);
}

142
PlaybackThread.cpp Normal file
View File

@ -0,0 +1,142 @@
#include "MidiDriver.h"
#include "TrackerTypes.h"
#include "UIThread.h"
#include "config.h"
#include "SharedState.h"
static Step local_sequence[NUM_TRACKS][NUM_STEPS];
static Step local_nextSequence[NUM_TRACKS][NUM_STEPS];
bool wasPlaying = false;
static void handlePlayback() {
bool nowPlaying = isPlaying;
int tracksToPlay = (playMode == MODE_POLY) ? NUM_TRACKS : 1;
if (!wasPlaying && nowPlaying) {
midi.sendRealtime(0xFA); // MIDI Start
} else if (wasPlaying && !nowPlaying) {
midi.sendRealtime(0xFC); // MIDI Stop
for (int i=0; i<tracksToPlay; i++) midi.panic(midiChannels[i]);
}
wasPlaying = nowPlaying;
if (!nowPlaying) {
delay(1); // yield
return;
}
unsigned long currentMicros = micros();
unsigned long clockInterval = 2500000 / tempo; // 60s * 1000000us / (tempo * 24ppqn)
if (currentMicros - lastClockTime < clockInterval) {
delay(1); // yield
return;
} else {
lastClockTime += clockInterval;
midi.sendRealtime(0xF8); // MIDI Clock
clockCount++;
if (clockCount < 6) return; // 24 ppqn / 4 = 6 pulses per 16th note
clockCount = 0;
midi.lock();
memcpy(local_sequence, sequence, sizeof(local_sequence));
memcpy(local_nextSequence, nextSequence, sizeof(local_nextSequence));
midi.unlock();
for(int t=0; t<tracksToPlay; t++) {
int trackChannel = playMode == MODE_POLY ? midiChannels[t] : midiChannels[0];
int nextStep = playbackStep + 1;
if (nextStep >= NUM_STEPS) nextStep = 0;
// Determine if we are tying to the next note
bool isTied = local_sequence[t][playbackStep].tie && (local_sequence[t][nextStep].note != -1);
int prevNote = local_sequence[t][playbackStep].note;
// Note Off for previous step (if NOT tied)
if (!isTied && prevNote != -1) {
midi.sendNoteOff(prevNote, trackChannel);
}
}
playbackStep++;
if (playbackStep >= NUM_STEPS) {
playbackStep = 0;
// Theme change
if (sequenceChangeScheduled && queuedTheme != -1) {
currentThemeIndex = queuedTheme;
queuedTheme = -1;
// nextSequence is already generated
}
// Mutation
if (mutationEnabled) {
if (!sequenceChangeScheduled) {
memcpy(local_nextSequence, local_sequence, sizeof(sequence));
}
mutateSequence(local_nextSequence);
midi.lock();
memcpy(nextSequence, local_nextSequence, sizeof(sequence));
sequenceChangeScheduled = true;
midi.unlock();
}
for (int i=0; i<tracksToPlay; i++) midi.panic(midiChannels[i]);
if (sequenceChangeScheduled) {
memcpy(local_sequence, local_nextSequence, sizeof(local_sequence));
midi.lock();
memcpy(sequence, local_sequence, sizeof(local_sequence));
sequenceChangeScheduled = false;
midi.unlock();
}
// Song Mode? Advance repeats
if (songModeEnabled) {
// we just used one repeat
if (songRepeatsRemaining <= 1) {
// let's start another round
songRepeatsRemaining = nextSongRepeats;
} else {
// next repeat
songRepeatsRemaining--;
}
// Trigger next song segment generation if we are on the last repeat
if (songRepeatsRemaining <= 1 && !sequenceChangeScheduled && !songModeNeedsNext) {
songModeNeedsNext = true;
}
}
}
// Note On for new step
for(int t=0; t<tracksToPlay; t++) {
int trackChannel = playMode == MODE_POLY ? midiChannels[t] : midiChannels[0];
if (!trackMute[t] && local_sequence[t][playbackStep].note != -1) {
uint8_t velocity = local_sequence[t][playbackStep].accent ? 127 : 100;
midi.sendNoteOn(local_sequence[t][playbackStep].note, velocity, trackChannel);
}
int prevStep = (playbackStep == 0) ? NUM_STEPS - 1 : playbackStep - 1;
bool wasTied = local_sequence[t][prevStep].tie && (local_sequence[t][playbackStep].note != -1);
int prevNote = local_sequence[t][prevStep].note;
// Note Off for previous step (if tied - delayed Note Off)
if (wasTied && prevNote != -1) {
midi.sendNoteOff(prevNote, trackChannel);
}
}
}
}
void loopPlayback() {
if (needsPanic) {
if (playMode == MODE_POLY) {
for (int i=0; i<NUM_TRACKS; i++) midi.panic(midiChannels[i]);
} else {
midi.panic(midiChannels[0]);
}
needsPanic = false;
}
handlePlayback();
}

6
PlaybackThread.h Normal file
View File

@ -0,0 +1,6 @@
#ifndef PLAYBACK_THREAD_H
#define PLAYBACK_THREAD_H
void loopPlayback();
#endif

View File

@ -2,85 +2,22 @@
#include <Wire.h>
#include <EEPROM.h>
#include "TrackerTypes.h"
#include "MelodyStrategy.h"
#include "LuckyStrategy.h"
#include "ArpStrategy.h"
#include "MidiDriver.h"
#include "UIManager.h"
#include "config.h"
#include "UIThread.h"
#include "PlaybackThread.h"
#include "SharedState.h"
// --- HARDWARE CONFIGURATION ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C // See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
// Pin Definitions for Raspberry Pi Pico (RP2040)
#define PIN_SDA 4
#define PIN_SCL 5
#define ENC_CLK 12
#define ENC_DT 13
#define ENC_SW 14
// --- TRACKER DATA ---
#define NUM_STEPS 16
Step sequence[NUM_STEPS];
Step nextSequence[NUM_STEPS];
volatile bool sequenceChangeScheduled = false;
volatile bool needsPanic = false;
UIState currentState = UI_MENU_MAIN;
const char* mainMenu[] = { "Tracker", "Randomize", "Setup" };
const int mainMenuCount = sizeof(mainMenu) / sizeof(char*);
const char* randomizeMenu[] = { "Back", "Melody", "Flavour", "Scale", "Tempo", "Mutation", "Song Mode", "Theme 1", "Theme 2", "Theme 3", "Theme 4", "Theme 5", "Theme 6", "Theme 7" };
const int THEME_1_INDEX = 7;
const int randomizeMenuCount = sizeof(randomizeMenu) / sizeof(char*);
const char* setupMenu[] = { "Back", "Channel", "Factory Reset" };
const int setupMenuCount = sizeof(setupMenu) / sizeof(char*);
int menuSelection = 0;
volatile int navigationSelection = 1;
volatile int playbackStep = 0;
int midiChannel = 1;
volatile int shMidiChannel = midiChannel;
int scaleNotes[12];
int numScaleNotes = 0;
int melodySeed = 0;
volatile int queuedTheme = -1;
volatile int currentThemeIndex = 1;
const uint32_t EEPROM_MAGIC = 0x42424246;
MelodyStrategy* strategies[] = { new LuckyStrategy(), new ArpStrategy() };
const int numStrategies = 2;
int currentStrategyIndex = 0;
volatile bool mutationEnabled = false;
volatile bool songModeEnabled = false;
volatile int songRepeatsRemaining = 0;
volatile int nextSongRepeats = 0;
volatile bool songModeNeedsNext = false;
volatile bool isEditing = false;
volatile int scrollOffset = 0;
volatile bool isPlaying = false;
volatile int tempo = 120; // BPM
volatile unsigned long lastClockTime = 0;
volatile int clockCount = 0;
// Watchdog
volatile unsigned long lastLoop0Time = 0;
volatile unsigned long lastLoop1Time = 0;
volatile bool watchdogActive = false;
// Encoder State
volatile int encoderDelta = 0;
static uint8_t prevNextCode = 0;
static uint16_t store = 0;
// Button State
bool lastButtonState = HIGH;
unsigned long lastDebounceTime = 0;
bool buttonActive = false;
bool buttonConsumed = false;
unsigned long buttonPressTime = 0;
// --- ENCODER INTERRUPT ---
// Robust Rotary Encoder reading
void readEncoder() {
@ -100,64 +37,6 @@ void readEncoder() {
}
}
void saveSequence(bool quiet = false) {
int addr = 0;
EEPROM.put(addr, EEPROM_MAGIC); addr += sizeof(EEPROM_MAGIC);
EEPROM.put(addr, midiChannel); addr += sizeof(midiChannel);
EEPROM.put(addr, melodySeed); addr += sizeof(melodySeed);
EEPROM.put(addr, currentStrategyIndex); addr += sizeof(currentStrategyIndex);
EEPROM.put(addr, (int)tempo); addr += sizeof(int);
EEPROM.put(addr, numScaleNotes); addr += sizeof(numScaleNotes);
for (int i=0; i<12; i++) {
EEPROM.put(addr, scaleNotes[i]); addr += sizeof(int);
}
midi.lock();
for (int i=0; i<NUM_STEPS; i++) {
EEPROM.put(addr, sequence[i]); addr += sizeof(Step);
}
midi.unlock();
EEPROM.commit();
if (!quiet) ui.showMessage("SAVED!");
}
bool loadSequence() {
int addr = 0;
uint32_t magic;
EEPROM.get(addr, magic); addr += sizeof(magic);
if (magic != EEPROM_MAGIC) return false;
EEPROM.get(addr, midiChannel); addr += sizeof(midiChannel);
shMidiChannel = midiChannel;
EEPROM.get(addr, melodySeed); addr += sizeof(melodySeed);
EEPROM.get(addr, currentStrategyIndex); addr += sizeof(currentStrategyIndex);
int t;
EEPROM.get(addr, t); addr += sizeof(int);
tempo = t;
EEPROM.get(addr, numScaleNotes); addr += sizeof(numScaleNotes);
for (int i=0; i<12; i++) {
EEPROM.get(addr, scaleNotes[i]); addr += sizeof(int);
}
midi.lock();
for (int i=0; i<NUM_STEPS; i++) {
EEPROM.get(addr, sequence[i]); addr += sizeof(Step);
}
midi.unlock();
return true;
}
void factoryReset() {
ui.showMessage("RESETTING...");
uint32_t magic = 0;
EEPROM.put(0, magic);
EEPROM.commit();
delay(500);
rp2040.reboot();
}
void setup() {
Serial.begin(115200);
@ -178,371 +57,46 @@ void setup() {
// 5. Init Sequence
randomSeed(micros());
generateRandomScale();
melodySeed = random(10000);
EEPROM.begin(512);
if (!loadSequence()) {
generateRandomScale();
for(int i=0; i<NUM_TRACKS; i++) {
midiChannels[i] = i + 1;
melodySeeds[i] = random(10000);
currentStrategyIndices[i] = 0;
trackMute[i] = false;
}
generateTheme(1);
}
isPlaying = false; // Don't start playing on boot
Serial.println(F("Started."));
}
void generateRandomScale() {
strategies[currentStrategyIndex]->generateScale(scaleNotes, numScaleNotes);
}
void generateSequenceData(int themeType, Step* target) {
randomSeed(melodySeed + themeType * 12345); // Deterministic seed for this theme
strategies[currentStrategyIndex]->generate(target, NUM_STEPS, scaleNotes, numScaleNotes, melodySeed + themeType * 12345);
}
void generateTheme(int themeType) {
currentThemeIndex = themeType;
needsPanic = true;
midi.lock();
generateSequenceData(themeType, sequence);
midi.unlock();
clockCount = 0;
lastClockTime = micros();
playbackStep = 0;
midi.sendRealtime(0xFA); // MIDI Start
isPlaying = true;
}
void mutateSequence(Step* target) {
strategies[currentStrategyIndex]->mutate(target, NUM_STEPS, scaleNotes, numScaleNotes);
}
void handleInput() {
// Handle Encoder Rotation
int delta = 0;
noInterrupts();
delta = encoderDelta;
encoderDelta = 0;
interrupts();
if (delta != 0) {
switch(currentState) {
case UI_TRACKER:
if (isEditing && navigationSelection > 0) {
// Change Note
int stepIndex = navigationSelection - 1;
int newNote = sequence[stepIndex].note + delta;
if (newNote < -1) newNote = -1;
if (newNote > 127) newNote = 127;
midi.lock();
sequence[stepIndex].note = newNote;
midi.unlock();
} else {
// Move Cursor
navigationSelection += (delta > 0 ? 1 : -1);
if (navigationSelection < 0) navigationSelection = NUM_STEPS;
if (navigationSelection > NUM_STEPS) navigationSelection = 0;
// Adjust Scroll to keep cursor in view
if (navigationSelection < scrollOffset) scrollOffset = navigationSelection;
if (navigationSelection >= scrollOffset + 6) scrollOffset = navigationSelection - 5;
}
break;
case UI_MENU_MAIN:
menuSelection += (delta > 0 ? 1 : -1);
if (menuSelection < 0) menuSelection = mainMenuCount - 1;
if (menuSelection >= mainMenuCount) menuSelection = 0;
break;
case UI_MENU_RANDOMIZE:
menuSelection += (delta > 0 ? 1 : -1);
if (menuSelection < 0) menuSelection = randomizeMenuCount - 1;
if (menuSelection >= randomizeMenuCount) menuSelection = 0;
break;
case UI_MENU_SETUP:
menuSelection += (delta > 0 ? 1 : -1);
if (menuSelection < 0) menuSelection = setupMenuCount - 1;
if (menuSelection >= setupMenuCount) menuSelection = 0;
break;
case UI_SETUP_CHANNEL_EDIT:
midiChannel += (delta > 0 ? 1 : -1);
if (midiChannel < 1) midiChannel = 16;
if (midiChannel > 16) midiChannel = 1;
shMidiChannel = midiChannel;
break;
case UI_EDIT_TEMPO:
tempo += delta;
if (tempo < 40) tempo = 40;
if (tempo > 240) tempo = 240;
break;
case UI_EDIT_FLAVOUR:
currentStrategyIndex += (delta > 0 ? 1 : -1);
if (currentStrategyIndex < 0) currentStrategyIndex = numStrategies - 1;
if (currentStrategyIndex >= numStrategies) currentStrategyIndex = 0;
break;
}
}
// Handle Button
int reading = digitalRead(ENC_SW);
if (reading != lastButtonState) {
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > 50) {
if (reading == LOW && !buttonActive) {
// Button Pressed
buttonActive = true;
buttonPressTime = millis();
buttonConsumed = false;
Serial.println(F("Button Down"));
}
if (reading == HIGH && buttonActive) {
// Button Released
buttonActive = false;
if (!buttonConsumed) { // Short press action
switch(currentState) {
case UI_TRACKER:
if (navigationSelection == 0) { // Menu item selected
currentState = UI_MENU_MAIN;
menuSelection = 0;
} else { // A step is selected
isEditing = !isEditing;
}
break;
case UI_MENU_MAIN:
if (menuSelection == 0) currentState = UI_TRACKER;
if (menuSelection == 1) { currentState = UI_MENU_RANDOMIZE; menuSelection = 0; }
if (menuSelection == 2) { currentState = UI_MENU_SETUP; menuSelection = 0; }
break;
case UI_MENU_RANDOMIZE:
if (menuSelection == 0) { currentState = UI_MENU_MAIN; menuSelection = 1; }
if (menuSelection == 1) {
melodySeed = random(10000); // Melody
if (isPlaying) {
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
midi.lock();
generateSequenceData(theme, nextSequence);
sequenceChangeScheduled = true;
midi.unlock();
}
saveSequence(true);
}
if (menuSelection == 2) currentState = UI_EDIT_FLAVOUR; // Flavour
if (menuSelection == 3) { // Scale
generateRandomScale();
if (isPlaying) {
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
midi.lock();
generateSequenceData(theme, nextSequence);
sequenceChangeScheduled = true;
midi.unlock();
}
saveSequence(true);
}
if (menuSelection == 4) currentState = UI_EDIT_TEMPO;
if (menuSelection == 5) mutationEnabled = !mutationEnabled;
if (menuSelection == 6) {
songModeEnabled = !songModeEnabled;
if (songModeEnabled) {
songModeNeedsNext = true;
}
}
if (menuSelection >= THEME_1_INDEX) { // Themes
const int selectedTheme = menuSelection - THEME_1_INDEX + 1;
if (isPlaying) {
queuedTheme = selectedTheme;
midi.lock();
generateSequenceData(queuedTheme, nextSequence);
sequenceChangeScheduled = true;
midi.unlock();
} else {
generateTheme(selectedTheme);
}
}
break;
case UI_MENU_SETUP:
if (menuSelection == 0) { currentState = UI_MENU_MAIN; menuSelection = 2; }
if (menuSelection == 1) currentState = UI_SETUP_CHANNEL_EDIT;
if (menuSelection == 2) factoryReset();
break;
case UI_SETUP_CHANNEL_EDIT:
currentState = UI_MENU_SETUP;
saveSequence(true);
break;
case UI_EDIT_TEMPO:
currentState = UI_MENU_RANDOMIZE;
saveSequence(true);
break;
case UI_EDIT_FLAVOUR:
currentState = UI_MENU_RANDOMIZE;
if (isPlaying) {
queuedTheme = currentThemeIndex;
midi.lock();
generateSequenceData(currentThemeIndex, nextSequence);
sequenceChangeScheduled = true;
midi.unlock();
}
saveSequence(true);
break;
}
}
}
}
// Check for Long Press (Start/Stop Playback)
if (buttonActive && !buttonConsumed && (millis() - buttonPressTime > 600)) {
// Long press only works from tracker view
if (currentState == UI_TRACKER) {
isPlaying = !isPlaying;
buttonConsumed = true; // Prevent short press action
Serial.print(F("Playback: ")); Serial.println(isPlaying ? F("ON") : F("OFF"));
if (isPlaying) {
playbackStep = 0;
clockCount = 0;
lastClockTime = micros();
midi.sendRealtime(0xFA); // MIDI Start
} else {
// Send All Notes Off on stop (CC 123)
needsPanic = true;
midi.sendRealtime(0xFC); // MIDI Stop
queuedTheme = -1;
}
}
}
lastButtonState = reading;
}
void handlePlayback() {
if (!isPlaying) return;
unsigned long currentMicros = micros();
unsigned long clockInterval = 2500000 / tempo; // 60s * 1000000us / (tempo * 24ppqn)
if (currentMicros - lastClockTime >= clockInterval) {
lastClockTime += clockInterval;
midi.sendRealtime(0xF8); // MIDI Clock
clockCount++;
if (clockCount < 6) return; // 24 ppqn / 4 = 6 pulses per 16th note
clockCount = 0;
midi.lock();
// Determine if we are tying to the next note
int nextStep = playbackStep + 1;
if (nextStep >= NUM_STEPS) nextStep = 0;
bool isTied = sequence[playbackStep].tie && (sequence[nextStep].note != -1);
int prevNote = sequence[playbackStep].note;
// Note Off for previous step (if NOT tied)
if (!isTied && prevNote != -1) {
midi.sendNoteOff(prevNote, shMidiChannel);
}
playbackStep++;
if (playbackStep >= NUM_STEPS) {
playbackStep = 0;
// Theme change
if (sequenceChangeScheduled && queuedTheme != -1) {
currentThemeIndex = queuedTheme;
queuedTheme = -1;
// nextSequence is already generated
}
// Mutation
if (mutationEnabled) {
if (!sequenceChangeScheduled) {
memcpy(nextSequence, sequence, sizeof(sequence));
}
mutateSequence(nextSequence);
sequenceChangeScheduled = true;
}
midi.panic(shMidiChannel); // Panic / All Notes Off
if (sequenceChangeScheduled) {
memcpy(sequence, nextSequence, sizeof(sequence));
sequenceChangeScheduled = false;
}
// Song Mode? Advance repeats
if (songModeEnabled) {
// we just used one repeat
if (songRepeatsRemaining <= 1) {
// let's start another round
songRepeatsRemaining = nextSongRepeats;
} else {
// next repeat
songRepeatsRemaining--;
}
// Trigger next song segment generation if we are on the last repeat
if (songRepeatsRemaining <= 1 && !sequenceChangeScheduled && !songModeNeedsNext) {
songModeNeedsNext = true;
}
}
}
// Note On for new step
if (sequence[playbackStep].note != -1) {
uint8_t velocity = sequence[playbackStep].accent ? 127 : 100;
midi.sendNoteOn(sequence[playbackStep].note, velocity, shMidiChannel);
}
// Note Off for previous step (if tied - delayed Note Off)
if (isTied && prevNote != -1) {
midi.sendNoteOff(prevNote, shMidiChannel);
}
midi.unlock();
}
}
void drawUI() {
midi.lock();
ui.draw(currentState, menuSelection, navigationSelection, isEditing, midiChannel, tempo, strategies[currentStrategyIndex], queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, sequence, scrollOffset, playbackStep, isPlaying, mainMenu, mainMenuCount, randomizeMenu, randomizeMenuCount, setupMenu, setupMenuCount, THEME_1_INDEX);
midi.unlock();
}
void updateLeds() {
midi.lock();
ui.updateLeds(sequence, navigationSelection, playbackStep, isPlaying, currentState, isEditing, songModeEnabled, songRepeatsRemaining, sequenceChangeScheduled);
midi.unlock();
// Enable Watchdog
lastLoop0Time = millis();
lastLoop1Time = millis();
watchdogActive = true;
}
void loop1() {
if (needsPanic) {
midi.lock();
midi.panic(shMidiChannel);
midi.unlock();
needsPanic = false;
unsigned long now = millis();
lastLoop1Time = now;
if (watchdogActive && (now - lastLoop0Time > 1000)) {
Serial.println("Core 0 Freeze detected");
rp2040.reboot();
}
handlePlayback();
loopPlayback();
}
void loop() {
// Handle Song Mode Generation in UI Thread
if (songModeNeedsNext) {
int nextTheme = random(1, 8); // Themes 1-7
int repeats = random(1, 9); // 1-8 repeats
midi.lock();
generateSequenceData(nextTheme, nextSequence);
queuedTheme = nextTheme;
nextSongRepeats = repeats;
sequenceChangeScheduled = true;
midi.unlock();
songModeNeedsNext = false;
unsigned long now = millis();
lastLoop0Time = now;
if (watchdogActive && (now - lastLoop1Time > 1000)) {
Serial.println("Core 1 Freeze detected");
rp2040.reboot();
}
handleInput();
drawUI();
updateLeds();
delay(10); // Small delay to prevent screen tearing/excessive refresh
loopUI();
}

69
SharedState.cpp Normal file
View File

@ -0,0 +1,69 @@
#include "SharedState.h"
#include "LuckyStrategy.h"
#include "ArpStrategy.h"
#include "EuclideanStrategy.h"
#include "MarkovStrategy.h"
#include "CellularAutomataStrategy.h"
#include "LSystemStrategy.h"
// Global state variables
Step sequence[NUM_TRACKS][NUM_STEPS];
Step nextSequence[NUM_TRACKS][NUM_STEPS];
volatile bool sequenceChangeScheduled = false;
volatile bool needsPanic = false;
UIState currentState = UI_MENU_RANDOMIZE;
// Menus
const char* mainMenu[] = { "Randomize", "Setup" };
extern const int mainMenuCount = sizeof(mainMenu) / sizeof(char*);
const char* randomizeMenuMono[] = { "Setup", "Melody", "Flavour", "Scale", "Tempo", "Mutation", "Song Mode", "Theme 1", "Theme 2", "Theme 3", "Theme 4", "Theme 5", "Theme 6", "Theme 7" };
extern const int randomizeMenuMonoCount = sizeof(randomizeMenuMono) / sizeof(char*);
extern const int THEME_1_INDEX_MONO = 7;
const char* randomizeMenuPoly[] = { "Setup", "Track", "Mute", "Melody", "Flavour", "Scale", "Tempo", "Mutation", "Song Mode", "Theme 1", "Theme 2", "Theme 3", "Theme 4", "Theme 5", "Theme 6", "Theme 7" };
extern const int randomizeMenuPolyCount = sizeof(randomizeMenuPoly) / sizeof(char*);
extern const int THEME_1_INDEX_POLY = 9;
const char* setupMenu[] = { "Back", "Play Mode", "Channel", "Factory Reset" };
extern const int setupMenuCount = sizeof(setupMenu) / sizeof(char*);
int menuSelection = 0;
volatile bool trackMute[NUM_TRACKS];
int randomizeTrack = 0;
volatile int playbackStep = 0;
volatile int midiChannels[NUM_TRACKS];
int scaleNotes[12];
int numScaleNotes = 0;
int melodySeeds[NUM_TRACKS];
volatile int queuedTheme = -1;
volatile int currentThemeIndex = 1;
extern const uint32_t EEPROM_MAGIC = 0x4242424B;
MelodyStrategy* strategies[] = {
new LuckyStrategy(), new ArpStrategy(), new EuclideanStrategy(),
new MarkovStrategy(), new CellularAutomataStrategy(), new LSystemStrategy()};
extern const int numStrategies = 6;
int currentStrategyIndices[NUM_TRACKS];
volatile PlayMode playMode = MODE_MONO;
volatile bool mutationEnabled = false;
volatile bool songModeEnabled = false;
volatile int songRepeatsRemaining = 0;
volatile int nextSongRepeats = 0;
volatile bool songModeNeedsNext = false;
volatile bool isPlaying = false;
volatile int tempo = 120; // BPM
volatile unsigned long lastClockTime = 0;
volatile int clockCount = 0;
// Encoder State
volatile int encoderDelta = 0;
// Button State
bool lastButtonState = HIGH;
unsigned long lastDebounceTime = 0;
bool buttonActive = false;
bool buttonConsumed = false;
unsigned long buttonPressTime = 0;

63
SharedState.h Normal file
View File

@ -0,0 +1,63 @@
#ifndef SHARED_STATE_H
#define SHARED_STATE_H
#include "TrackerTypes.h"
#include "MelodyStrategy.h"
#include "config.h"
// Global state variables defined in main .ino
extern Step sequence[NUM_TRACKS][NUM_STEPS];
extern Step nextSequence[NUM_TRACKS][NUM_STEPS];
extern volatile bool sequenceChangeScheduled;
extern volatile bool needsPanic;
extern UIState currentState;
// Menus
extern const char* mainMenu[];
extern const int mainMenuCount;
extern const char* randomizeMenuMono[];
extern const int randomizeMenuMonoCount;
extern const int THEME_1_INDEX_MONO;
extern const char* randomizeMenuPoly[];
extern const int randomizeMenuPolyCount;
extern const int THEME_1_INDEX_POLY;
extern const char* setupMenu[];
extern const int setupMenuCount;
extern int menuSelection;
extern volatile bool trackMute[NUM_TRACKS];
extern int randomizeTrack;
extern volatile int playbackStep;
extern volatile int midiChannels[NUM_TRACKS];
extern int scaleNotes[12];
extern int numScaleNotes;
extern int melodySeeds[NUM_TRACKS];
extern volatile int queuedTheme;
extern volatile int currentThemeIndex;
extern const uint32_t EEPROM_MAGIC;
extern MelodyStrategy* strategies[];
extern const int numStrategies;
extern int currentStrategyIndices[NUM_TRACKS];
extern volatile PlayMode playMode;
extern volatile bool mutationEnabled;
extern volatile bool songModeEnabled;
extern volatile int songRepeatsRemaining;
extern volatile int nextSongRepeats;
extern volatile bool songModeNeedsNext;
extern volatile bool isPlaying;
extern volatile int tempo;
extern volatile unsigned long lastClockTime;
extern volatile int clockCount;
// Input state
extern volatile int encoderDelta;
extern bool lastButtonState;
extern unsigned long lastDebounceTime;
extern bool buttonActive;
extern bool buttonConsumed;
extern unsigned long buttonPressTime;
#endif

View File

@ -2,6 +2,7 @@
#define TRACKER_TYPES_H
#include <stdint.h>
#include "config.h"
struct Step {
int8_t note; // MIDI Note (0-127), -1 for OFF
@ -9,14 +10,20 @@ struct Step {
bool tie;
};
enum PlayMode {
MODE_MONO,
MODE_POLY
};
enum UIState {
UI_TRACKER,
UI_MENU_MAIN,
UI_MENU_RANDOMIZE,
UI_MENU_SETUP,
UI_SETUP_CHANNEL_EDIT,
UI_EDIT_TEMPO,
UI_EDIT_FLAVOUR
UI_EDIT_FLAVOUR,
UI_SETUP_PLAYMODE_EDIT,
UI_RANDOMIZE_TRACK_EDIT
};
inline void sortArray(int arr[], int size) {

View File

@ -1,14 +1,13 @@
#include "UIManager.h"
#include "config.h"
// --- HARDWARE CONFIGURATION ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
#define PIN_NEOPIXEL 16
#define NUM_PIXELS 64
#define NUM_STEPS 16
UIManager ui;
@ -46,16 +45,16 @@ void UIManager::showMessage(const char* msg) {
display.setTextSize(1);
}
void UIManager::draw(UIState currentState, int menuSelection, int navSelection, bool isEditing,
void UIManager::draw(UIState currentState, int menuSelection,
int midiChannel, int tempo, MelodyStrategy* currentStrategy,
int queuedTheme, int currentThemeIndex,
int numScaleNotes, const int* scaleNotes, int melodySeed,
bool mutationEnabled, bool songModeEnabled,
const Step* sequence, int scrollOffset, int playbackStep, bool isPlaying,
const Step sequence[][NUM_STEPS], int playbackStep, bool isPlaying,
const char* mainMenu[], int mainMenuCount,
const char* randomizeMenu[], int randomizeMenuCount,
const char* setupMenu[], int setupMenuCount,
int theme1Index) {
const char* setupMenu[], int setupMenuCount, int theme1Index,
PlayMode playMode, int randomizeTrack, const bool* trackMute) {
display.clearDisplay();
display.setTextSize(1);
@ -63,17 +62,14 @@ void UIManager::draw(UIState currentState, int menuSelection, int navSelection,
display.setCursor(0, 0);
switch(currentState) {
case UI_TRACKER:
drawTracker(navSelection, isEditing, midiChannel, sequence, scrollOffset, playbackStep, isPlaying);
break;
case UI_MENU_MAIN:
drawMenu("MAIN MENU", mainMenu, mainMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index);
drawMenu("MAIN MENU", mainMenu, mainMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index, playMode, randomizeTrack, trackMute);
break;
case UI_MENU_RANDOMIZE:
drawMenu("RANDOMIZE", randomizeMenu, randomizeMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index);
drawMenu("PLAY", randomizeMenu, randomizeMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index, playMode, randomizeTrack, trackMute);
break;
case UI_MENU_SETUP:
drawMenu("SETUP", setupMenu, setupMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index);
drawMenu("SETUP", setupMenu, setupMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index, playMode, randomizeTrack, trackMute);
break;
case UI_SETUP_CHANNEL_EDIT:
display.println(F("SET MIDI CHANNEL"));
@ -108,15 +104,36 @@ void UIManager::draw(UIState currentState, int menuSelection, int navSelection,
display.setCursor(0, 50);
display.println(F(" (Press to confirm)"));
break;
case UI_SETUP_PLAYMODE_EDIT:
display.println(F("SET PLAY MODE"));
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
display.setCursor(20, 25);
display.setTextSize(2);
display.print(playMode == MODE_MONO ? "Mono" : "Poly");
display.setTextSize(1);
display.setCursor(0, 50);
display.println(F(" (Press to confirm)"));
break;
case UI_RANDOMIZE_TRACK_EDIT:
display.println(F("SET TRACK"));
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
display.setCursor(20, 25);
display.setTextSize(2);
display.print(F("TRK: "));
display.print(randomizeTrack + 1);
display.setTextSize(1);
display.setCursor(0, 50);
display.println(F(" (Press to confirm)"));
break;
}
display.display();
}
void UIManager::drawMenu(const char* title, const char* items[], int count, int selection,
UIState currentState, int midiChannel, int tempo, const char* flavourName,
int queuedTheme, int currentThemeIndex,
int numScaleNotes, const int* scaleNotes, int melodySeed,
bool mutationEnabled, bool songModeEnabled, int theme1Index) {
int queuedTheme, int currentThemeIndex, int numScaleNotes,
const int* scaleNotes, int melodySeed, bool mutationEnabled,
bool songModeEnabled, int theme1Index, PlayMode playMode, int randomizeTrack, const bool* trackMute) {
display.println(title);
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
@ -137,8 +154,17 @@ void UIManager::drawMenu(const char* title, const char* items[], int count, int
display.print(items[i]);
if (currentState == UI_MENU_SETUP && i == 1) {
display.print(F(": ")); display.print(playMode == MODE_MONO ? "Mono" : "Poly");
}
if (currentState == UI_MENU_SETUP && i == 2) {
display.print(F(": ")); display.print(midiChannel);
}
if (currentState == UI_MENU_RANDOMIZE && playMode == MODE_POLY && i == 1) {
display.print(F(": ")); display.print(randomizeTrack + 1);
}
if (currentState == UI_MENU_RANDOMIZE && playMode == MODE_POLY && i == 2) {
display.print(F(": ")); display.print(trackMute[randomizeTrack] ? F("YES") : F("NO"));
}
if (currentState == UI_MENU_RANDOMIZE && i >= theme1Index && queuedTheme == (i - theme1Index + 1)) {
display.print(F(" [NEXT]"));
}
@ -146,11 +172,12 @@ void UIManager::drawMenu(const char* title, const char* items[], int count, int
display.print(F(" *"));
}
if (currentState == UI_MENU_RANDOMIZE) {
if (i == 1) { // Melody
int track_offset = (playMode == MODE_POLY) ? 2 : 0;
if (i == 1 + track_offset) { // Melody
display.print(F(": ")); display.print(melodySeed);
} else if (i == 2) { // Flavour
} else if (i == 2 + track_offset) { // Flavour
display.print(F(": ")); display.print(flavourName);
} else if (i == 3) { // Scale
} else if (i == 3 + track_offset) { // Scale
display.print(F(": "));
if (numScaleNotes > 0) {
const char* noteNames[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
@ -159,59 +186,9 @@ void UIManager::drawMenu(const char* title, const char* items[], int count, int
if (j < min(numScaleNotes, 6) - 1) display.print(F(" "));
}
}
} else if (i == 4) { display.print(F(": ")); display.print(tempo); }
else if (i == 5) { display.print(F(": ")); display.print(mutationEnabled ? F("ON") : F("OFF")); }
else if (i == 6) { display.print(F(": ")); display.print(songModeEnabled ? F("ON") : F("OFF")); }
}
y += 9;
}
}
void UIManager::drawTracker(int navSelection, bool isEditing, int midiChannel,
const Step* sequence, int scrollOffset, int playbackStep, bool isPlaying) {
display.print(F("SEQ "));
if (navSelection > 0 && isEditing) display.print(F("[EDIT]"));
else display.print(F("[NAV] "));
display.print(F(" CH:")); display.print(midiChannel);
display.println();
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
int y = 10;
for (int i = 0; i < 6; i++) {
int itemIndex = i + scrollOffset;
if (itemIndex > NUM_STEPS) break;
if (itemIndex == navSelection) {
display.fillRect(0, y, 128, 8, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
} else {
display.setTextColor(SSD1306_WHITE);
}
display.setCursor(2, y);
if (itemIndex == 0) {
display.print(F(">> MENU"));
} else {
int stepIndex = itemIndex - 1;
bool isPlayback = isPlaying && (stepIndex == playbackStep);
if (isPlayback) {
if (itemIndex == navSelection) display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
else display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
}
if (stepIndex < 10) display.print(F("0"));
display.print(stepIndex);
if (isPlayback) {
if (itemIndex == navSelection) display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
else display.setTextColor(SSD1306_WHITE);
}
display.print(F(" | "));
int n = sequence[stepIndex].note;
if (n == -1) display.print(F("---"));
else {
const char* noteNames[] = {"C-", "C#", "D-", "D#", "E-", "F-", "F#", "G-", "G#", "A-", "A#", "B-"};
display.print(noteNames[n % 12]);
display.print(n / 12 - 1);
}
} else if (i == 4 + track_offset) { display.print(F(": ")); display.print(tempo); }
else if (i == 5 + track_offset) { display.print(F(": ")); display.print(mutationEnabled ? F("ON") : F("OFF")); }
else if (i == 6 + track_offset) { display.print(F(": ")); display.print(songModeEnabled ? F("ON") : F("OFF")); }
}
y += 9;
}
@ -227,48 +204,81 @@ int UIManager::getPixelIndex(int x, int y) {
return y * 8 + x;
}
void UIManager::updateLeds(const Step* sequence, int navSelection, int playbackStep, bool isPlaying,
UIState currentState, bool isEditing, bool songModeEnabled,
int songRepeatsRemaining, bool sequenceChangeScheduled) {
void UIManager::updateLeds(const Step sequence[][NUM_STEPS], int playbackStep, bool isPlaying,
UIState currentState, bool songModeEnabled,
int songRepeatsRemaining, bool sequenceChangeScheduled, PlayMode playMode,
int numScaleNotes, const int* scaleNotes, const bool* trackMute) {
pixels.clear();
for (int s = 0; s < NUM_STEPS; s++) {
int x = s % 8;
int yBase = (s / 8) * 4;
uint32_t color = 0, dimColor = 0;
if (sequence[s].note != -1) {
color = getNoteColor(sequence[s].note, sequence[s].tie);
dimColor = getNoteColor(sequence[s].note, true);
}
uint32_t c[4] = {0};
if (sequence[s].note != -1) {
int octave = sequence[s].note / 12;
if (octave > 4) { c[0] = color; if (sequence[s].accent) c[1] = dimColor; }
else if (octave < 4) { c[2] = color; if (sequence[s].accent) c[1] = dimColor; }
else { c[1] = color; if (sequence[s].accent) { c[0] = dimColor; c[2] = dimColor; } }
}
int stepNavIndex = navSelection - 1;
// Apply cursor color logic from original code more strictly
uint32_t cursorColor = 0;
if (isPlaying) {
cursorColor = pixels.Color(0, 50, 0);
if (songModeEnabled && s >= 8) {
int repeats = min(songRepeatsRemaining, 8);
if (x >= (8 - repeats)) cursorColor = (songRepeatsRemaining == 1 && x == 7 && (millis()/250)%2) ? pixels.Color(255, 200, 0) : pixels.Color(100, 220, 40);
}
} else if (currentState == UI_TRACKER) {
cursorColor = isEditing ? pixels.Color(50, 0, 0) : pixels.Color(40, 40, 40);
}
bool isCursorHere = (isPlaying && s == playbackStep) || (!isPlaying && currentState == UI_TRACKER && s == stepNavIndex);
if (cursorColor != 0) {
if (isCursorHere) c[3] = cursorColor;
else {
uint8_t r = (uint8_t)(cursorColor >> 16), g = (uint8_t)(cursorColor >> 8), b = (uint8_t)cursorColor;
c[3] = pixels.Color(r/5, g/5, b/5);
const uint32_t COLOR_PLAYHEAD = pixels.Color(0, 255, 0);
const uint32_t COLOR_PLAYHEAD_DIM = pixels.Color(0, 32, 0);
const uint32_t COLOR_MUTED_PLAYHEAD = pixels.Color(0, 0, 255);
const uint32_t COLOR_CURSOR = pixels.Color(255, 255, 255);
const uint32_t COLOR_CURSOR_DIM = pixels.Color(32, 0, 0);
if(playMode == MODE_POLY) {
for(int t=0; t<NUM_TRACKS; t++) {
for(int s=0; s<NUM_STEPS; s++) {
int col = t * 2 + (s / 8);
int row = s % 8;
uint32_t color = 0;
int note = sequence[t][s].note;
if (note != -1) {
color = getNoteColor(note, !sequence[t][s].accent);
}
if (isPlaying && s == playbackStep) {
if (trackMute[t]) {
color = COLOR_MUTED_PLAYHEAD;
} else {
color = (note != -1) ? COLOR_PLAYHEAD : COLOR_PLAYHEAD_DIM;
}
}
pixels.setPixelColor(getPixelIndex(col, row), color);
}
}
} else {
// --- Mono Mode (original) ---
for (int s = 0; s < NUM_STEPS; s++) {
int x = s % 8;
int yBase = (s / 8) * 4;
uint32_t color = 0, dimColor = 0;
bool isCursorHere = (isPlaying && s == playbackStep);
if (sequence[0][s].note != -1) {
color = getNoteColor(sequence[0][s].note, sequence[0][s].tie);
dimColor = getNoteColor(sequence[0][s].note, true);
}
uint32_t c[4] = {0};
if (sequence[0][s].note != -1) {
int octave = sequence[0][s].note / 12;
if (octave > 4) { c[0] = color; if (sequence[0][s].accent) c[1] = dimColor; }
else if (octave < 4) { c[2] = color; if (sequence[0][s].accent) c[1] = dimColor; }
else { c[1] = color; if (sequence[0][s].accent) { c[0] = dimColor; c[2] = dimColor; } }
}
uint32_t cursorColor = pixels.Color(0, 0, 50);
if (isPlaying) {
cursorColor = pixels.Color(0, 50, 0);
if (songModeEnabled && s >= 8) {
int repeats = min(songRepeatsRemaining, 8);
if (x >= (8 - repeats)) cursorColor = (songRepeatsRemaining == 1 && x == 7 && (millis()/250)%2) ? pixels.Color(255, 200, 0) : pixels.Color(100, 220, 40);
}
}
for(int i=0; i<4; i++) pixels.setPixelColor(getPixelIndex(x, yBase + i), c[i]);
if (cursorColor != 0) {
if (isCursorHere) {
for(int i=0; i<4; i++) {
if (c[i] == 0) c[i] = cursorColor;
}
} else {
uint8_t r = (uint8_t)(cursorColor >> 16), g = (uint8_t)(cursorColor >> 8), b = (uint8_t)cursorColor;
c[3] = pixels.Color(r/5, g/5, b/5);
}
}
for(int i=0; i<4; i++) pixels.setPixelColor(getPixelIndex(x, yBase + i), c[i]);
}
}
if (sequenceChangeScheduled && (millis() / 125) % 2) pixels.setPixelColor(NUM_PIXELS - 1, pixels.Color(127, 50, 0));
pixels.show();

View File

@ -14,33 +14,32 @@ public:
void showMessage(const char* msg);
void draw(UIState currentState, int menuSelection, int navSelection, bool isEditing,
void draw(UIState currentState, int menuSelection,
int midiChannel, int tempo, MelodyStrategy* currentStrategy,
int queuedTheme, int currentThemeIndex,
int numScaleNotes, const int* scaleNotes, int melodySeed,
bool mutationEnabled, bool songModeEnabled,
const Step* sequence, int scrollOffset, int playbackStep, bool isPlaying,
const Step sequence[][NUM_STEPS], int playbackStep, bool isPlaying,
const char* mainMenu[], int mainMenuCount,
const char* randomizeMenu[], int randomizeMenuCount,
const char* setupMenu[], int setupMenuCount,
int theme1Index);
const char* setupMenu[], int setupMenuCount, int theme1Index,
PlayMode playMode, int randomizeTrack, const bool* trackMute);
void updateLeds(const Step* sequence, int navSelection, int playbackStep, bool isPlaying,
UIState currentState, bool isEditing, bool songModeEnabled,
int songRepeatsRemaining, bool sequenceChangeScheduled);
void updateLeds(const Step sequence[][NUM_STEPS], int playbackStep, bool isPlaying,
UIState currentState, bool songModeEnabled,
int songRepeatsRemaining, bool sequenceChangeScheduled, PlayMode playMode,
int numScaleNotes, const int* scaleNotes, const bool* trackMute);
private:
Adafruit_SSD1306 display;
Adafruit_NeoPixel pixels;
uint32_t leds_buffer[8][8]; // For piano roll
void drawMenu(const char* title, const char* items[], int count, int selection,
UIState currentState, int midiChannel, int tempo, const char* flavourName,
int queuedTheme, int currentThemeIndex,
int numScaleNotes, const int* scaleNotes, int melodySeed,
bool mutationEnabled, bool songModeEnabled, int theme1Index);
void drawTracker(int navSelection, bool isEditing, int midiChannel,
const Step* sequence, int scrollOffset, int playbackStep, bool isPlaying);
bool mutationEnabled, bool songModeEnabled, int theme1Index, PlayMode playMode, int randomizeTrack, const bool* trackMute);
uint32_t getNoteColor(int note, bool dim);
int getPixelIndex(int x, int y);

450
UIThread.cpp Normal file
View File

@ -0,0 +1,450 @@
#include <SPI.h>
#include <Wire.h>
#include <EEPROM.h>
#include "TrackerTypes.h"
#include "MelodyStrategy.h"
#include "LuckyStrategy.h"
#include "ArpStrategy.h"
#include "EuclideanStrategy.h"
#include "MarkovStrategy.h"
#include "CellularAutomataStrategy.h"
#include "LSystemStrategy.h"
#include "MidiDriver.h"
#include "UIManager.h"
#include "config.h"
#include "UIThread.h"
#include "SharedState.h"
static Step local_sequence[NUM_TRACKS][NUM_STEPS];
static void handleInput();
static void drawUI();
static void updateLeds();
static void generateTrackData(int track, int themeType, Step (*target)[NUM_STEPS]);
static void generateSequenceData(int themeType, Step (*target)[NUM_STEPS]);
void saveSequence(bool quiet) {
midi.lock();
int addr = 0;
EEPROM.put(addr, EEPROM_MAGIC); addr += sizeof(EEPROM_MAGIC);
int channels[NUM_TRACKS];
for(int i=0; i<NUM_TRACKS; i++) channels[i] = midiChannels[i];
EEPROM.put(addr, channels); addr += sizeof(channels);
EEPROM.put(addr, melodySeeds); addr += sizeof(melodySeeds);
EEPROM.put(addr, currentStrategyIndices); addr += sizeof(currentStrategyIndices);
bool mutes[NUM_TRACKS];
for(int i=0; i<NUM_TRACKS; i++) mutes[i] = trackMute[i];
EEPROM.put(addr, mutes); addr += sizeof(mutes);
EEPROM.put(addr, (int)tempo); addr += sizeof(int);
EEPROM.put(addr, (int)playMode); addr += sizeof(int);
EEPROM.put(addr, numScaleNotes); addr += sizeof(numScaleNotes);
for (int i = 0; i<12; i++) {
EEPROM.put(addr, scaleNotes[i]); addr += sizeof(int);
}
EEPROM.put(addr, sequence); addr += sizeof(sequence);
midi.unlock();
EEPROM.commit();
if (!quiet) ui.showMessage("SAVED!");
}
bool loadSequence() {
midi.lock();
int addr = 0;
uint32_t magic;
EEPROM.get(addr, magic); addr += sizeof(magic);
if (magic != EEPROM_MAGIC) return false;
int channels[NUM_TRACKS];
EEPROM.get(addr, channels); addr += sizeof(channels);
for(int i=0; i<NUM_TRACKS; i++) midiChannels[i] = channels[i];
EEPROM.get(addr, melodySeeds); addr += sizeof(melodySeeds);
EEPROM.get(addr, currentStrategyIndices); addr += sizeof(currentStrategyIndices);
bool mutes[NUM_TRACKS];
EEPROM.get(addr, mutes); addr += sizeof(mutes);
for(int i=0; i<NUM_TRACKS; i++) trackMute[i] = mutes[i];
int t;
EEPROM.get(addr, t); addr += sizeof(int);
tempo = t;
EEPROM.get(addr, t); addr += sizeof(int);
playMode = (PlayMode)t;
EEPROM.get(addr, numScaleNotes); addr += sizeof(numScaleNotes);
for (int i = 0; i<12; i++) {
EEPROM.get(addr, scaleNotes[i]); addr += sizeof(int);
}
EEPROM.get(addr, sequence); addr += sizeof(sequence);
midi.unlock();
return true;
}
void factoryReset() {
ui.showMessage("RESETTING...");
uint32_t magic = 0;
EEPROM.put(0, magic);
EEPROM.commit();
delay(500);
rp2040.reboot();
}
static void generateTrackData(int track, int themeType, Step (*target)[NUM_STEPS]) {
randomSeed(melodySeeds[track] + themeType * 12345);
strategies[currentStrategyIndices[track]]->generate(target, track, NUM_STEPS, scaleNotes, numScaleNotes, melodySeeds[track] + themeType * 12345);
}
void generateRandomScale() {
// All tracks share the same scale for now
strategies[currentStrategyIndices[0]]->generateScale(scaleNotes, numScaleNotes);
}
static void generateSequenceData(int themeType, Step (*target)[NUM_STEPS]) {
for(int i=0; i<NUM_TRACKS; i++) {
generateTrackData(i, themeType, target);
}
}
void generateTheme(int themeType) {
generateSequenceData(themeType, local_sequence);
midi.lock();
memcpy(sequence, local_sequence, sizeof(local_sequence));
needsPanic = true;
midi.unlock();
currentThemeIndex = themeType;
clockCount = 0;
lastClockTime = micros();
playbackStep = 0;
isPlaying = true;
}
void mutateSequence(Step (*target)[NUM_STEPS]) {
if (playMode == MODE_POLY) {
for(int i=0; i<NUM_TRACKS; i++) strategies[currentStrategyIndices[i]]->mutate(target, i, NUM_STEPS, scaleNotes, numScaleNotes);
} else {
strategies[currentStrategyIndices[0]]->mutate(target, 0, NUM_STEPS, scaleNotes, numScaleNotes);
}
}
static void handleInput() {
// Handle Encoder Rotation
int delta = 0;
noInterrupts();
delta = encoderDelta;
encoderDelta = 0;
interrupts();
if (delta != 0) {
switch(currentState) {
case UI_MENU_MAIN:
menuSelection += (delta > 0 ? 1 : -1);
if (menuSelection < 0) menuSelection = mainMenuCount - 1;
if (menuSelection >= mainMenuCount) menuSelection = 0;
break;
case UI_MENU_RANDOMIZE:
{
menuSelection += (delta > 0 ? 1 : -1);
int count = (playMode == MODE_POLY) ? randomizeMenuPolyCount : randomizeMenuMonoCount;
if (menuSelection < 0) menuSelection = count - 1;
if (menuSelection >= count) menuSelection = 0;
}
break;
case UI_MENU_SETUP:
menuSelection += (delta > 0 ? 1 : -1);
if (menuSelection < 0) menuSelection = setupMenuCount - 1;
if (menuSelection >= setupMenuCount) menuSelection = 0;
break;
case UI_SETUP_CHANNEL_EDIT:
{
int trackToEdit = (playMode == MODE_POLY) ? randomizeTrack : 0;
midiChannels[trackToEdit] += (delta > 0 ? 1 : -1);
if (midiChannels[trackToEdit] < 1) midiChannels[trackToEdit] = 16;
if (midiChannels[trackToEdit] > 16) midiChannels[trackToEdit] = 1;
}
break;
case UI_EDIT_TEMPO:
tempo += delta;
if (tempo < 40) tempo = 40;
if (tempo > 240) tempo = 240;
break;
case UI_EDIT_FLAVOUR:
{
int trackToEdit = playMode == MODE_POLY ? randomizeTrack : 0;
currentStrategyIndices[trackToEdit] += (delta > 0 ? 1 : -1);
if (currentStrategyIndices[trackToEdit] < 0) currentStrategyIndices[trackToEdit] = numStrategies - 1;
if (currentStrategyIndices[trackToEdit] >= numStrategies) currentStrategyIndices[trackToEdit] = 0;
}
break;
case UI_SETUP_PLAYMODE_EDIT:
playMode = (playMode == MODE_MONO) ? MODE_POLY : MODE_MONO;
break;
}
if (currentState == UI_RANDOMIZE_TRACK_EDIT) {
randomizeTrack += (delta > 0 ? 1 : -1);
if (randomizeTrack < 0) randomizeTrack = NUM_TRACKS - 1;
if (randomizeTrack >= NUM_TRACKS) randomizeTrack = 0;
}
}
// Handle Button
int reading = digitalRead(ENC_SW);
if (reading != lastButtonState) {
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > 50) {
if (reading == LOW && !buttonActive) {
// Button Pressed
buttonActive = true;
buttonPressTime = millis();
buttonConsumed = false;
Serial.println(F("Button Down"));
}
if (reading == HIGH && buttonActive) {
// Button Released
buttonActive = false;
if (!buttonConsumed) { // Short press action
switch(currentState) {
case UI_MENU_MAIN:
if (menuSelection == 0) { currentState = UI_MENU_RANDOMIZE; menuSelection = 0; break; }
if (menuSelection == 1) { currentState = UI_MENU_SETUP; menuSelection = 0; break; }
break;
case UI_MENU_RANDOMIZE:
{
int track_offset = (playMode == MODE_POLY) ? 2 : 0;
int theme_1_index = (playMode == MODE_POLY) ? THEME_1_INDEX_POLY : THEME_1_INDEX_MONO;
if (menuSelection == 0) { currentState = UI_MENU_SETUP; menuSelection = 0; break; }
if (playMode == MODE_POLY) {
if (menuSelection == 1) { currentState = UI_RANDOMIZE_TRACK_EDIT; break; }
if (menuSelection == 2) { trackMute[randomizeTrack] = !trackMute[randomizeTrack]; break; }
}
if (menuSelection == 1 + track_offset) { // Melody
int track = playMode == MODE_POLY ? randomizeTrack : 0;
midi.lock();
melodySeeds[track] = random(10000);
if (isPlaying) {
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
if (!sequenceChangeScheduled) {
memcpy(nextSequence, sequence, sizeof(sequence));
}
generateTrackData(track, theme, nextSequence);
sequenceChangeScheduled = true;
}
midi.unlock();
saveSequence(true);
break;
}
if (menuSelection == 2 + track_offset) { currentState = UI_EDIT_FLAVOUR; break; } // Flavour
if (menuSelection == 3 + track_offset) { // Scale
generateRandomScale();
if (isPlaying) {
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
midi.lock();
// Regenerate all tracks with new scale
generateSequenceData(theme, nextSequence);
sequenceChangeScheduled = true;
midi.unlock();
}
saveSequence(true);
break;
}
if (menuSelection == 4 + track_offset) { currentState = UI_EDIT_TEMPO; break; }
if (menuSelection == 5 + track_offset) { mutationEnabled = !mutationEnabled; break; }
if (menuSelection == 6 + track_offset) {
songModeEnabled = !songModeEnabled;
if (songModeEnabled) {
songModeNeedsNext = true;
}
break;
}
if (menuSelection >= theme_1_index) { // Themes
const int selectedTheme = menuSelection - theme_1_index + 1;
if (isPlaying) {
queuedTheme = selectedTheme;
midi.lock();
generateSequenceData(queuedTheme, nextSequence);
sequenceChangeScheduled = true;
midi.unlock();
} else {
generateTheme(selectedTheme);
}
break;
}
}
break;
case UI_MENU_SETUP:
if (menuSelection == 0) { currentState = UI_MENU_RANDOMIZE; menuSelection = 0; break; }
if (menuSelection == 1) { currentState = UI_SETUP_PLAYMODE_EDIT; break; }
if (menuSelection == 2) { currentState = UI_SETUP_CHANNEL_EDIT; break; }
if (menuSelection == 3) { factoryReset(); break; }
break;
case UI_SETUP_CHANNEL_EDIT:
currentState = UI_MENU_SETUP;
saveSequence(true);
break;
case UI_EDIT_TEMPO:
currentState = UI_MENU_RANDOMIZE;
saveSequence(true);
break;
case UI_EDIT_FLAVOUR:
currentState = UI_MENU_RANDOMIZE;
if (isPlaying) {
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
int track = playMode == MODE_POLY ? randomizeTrack : 0;
midi.lock();
if (!sequenceChangeScheduled) {
memcpy(nextSequence, sequence, sizeof(sequence));
}
generateTrackData(track, theme, nextSequence);
sequenceChangeScheduled = true;
midi.unlock();
}
saveSequence(true);
break;
case UI_SETUP_PLAYMODE_EDIT:
currentState = UI_MENU_SETUP;
saveSequence(true);
break;
case UI_RANDOMIZE_TRACK_EDIT:
currentState = UI_MENU_RANDOMIZE;
saveSequence(true);
break;
}
}
}
}
// Check for Long Press (Start/Stop Playback)
if (buttonActive && !buttonConsumed && (millis() - buttonPressTime > 600)) {
isPlaying = !isPlaying;
buttonConsumed = true; // Prevent short press action
Serial.print(F("Playback: ")); Serial.println(isPlaying ? F("ON") : F("OFF"));
if (isPlaying) {
playbackStep = 0;
clockCount = 0;
lastClockTime = micros();
} else {
queuedTheme = -1;
}
}
lastButtonState = reading;
}
static void drawUI() {
const char **randMenu;
int randMenuCount;
int themeIndex;
// Make local copies of shared data inside a critical section
// to avoid holding the lock during slow display operations.
UIState local_currentState;
PlayMode local_playMode;
int local_menuSelection, local_randomizeTrack, local_tempo, local_currentThemeIndex, local_queuedTheme, local_numScaleNotes;
int local_melodySeed;
bool local_mutationEnabled, local_songModeEnabled, local_isPlaying;
bool local_trackMute[NUM_TRACKS];
int local_midiChannel;
MelodyStrategy* local_strategy;
int local_playbackStep;
int local_scaleNotes[12];
midi.lock();
local_playMode = playMode;
local_randomizeTrack = randomizeTrack;
int ui_track = (local_playMode == MODE_POLY) ? local_randomizeTrack : 0;
if (local_playMode == MODE_POLY) {
randMenu = randomizeMenuPoly;
randMenuCount = randomizeMenuPolyCount;
themeIndex = THEME_1_INDEX_POLY;
} else {
randMenu = randomizeMenuMono;
randMenuCount = randomizeMenuMonoCount;
themeIndex = THEME_1_INDEX_MONO;
}
local_currentState = currentState;
local_menuSelection = menuSelection;
local_midiChannel = midiChannels[ui_track];
local_tempo = tempo;
local_strategy = strategies[currentStrategyIndices[ui_track]];
local_queuedTheme = queuedTheme;
local_currentThemeIndex = currentThemeIndex;
local_numScaleNotes = numScaleNotes;
memcpy(local_scaleNotes, scaleNotes, sizeof(local_scaleNotes));
local_melodySeed = melodySeeds[ui_track];
local_mutationEnabled = mutationEnabled;
local_songModeEnabled = songModeEnabled;
memcpy(local_sequence, sequence, sizeof(local_sequence));
local_playbackStep = playbackStep;
local_isPlaying = isPlaying;
memcpy(local_trackMute, (const void*)trackMute, sizeof(local_trackMute));
midi.unlock();
ui.draw(local_currentState, local_menuSelection,
local_midiChannel, local_tempo, local_strategy,
local_queuedTheme, local_currentThemeIndex, local_numScaleNotes, local_scaleNotes, local_melodySeed,
local_mutationEnabled, local_songModeEnabled, (const Step (*)[NUM_STEPS])local_sequence, local_playbackStep, local_isPlaying,
mainMenu, mainMenuCount, randMenu, randMenuCount, setupMenu, setupMenuCount,
themeIndex, local_playMode, local_randomizeTrack, (const bool*)local_trackMute);
}
static void updateLeds() {
// Make local copies of shared data inside a critical section
// to avoid holding the lock during slow LED update operations.
int local_playbackStep;
bool local_isPlaying;
UIState local_currentState;
bool local_songModeEnabled;
int local_songRepeatsRemaining;
bool local_sequenceChangeScheduled;
PlayMode local_playMode;
int local_numScaleNotes;
int local_scaleNotes[12];
bool local_trackMute[NUM_TRACKS];
midi.lock();
memcpy(local_sequence, sequence, sizeof(local_sequence));
local_playbackStep = playbackStep;
local_isPlaying = isPlaying;
local_currentState = currentState;
local_songModeEnabled = songModeEnabled;
local_songRepeatsRemaining = songRepeatsRemaining;
local_sequenceChangeScheduled = sequenceChangeScheduled;
local_playMode = playMode;
local_numScaleNotes = numScaleNotes;
memcpy(local_scaleNotes, scaleNotes, sizeof(local_scaleNotes));
memcpy(local_trackMute, (const void*)trackMute, sizeof(local_trackMute));
midi.unlock();
ui.updateLeds((const Step (*)[NUM_STEPS])local_sequence, local_playbackStep, local_isPlaying,
local_currentState, local_songModeEnabled, local_songRepeatsRemaining,
local_sequenceChangeScheduled, local_playMode, local_numScaleNotes,
local_scaleNotes, (const bool*)local_trackMute);
}
void loopUI() {
// Handle Song Mode Generation in UI Thread
if (songModeNeedsNext) {
int nextTheme = random(1, 8); // Themes 1-7
int repeats = random(1, 9); // 1-8 repeats
generateSequenceData(nextTheme, nextSequence);
queuedTheme = nextTheme;
nextSongRepeats = repeats;
sequenceChangeScheduled = true;
songModeNeedsNext = false;
}
handleInput();
drawUI();
updateLeds();
delay(10); // Small delay to prevent screen tearing/excessive refresh
}

14
UIThread.h Normal file
View File

@ -0,0 +1,14 @@
#ifndef UI_THREAD_H
#define UI_THREAD_H
#include "TrackerTypes.h"
void loopUI();
void mutateSequence(Step (*target)[NUM_STEPS]);
void generateRandomScale();
void generateTheme(int themeType);
bool loadSequence();
void factoryReset();
void saveSequence(bool quiet = false);
#endif

22
config.h Normal file
View File

@ -0,0 +1,22 @@
#ifndef CONFIG_H
#define CONFIG_H
// --- HARDWARE CONFIGURATION ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C // See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
// Pin Definitions for Raspberry Pi Pico (RP2040)
#define PIN_SDA 4
#define PIN_SCL 5
#define ENC_CLK 12
#define ENC_DT 13
#define ENC_SW 14
// --- TRACKER DATA ---
#define NUM_STEPS 16
#define NUM_TRACKS 4
#endif