Sequence mutation

This commit is contained in:
Dejvino 2026-02-16 22:55:09 +01:00
parent 5f7f241e82
commit 46d0ca5250

View File

@ -60,7 +60,7 @@ UIState currentState = UI_MENU_MAIN;
const char* mainMenu[] = { "Tracker", "Randomize", "Setup" }; const char* mainMenu[] = { "Tracker", "Randomize", "Setup" };
const int mainMenuCount = sizeof(mainMenu) / sizeof(char*); const int mainMenuCount = sizeof(mainMenu) / sizeof(char*);
const char* randomizeMenu[] = { "Back", "Scale", "Melody", "Theme 1", "Theme 2", "Theme 3", "Theme 4", "Theme 5", "Theme 6", "Theme 7" }; const char* randomizeMenu[] = { "Back", "Scale", "Melody", "Mutation", "Song Mode", "Theme 1", "Theme 2", "Theme 3", "Theme 4", "Theme 5", "Theme 6", "Theme 7" };
const int randomizeMenuCount = sizeof(randomizeMenu) / sizeof(char*); const int randomizeMenuCount = sizeof(randomizeMenu) / sizeof(char*);
const char* setupMenu[] = { "Back", "Channel", "Tempo", "Save", "Load" }; const char* setupMenu[] = { "Back", "Channel", "Tempo", "Save", "Load" };
const int setupMenuCount = sizeof(setupMenu) / sizeof(char*); const int setupMenuCount = sizeof(setupMenu) / sizeof(char*);
@ -73,8 +73,14 @@ int scaleNotes[12];
int numScaleNotes = 0; int numScaleNotes = 0;
int melodySeed = 0; int melodySeed = 0;
volatile int queuedTheme = -1; volatile int queuedTheme = -1;
volatile int currentThemeIndex = 1;
const uint32_t EEPROM_MAGIC = 0x42424244; const uint32_t EEPROM_MAGIC = 0x42424244;
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 bool isEditing = false;
volatile int scrollOffset = 0; volatile int scrollOffset = 0;
volatile bool isPlaying = false; volatile bool isPlaying = false;
@ -275,13 +281,34 @@ void generateSequenceData(int themeType, Step* target) {
} }
void generateTheme(int themeType) { void generateTheme(int themeType) {
currentThemeIndex = themeType;
needsPanic = true; needsPanic = true;
mutex_enter_blocking(&midiMutex); mutex_enter_blocking(&midiMutex);
generateSequenceData(themeType, sequence); generateSequenceData(themeType, sequence);
mutex_exit(&midiMutex); mutex_exit(&midiMutex);
clockCount = 0;
lastClockTime = micros();
playbackStep = 0;
sendMidiRealtime(0xFA); // MIDI Start
isPlaying = true; isPlaying = true;
} }
void mutateSequence(Step* target) {
// Mutate 1 or 2 steps
int count = random(1, 3);
for (int i = 0; i < count; i++) {
int s = random(NUM_STEPS);
if (target[s].note != -1) {
int r = random(100);
if (r < 30) target[s].accent = !target[s].accent;
else if (r < 60) target[s].tie = !target[s].tie;
else if (r < 80) target[s].note += 12; // Up octave
else target[s].note -= 12; // Down octave
}
}
}
void handleInput() { void handleInput() {
// Handle Encoder Rotation // Handle Encoder Rotation
int delta = 0; int delta = 0;
@ -377,13 +404,47 @@ void handleInput() {
break; break;
case UI_MENU_RANDOMIZE: case UI_MENU_RANDOMIZE:
if (menuSelection == 0) { currentState = UI_MENU_MAIN; menuSelection = 1; } if (menuSelection == 0) { currentState = UI_MENU_MAIN; menuSelection = 1; }
if (menuSelection == 1) generateRandomScale(); // Scale if (menuSelection == 1) {
if (menuSelection == 2) melodySeed = random(10000); // Melody generateRandomScale(); // Scale
if (menuSelection >= 3) { // Themes if (isPlaying) {
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
mutex_enter_blocking(&midiMutex);
generateSequenceData(theme, nextSequence);
nextSequenceReady = true;
mutex_exit(&midiMutex);
}
}
if (menuSelection == 2) {
melodySeed = random(10000); // Melody
if (isPlaying) {
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
mutex_enter_blocking(&midiMutex);
generateSequenceData(theme, nextSequence);
nextSequenceReady = true;
mutex_exit(&midiMutex);
}
}
if (menuSelection == 3) mutationEnabled = !mutationEnabled;
if (menuSelection == 4) {
songModeEnabled = !songModeEnabled;
if (songModeEnabled) {
// Start Song Mode immediately
if (isPlaying) {
songModeNeedsNext = true;
} else {
// If stopped, just generate a start
songModeNeedsNext = true;
// We rely on the loop() to pick it up, or we can force it here if not playing
}
}
}
if (menuSelection >= 5) { // Themes
if (isPlaying) { if (isPlaying) {
queuedTheme = menuSelection - 2; queuedTheme = menuSelection - 2;
mutex_enter_blocking(&midiMutex);
generateSequenceData(queuedTheme, nextSequence); generateSequenceData(queuedTheme, nextSequence);
nextSequenceReady = true; nextSequenceReady = true;
mutex_exit(&midiMutex);
} else { } else {
generateTheme(menuSelection - 2); generateTheme(menuSelection - 2);
} }
@ -421,7 +482,7 @@ void handleInput() {
buttonConsumed = true; // Prevent short press action buttonConsumed = true; // Prevent short press action
Serial.print(F("Playback: ")); Serial.println(isPlaying ? F("ON") : F("OFF")); Serial.print(F("Playback: ")); Serial.println(isPlaying ? F("ON") : F("OFF"));
if (isPlaying) { if (isPlaying) {
playbackStep = navigationSelection > 0 ? navigationSelection - 1 : 0; playbackStep = 0;
clockCount = 0; clockCount = 0;
lastClockTime = micros(); lastClockTime = micros();
sendMidiRealtime(0xFA); // MIDI Start sendMidiRealtime(0xFA); // MIDI Start
@ -469,11 +530,30 @@ void handlePlayback() {
playbackStep++; playbackStep++;
if (playbackStep >= NUM_STEPS) { if (playbackStep >= NUM_STEPS) {
playbackStep = 0; playbackStep = 0;
// Mutation
if (mutationEnabled && !nextSequenceReady) {
memcpy(nextSequence, sequence, sizeof(sequence));
mutateSequence(nextSequence);
nextSequenceReady = true;
}
// Song Mode Logic
if (songModeEnabled && songRepeatsRemaining > 0) {
songRepeatsRemaining--;
}
if (nextSequenceReady) { if (nextSequenceReady) {
sendMidi(0xB0, 123, 0); // Panic / All Notes Off sendMidi(0xB0, 123, 0); // Panic / All Notes Off
memcpy(sequence, nextSequence, sizeof(sequence)); memcpy(sequence, nextSequence, sizeof(sequence));
nextSequenceReady = false; nextSequenceReady = false;
queuedTheme = -1; if (queuedTheme != -1) {
currentThemeIndex = queuedTheme;
queuedTheme = -1;
}
if (songModeEnabled) {
songRepeatsRemaining = nextSongRepeats;
}
} }
} }
@ -488,6 +568,11 @@ void handlePlayback() {
sendMidi(0x80, prevNote, 0); sendMidi(0x80, prevNote, 0);
} }
// Trigger next song segment generation if we are on the last repeat
if (songModeEnabled && songRepeatsRemaining == 1 && !nextSequenceReady && !songModeNeedsNext) {
songModeNeedsNext = true;
}
mutex_exit(&midiMutex); mutex_exit(&midiMutex);
} }
} }
@ -527,10 +612,15 @@ void drawMenu(const char* title, const char* items[], int count, int selection)
} }
// Special case for queued theme // Special case for queued theme
if (currentState == UI_MENU_RANDOMIZE && i >= 3 && queuedTheme == (i - 2)) { if (currentState == UI_MENU_RANDOMIZE && i >= 5 && queuedTheme == (i - 4)) {
display.print(F(" [NEXT]")); display.print(F(" [NEXT]"));
} }
// Special case for active theme
if (currentState == UI_MENU_RANDOMIZE && i >= 5 && currentThemeIndex == (i - 4)) {
display.print(F(" *"));
}
// Special cases for Randomize Menu values // Special cases for Randomize Menu values
if (currentState == UI_MENU_RANDOMIZE) { if (currentState == UI_MENU_RANDOMIZE) {
if (i == 1) { // Scale if (i == 1) { // Scale
@ -545,6 +635,12 @@ void drawMenu(const char* title, const char* items[], int count, int selection)
} else if (i == 2) { // Melody } else if (i == 2) { // Melody
display.print(F(": ")); display.print(F(": "));
display.print(melodySeed); display.print(melodySeed);
} else if (i == 3) { // Mutation
display.print(F(": "));
display.print(mutationEnabled ? F("ON") : F("OFF"));
} else if (i == 4) { // Song Mode
display.print(F(": "));
display.print(songModeEnabled ? F("ON") : F("OFF"));
} }
} }
y += 9; y += 9;
@ -679,7 +775,7 @@ int getPixelIndex(int x, int y) {
return y * 8 + x; return y * 8 + x;
} }
uint32_t getNoteColor(int note) { uint32_t getNoteColor(int note, bool dim) {
if (note == -1) return 0; if (note == -1) return 0;
// Map note to hue, avoiding Green (approx 21845) which is used for playback cursor. // Map note to hue, avoiding Green (approx 21845) which is used for playback cursor.
// We start from Cyan (~30000) and go up to Orange (~8000 wrapped). // We start from Cyan (~30000) and go up to Orange (~8000 wrapped).
@ -687,7 +783,7 @@ uint32_t getNoteColor(int note) {
// Step per semitone: 43536 / 12 = 3628. // Step per semitone: 43536 / 12 = 3628.
// This ensures notes are distinct colors but never pure Green. // This ensures notes are distinct colors but never pure Green.
uint16_t hue = 30000 + (note % 12) * 3628; uint16_t hue = 30000 + (note % 12) * 3628;
return Adafruit_NeoPixel::ColorHSV(hue, 255, 50); return Adafruit_NeoPixel::ColorHSV(hue, 255, dim ? 10 : 50);
} }
void updateLeds() { void updateLeds() {
@ -695,45 +791,88 @@ void updateLeds() {
mutex_enter_blocking(&midiMutex); mutex_enter_blocking(&midiMutex);
for (int s = 0; s < NUM_STEPS; s++) { for (int s = 0; s < NUM_STEPS; s++) {
int blockX = (s % 4) * 2; int x = s % 8;
int blockY = (s / 4) * 2; int yBase = (s / 8) * 4;
// --- Top Row: Attributes --- uint32_t color = 0;
uint32_t colorTL = 0; // Octave uint32_t dimColor = 0;
uint32_t colorTR = 0; // Accent/Tie if (sequence[s].note != -1) {
color = getNoteColor(sequence[s].note, sequence[s].tie);
dimColor = getNoteColor(sequence[s].note, true);
}
uint32_t colorP0 = 0;
uint32_t colorP1 = 0;
uint32_t colorP2 = 0;
uint32_t colorP3 = 0;
if (sequence[s].note != -1) { if (sequence[s].note != -1) {
int octave = sequence[s].note / 12; int octave = sequence[s].note / 12;
// Base octave 4 (MIDI 48-59).
if (octave > 4) colorTL = pixels.Color(0, 50, 0); // Up (Green)
else if (octave < 4) colorTL = pixels.Color(50, 0, 0); // Down (Red)
if (sequence[s].accent && sequence[s].tie) colorTR = pixels.Color(50, 0, 50); // Both (Purple) if (octave > 4) {
else if (sequence[s].accent) colorTR = pixels.Color(50, 50, 50); // Accent (White) // Octave Up -> Top Row (P0)
else if (sequence[s].tie) colorTR = pixels.Color(50, 50, 0); // Tie (Yellow) colorP0 = color;
} if (sequence[s].accent) colorP1 = dimColor;
} else if (octave < 4) {
// --- Bottom Row: Note / Cursor --- // Octave Down -> Bottom Row (P2)
uint32_t colorBL = 0; colorP2 = color;
uint32_t colorBR = 0; if (sequence[s].accent) colorP1 = dimColor;
} else {
if (sequence[s].note != -1) { // Normal -> Middle Row (P1)
uint32_t noteColor = getNoteColor(sequence[s].note); colorP1 = color;
colorBL = noteColor; if (sequence[s].accent) {
colorBR = noteColor; colorP0 = dimColor;
colorP2 = dimColor;
}
}
} }
int stepNavIndex = navigationSelection - 1; int stepNavIndex = navigationSelection - 1;
if (isPlaying && s == playbackStep) { uint32_t cursorColor = pixels.Color(0 + (s%2) * 20, 0 + (s%2) * 20, 50 + (s%2) * 20); // Blue for paused
colorBL = colorBR = pixels.Color(0, 50, 0); // Green for playback if (isPlaying) {
} else if (currentState == UI_TRACKER && s == stepNavIndex) { cursorColor = pixels.Color(0, 50, 0); // Green for playback
colorBL = colorBR = isEditing ? pixels.Color(50, 0, 0) : pixels.Color(40, 40, 40); if (songModeEnabled) {
// Song Mode: Show repeats on bottom row
// Right aligned.
// If repeats = 1 (last one), blink yellow.
if (s >= NUM_STEPS/2) { // second half = bottom row
int col = x;
int repeatsToDraw = min(songRepeatsRemaining, 8);
if (col >= (8 - repeatsToDraw)) {
if (songRepeatsRemaining == 1 && col == 7 && (millis() / 250) % 2) {
cursorColor = pixels.Color(0, 250, 0); // Max green, change is about to happen
} else {
cursorColor = pixels.Color(80, 100, 0); // Yellow-green, remaining repeat
}
}
}
}
} else if (currentState == UI_TRACKER) {
cursorColor = isEditing ? pixels.Color(50, 0, 0) : pixels.Color(40, 40, 40);
} }
pixels.setPixelColor(getPixelIndex(blockX, blockY), colorTL); if (cursorColor != 0) {
pixels.setPixelColor(getPixelIndex(blockX + 1, blockY), colorTR); bool isCursorHere = (isPlaying && s == playbackStep) ||
pixels.setPixelColor(getPixelIndex(blockX, blockY + 1), colorBL); (!isPlaying && currentState == UI_TRACKER && s == stepNavIndex);
pixels.setPixelColor(getPixelIndex(blockX + 1, blockY + 1), colorBR); if (isCursorHere) {
colorP3 = cursorColor;
} else {
// Lightly colored background for cursor row
uint8_t r = (uint8_t)(cursorColor >> 16);
uint8_t g = (uint8_t)(cursorColor >> 8);
uint8_t b = (uint8_t)cursorColor;
colorP3 = pixels.Color(r/5, g/5, b/5);
}
}
pixels.setPixelColor(getPixelIndex(x, yBase), colorP0);
pixels.setPixelColor(getPixelIndex(x, yBase + 1), colorP1);
pixels.setPixelColor(getPixelIndex(x, yBase + 2), colorP2);
pixels.setPixelColor(getPixelIndex(x, yBase + 3), colorP3);
}
if (nextSequenceReady && (millis() / 125) % 2) {
pixels.setPixelColor(NUM_PIXELS - 1, pixels.Color(127, 50, 0));
} }
mutex_exit(&midiMutex); mutex_exit(&midiMutex);
@ -742,13 +881,30 @@ void updateLeds() {
void loop1() { void loop1() {
if (needsPanic) { if (needsPanic) {
mutex_enter_blocking(&midiMutex);
sendMidi(0xB0, 123, 0); sendMidi(0xB0, 123, 0);
mutex_exit(&midiMutex);
needsPanic = false; needsPanic = false;
} }
handlePlayback(); handlePlayback();
} }
void loop() { 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
mutex_enter_blocking(&midiMutex);
generateSequenceData(nextTheme, nextSequence);
queuedTheme = nextTheme;
nextSongRepeats = repeats;
nextSequenceReady = true;
mutex_exit(&midiMutex);
songModeNeedsNext = false;
}
handleInput(); handleInput();
drawUI(); drawUI();
updateLeds(); updateLeds();