From 5a773d31a40b494ff3f457d66f7831e66b5d7f6a Mon Sep 17 00:00:00 2001 From: Dejvino Date: Mon, 16 Feb 2026 21:04:25 +0100 Subject: [PATCH] Accents and ties with saving --- RP2040_Tracker.ino | 227 ++++++++++++++++++++++++++++++++------------- 1 file changed, 160 insertions(+), 67 deletions(-) diff --git a/RP2040_Tracker.ino b/RP2040_Tracker.ino index 2616e76..ac1a189 100644 --- a/RP2040_Tracker.ino +++ b/RP2040_Tracker.ino @@ -3,6 +3,7 @@ #include #include #include +#include // --- HARDWARE CONFIGURATION --- #define SCREEN_WIDTH 128 @@ -30,6 +31,8 @@ struct Step { int8_t note; // MIDI Note (0-127), -1 for OFF + bool accent; + bool tie; }; Step sequence[NUM_STEPS]; @@ -50,9 +53,9 @@ UIState currentState = UI_TRACKER; const char* mainMenu[] = { "Tracker", "Randomize", "Setup" }; const int mainMenuCount = sizeof(mainMenu) / sizeof(char*); -const char* randomizeMenu[] = { "Back", "Gen Scale", "Theme 1", "Theme 2", "Theme 3", "Theme 4", "Theme 5", "Theme 6", "Theme 7" }; +const char* randomizeMenu[] = { "Back", "Scale", "Melody", "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" }; +const char* setupMenu[] = { "Back", "Channel", "Save", "Load" }; const int setupMenuCount = sizeof(setupMenu) / sizeof(char*); int menuSelection = 0; @@ -61,7 +64,9 @@ int playbackStep = 0; int midiChannel = 1; int scaleNotes[12]; int numScaleNotes = 0; +int melodySeed = 0; int queuedTheme = -1; +const uint32_t EEPROM_MAGIC = 0x42424244; bool isEditing = false; int scrollOffset = 0; @@ -113,6 +118,55 @@ void sortArray(int arr[], int size) { } } +void showMessage(const char* msg) { + display.clearDisplay(); + display.setCursor(10, 25); + display.setTextColor(SSD1306_WHITE); + display.setTextSize(2); + display.print(msg); + display.display(); + delay(500); + display.setTextSize(1); +} + +void saveSequence() { + 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, numScaleNotes); addr += sizeof(numScaleNotes); + for (int i=0; i<12; i++) { + EEPROM.put(addr, scaleNotes[i]); addr += sizeof(int); + } + + for (int i=0; i= 2) { + if (menuSelection == 1) generateRandomScale(); // Scale + if (menuSelection == 2) melodySeed = random(10000); // Melody + if (menuSelection >= 3) { // Themes if (isPlaying) { - queuedTheme = menuSelection - 1; + queuedTheme = menuSelection - 2; } else { - generateTheme(menuSelection - 1); + generateTheme(menuSelection - 2); } } 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) { + saveSequence(); + currentState = UI_MENU_MAIN; + } + if (menuSelection == 3) { + if (loadSequence()) showMessage("LOADED!"); + currentState = UI_MENU_MAIN; + } break; case UI_SETUP_CHANNEL_EDIT: currentState = UI_MENU_SETUP; @@ -342,9 +402,16 @@ void handlePlayback() { if (millis() - lastStepTime > interval) { lastStepTime = millis(); - // Note Off for previous step - if (sequence[playbackStep].note != -1) { - sendMidi(0x80, sequence[playbackStep].note, 0); + // 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) { + sendMidi(0x80, prevNote, 0); } playbackStep++; @@ -358,7 +425,13 @@ void handlePlayback() { // Note On for new step if (sequence[playbackStep].note != -1) { - sendMidi(0x90, sequence[playbackStep].note, 100); + uint8_t velocity = sequence[playbackStep].accent ? 127 : 100; + sendMidi(0x90, sequence[playbackStep].note, velocity); + } + + // Note Off for previous step (if tied - delayed Note Off) + if (isTied && prevNote != -1) { + sendMidi(0x80, prevNote, 0); } // Auto-scroll navigation cursor if not editing @@ -374,8 +447,16 @@ void drawMenu(const char* title, const char* items[], int count, int selection) display.println(title); display.drawLine(0, 8, 128, 8, SSD1306_WHITE); + // Simple scrolling: keep selection visible + int start = 0; + if (selection >= 5) { + start = selection - 4; + } + int y = 10; - for (int i = 0; i < count; i++) { + for (int i = start; i < count; i++) { + if (y > 55) break; // Stop if we run out of screen space + if (i == selection) { display.fillRect(0, y, 128, 8, SSD1306_WHITE); display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); @@ -392,27 +473,27 @@ void drawMenu(const char* title, const char* items[], int count, int selection) } // Special case for queued theme - if (currentState == UI_MENU_RANDOMIZE && i >= 2 && queuedTheme == (i - 1)) { + if (currentState == UI_MENU_RANDOMIZE && i >= 3 && queuedTheme == (i - 2)) { display.print(F(" [NEXT]")); } - y += 9; - - // Special case for scale display - if (currentState == UI_MENU_RANDOMIZE && i == 1) { // After "Gen Scale" - display.setTextColor(SSD1306_WHITE); // Ensure it's not highlighted - display.setCursor(2, y); - if (numScaleNotes > 0) { - const char* noteNames[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}; - for (int j = 0; j < numScaleNotes; j++) { - display.print(noteNames[scaleNotes[j]]); - if (j < numScaleNotes - 1) display.print(F(" ")); + // Special cases for Randomize Menu values + if (currentState == UI_MENU_RANDOMIZE) { + if (i == 1) { // Scale + display.print(F(": ")); + if (numScaleNotes > 0) { + const char* noteNames[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}; + for (int j = 0; j < min(numScaleNotes, 6); j++) { + display.print(noteNames[scaleNotes[j]]); + if (j < min(numScaleNotes, 6) - 1) display.print(F(" ")); + } } - } else { - display.print(F("[No scale generated]")); + } else if (i == 2) { // Melody + display.print(F(": ")); + display.print(melodySeed); } - y += 9; } + y += 9; } } @@ -533,33 +614,45 @@ void updateLeds() { pixels.clear(); // Clear buffer for (int s = 0; s < NUM_STEPS; s++) { - uint32_t color = 0; // Default off - int stepNavIndex = navigationSelection - 1; + int blockX = (s % 4) * 2; + int blockY = (s / 4) * 2; + + // --- Top Row: Attributes --- + uint32_t colorTL = 0; // Octave + uint32_t colorTR = 0; // Accent/Tie if (sequence[s].note != -1) { - color = getNoteColor(sequence[s].note); + 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 (currentState == UI_TRACKER && s == stepNavIndex && !isEditing) { - color = pixels.Color(40, 40, 40); // White for navigation + // --- 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) { - color = pixels.Color(0, 50, 0); // Green for playback (overwrites nav cursor) + 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); } - if (currentState == UI_TRACKER && s == stepNavIndex && isEditing) { - color = pixels.Color(50, 0, 0); // Red for editing (highest precedence) - } - - if (color != 0) { - int blockX = (s % 4) * 2; - int blockY = (s / 4) * 2; - pixels.setPixelColor(getPixelIndex(blockX, blockY), color); - pixels.setPixelColor(getPixelIndex(blockX + 1, blockY), color); - pixels.setPixelColor(getPixelIndex(blockX, blockY + 1), color); - pixels.setPixelColor(getPixelIndex(blockX + 1, blockY + 1), color); - } + 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); } pixels.show();