From 6734572a672065d76a2bf97160173beb857945b7 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Tue, 17 Feb 2026 00:26:53 +0100 Subject: [PATCH] Refactor into files --- MidiDriver.cpp | 49 +++++ MidiDriver.h | 26 +++ RP2040_Tracker.ino | 454 ++++----------------------------------------- TrackerTypes.h | 10 + UIManager.cpp | 283 ++++++++++++++++++++++++++++ UIManager.h | 51 +++++ 6 files changed, 460 insertions(+), 413 deletions(-) create mode 100644 MidiDriver.cpp create mode 100644 MidiDriver.h create mode 100644 UIManager.cpp create mode 100644 UIManager.h diff --git a/MidiDriver.cpp b/MidiDriver.cpp new file mode 100644 index 0000000..f716215 --- /dev/null +++ b/MidiDriver.cpp @@ -0,0 +1,49 @@ +#include "MidiDriver.h" + +// MIDI UART Pins (GP0/GP1) +#define PIN_MIDI_TX 0 + +MidiDriver midi; + +MidiDriver::MidiDriver() { +} + +void MidiDriver::begin() { + mutex_init(&_mutex); + Serial1.setTX(PIN_MIDI_TX); + Serial1.begin(31250); + Serial.println(F("MIDI Serial initialized on GP0/GP1")); +} + +void MidiDriver::lock() { + mutex_enter_blocking(&_mutex); +} + +void MidiDriver::unlock() { + mutex_exit(&_mutex); +} + +void MidiDriver::sendNoteOn(uint8_t note, uint8_t velocity, uint8_t channel) { + uint8_t status = 0x90 | (channel - 1); + Serial1.write(status); + Serial1.write(note); + Serial1.write(velocity); +} + +void MidiDriver::sendNoteOff(uint8_t note, uint8_t channel) { + uint8_t status = 0x80 | (channel - 1); + Serial1.write(status); + Serial1.write(note); + Serial1.write((uint8_t)0); +} + +void MidiDriver::sendRealtime(uint8_t status) { + Serial1.write(status); +} + +void MidiDriver::panic(uint8_t channel) { + uint8_t status = 0xB0 | (channel - 1); + Serial1.write(status); + Serial1.write(123); // All Notes Off + Serial1.write((uint8_t)0); +} \ No newline at end of file diff --git a/MidiDriver.h b/MidiDriver.h new file mode 100644 index 0000000..4fb1673 --- /dev/null +++ b/MidiDriver.h @@ -0,0 +1,26 @@ +#ifndef MIDI_DRIVER_H +#define MIDI_DRIVER_H + +#include +#include + +class MidiDriver { +public: + MidiDriver(); + void begin(); + + void sendNoteOn(uint8_t note, uint8_t velocity, uint8_t channel); + void sendNoteOff(uint8_t note, uint8_t channel); + void sendRealtime(uint8_t status); + void panic(uint8_t channel); + + void lock(); + void unlock(); + +private: + mutex_t _mutex; +}; + +extern MidiDriver midi; + +#endif \ No newline at end of file diff --git a/RP2040_Tracker.ino b/RP2040_Tracker.ino index a285ef8..790be42 100644 --- a/RP2040_Tracker.ino +++ b/RP2040_Tracker.ino @@ -1,14 +1,12 @@ #include #include -#include -#include -#include #include -#include #include "TrackerTypes.h" #include "MelodyStrategy.h" #include "LuckyStrategy.h" #include "ArpStrategy.h" +#include "MidiDriver.h" +#include "UIManager.h" // --- HARDWARE CONFIGURATION --- #define SCREEN_WIDTH 128 @@ -24,13 +22,6 @@ #define ENC_DT 13 #define ENC_SW 14 -// MIDI UART Pins (GP0/GP1) -- OUT only so far -#define PIN_MIDI_TX 0 - -// NeoPixel Pin (any GPIO is fine, I've chosen 16) -#define PIN_NEOPIXEL 16 -#define NUM_PIXELS 64 // For 8x8 WS2812B matrix - // --- TRACKER DATA --- #define NUM_STEPS 16 @@ -39,22 +30,6 @@ Step nextSequence[NUM_STEPS]; volatile bool sequenceChangeScheduled = false; volatile bool needsPanic = false; -mutex_t midiMutex; - -// --- STATE --- -Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); -Adafruit_NeoPixel pixels(NUM_PIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800); - -enum UIState { - UI_TRACKER, - UI_MENU_MAIN, - UI_MENU_RANDOMIZE, - UI_MENU_SETUP, - UI_SETUP_CHANNEL_EDIT, - UI_EDIT_TEMPO, - UI_EDIT_FLAVOUR -}; - UIState currentState = UI_MENU_MAIN; const char* mainMenu[] = { "Tracker", "Randomize", "Setup" }; @@ -137,17 +112,6 @@ 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(bool quiet = false) { int addr = 0; EEPROM.put(addr, EEPROM_MAGIC); addr += sizeof(EEPROM_MAGIC); @@ -161,13 +125,13 @@ void saveSequence(bool quiet = false) { EEPROM.put(addr, scaleNotes[i]); addr += sizeof(int); } - mutex_enter_blocking(&midiMutex); + midi.lock(); for (int i=0; i 127) newNote = 127; - mutex_enter_blocking(&midiMutex); + midi.lock(); sequence[stepIndex].note = newNote; - mutex_exit(&midiMutex); + midi.unlock(); } else { // Move Cursor navigationSelection += (delta > 0 ? 1 : -1); @@ -407,10 +336,10 @@ void handleInput() { generateRandomScale(); // Scale if (isPlaying) { int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex; - mutex_enter_blocking(&midiMutex); + midi.lock(); generateSequenceData(theme, nextSequence); sequenceChangeScheduled = true; - mutex_exit(&midiMutex); + midi.unlock(); } saveSequence(true); } @@ -418,10 +347,10 @@ void handleInput() { melodySeed = random(10000); // Melody if (isPlaying) { int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex; - mutex_enter_blocking(&midiMutex); + midi.lock(); generateSequenceData(theme, nextSequence); sequenceChangeScheduled = true; - mutex_exit(&midiMutex); + midi.unlock(); } saveSequence(true); } @@ -438,10 +367,10 @@ void handleInput() { const int selectedTheme = menuSelection - THEME_1_INDEX + 1; if (isPlaying) { queuedTheme = selectedTheme; - mutex_enter_blocking(&midiMutex); + midi.lock(); generateSequenceData(queuedTheme, nextSequence); sequenceChangeScheduled = true; - mutex_exit(&midiMutex); + midi.unlock(); } else { generateTheme(selectedTheme); } @@ -455,7 +384,7 @@ void handleInput() { currentState = UI_MENU_MAIN; } if (menuSelection == 3) { - if (loadSequence()) showMessage("LOADED!"); + if (loadSequence()) ui.showMessage("LOADED!"); currentState = UI_MENU_MAIN; } break; @@ -487,11 +416,11 @@ void handleInput() { playbackStep = 0; clockCount = 0; lastClockTime = micros(); - sendMidiRealtime(0xFA); // MIDI Start + midi.sendRealtime(0xFA); // MIDI Start } else { // Send All Notes Off on stop (CC 123) needsPanic = true; - sendMidiRealtime(0xFC); // MIDI Stop + midi.sendRealtime(0xFC); // MIDI Stop queuedTheme = -1; } } @@ -509,13 +438,13 @@ void handlePlayback() { if (currentMicros - lastClockTime >= clockInterval) { lastClockTime += clockInterval; - sendMidiRealtime(0xF8); // MIDI Clock + midi.sendRealtime(0xF8); // MIDI Clock clockCount++; if (clockCount < 6) return; // 24 ppqn / 4 = 6 pulses per 16th note clockCount = 0; - mutex_enter_blocking(&midiMutex); + midi.lock(); // Determine if we are tying to the next note int nextStep = playbackStep + 1; @@ -526,7 +455,7 @@ void handlePlayback() { // Note Off for previous step (if NOT tied) if (!isTied && prevNote != -1) { - sendMidi(0x80, prevNote, 0); + midi.sendNoteOff(prevNote, shMidiChannel); } playbackStep++; @@ -549,7 +478,7 @@ void handlePlayback() { sequenceChangeScheduled = true; } - sendMidi(0xB0, 123, 0); // Panic / All Notes Off + midi.panic(shMidiChannel); // Panic / All Notes Off if (sequenceChangeScheduled) { memcpy(sequence, nextSequence, sizeof(sequence)); @@ -576,336 +505,35 @@ void handlePlayback() { // Note On for new step if (sequence[playbackStep].note != -1) { uint8_t velocity = sequence[playbackStep].accent ? 127 : 100; - sendMidi(0x90, sequence[playbackStep].note, velocity); + midi.sendNoteOn(sequence[playbackStep].note, velocity, shMidiChannel); } // Note Off for previous step (if tied - delayed Note Off) if (isTied && prevNote != -1) { - sendMidi(0x80, prevNote, 0); + midi.sendNoteOff(prevNote, shMidiChannel); } - mutex_exit(&midiMutex); + midi.unlock(); } } -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 = 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); - } 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 >= THEME_1_INDEX && queuedTheme == (i - THEME_1_INDEX + 1)) { - display.print(F(" [NEXT]")); - } - - // Special case for active theme - if (currentState == UI_MENU_RANDOMIZE && i >= THEME_1_INDEX && currentThemeIndex == (i - THEME_1_INDEX + 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 if (i == 2) { // Melody - display.print(F(": ")); - display.print(melodySeed); - } else if (i == 3) { // Flavour - display.print(F(": ")); - display.print(strategies[currentStrategyIndex]->getName()); - } else if (i == 4) { // Tempo - display.print(F(": ")); - display.print(tempo); - } else if (i == 5) { // Mutation - display.print(F(": ")); - display.print(mutationEnabled ? F("ON") : F("OFF")); - } else if (i == 6) { // Song Mode - display.print(F(": ")); - display.print(songModeEnabled ? F("ON") : F("OFF")); - } - } - 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; - mutex_enter_blocking(&midiMutex); - 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; - bool isPlayback = isPlaying && (stepIndex == playbackStep); - - if (isPlayback) { - if (itemIndex == navigationSelection) display.setTextColor(SSD1306_WHITE, SSD1306_BLACK); - else display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); - } - - // Step Number - if (stepIndex < 10) display.print(F("0")); - display.print(stepIndex); - - if (isPlayback) { - if (itemIndex == navigationSelection) display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); - else display.setTextColor(SSD1306_WHITE); - } - - 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; - } - mutex_exit(&midiMutex); -} - void drawUI() { - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - display.setCursor(0, 0); - - 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; - case UI_EDIT_TEMPO: - display.println(F("SET TEMPO")); - display.drawLine(0, 8, 128, 8, SSD1306_WHITE); - display.setCursor(20, 25); - display.setTextSize(2); - display.print(F("BPM: ")); - display.print(tempo); - display.setTextSize(1); - display.setCursor(0, 50); - display.println(F(" (Press to confirm)")); - break; - case UI_EDIT_FLAVOUR: - display.println(F("SET FLAVOUR")); - display.drawLine(0, 8, 128, 8, SSD1306_WHITE); - display.setCursor(20, 25); - display.setTextSize(2); - display.print(strategies[currentStrategyIndex]->getName()); - display.setTextSize(1); - display.setCursor(0, 50); - display.println(F(" (Press to confirm)")); - break; - } - - display.display(); -} - -// Helper to convert X,Y to pixel index for an 8x8 matrix. -// Assumes row-major wiring (NOT serpentine). -// If your matrix is wired differently, you'll need to change this function. -int getPixelIndex(int x, int y) { - return y * 8 + x; -} - -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). - // 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, dim ? 10 : 50); + midi.lock(); + ui.draw(currentState, menuSelection, navigationSelection, isEditing, midiChannel, tempo, strategies[currentStrategyIndex], queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, sequence, scrollOffset, playbackStep, isPlaying, mainMenu, mainMenuCount, randomizeMenu, randomizeMenuCount, setupMenu, setupMenuCount, THEME_1_INDEX); + midi.unlock(); } void updateLeds() { - pixels.clear(); // Clear buffer - - mutex_enter_blocking(&midiMutex); - for (int s = 0; s < NUM_STEPS; s++) { - int x = s % 8; - int yBase = (s / 8) * 4; - - 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; - - 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; - 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); - } - - 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 (sequenceChangeScheduled && (millis() / 125) % 2) { - pixels.setPixelColor(NUM_PIXELS - 1, pixels.Color(127, 50, 0)); - } - mutex_exit(&midiMutex); - - pixels.show(); + midi.lock(); + ui.updateLeds(sequence, navigationSelection, playbackStep, isPlaying, currentState, isEditing, songModeEnabled, songRepeatsRemaining, sequenceChangeScheduled); + midi.unlock(); } void loop1() { if (needsPanic) { - mutex_enter_blocking(&midiMutex); - sendMidi(0xB0, 123, 0); - mutex_exit(&midiMutex); + midi.lock(); + midi.panic(shMidiChannel); + midi.unlock(); needsPanic = false; } handlePlayback(); @@ -917,12 +545,12 @@ void loop() { int nextTheme = random(1, 8); // Themes 1-7 int repeats = random(1, 9); // 1-8 repeats - mutex_enter_blocking(&midiMutex); + midi.lock(); generateSequenceData(nextTheme, nextSequence); queuedTheme = nextTheme; nextSongRepeats = repeats; sequenceChangeScheduled = true; - mutex_exit(&midiMutex); + midi.unlock(); songModeNeedsNext = false; } diff --git a/TrackerTypes.h b/TrackerTypes.h index 70335e8..f46c750 100644 --- a/TrackerTypes.h +++ b/TrackerTypes.h @@ -9,4 +9,14 @@ struct Step { bool tie; }; +enum UIState { + UI_TRACKER, + UI_MENU_MAIN, + UI_MENU_RANDOMIZE, + UI_MENU_SETUP, + UI_SETUP_CHANNEL_EDIT, + UI_EDIT_TEMPO, + UI_EDIT_FLAVOUR +}; + #endif \ No newline at end of file diff --git a/UIManager.cpp b/UIManager.cpp new file mode 100644 index 0000000..a9e0522 --- /dev/null +++ b/UIManager.cpp @@ -0,0 +1,283 @@ +#include "UIManager.h" + +// --- HARDWARE CONFIGURATION --- +#define SCREEN_WIDTH 128 +#define SCREEN_HEIGHT 64 +#define OLED_RESET -1 +#define SCREEN_ADDRESS 0x3C + +#define PIN_NEOPIXEL 16 +#define NUM_PIXELS 64 +#define NUM_STEPS 16 + +UIManager ui; + +UIManager::UIManager() + : display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET), + pixels(NUM_PIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800) +{ +} + +void UIManager::begin() { + // Setup Display + Wire.begin(); + if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { + Serial.println(F("SSD1306 allocation failed")); + for(;;); + } + display.clearDisplay(); + display.display(); + + // Setup NeoPixel Matrix + pixels.begin(); + pixels.setBrightness(40); + pixels.clear(); + pixels.show(); +} + +void UIManager::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 UIManager::draw(UIState currentState, int menuSelection, int navSelection, bool isEditing, + int midiChannel, int tempo, MelodyStrategy* currentStrategy, + int queuedTheme, int currentThemeIndex, + int numScaleNotes, const int* scaleNotes, int melodySeed, + bool mutationEnabled, bool songModeEnabled, + const Step* sequence, int scrollOffset, int playbackStep, bool isPlaying, + const char* mainMenu[], int mainMenuCount, + const char* randomizeMenu[], int randomizeMenuCount, + const char* setupMenu[], int setupMenuCount, + int theme1Index) { + + display.clearDisplay(); + display.setTextSize(1); + display.setTextColor(SSD1306_WHITE); + display.setCursor(0, 0); + + switch(currentState) { + case UI_TRACKER: + drawTracker(navSelection, isEditing, midiChannel, sequence, scrollOffset, playbackStep, isPlaying); + break; + case UI_MENU_MAIN: + drawMenu("MAIN MENU", mainMenu, mainMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index); + break; + case UI_MENU_RANDOMIZE: + drawMenu("RANDOMIZE", randomizeMenu, randomizeMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index); + break; + case UI_MENU_SETUP: + drawMenu("SETUP", setupMenu, setupMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index); + 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; + case UI_EDIT_TEMPO: + display.println(F("SET TEMPO")); + display.drawLine(0, 8, 128, 8, SSD1306_WHITE); + display.setCursor(20, 25); + display.setTextSize(2); + display.print(F("BPM: ")); + display.print(tempo); + display.setTextSize(1); + display.setCursor(0, 50); + display.println(F(" (Press to confirm)")); + break; + case UI_EDIT_FLAVOUR: + display.println(F("SET FLAVOUR")); + display.drawLine(0, 8, 128, 8, SSD1306_WHITE); + display.setCursor(20, 25); + display.setTextSize(2); + display.print(currentStrategy->getName()); + display.setTextSize(1); + display.setCursor(0, 50); + display.println(F(" (Press to confirm)")); + break; + } + display.display(); +} + +void UIManager::drawMenu(const char* title, const char* items[], int count, int selection, + UIState currentState, int midiChannel, int tempo, const char* flavourName, + int queuedTheme, int currentThemeIndex, + int numScaleNotes, const int* scaleNotes, int melodySeed, + bool mutationEnabled, bool songModeEnabled, int theme1Index) { + display.println(title); + display.drawLine(0, 8, 128, 8, SSD1306_WHITE); + + int start = 0; + if (selection >= 5) start = selection - 4; + + int y = 10; + for (int i = start; i < count; i++) { + if (y > 55) break; + + 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]); + + if (currentState == UI_MENU_SETUP && i == 1) { + display.print(F(": ")); display.print(midiChannel); + } + if (currentState == UI_MENU_RANDOMIZE && i >= theme1Index && queuedTheme == (i - theme1Index + 1)) { + display.print(F(" [NEXT]")); + } + if (currentState == UI_MENU_RANDOMIZE && i >= theme1Index && currentThemeIndex == (i - theme1Index + 1)) { + display.print(F(" *")); + } + 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 if (i == 2) { display.print(F(": ")); display.print(melodySeed); } + else if (i == 3) { display.print(F(": ")); display.print(flavourName); } + else if (i == 4) { display.print(F(": ")); display.print(tempo); } + else if (i == 5) { display.print(F(": ")); display.print(mutationEnabled ? F("ON") : F("OFF")); } + else if (i == 6) { display.print(F(": ")); display.print(songModeEnabled ? F("ON") : F("OFF")); } + } + y += 9; + } +} + +void UIManager::drawTracker(int navSelection, bool isEditing, int midiChannel, + const Step* sequence, int scrollOffset, int playbackStep, bool isPlaying) { + display.print(F("SEQ ")); + if (navSelection > 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); + + int y = 10; + for (int i = 0; i < 6; i++) { + int itemIndex = i + scrollOffset; + if (itemIndex > NUM_STEPS) break; + + if (itemIndex == navSelection) { + display.fillRect(0, y, 128, 8, SSD1306_WHITE); + display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); + } else { + display.setTextColor(SSD1306_WHITE); + } + display.setCursor(2, y); + + if (itemIndex == 0) { + display.print(F(">> MENU")); + } else { + int stepIndex = itemIndex - 1; + bool isPlayback = isPlaying && (stepIndex == playbackStep); + if (isPlayback) { + if (itemIndex == navSelection) display.setTextColor(SSD1306_WHITE, SSD1306_BLACK); + else display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); + } + if (stepIndex < 10) display.print(F("0")); + display.print(stepIndex); + if (isPlayback) { + if (itemIndex == navSelection) display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); + else display.setTextColor(SSD1306_WHITE); + } + display.print(F(" | ")); + int n = sequence[stepIndex].note; + if (n == -1) display.print(F("---")); + else { + 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); + } + } + y += 9; + } +} + +uint32_t UIManager::getNoteColor(int note, bool dim) { + if (note == -1) return 0; + uint16_t hue = 30000 + (note % 12) * 3628; + return Adafruit_NeoPixel::ColorHSV(hue, 255, dim ? 10 : 50); +} + +int UIManager::getPixelIndex(int x, int y) { + return y * 8 + x; +} + +void UIManager::updateLeds(const Step* sequence, int navSelection, int playbackStep, bool isPlaying, + UIState currentState, bool isEditing, bool songModeEnabled, + int songRepeatsRemaining, bool sequenceChangeScheduled) { + pixels.clear(); + for (int s = 0; s < NUM_STEPS; s++) { + int x = s % 8; + int yBase = (s / 8) * 4; + uint32_t color = 0, dimColor = 0; + if (sequence[s].note != -1) { + color = getNoteColor(sequence[s].note, sequence[s].tie); + dimColor = getNoteColor(sequence[s].note, true); + } + uint32_t c[4] = {0}; + if (sequence[s].note != -1) { + int octave = sequence[s].note / 12; + if (octave > 4) { c[0] = color; if (sequence[s].accent) c[1] = dimColor; } + else if (octave < 4) { c[2] = color; if (sequence[s].accent) c[1] = dimColor; } + else { c[1] = color; if (sequence[s].accent) { c[0] = dimColor; c[2] = dimColor; } } + } + int stepNavIndex = navSelection - 1; + if (isPlaying) { + c[3] = pixels.Color(0, 50, 0); + if (songModeEnabled && s >= NUM_STEPS/2 && x >= (8 - min(songRepeatsRemaining, 8))) { + c[3] = (songRepeatsRemaining == 1 && x == 7 && (millis()/250)%2) ? pixels.Color(0, 250, 0) : pixels.Color(80, 100, 0); + } + } else if (currentState == UI_TRACKER && s == stepNavIndex) { + c[3] = isEditing ? pixels.Color(50, 0, 0) : pixels.Color(40, 40, 40); + } else if (c[3] == 0 && (isPlaying && s == playbackStep || (!isPlaying && currentState == UI_TRACKER && s == stepNavIndex))) { + // Cursor logic handled above, this block seems redundant or malformed in original logic translation, simplifying: + } + // Apply cursor color logic from original code more strictly + uint32_t cursorColor = 0; + if (isPlaying) { + cursorColor = pixels.Color(0, 50, 0); + if (songModeEnabled && s >= 56) { + int repeats = min(songRepeatsRemaining, 8); + if (x >= (8 - repeats)) cursorColor = (songRepeatsRemaining == 1 && x == 7 && (millis()/250)%2) ? pixels.Color(255, 200, 0) : pixels.Color(154, 205, 50); + } + } else if (currentState == UI_TRACKER) { + cursorColor = isEditing ? pixels.Color(50, 0, 0) : pixels.Color(40, 40, 40); + } + + bool isCursorHere = (isPlaying && s == playbackStep) || (!isPlaying && currentState == UI_TRACKER && s == stepNavIndex); + if (cursorColor != 0) { + if (isCursorHere) c[3] = cursorColor; + else { + uint8_t r = (uint8_t)(cursorColor >> 16), g = (uint8_t)(cursorColor >> 8), b = (uint8_t)cursorColor; + c[3] = pixels.Color(r/5, g/5, b/5); + } + } + + for(int i=0; i<4; i++) pixels.setPixelColor(getPixelIndex(x, yBase + i), c[i]); + } + if (sequenceChangeScheduled && (millis() / 125) % 2) pixels.setPixelColor(NUM_PIXELS - 1, pixels.Color(127, 50, 0)); + pixels.show(); +} \ No newline at end of file diff --git a/UIManager.h b/UIManager.h new file mode 100644 index 0000000..a6e9b20 --- /dev/null +++ b/UIManager.h @@ -0,0 +1,51 @@ +#ifndef UI_MANAGER_H +#define UI_MANAGER_H + +#include +#include +#include +#include "TrackerTypes.h" +#include "MelodyStrategy.h" + +class UIManager { +public: + UIManager(); + void begin(); + + void showMessage(const char* msg); + + void draw(UIState currentState, int menuSelection, int navSelection, bool isEditing, + int midiChannel, int tempo, MelodyStrategy* currentStrategy, + int queuedTheme, int currentThemeIndex, + int numScaleNotes, const int* scaleNotes, int melodySeed, + bool mutationEnabled, bool songModeEnabled, + const Step* sequence, int scrollOffset, int playbackStep, bool isPlaying, + const char* mainMenu[], int mainMenuCount, + const char* randomizeMenu[], int randomizeMenuCount, + const char* setupMenu[], int setupMenuCount, + int theme1Index); + + void updateLeds(const Step* sequence, int navSelection, int playbackStep, bool isPlaying, + UIState currentState, bool isEditing, bool songModeEnabled, + int songRepeatsRemaining, bool sequenceChangeScheduled); + +private: + Adafruit_SSD1306 display; + Adafruit_NeoPixel pixels; + + void drawMenu(const char* title, const char* items[], int count, int selection, + UIState currentState, int midiChannel, int tempo, const char* flavourName, + int queuedTheme, int currentThemeIndex, + int numScaleNotes, const int* scaleNotes, int melodySeed, + bool mutationEnabled, bool songModeEnabled, int theme1Index); + + void drawTracker(int navSelection, bool isEditing, int midiChannel, + const Step* sequence, int scrollOffset, int playbackStep, bool isPlaying); + + uint32_t getNoteColor(int note, bool dim); + int getPixelIndex(int x, int y); +}; + +extern UIManager ui; + +#endif \ No newline at end of file