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 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 char* setupMenu[] = { "Back", "Channel", "Tempo", "Save", "Load" };
const int setupMenuCount = sizeof(setupMenu) / sizeof(char*);
@ -73,8 +73,14 @@ int scaleNotes[12];
int numScaleNotes = 0;
int melodySeed = 0;
volatile int queuedTheme = -1;
volatile int currentThemeIndex = 1;
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 int scrollOffset = 0;
volatile bool isPlaying = false;
@ -275,13 +281,34 @@ void generateSequenceData(int themeType, Step* target) {
}
void generateTheme(int themeType) {
currentThemeIndex = themeType;
needsPanic = true;
mutex_enter_blocking(&midiMutex);
generateSequenceData(themeType, sequence);
mutex_exit(&midiMutex);
clockCount = 0;
lastClockTime = micros();
playbackStep = 0;
sendMidiRealtime(0xFA); // MIDI Start
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() {
// Handle Encoder Rotation
int delta = 0;
@ -377,13 +404,47 @@ void handleInput() {
break;
case UI_MENU_RANDOMIZE:
if (menuSelection == 0) { currentState = UI_MENU_MAIN; menuSelection = 1; }
if (menuSelection == 1) generateRandomScale(); // Scale
if (menuSelection == 2) melodySeed = random(10000); // Melody
if (menuSelection >= 3) { // Themes
if (menuSelection == 1) {
generateRandomScale(); // Scale
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) {
queuedTheme = menuSelection - 2;
mutex_enter_blocking(&midiMutex);
generateSequenceData(queuedTheme, nextSequence);
nextSequenceReady = true;
mutex_exit(&midiMutex);
} else {
generateTheme(menuSelection - 2);
}
@ -421,7 +482,7 @@ void handleInput() {
buttonConsumed = true; // Prevent short press action
Serial.print(F("Playback: ")); Serial.println(isPlaying ? F("ON") : F("OFF"));
if (isPlaying) {
playbackStep = navigationSelection > 0 ? navigationSelection - 1 : 0;
playbackStep = 0;
clockCount = 0;
lastClockTime = micros();
sendMidiRealtime(0xFA); // MIDI Start
@ -469,12 +530,31 @@ void handlePlayback() {
playbackStep++;
if (playbackStep >= NUM_STEPS) {
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) {
sendMidi(0xB0, 123, 0); // Panic / All Notes Off
memcpy(sequence, nextSequence, sizeof(sequence));
nextSequenceReady = false;
if (queuedTheme != -1) {
currentThemeIndex = queuedTheme;
queuedTheme = -1;
}
if (songModeEnabled) {
songRepeatsRemaining = nextSongRepeats;
}
}
}
// Note On for new step
@ -488,6 +568,11 @@ void handlePlayback() {
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);
}
}
@ -527,10 +612,15 @@ void drawMenu(const char* title, const char* items[], int count, int selection)
}
// 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]"));
}
// Special case for active theme
if (currentState == UI_MENU_RANDOMIZE && i >= 5 && currentThemeIndex == (i - 4)) {
display.print(F(" *"));
}
// Special cases for Randomize Menu values
if (currentState == UI_MENU_RANDOMIZE) {
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
display.print(F(": "));
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;
@ -679,7 +775,7 @@ int getPixelIndex(int x, int y) {
return y * 8 + x;
}
uint32_t getNoteColor(int note) {
uint32_t getNoteColor(int note, bool dim) {
if (note == -1) return 0;
// 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).
@ -687,7 +783,7 @@ uint32_t getNoteColor(int note) {
// Step per semitone: 43536 / 12 = 3628.
// This ensures notes are distinct colors but never pure Green.
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() {
@ -695,45 +791,88 @@ void updateLeds() {
mutex_enter_blocking(&midiMutex);
for (int s = 0; s < NUM_STEPS; s++) {
int blockX = (s % 4) * 2;
int blockY = (s / 4) * 2;
int x = s % 8;
int yBase = (s / 8) * 4;
// --- Top Row: Attributes ---
uint32_t colorTL = 0; // Octave
uint32_t colorTR = 0; // Accent/Tie
uint32_t color = 0;
uint32_t dimColor = 0;
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) {
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)
else if (sequence[s].accent) colorTR = pixels.Color(50, 50, 50); // Accent (White)
else if (sequence[s].tie) colorTR = pixels.Color(50, 50, 0); // Tie (Yellow)
if (octave > 4) {
// Octave Up -> Top Row (P0)
colorP0 = color;
if (sequence[s].accent) colorP1 = dimColor;
} else if (octave < 4) {
// Octave Down -> Bottom Row (P2)
colorP2 = color;
if (sequence[s].accent) colorP1 = dimColor;
} else {
// Normal -> Middle Row (P1)
colorP1 = color;
if (sequence[s].accent) {
colorP0 = dimColor;
colorP2 = dimColor;
}
}
// --- Bottom Row: Note / Cursor ---
uint32_t colorBL = 0;
uint32_t colorBR = 0;
if (sequence[s].note != -1) {
uint32_t noteColor = getNoteColor(sequence[s].note);
colorBL = noteColor;
colorBR = noteColor;
}
int stepNavIndex = navigationSelection - 1;
if (isPlaying && s == playbackStep) {
colorBL = colorBR = pixels.Color(0, 50, 0); // Green for playback
} else if (currentState == UI_TRACKER && s == stepNavIndex) {
colorBL = colorBR = isEditing ? pixels.Color(50, 0, 0) : pixels.Color(40, 40, 40);
uint32_t cursorColor = pixels.Color(0 + (s%2) * 20, 0 + (s%2) * 20, 50 + (s%2) * 20); // Blue for paused
if (isPlaying) {
cursorColor = pixels.Color(0, 50, 0); // Green for playback
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);
pixels.setPixelColor(getPixelIndex(blockX + 1, blockY), colorTR);
pixels.setPixelColor(getPixelIndex(blockX, blockY + 1), colorBL);
pixels.setPixelColor(getPixelIndex(blockX + 1, blockY + 1), colorBR);
if (cursorColor != 0) {
bool isCursorHere = (isPlaying && s == playbackStep) ||
(!isPlaying && currentState == UI_TRACKER && s == stepNavIndex);
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);
@ -742,13 +881,30 @@ void updateLeds() {
void loop1() {
if (needsPanic) {
mutex_enter_blocking(&midiMutex);
sendMidi(0xB0, 123, 0);
mutex_exit(&midiMutex);
needsPanic = false;
}
handlePlayback();
}
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();
drawUI();
updateLeds();