From 8759d7f46ad8034c4156da2b3cbe3b259d26cbf5 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Mon, 16 Feb 2026 20:35:16 +0100 Subject: [PATCH] Randomized melody --- RP2040_Tracker.ino | 437 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 342 insertions(+), 95 deletions(-) diff --git a/RP2040_Tracker.ino b/RP2040_Tracker.ino index 969973f..2616e76 100644 --- a/RP2040_Tracker.ino +++ b/RP2040_Tracker.ino @@ -38,7 +38,31 @@ Step sequence[NUM_STEPS]; Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); Adafruit_NeoPixel pixels(NUM_PIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800); -int currentStep = 0; +enum UIState { + UI_TRACKER, + UI_MENU_MAIN, + UI_MENU_RANDOMIZE, + UI_MENU_SETUP, + UI_SETUP_CHANNEL_EDIT +}; + +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 int randomizeMenuCount = sizeof(randomizeMenu) / sizeof(char*); +const char* setupMenu[] = { "Back", "Channel" }; +const int setupMenuCount = sizeof(setupMenu) / sizeof(char*); + +int menuSelection = 0; +int navigationSelection = 1; +int playbackStep = 0; +int midiChannel = 1; +int scaleNotes[12]; +int numScaleNotes = 0; +int queuedTheme = -1; + bool isEditing = false; int scrollOffset = 0; bool isPlaying = false; @@ -77,8 +101,21 @@ void readEncoder() { } } +void sortArray(int arr[], int size) { + for (int i = 0; i < size - 1; i++) { + for (int j = 0; j < size - i - 1; j++) { + if (arr[j] > arr[j + 1]) { + int temp = arr[j]; + arr[j] = arr[j + 1]; + arr[j + 1] = temp; + } + } + } +} + void setup() { Serial.begin(115200); + // Use random ADC noise for seed delay(5000); Serial.println(F("Starting.")); @@ -119,6 +156,10 @@ void setup() { sequence[9].note = 62; // D4 sequence[12].note = 64; // E4 + // Init randomizer + randomSeed(micros()); + generateRandomScale(); + display.clearDisplay(); display.display(); @@ -131,11 +172,39 @@ void setup() { } void sendMidi(uint8_t status, uint8_t note, uint8_t velocity) { - Serial1.write(status); + uint8_t channelStatus = status | (midiChannel - 1); + Serial1.write(channelStatus); Serial1.write(note); Serial1.write(velocity); } +void generateRandomScale() { + 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 generateTheme(int themeType) { + sendMidi(0xB0, 123, 0); // Panic / All Notes Off + randomSeed(themeType * 12345); // Deterministic seed for this theme + if (numScaleNotes == 0) generateRandomScale(); + + for (int i = 0; i < NUM_STEPS; i++) { + sequence[i].note = (random(100) < 50) ? (12 * 4 + scaleNotes[random(numScaleNotes)]) : -1; + } + randomSeed(micros()); // Restore randomness + isPlaying = true; +} + void handleInput() { // Handle Encoder Rotation int delta = 0; @@ -145,22 +214,46 @@ void handleInput() { interrupts(); if (delta != 0) { - if (isEditing) { - // Change Note - int newNote = sequence[currentStep].note + delta; - if (newNote < -1) newNote = -1; - if (newNote > 127) newNote = 127; - sequence[currentStep].note = newNote; - Serial.print(F("Note changed: ")); Serial.println(newNote); - } else { - // Move Cursor - currentStep += (delta > 0 ? 1 : -1); - if (currentStep < 0) currentStep = NUM_STEPS - 1; - if (currentStep >= NUM_STEPS) currentStep = 0; - - // Adjust Scroll to keep cursor in view - if (currentStep < scrollOffset) scrollOffset = currentStep; - if (currentStep >= scrollOffset + 6) scrollOffset = currentStep - 5; + 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; + sequence[stepIndex].note = newNote; + } 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; + break; } } @@ -183,21 +276,59 @@ void handleInput() { if (reading == HIGH && buttonActive) { // Button Released buttonActive = false; - if (!buttonConsumed) { - isEditing = !isEditing; - Serial.print(F("Mode toggled: ")); Serial.println(isEditing ? F("EDIT") : F("NAV")); + 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) generateRandomScale(); + if (menuSelection >= 2) { + if (isPlaying) { + queuedTheme = menuSelection - 1; + } else { + generateTheme(menuSelection - 1); + } + } + break; + case UI_MENU_SETUP: + if (menuSelection == 0) { currentState = UI_MENU_MAIN; menuSelection = 2; } + if (menuSelection == 1) currentState = UI_SETUP_CHANNEL_EDIT; + break; + case UI_SETUP_CHANNEL_EDIT: + currentState = UI_MENU_SETUP; + 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) { - // Send All Notes Off on stop (CC 123) - sendMidi(0xB0, 123, 0); + // 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 = navigationSelection > 0 ? navigationSelection - 1 : 0; + lastStepTime = millis(); // Reset timer to start immediately + } else { + // Send All Notes Off on stop (CC 123) + sendMidi(0xB0, 123, 0); + queuedTheme = -1; + } } } @@ -211,22 +342,136 @@ void handlePlayback() { if (millis() - lastStepTime > interval) { lastStepTime = millis(); - // Note Off for current step (before advancing) - if (sequence[currentStep].note != -1) { - sendMidi(0x80, sequence[currentStep].note, 0); + // Note Off for previous step + if (sequence[playbackStep].note != -1) { + sendMidi(0x80, sequence[playbackStep].note, 0); } - currentStep++; - if (currentStep >= NUM_STEPS) currentStep = 0; + playbackStep++; + if (playbackStep >= NUM_STEPS) { + playbackStep = 0; + if (queuedTheme != -1) { + generateTheme(queuedTheme); + queuedTheme = -1; + } + } // Note On for new step - if (sequence[currentStep].note != -1) { - sendMidi(0x90, sequence[currentStep].note, 100); + if (sequence[playbackStep].note != -1) { + sendMidi(0x90, sequence[playbackStep].note, 100); } - // Auto-scroll logic is handled in drawUI based on currentStep - if (currentStep < scrollOffset) scrollOffset = currentStep; - if (currentStep >= scrollOffset + 6) scrollOffset = currentStep - 5; + // Auto-scroll navigation cursor if not editing + if (!isEditing) { + navigationSelection = playbackStep + 1; // +1 because 0 is menu + if (navigationSelection < scrollOffset) scrollOffset = navigationSelection; + if (navigationSelection >= scrollOffset + 6) scrollOffset = navigationSelection - 5; + } + } +} + +void drawMenu(const char* title, const char* items[], int count, int selection) { + display.println(title); + display.drawLine(0, 8, 128, 8, SSD1306_WHITE); + + int y = 10; + for (int i = 0; i < count; i++) { + if (i == selection) { + display.fillRect(0, y, 128, 8, SSD1306_WHITE); + display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); + } else { + display.setTextColor(SSD1306_WHITE); + } + display.setCursor(2, y); + display.print(items[i]); + + // Special case for channel display + if (currentState == UI_MENU_SETUP && i == 1) { + display.print(F(": ")); + display.print(midiChannel); + } + + // Special case for queued theme + if (currentState == UI_MENU_RANDOMIZE && i >= 2 && queuedTheme == (i - 1)) { + 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(" ")); + } + } else { + display.print(F("[No scale generated]")); + } + y += 9; + } + } +} + +void drawTracker() { + display.clearDisplay(); + display.setTextSize(1); + display.setTextColor(SSD1306_WHITE); + display.setCursor(0, 0); + + // Header + display.print(F("SEQ ")); + if (navigationSelection > 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); + + // Steps + int y = 10; + for (int i = 0; i < 6; i++) { + int itemIndex = i + scrollOffset; + if (itemIndex > NUM_STEPS) break; + + // Draw Cursor + if (itemIndex == navigationSelection) { + display.fillRect(0, y, 128, 8, SSD1306_WHITE); + display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); // Invert text + } else { + display.setTextColor(SSD1306_WHITE); + } + display.setCursor(2, y); + + if (itemIndex == 0) { + display.print(F(">> MENU")); + } else { + int stepIndex = itemIndex - 1; + // Step Number + if (stepIndex < 10) display.print(F("0")); + display.print(stepIndex); + display.print(F(" | ")); + + // Note Value + int n = sequence[stepIndex].note; + if (n == -1) { + display.print(F("---")); + } else { + // Basic Note to String conversion + 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); // Octave + } + } + + y += 9; } } @@ -236,43 +481,31 @@ void drawUI() { display.setTextColor(SSD1306_WHITE); display.setCursor(0, 0); - // Header - display.print(F("TRACKER ")); - display.print(isEditing ? F("[EDIT]") : F("[NAV]")); - display.println(); - display.drawLine(0, 8, 128, 8, SSD1306_WHITE); - - // Steps - int y = 10; - for (int i = scrollOffset; i < min(scrollOffset + 6, NUM_STEPS); i++) { - - // Draw Cursor - if (i == currentStep) { - display.fillRect(0, y, 128, 8, SSD1306_WHITE); - display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); // Invert text - } else { - display.setTextColor(SSD1306_WHITE); - } - - display.setCursor(2, y); - - // Step Number - if (i < 10) display.print(F("0")); - display.print(i); - display.print(F(" | ")); - - // Note Value - int n = sequence[i].note; - if (n == -1) { - display.print(F("---")); - } else { - // Basic Note to String conversion - 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); // Octave - } - - y += 9; + switch(currentState) { + case UI_TRACKER: + drawTracker(); + break; + case UI_MENU_MAIN: + drawMenu("MAIN MENU", mainMenu, mainMenuCount, menuSelection); + break; + case UI_MENU_RANDOMIZE: + drawMenu("RANDOMIZE", randomizeMenu, randomizeMenuCount, menuSelection); + break; + case UI_MENU_SETUP: + drawMenu("SETUP", setupMenu, setupMenuCount, menuSelection); + break; + case UI_SETUP_CHANNEL_EDIT: + display.println(F("SET MIDI CHANNEL")); + display.drawLine(0, 8, 128, 8, SSD1306_WHITE); + display.setCursor(20, 25); + display.setTextSize(2); + display.print(F("CH: ")); + if (midiChannel < 10) display.print(F(" ")); + display.print(midiChannel); + display.setTextSize(1); + display.setCursor(0, 50); + display.println(F(" (Press to confirm)")); + break; } display.display(); @@ -285,34 +518,48 @@ int getPixelIndex(int x, int y) { return y * 8 + x; } +uint32_t getNoteColor(int note) { + 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). + // Range: 30000 to 65536+8000 = 73536. Width = 43536. + // 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); +} + void updateLeds() { pixels.clear(); // Clear buffer for (int s = 0; s < NUM_STEPS; s++) { - int blockX = (s % 4) * 2; - int blockY = (s / 4) * 2; + uint32_t color = 0; // Default off + int stepNavIndex = navigationSelection - 1; - uint32_t color; - - if (s == currentStep) { - if (isEditing) { - color = pixels.Color(50, 0, 0); // Dim Red for editing - } else { - color = pixels.Color(40, 40, 40); // Dim White for current step - } - } else { - if (sequence[s].note != -1) { - color = pixels.Color(0, 0, 50); // Dim Blue for step with note - } else { - color = 0; // Off - } + if (sequence[s].note != -1) { + color = getNoteColor(sequence[s].note); } - // Set the 4 pixels for the 2x2 block - 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); + if (currentState == UI_TRACKER && s == stepNavIndex && !isEditing) { + color = pixels.Color(40, 40, 40); // White for navigation + } + + if (isPlaying && s == playbackStep) { + color = pixels.Color(0, 50, 0); // Green for playback (overwrites nav cursor) + } + + 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.show();