diff --git a/RP2040_Tracker.ino b/RP2040_Tracker.ino index 17d6f2e..7f8adff 100644 --- a/RP2040_Tracker.ino +++ b/RP2040_Tracker.ino @@ -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,11 +530,30 @@ 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; - queuedTheme = -1; + if (queuedTheme != -1) { + currentThemeIndex = queuedTheme; + queuedTheme = -1; + } + if (songModeEnabled) { + songRepeatsRemaining = nextSongRepeats; + } } } @@ -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) - } - - // --- 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; + 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; + } + } } 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();