#include #include #include #include #include #include #include #include "menu.h" #include "hardware.h" #include "config.h" #include "settings.h" #include "globals.h" #include "midi.h" #include "led.h" // constants const unsigned long debounceDelay = 30; // the debounce time; increase if the output flickers static unsigned long menuTime = 0; bool displayOn = true; std::array CC_NAMES = { "Bank Select", // 0 "Mod Wheel", // 1 "Breath", // 2 "Undefined", // 3 "Foot Pedal", // 4 "Port. Time", // 5 "Data Entry", // 6 "Volume", // 7 "Balance", // 8 "Undefined", // 9 "Pan", // 10 "Expression", // 11 "Effect 1", // 12 "Effect 2", // 13 "Undefined", // 14 "Undefined", // 15 "GP 1", // 16 "GP 2", // 17 "GP 3", // 18 "GP 3", // 19 "Undefined", // 20 "Undefined", // 21 "Undefined", // 22 "Undefined", // 23 "Undefined", // 24 "Undefined", // 25 "Undefined", // 26 "Undefined", // 27 "Undefined", // 28 "Undefined", // 29 "Undefined", // 30 "Undefined", // 31 "LSB 0", // 32 "LSB 1", // 33 "LSB 2", // 34 "LSB 3", // 35 "LSB 4", // 36 "LSB 5", // 37 "LSB 6", // 38 "LSB 7", // 39 "LSB 8", // 40 "LSB 9", // 41 "LSB 10", // 42 "LSB 11", // 43 "LSB 12", // 44 "LSB 13", // 45 "LSB 14", // 46 "LSB 15", // 47 "LSB 16", // 48 "LSB 17", // 49 "LSB 18", // 50 "LSB 19", // 51 "LSB 20", // 52 "LSB 21", // 53 "LSB 22", // 54 "LSB 23", // 55 "LSB 24", // 56 "LSB 25", // 57 "LSB 26", // 58 "LSB 27", // 59 "LSB 28", // 60 "LSB 29", // 61 "LSB 30", // 62 "LSB 31", // 63 "Sustain", // 64 "Portamento", // 65 "Sostenuto", // 66 "Soft Pedal", // 67 "Legato", // 68 "Hold 2", // 69 "Variation", // 70 "Resonance", // 71 "Release", // 72 "Attack", // 73 "Cutoff", // 74 "Sound 6", // 75 "Sound 7", // 76 "Sound 8", // 77 "Sound 9", // 78 "Sound 10", // 79 "Decay", // 80 "Hi Pass", // 81 "GP Button 3", // 82 "GP Button 4", // 83 "Port. Amount", // 84 "Undefined", // 85 "Undefined", // 86 "Undefined", // 87 "Undefined", // 88 "Undefined", // 89 "Undefined", // 90 "Reverb", // 91 "Tremolo", // 92 "Chorus", // 93 "Detune", // 94 "Phaser", // 95 "Data Inc", // 96 "Data Dec", // 97 "NRPN LSB", // 98 "NRPN MSB", // 99 "RP LSB", // 100 "RP MSB", // 101 "Undefined", // 102 "Undefined", // 103 "Undefined", // 104 "Undefined", // 105 "Undefined", // 106 "Undefined", // 107 "Undefined", // 108 "Undefined", // 109 "Undefined", // 110 "Undefined", // 111 "Undefined", // 112 "Undefined", // 113 "Undefined", // 114 "Undefined", // 115 "Undefined", // 116 "Undefined", // 117 "Undefined", // 118 "Undefined", // 119 "All Sound Off", // 120 "All CCC Off", // 121 "Keyboard On", // 122 "All Notes Off", // 123 "Omni Mode Off", // 124 "Omni Mode On", // 125 "Mono", // 126 "Poly Mode", // 127 }; // 'NuEVI' or 'NuRAD' logo #define LOGO16_GLCD_WIDTH 128 #define LOGO16_GLCD_HEIGHT 64 static const unsigned char PROGMEM nuevi_logo_bmp[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xe3, 0x60, 0x00, 0x07, 0x73, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xe3, 0x60, 0x00, 0x0e, 0xe3, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x03, 0x60, 0x00, 0x1d, 0xc3, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xbf, 0xff, 0xff, 0xe3, 0x60, 0x00, 0x3b, 0x83, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xbf, 0xff, 0xff, 0xe3, 0x60, 0x00, 0x77, 0x03, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xb0, 0x00, 0x00, 0x03, 0x60, 0x00, 0xee, 0x03, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xb0, 0x00, 0x00, 0x03, 0x60, 0x01, 0xdc, 0x03, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xb0, 0x00, 0x00, 0x03, 0x60, 0x03, 0xb8, 0x03, 0x60, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x01, 0xb0, 0x00, 0x00, 0x03, 0x60, 0x07, 0x70, 0x03, 0x60, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x01, 0xbf, 0xff, 0xff, 0xe3, 0x60, 0x0e, 0xe0, 0x03, 0x60, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x01, 0xbf, 0xff, 0xff, 0xe3, 0x60, 0x1d, 0xc0, 0x03, 0x60, 0x00, 0x00, 0x03, 0x00, 0x60, 0x00, 0x01, 0x80, 0x00, 0x00, 0x03, 0x60, 0x3b, 0x80, 0x03, 0x60, 0x00, 0x00, 0x03, 0x00, 0xe0, 0x00, 0x01, 0xbf, 0xff, 0xff, 0xe3, 0x60, 0x77, 0x00, 0x03, 0x60, 0x00, 0x00, 0x03, 0x00, 0xc0, 0x00, 0x01, 0xbf, 0xff, 0xff, 0xe3, 0x60, 0xee, 0x00, 0x03, 0x60, 0x00, 0x00, 0x03, 0x80, 0xc0, 0x00, 0x01, 0xb0, 0x00, 0x00, 0x03, 0x61, 0xdc, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x80, 0xc0, 0x00, 0x01, 0xb0, 0x00, 0x00, 0x03, 0x63, 0xb8, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0xc0, 0xc0, 0x00, 0x01, 0xb0, 0x00, 0x00, 0x03, 0x67, 0x70, 0x00, 0x03, 0x60, 0x00, 0x00, 0x06, 0xc0, 0xc0, 0x00, 0x01, 0xb0, 0x00, 0x00, 0x03, 0x6e, 0xe0, 0x00, 0x03, 0x60, 0x00, 0x00, 0x06, 0x60, 0xc1, 0x01, 0x01, 0xb0, 0x00, 0x00, 0x03, 0x7d, 0xc0, 0x00, 0x03, 0x60, 0x00, 0x00, 0x06, 0x30, 0xc3, 0x03, 0x01, 0xbf, 0xff, 0xff, 0xe3, 0x7b, 0x80, 0x00, 0x03, 0x60, 0x00, 0x00, 0x0c, 0x30, 0xc3, 0x07, 0x01, 0xbf, 0xff, 0xff, 0xe3, 0x77, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x0c, 0x1c, 0xc3, 0x06, 0x01, 0x80, 0x00, 0x00, 0x03, 0x0e, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x0c, 0x0c, 0xc2, 0x0e, 0x01, 0xff, 0xff, 0xff, 0xe3, 0xfc, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x0c, 0x0e, 0xc6, 0x1e, 0x01, 0xff, 0xff, 0xff, 0xe3, 0xf8, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x0c, 0x07, 0xc6, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x03, 0xc6, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x01, 0xc7, 0xe6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0xc7, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x03, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; #define OLED_RESET 4 Adafruit_SSD1306 display(128, 64, &Wire1, OLED_RESET, 1000000, 1000000); int16_t ctouchVal = 0; MenuScreen* currentMenu = NULL; // Track pixels for faster redrawing struct AdjustDrawing { int row; int thrX; int maxX; int valX; }; struct AdjustValue { const char* title; const int16_t instrument_state_t::* value; int16_t calibration_t::* thrVal; int16_t calibration_t::* maxVal; const int16_t limitLow; const int16_t limitHigh; // If not null, thr and max are relative to zeroPoint const int16_t instrument_state_t::* zeroPoint; }; void autoCalSelected() { } static void drawIndicator(int x, int row, int color) { display.fillTriangle(x - 2, row + 1, x + 2, row + 1, x, row + 3, color); display.fillTriangle(x - 2, row + 10, x + 2, row + 10, x, row + 7, color); } static void drawAdjustIndicators(state_t& state, const AdjustValue& value, AdjustDrawing& drawing) { const int thrX = mapConstrain(state.calibration->*value.thrVal, value.limitLow, value.limitHigh, 1, 127); const int maxX = mapConstrain(state.calibration->*value.maxVal, value.limitLow, value.limitHigh, 1, 127); if (drawing.maxX != maxX) { drawIndicator(drawing.thrX, drawing.row, BLACK); drawIndicator(thrX, drawing.row, WHITE); drawing.maxX = maxX; } if (drawing.thrX != thrX) { drawIndicator(drawing.thrX, drawing.row, BLACK); drawIndicator(thrX, drawing.row, WHITE); drawing.thrX = thrX; } } static void drawAdjustTitle(const AdjustValue& value, AdjustDrawing& drawing, bool highlight) { display.setTextSize(1); if (highlight) { display.setTextColor(BLACK, WHITE); } else { display.setTextColor(WHITE, BLACK); } display.setCursor(0, drawing.row); display.println(value.title); } static void drawAdjustValues(state_t& state, const AdjustValue& value, AdjustDrawing& drawing) { char buffer[13]; snprintf(buffer, 13, "%d>%d<%d", value.thrVal, value.value, value.maxVal); display.setTextSize(1); display.setCursor(128 - 6 * strlen(buffer), drawing.row); display.println(buffer); const int valX = mapConstrain(state.instrument->*value.value, value.limitLow, value.limitHigh, 1, 127); if (drawing.valX != valX) { display.drawFastVLine(drawing.valX, drawing.row + 4, 4, BLACK); display.drawFastVLine(valX, drawing.row + 4, 4, WHITE); drawing.valX = valX; } } void drawAdjustFrame(int line) { display.drawLine(25, line, 120, line, WHITE); // Top line display.drawLine(25, line + 12, 120, line + 12, WHITE); // Bottom line display.drawLine(25, line + 1, 25, line + 2, WHITE); display.drawLine(120, line + 1, 120, line + 2, WHITE); display.drawLine(120, line + 10, 120, line + 11, WHITE); display.drawLine(25, line + 10, 25, line + 11, WHITE); } void drawAdjustRow(state_t& state, const AdjustValue& value, AdjustDrawing& drawing, bool highlight) { display.fillRect(0, drawing.row, 128, 21, BLACK); drawAdjustFrame(drawing.row); drawAdjustTitle(value, drawing, highlight); drawAdjustValues(state, value, drawing); drawAdjustIndicators(state, value, drawing); } void plotSubOption(const char* label, const char* unit = nullptr) { int text_x, unit_x; int label_pixel_width = strlen(label) * 12; display.setTextColor(WHITE); if (unit == nullptr) { text_x = 96 - (label_pixel_width / 2); } else { int unit_pixel_width = strlen(unit) * 6; int halfSum = (label_pixel_width + unit_pixel_width) / 2; text_x = 96 - halfSum; unit_x = 96 + halfSum - unit_pixel_width; display.setCursor(unit_x, 40); display.setTextSize(1); display.println(unit); } display.setTextSize(2); display.setCursor(text_x, 33); display.println(label); } template static void plotMenuEntries(std::array entries, size_t cursorPos, int xOffset = 0) { display.fillRect(0 + xOffset, MENU_HEADER_OFFSET, 63 + xOffset, 64 - MENU_HEADER_OFFSET, BLACK); display.setTextSize(1); int scrollPos = 0; if (entries.size() >= MENU_NUM_ROWS) { if ((cursorPos - scrollPos) > (MENU_NUM_ROWS - 2)) { scrollPos = cursorPos - (MENU_NUM_ROWS - 2); } else if ((cursorPos - scrollPos) < 1) { scrollPos = cursorPos - 1; } scrollPos = constrain(scrollPos, 0, entries.size() - MENU_NUM_ROWS); } size_t row = 0; size_t end = constrain(scrollPos + MENU_NUM_ROWS, 0, entries.size()); for (size_t i = scrollPos; i < end; i++, row++) { int rowPixel = (row)*MENU_ROW_HEIGHT + MENU_HEADER_OFFSET; display.setCursor(xOffset, rowPixel); if (cursorPos == i) { display.setTextColor(BLACK, WHITE); } else { display.setTextColor(WHITE); } display.println(entries[i]->title()); } } class AboutMenu : public MenuScreen { const char* title() { return "ABOUT"; } bool update(state_t& state, InputState& input, bool redraw) { if (redraw) { display.fillRect(63, 11, 64, 52, BLACK); display.setTextColor(WHITE); display.setTextSize(0); display.setCursor(64, 12); display.print("xEVI"); display.setCursor(64, 21); display.print("firmware"); display.setCursor(74, 30); display.print("v"); display.print(FIRMWARE_VERSION); display.setCursor(64, 39); display.print("eeprom"); display.setCursor(74, 48); display.print("v"); display.print(EEPROM_VERSION); return true; } } }; template class MainMenu : public MenuScreen { public: MainMenu(std::array entries) : _entries(entries) {} bool update(state_t& state, InputState& input, bool redraw) { bool dirty = redraw; if (input.changed && input.knobMenu) { _cursorPos = (_cursorPos + input.knobMenu + _entries.size()) % _entries.size(); input.changed = false; _entries[_cursorPos]->update(state, input, true); dirty = true; draw(redraw); } else { dirty |= _entries[_cursorPos]->update(state, input, redraw); if (redraw) { draw(redraw); dirty = true; } } return dirty; }; const char* title() { return "MENU"; } private: void draw(bool redraw) { if (redraw) { Serial.println("main menu redraw"); display.clearDisplay(); display.setTextSize(1); display.setCursor(0, 0); display.drawLine(0, MENU_ROW_HEIGHT, 127, MENU_ROW_HEIGHT, WHITE); display.println("MENU"); } plotMenuEntries(_entries, _cursorPos); } size_t _cursorPos = 0; std::array _entries; }; template class SubMenu : public MenuScreen { public: SubMenu(const char* title, std::array entries) : _title(title), _entries(entries) {} bool update(state_t& state, InputState& input, bool redraw) { bool redrawValue = false; bool dirty = false; return false; if (input.changed && input.btnMenu) { _focused = !_focused; if (_focused) { lastMenu = currentMenu; currentMenu = this; redraw = true; } else { currentMenu = lastMenu; redraw = true; } } if (_focused && input.changed && input.knobMenu != 0) { _cursorPos = (_cursorPos + input.knobMenu) % _entries.size(); draw(false); input.changed = false; redrawValue = true; dirty = true; } if (redraw) { draw(redraw); dirty = true; } if (_focused) { dirty |= _entries[_cursorPos]->update(state, input, redrawValue); } return dirty; }; const char* title() { return _title; } private: const char* _title; size_t _cursorPos; std::array _entries; bool _focused = false; MenuScreen* lastMenu; void draw(bool redraw) { if (redraw) { if (_focused) { display.clearDisplay(); } else { display.fillRect(63, 11, 64, 52, BLACK); } display.setTextSize(1); display.drawLine(_focused ? 0 : 63, MENU_ROW_HEIGHT, 127, MENU_ROW_HEIGHT, WHITE); display.setCursor(_focused ? 0 : 63, 0); display.println(_title); } plotMenuEntries(_entries, _cursorPos, _focused ? 0 : 63); } }; template< size_t L, typename T, typename U, typename U_p > class ValueMenu : public MenuScreen { public: ValueMenu( const char* title, U_p state_t::* obj, T U::* value, const T min, const T max, const bool wrap = false, const std::array labels = {} ) : _title(title), _obj(obj), _value(value), _min(min), _max(max), _wrap(wrap), _labels(labels) { } bool update(state_t& state, InputState& input, bool redraw) { if (input.knobVal1) { int32_t newVal = (state.*_obj)->*_value + input.knobVal1; if (newVal > _max) { if (_wrap) { newVal = _min; } else { newVal = _max; } } else if (newVal < _min) { if (_wrap) { newVal = _max; } else { newVal = _min; } } (state.*_obj)->*_value = newVal; draw(state, redraw); return true; } else if (redraw) { draw(state, redraw); return true; } return false; } const char* title() { return _title; } private: const char* _title; U_p state_t::* _obj; T U::* _value; const T _min; const T _max; const bool _wrap; const std::array _labels; void draw(state_t &state, bool redraw) { if (redraw) { display.fillRect(63, 11, 64, 52, BLACK); display.drawRect(63, 11, 64, 52, WHITE); display.setTextSize(1); int len = strlen(this->_title); display.setCursor(95 - len * 3, 15); display.println(this->_title); } else { display.fillRect(64, 33, 62, 29, BLACK); } char buffer[12]; snprintf(buffer, 12, "%+d", (state.*_obj)->*_value); size_t label_idx = (state.*_obj)->*_value - _min; if (_labels.size() > 0 && label_idx >= 0 && label_idx < _labels.size()) { plotSubOption(_labels[label_idx], buffer); } else { plotSubOption(buffer); } } }; template< size_t L, typename T > class PresetValueMenu : public ValueMenu { public: PresetValueMenu( const char* title, T preset_t::* value, const T min, const T max, const bool wrap = false, const std::array labels = {} ) : ValueMenu(title, &state_t::currentPreset, value, min, max, wrap, labels) {} }; template< size_t L, typename T > class StateValueMenu : public ValueMenu { public: StateValueMenu( const char* title, T instrument_state_t::* value, const T min, const T max, const bool wrap = false, const std::array labels = {} ) : ValueMenu(title, &state_t::instrument, value, min, max, wrap, labels) {} }; template< size_t N, typename T > class ChoiceMenu : public MenuScreen { public: ChoiceMenu( const char* title, T preset_t::* value, std::array choices, const std::array labels = {} ) : _title(title), _value(value), _choices(choices), _labels(labels) { } bool update(state_t& state, InputState& input, bool redraw) { auto it = std::find(_choices.begin(), _choices.end(), state.currentPreset->*_value); if (input.knobVal1) { it += input.knobVal1; if (it >= _choices.end()) { state.currentPreset->*_value = _choices.front(); it = _choices.begin(); } else if (it < _choices.begin()) { state.currentPreset->*_value = _choices.back(); it = _choices.end(); } else { state.currentPreset->*_value = _choices[it]; } draw(redraw, it - _choices.begin()); return true; } else if (redraw) { draw(redraw, it - _choices.begin()); return true; } return false; } const char* title() { return _title; } private: const char* _title; T preset_t::*_value; const std::array _choices; const std::array _labels; void draw(bool redraw, size_t label_idx) { if (redraw) { display.fillRect(63, 11, 64, 52, BLACK); display.drawRect(63, 11, 64, 52, WHITE); display.setTextSize(1); int len = strlen(this->_title); display.setCursor(95 - len * 3, 15); display.println(this->_title); } char buffer[12]; snprintf(buffer, 12, "%+d", _value); if (_labels.size() > 0 && label_idx >= 0 && label_idx <= _labels.size()) { plotSubOption(_labels[label_idx], buffer); } else { plotSubOption(buffer); } } }; class StatusMenu : public MenuScreen { public: StatusMenu(MenuScreen& mainMenu) : _mainMenu(mainMenu) { Serial.println("init statusmenu"); } bool update(state_t& state, InputState& input, bool redraw) { // TODO: handle cc/patch/preset changes Serial.println("status update"); if (input.changed && input.btnMenu) { Serial.println("menu"); currentMenu = &_mainMenu; input.changed = false; return currentMenu->update(state, input, true); } return false; }; private: MenuScreen& _mainMenu; }; class ExitMenu : public MenuScreen { public: ExitMenu() {} bool update(state_t& state, InputState& input, bool redraw) { if (redraw) { display.fillRect(63, 11, 64, 52, BLACK); } if (input.changed && input.btnMenu) { displayOff(); } return false; } const char* title() { return "EXIT"; } }; template class AdjustMenuScreen : public MenuScreen { public: AdjustMenuScreen(const char* title, std::array entries) : _title(title), _entries(entries) {} bool update(state_t& state, InputState& input, bool redraw) { if (input.changed && input.btnMenu) { _focused = !_focused; } if (!_focused) { display.fillRect(63, 11, 64, 52, BLACK); return true; } bool redrawIndicators = false; if (input.changed) { if (input.knobMenu) { _selectedEntry = _selectedEntry + input.knobMenu; redraw = true; } AdjustValue value = _entries[_selectedEntry]; if (input.knobVal1) { state.calibration->*value.thrVal += input.knobVal1; state.calibration->*value.thrVal = constrain(state.calibration->*value.thrVal, value.limitLow, value.limitHigh); redrawIndicators = true; } if (input.knobVal2) { state.calibration->*value.maxVal += input.knobVal2; state.calibration->*value.maxVal = constrain(state.calibration->*value.maxVal, value.limitLow, value.limitHigh); redrawIndicators = true; } } else { draw(state, redrawIndicators, redraw); } return true; } const char* title() { return _title; } private: const char* _title; size_t _selectedEntry = 0; std::array _entries; std::array _rowDrawings; bool _focused = false; void draw(state_t& state, bool redrawIndicators, bool redraw) { size_t scrollPos = 0; if (_entries.size() >= ADJUST_NUM_ROWS) { if ((_selectedEntry - scrollPos) > (ADJUST_NUM_ROWS - 2)) { scrollPos = _selectedEntry - (ADJUST_NUM_ROWS - 2); } else if ((_selectedEntry - scrollPos) < 1) { scrollPos = _selectedEntry - 1; } scrollPos = constrain(scrollPos, 0, _entries.size() - ADJUST_NUM_ROWS); } int end = constrain(scrollPos + ADJUST_NUM_ROWS, 0, N); for (size_t i = scrollPos; i < end; i++) { if (redraw) { drawAdjustRow(state, _entries[i], _rowDrawings[i], i == _selectedEntry); } else if (redrawIndicators && i == _selectedEntry) { drawAdjustIndicators(state, _entries[i], _rowDrawings[i]); } else { drawAdjustValues(state, _entries[i], _rowDrawings[i]); } } } }; std::array adjustValues = { { {"BREATH", &instrument_state_t::breathSignal, &calibration_t::breathThrValOffset, &calibration_t::breathMaxValOffset, BREATH_LO_LIMIT, BREATH_HI_LIMIT, &instrument_state_t::breathZero}, {"BR ALT", &instrument_state_t::breathAltSignal, &calibration_t::breathAltThrValOffset, &calibration_t::breathAltMaxValOffset, BREATH_LO_LIMIT, BREATH_HI_LIMIT, &instrument_state_t::breathAltZero}, {"BITE",&instrument_state_t::biteSignal, &calibration_t::biteThrVal, &calibration_t::biteMaxVal, BITE_LO_LIMIT, BITE_HI_LIMIT, NULL}, {"PB DOWN",&instrument_state_t::pbDnSignal, &calibration_t::pbDnThrVal, &calibration_t::pbDnMaxVal, PITCHB_LO_LIMIT, PITCHB_HI_LIMIT, NULL}, {"PB UP", &instrument_state_t::pbUpSignal, &calibration_t::pbUpThrVal, &calibration_t::pbUpMaxVal, PITCHB_LO_LIMIT, PITCHB_HI_LIMIT, NULL}, {"EXTRA", &instrument_state_t::extraSignal, &calibration_t::extraThrVal, &calibration_t::extraMaxVal, EXTRA_LO_LIMIT, EXTRA_HI_LIMIT, NULL}, {"LEVER", &instrument_state_t::leverSignal, &calibration_t::leverThrVal, &calibration_t::leverMaxVal, LEVER_LO_LIMIT, LEVER_HI_LIMIT, NULL}, {"TOUCH", &instrument_state_t::avgCTouchSignal, &calibration_t::ctouchThrVal, &calibration_t::ctouchThrVal, CTOUCH_LO_LIMIT, CTOUCH_HI_LIMIT, NULL}, }}; const AdjustMenuScreen<8> adjustMenu("ADJUST", adjustValues); //*********************************************************** const ChoiceMenu<4, BreathMode> breathModeMenu("BREATH MODE", &preset_t::breathMode, { { BREATH_STD, BREATH_LSB, BREATH_AT, BREATH_LSB_AT} }, { { "STANDARD", "LSB", "AT", "LSB_AT" } }); const PresetValueMenu<128, uint8_t> breathCCMenu("BREATH CC", &preset_t::breathCC, 0, 127, true, CC_NAMES); const PresetValueMenu<1, uint8_t> velocityMenu("VELOCITY", &preset_t::fixedVelocity, 0, 127, true, { { "DYN" } }); const PresetValueMenu<0, uint8_t> curveMenu("CURVE", &preset_t::breathCurve, 0, 12); // TODO: curve display const PresetValueMenu<1, uint8_t> velSmpDlMenu("VEL DELAY", &preset_t::velSmpDl, 0, 30, true, { "OFF" }); // TODO: unit ms const PresetValueMenu<1, uint8_t> velBiasMenu("VEL BOOST", &preset_t::velBias, 0, 30, true, { "OFF" }); const PresetValueMenu<0, uint8_t> breathIntervalMenu("BR INTERV", &preset_t::breathInterval, 0, 30, true); const PresetValueMenu<0, int8_t> trill3Menu("TRILL3", &preset_t::trill3_interval, 3, 4, true, {}); const PresetValueMenu<0, int8_t> cvTuneMenu("CV Tune", &preset_t::cvTune, -100, 100, false, {}); const PresetValueMenu<0, uint8_t> cvVibMenu("CV Vib LFO", &preset_t::cvVibRate, 0, 8, false, {}); const PresetValueMenu<0, int8_t> cvScaleMenu("CV SCALING", &preset_t::cvScale, -100, 100, false, {}); const std::array transposeLabels = { "C>", "C#>", "D>", "D#>", "E>", "F>", "F#>", "G>", "G#>", "A>", "Bb>", "B>", ">C<", " transposeMenu("TRANSPOSE", &instrument_state_t::transpose, -12, 12, true, transposeLabels); const StateValueMenu<0, int8_t> octaveMenu("OCTAVE", &instrument_state_t::octave, -3, 3, true, {}); const PresetValueMenu<0, byte> midiMenu("MIDI CH", &preset_t::MIDIchannel, 1, 16, false, {}); const PresetValueMenu<1, uint8_t> vibDepthMenu("DEPTH", &preset_t::vibratoDepth, 0, 9, true, { "OFF" }); const PresetValueMenu<1, uint8_t> vibRetnMenu("RETURN", &preset_t::vibRetn, 0, 4, true, { "OFF" }); const PresetValueMenu<0, uint8_t> vibSenseMenu("SENSE LVR", &preset_t::vibSens, 0, 12); const PresetValueMenu<0, uint8_t> vibSquelchMenu("SQUELCH LVR", &preset_t::vibSquelch, 0, 12); const ChoiceMenu<2, VibratoMode> vibDirMenu("DIRECTION", &preset_t::vibratoMode, { {VSTART_DOWN, VSTART_UP} }, { "START DOWN", "START UP" }); const ChoiceMenu<4, ExtraControl> biteCtlMenu("BITE CTL", &preset_t::biteControl, { {OFF, VIBRATO, GLIDE, CC} }, { { "OFF", "VIBRATO", "GLIDE", "CC" } }); const PresetValueMenu<128, uint8_t> biteCCMenu("BITE CC", &preset_t::biteCC, 0, 127, true, CC_NAMES); const ChoiceMenu<4, ExtraControl> leverCtlMenu("LEVER CTL", &preset_t::leverControl, { {OFF, VIBRATO, GLIDE, CC} }, { "OFF", "VIBRATO", "GLIDE", "CC" }); const PresetValueMenu<128, uint8_t> leverCCMenu("LEVER CC", &preset_t::leverCC, 0, 127, true, CC_NAMES); const ChoiceMenu<4, PortamentoMode> portMenu("GLIDE MOD", &preset_t::portamentoMode, { {POFF, PON, PSWITCH_ONLY, PGLIDE_ONLY} }, { "OFF", "ON", "SWITCH_ONLY", "GLIDE_ONLY", }); const PresetValueMenu<0, uint8_t> portLimitMenu("GLIDE LMT", &preset_t::portamentoLimit, 1, 127, true); const PresetValueMenu<0, uint8_t> pitchBendMenu("PITCHBEND", &preset_t::PBdepth, 0, 12, true); const ChoiceMenu<4, ExtraControl> extraCtlMenu("EXCT CC A", &preset_t::extraControl, { {OFF, VIBRATO, GLIDE, CC} }, { "OFF", "ON", "SWITCH_ONLY", "GLIDE_ONLY", }); const PresetValueMenu<128, uint8_t> extraCCMenu("EXCT CC", &preset_t::extraCC, 0, 4, true, CC_NAMES); const PresetValueMenu<1, uint8_t> deglitchMenu("DEGLITCH", &preset_t::deglitch, 0, 70, true, { "OFF" }); const PresetValueMenu<29, uint8_t> pinkyMenu("PINKY KEY", &preset_t::pinkySetting, 0, 29, true, { "-12", "-11", "-10", "-9", "-8", "-7", "-6", "-5", "-4", "-3", "-2", "-1", "PB/2", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "PBD", "GLD", "MOD", "QTN" }); const ChoiceMenu<4, FingeringMode> fingeringMenu("FINGERING", &preset_t::fingering, { EVI, EVR, TPT, HRN }, { "EVI", "EVR", "TPT", "HRN", }); const ChoiceMenu<6, RollerMode> rollerMenu("ROLLRMODE", &preset_t::rollerMode, { HIGHEST, HIGHEST_EXTEND, HIGHEST_PAIR, HIGHEST_PAIR_EXTEND, PARTIAL, PARTIAL_EXTEND }, { "HIGHEST", "HIGHEST_EXTEND", "HIGHEST_PAIR", "HIGHEST_PAIR_EXTEND", "PARTIAL", "PARTIAL_EXTEND", }); std::array breathMenuEntries = { &breathModeMenu, &breathCCMenu, &velocityMenu, &curveMenu, &velSmpDlMenu, &velBiasMenu, &breathIntervalMenu, }; const SubMenu<7> breathMenu("BR SETUP", breathMenuEntries); const std::array controlMenuEntries = { &fingeringMenu, &rollerMenu, &biteCtlMenu, &biteCCMenu, &leverCtlMenu, &leverCCMenu, &extraCtlMenu, &extraCCMenu, &portMenu, &portLimitMenu, °litchMenu, &pinkyMenu, &pitchBendMenu }; const SubMenu<13> controlMenu("CTL SETUP", controlMenuEntries); const std::array vibratoMenuEntries = { &vibDepthMenu, &vibRetnMenu, &vibDirMenu, &vibSenseMenu, &vibSquelchMenu, }; const SubMenu<5> vibratoMenu("VIBRATO", vibratoMenuEntries); const AboutMenu aboutMenu = AboutMenu(); std::array extrasMenuEntries = { &trill3Menu, &cvTuneMenu, &cvScaleMenu, &cvVibMenu, }; const SubMenu<4> extrasMenu("EXTRAS", extrasMenuEntries); const ExitMenu exitMenu = ExitMenu(); // Top-level screens const std::array mainMenuEntries = { &transposeMenu, &octaveMenu, &midiMenu, &breathMenu, &controlMenu, &vibratoMenu, &adjustMenu, &extrasMenu, &aboutMenu, &exitMenu, }; MainMenu<10> mainMenu(mainMenuEntries); StatusMenu statusMenu = StatusMenu(mainMenu); /* static void curveCustomDraw() { const char* curveMenuLabels[] = { "-4", "-3", "-2", "-1", "LIN", "+1", "+2", "+3", "+4", "S1", "S2", "Z1", "Z2" }; int y0 = 0, x0 = 0; int scale = ((1 << 14) - 1) / 60; for (int x = x0; x < 60; x += 1) { int y = multiMap(x * scale, curveIn, curves[currentPreset->breathCurve], 17); y = (y * 37) / ((1 << 14) - 1); display.drawLine(x0 + 65, 60 - y0, x + 65, 60 - y, WHITE); x0 = x; y0 = y; } display.setCursor(125 - 3 * 6, 60 - 8); display.setTextSize(0); display.print(curveMenuLabels[currentPreset->breathCurve]); } */ static bool updateSensorPixelsFlag = false; void drawSensorPixels() { updateSensorPixelsFlag = true; } //*********************************************************** static InputState readInput(uint32_t timeNow) { static uint32_t lastDebounceTime = 0; // the last time the output pin was toggled static uint8_t lastDeumButtons = 0; static uint8_t deumButtonState = 0; static int lastKnobs[] = { 0, 0, 0, 0 }; InputState input; uint8_t deumButtons = buttonState(); // Debounce buttons if (deumButtons != lastDeumButtons) { lastDebounceTime = timeNow; } if ((timeNow - lastDebounceTime) > debounceDelay) { if (deumButtons != deumButtonState) { input.btnMenu = deumButtons & BTN_MENU; input.btnVal1 = deumButtons & BTN_VAL1; input.btnVal2 = deumButtons & BTN_VAL2; input.btnPreset = deumButtons & BTN_PRESET; input.changed = true; deumButtonState = deumButtons; menuTime = timeNow; } } for (int i = 0; i < 4; i++) { int val = readKnob(i); if (val != lastKnobs[i]) { input.changed = true; switch (i) { case 0: input.knobMenu = val; break; case 1: input.knobVal1 = val; break; case 2: input.knobVal2 = val; break; case 3: input.knobPreset = val; break; } menuTime = timeNow; } } // save the reading. Next time through the loop, it'll be the lastButtonState: lastDeumButtons = deumButtons; return input; } void displayOff() { display.clearDisplay(); display.display(); display.ssd1306_command(SSD1306_DISPLAYOFF); currentMenu = &statusMenu; displayOn = false; } void displayError(const char* error) { display.clearDisplay(); display.setTextColor(WHITE); display.setTextSize(1); display.setCursor(0, 0); display.println(error); display.display(); Serial.print("ERROR: "); Serial.println(error); } void showVersion() { display.setTextColor(WHITE); display.setTextSize(1); display.setCursor(85, 52); display.print("v"); display.println(FIRMWARE_VERSION); display.display(); } void initDisplay() { // by default, we'll generate the high voltage from the 3.3v line internally! (neat!) display.begin(SSD1306_SWITCHCAPVCC, 0x3D); // initialize with the I2C addr 0x3D (for the 128x64) // Show image buffer on the display hardware. // Since the buffer is intialized with an Adafruit splashscreen // internally, this will display the splashscreen. display.clearDisplay(); display.drawBitmap(0, 0, nuevi_logo_bmp, LOGO16_GLCD_WIDTH, LOGO16_GLCD_HEIGHT, 1); display.display(); currentMenu = &statusMenu; } void handleMenu(state_t& state, bool draw) { unsigned long timeNow = millis(); InputState input = readInput(timeNow); // shut off menu system if not used for a while if (displayOn && ((timeNow - menuTime) > MENU_AUTO_OFF_TIME)) { displayOff(); } if (input.changed) { display.ssd1306_command(SSD1306_DISPLAYON); displayOn = true; } if (currentMenu && (draw || input.changed)) { if (currentMenu->update(state, input, false)) { display.display(); } } }