xevi/NuEVI/src/menu.cpp
2023-08-29 13:32:56 -05:00

1119 lines
No EOL
37 KiB
C++

#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_MPR121.h>
#include <Arduino.h>
#include <array>
#include <cstdio>
#include <algorithm>
#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<const char*, 128> 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<size_t N>
static void plotMenuEntries(std::array<MenuScreen const*, N> 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<size_t N>
class MainMenu : public MenuScreen {
public:
MainMenu(std::array<MenuScreen const*, N> 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<MenuScreen const*, N> _entries;
};
template<size_t N>
class SubMenu : public MenuScreen {
public:
SubMenu(const char* title, std::array<MenuScreen const*, N> 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<MenuScreen const*, N> _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<const char*, L> 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<const char*, L> _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<L, T, preset_t, preset_t*> {
public:
PresetValueMenu(
const char* title,
T preset_t::* value,
const T min, const T max, const bool wrap = false,
const std::array<const char*, L> labels = {}
) : ValueMenu<L, T, preset_t, preset_t*>(title, &state_t::currentPreset, value, min, max, wrap, labels) {}
};
template<
size_t L,
typename T
>
class StateValueMenu : public ValueMenu<L, T, instrument_state_t, instrument_state_t*> {
public:
StateValueMenu(
const char* title,
T instrument_state_t::* value,
const T min, const T max, const bool wrap = false,
const std::array<const char*, L> labels = {}
) : ValueMenu<L, T, instrument_state_t, instrument_state_t*>(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<T, N> choices,
const std::array<const char*, N> 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<T, N> _choices;
const std::array<const char*, N> _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<size_t N>
class AdjustMenuScreen : public MenuScreen {
public:
AdjustMenuScreen(const char* title, std::array<AdjustValue, N> 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<AdjustValue, N> _entries;
std::array<AdjustDrawing, N> _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<AdjustValue, 8> 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<const char*, 25> transposeLabels = {
"C>", "C#>", "D>", "D#>", "E>", "F>", "F#>", "G>", "G#>", "A>", "Bb>", "B>",
">C<", "<C#", "<D", "<D#", "<E", "<F", "<F#", "<G", "<G#", "<A", "<Bb", "<B", "<C"
};
const StateValueMenu<25, int8_t> 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<MenuScreen const*, 7> breathMenuEntries = {
&breathModeMenu,
&breathCCMenu,
&velocityMenu,
&curveMenu,
&velSmpDlMenu,
&velBiasMenu,
&breathIntervalMenu,
};
const SubMenu<7> breathMenu("BR SETUP", breathMenuEntries);
const std::array<MenuScreen const*, 13> controlMenuEntries = {
&fingeringMenu,
&rollerMenu,
&biteCtlMenu,
&biteCCMenu,
&leverCtlMenu,
&leverCCMenu,
&extraCtlMenu,
&extraCCMenu,
&portMenu,
&portLimitMenu,
&deglitchMenu,
&pinkyMenu,
&pitchBendMenu
};
const SubMenu<13> controlMenu("CTL SETUP", controlMenuEntries);
const std::array<MenuScreen const*, 5> vibratoMenuEntries = {
&vibDepthMenu,
&vibRetnMenu,
&vibDirMenu,
&vibSenseMenu,
&vibSquelchMenu,
};
const SubMenu<5> vibratoMenu("VIBRATO", vibratoMenuEntries);
const AboutMenu aboutMenu = AboutMenu();
std::array <MenuScreen const*, 4> extrasMenuEntries = {
&trill3Menu,
&cvTuneMenu,
&cvScaleMenu,
&cvVibMenu,
};
const SubMenu<4> extrasMenu("EXTRAS", extrasMenuEntries);
const ExitMenu exitMenu = ExitMenu();
// Top-level screens
const std::array<MenuScreen const*, 10> 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();
}
}
}