#include "UIManager.h" #include "config.h" #include "SharedState.h" // --- HARDWARE CONFIGURATION --- #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_RESET -1 #define SCREEN_ADDRESS 0x3C #define PIN_NEOPIXEL 2 #define NUM_PIXELS 64 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.setPin(PIN_NEOPIXEL); 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 midiChannel, int tempo, MelodyStrategy* currentStrategy, int queuedTheme, int currentThemeIndex, int numScaleNotes, const int* scaleNotes, int melodySeed, int numSteps, bool mutationEnabled, bool songModeEnabled, const Step sequence[][NUM_STEPS], int playbackStep, bool isPlaying, int randomizeTrack, const bool* trackMute) { display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(0, 0); switch(currentState) { case UI_MENU_MAIN: drawMenu(menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, numSteps, mutationEnabled, songModeEnabled, isPlaying, randomizeTrack, trackMute); 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_STEPS: display.println(F("SET STEPS")); display.drawLine(0, 8, 128, 8, SSD1306_WHITE); display.setCursor(20, 25); display.setTextSize(2); display.print(F("LEN: ")); display.print(numSteps); 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; case UI_RANDOMIZE_TRACK_EDIT: display.println(F("SET TRACK")); display.drawLine(0, 8, 128, 8, SSD1306_WHITE); display.setCursor(20, 25); display.setTextSize(2); display.print(F("TRK: ")); display.print(randomizeTrack + 1); display.setTextSize(1); display.setCursor(0, 50); display.println(F(" (Press to confirm)")); break; case UI_SCALE_EDIT: case UI_SCALE_NOTE_EDIT: case UI_SCALE_TRANSPOSE: display.println(F("EDIT SCALE")); display.drawLine(0, 8, 128, 8, SSD1306_WHITE); int totalItems = numScaleNotes + 5; // Back + Randomize + Notes + Add + Remove + Transpose int startIdx = 0; if (menuSelection >= 4) startIdx = menuSelection - 3; int y = 12; for (int i = startIdx; i < totalItems; i++) { if (y > 54) break; if (i == menuSelection) { display.fillRect(0, y, 75, 9, SSD1306_WHITE); display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); } else { display.setTextColor(SSD1306_WHITE); } display.setCursor(2, y + 1); if (i == 0) { display.print(F("Back")); } else if (i == 1) { display.print(F("Randomize")); } else if (i <= numScaleNotes + 1) { int noteIdx = i - 2; const char* noteNames[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}; display.print(noteNames[scaleNotes[noteIdx]]); if (currentState == UI_SCALE_NOTE_EDIT && i == menuSelection) { display.print(F(" <")); } } else if (i == numScaleNotes + 2) { display.print(F("Transpose")); if (currentState == UI_SCALE_TRANSPOSE) { display.print(F(" < >")); } } else if (i == numScaleNotes + 3) { display.print(F("Add Note")); } else if (i == numScaleNotes + 4) { display.print(F("Remove Note")); } y += 9; } // Piano Roll Preview int px = 82; int py = 20; int wk_w = 5; int wk_h = 20; int bk_w = 4; int bk_h = 12; // White keys: C, D, E, F, G, A, B int whiteNotes[] = {0, 2, 4, 5, 7, 9, 11}; for (int k = 0; k < 7; k++) { bool active = false; for (int j = 0; j < numScaleNotes; j++) { if (scaleNotes[j] == whiteNotes[k]) { active = true; break; } } if (active) display.fillRect(px + k*6, py, wk_w, wk_h, SSD1306_WHITE); else display.drawRect(px + k*6, py, wk_w, wk_h, SSD1306_WHITE); } // Black keys: C#, D#, F#, G#, A# int blackNotes[] = {1, 3, 6, 8, 10}; int blackOffsets[] = {3, 9, 21, 27, 33}; for (int k = 0; k < 5; k++) { bool active = false; for (int j = 0; j < numScaleNotes; j++) { if (scaleNotes[j] == blackNotes[k]) { active = true; break; } } int bx = px + blackOffsets[k]; display.fillRect(bx - 1, py - 1, bk_w + 2, bk_h + 2, SSD1306_BLACK); if (active) display.fillRect(bx, py, bk_w, bk_h, SSD1306_WHITE); else display.drawRect(bx, py, bk_w, bk_h, SSD1306_WHITE); } break; } display.display(); } void UIManager::drawMenu(int selection, UIState currentState, int midiChannel, int tempo, const char* flavourName, int queuedTheme, int currentThemeIndex, int numScaleNotes, const int* scaleNotes, int melodySeed, int numSteps, bool mutationEnabled, bool songModeEnabled, bool isPlaying, int randomizeTrack, const bool* trackMute) { // Calculate visual cursor position and scroll offset int visualCursor = 0; for(int i=0; i= MAX_LINES) { startVisualIndex = visualCursor - (MAX_LINES - 1); } int currentVisualIndex = 0; int y = 0; for (int i = 0; i < menuItemsCount; i++) { if (!isItemVisible(i)) continue; if (currentVisualIndex >= startVisualIndex) { if (y > 55) break; if (i == selection) { display.fillRect(0, y, 128, 9, SSD1306_WHITE); display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); } else { display.setTextColor(SSD1306_WHITE); } int x = 2 + (menuItems[i].indentLevel * 6); display.setCursor(x, y + 1); if (menuItems[i].isGroup) { display.print(menuItems[i].expanded ? F("v ") : F("> ")); } display.print(menuItems[i].label); MenuItemID id = menuItems[i].id; if (id == MENU_ID_CHANNEL) { display.print(F(": ")); display.print(midiChannel); } // Dynamic values if (id == MENU_ID_PLAYBACK) { display.print(F(": ")); display.print(isPlaying ? F("ON") : F("OFF")); } else if (id == MENU_ID_MELODY) { display.print(F(": ")); display.print(melodySeed); } else if (id == MENU_ID_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 (id == MENU_ID_TEMPO) { display.print(F(": ")); display.print(tempo); } else if (id == MENU_ID_STEPS) { display.print(F(": ")); display.print(numSteps); } else if (id == MENU_ID_SONG_MODE) { display.print(F(": ")); display.print(songModeEnabled ? F("ON") : F("OFF")); } else if (id == MENU_ID_TRACK_SELECT) { display.print(F(": ")); display.print(randomizeTrack + 1); } else if (id == MENU_ID_MUTE) { display.print(F(": ")); display.print(trackMute[randomizeTrack] ? F("YES") : F("NO")); } else if (id == MENU_ID_FLAVOUR) { display.print(F(": ")); display.print(flavourName); } else if (id == MENU_ID_MUTATION) { display.print(F(": ")); display.print(mutationEnabled ? F("ON") : F("OFF")); } else if (id == MENU_ID_PROTECTED_MODE) { display.print(F(": ")); display.print(protectedMode ? F("ON") : F("OFF")); } if (id >= MENU_ID_THEME_1 && id <= MENU_ID_THEME_7) { int themeIdx = id - MENU_ID_THEME_1 + 1; if (queuedTheme == themeIdx) display.print(F(" [NEXT]")); if (currentThemeIndex == themeIdx) display.print(F(" *")); } y += 9; } currentVisualIndex++; } } 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[][NUM_STEPS], int playbackStep, bool isPlaying, UIState currentState, bool songModeEnabled, int songRepeatsRemaining, bool sequenceChangeScheduled, PlayMode playMode, int selectedTrack, int numSteps, int numScaleNotes, const int* scaleNotes, const bool* trackMute) { pixels.clear(); const uint32_t COLOR_PLAYHEAD = pixels.Color(0, 255, 0); const uint32_t COLOR_PLAYHEAD_DIM = pixels.Color(0, 32, 0); const uint32_t COLOR_MUTED_PLAYHEAD = pixels.Color(0, 0, 255); const uint32_t COLOR_CURSOR = pixels.Color(255, 255, 255); const uint32_t COLOR_CURSOR_DIM = pixels.Color(32, 0, 0); if(playMode == MODE_POLY) { for(int t=0; t= numSteps) continue; int row = t * 2 + (s / 8); int col = s % 8; uint32_t color = 0; int note = sequence[t][s].note; if (note != -1) { color = getNoteColor(note, !sequence[t][s].accent); } if (isPlaying && s == playbackStep) { if (trackMute[t]) { color = COLOR_MUTED_PLAYHEAD; } else { color = (note != -1) ? COLOR_PLAYHEAD : COLOR_PLAYHEAD_DIM; } } pixels.setPixelColor(getPixelIndex(col, row), color); } } } else { // --- Mono Mode (original) --- const Step* trackSequence = sequence[selectedTrack]; for (int s = 0; s < NUM_STEPS; s++) { if (s >= numSteps) continue; int x = s % 8; int yBase = (s / 8) * 4; uint32_t color = 0, dimColor = 0; bool isCursorHere = (isPlaying && s == playbackStep); if (trackSequence[s].note != -1) { color = getNoteColor(trackSequence[s].note, trackSequence[s].tie); dimColor = getNoteColor(trackSequence[s].note, true); } uint32_t c[4] = {0}; if (trackSequence[s].note != -1) { int octave = trackSequence[s].note / 12; if (octave > 4) { c[0] = color; if (trackSequence[s].accent) c[1] = dimColor; } else if (octave < 4) { c[2] = color; if (trackSequence[s].accent) c[1] = dimColor; } else { c[1] = color; if (trackSequence[s].accent) { c[0] = dimColor; c[2] = dimColor; } } } uint32_t cursorColor = pixels.Color(0, 0, 50); if (isPlaying) { cursorColor = pixels.Color(0, 50, 0); if (songModeEnabled && s >= 8) { int repeats = min(songRepeatsRemaining, 8); if (x >= (8 - repeats)) cursorColor = (songRepeatsRemaining == 1 && x == 7 && (millis()/250)%2) ? pixels.Color(255, 200, 0) : pixels.Color(100, 220, 40); } } if (cursorColor != 0) { if (isCursorHere) { for(int i=0; i<4; i++) { if (c[i] == 0) c[i] = 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(); }