about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorScott Blomquist <scott@blomqui.st>2025-03-23 14:04:17 -0700
committerScott Blomquist <scott@blomqui.st>2025-03-23 14:14:29 -0700
commita79c6aa7a6eaaac246b775bbd34c1e1e8378fac8 (patch)
treeecde515d4a5d69468de5ab3e1f9945764a07761f /src
parent8f648008f9c468ae109d6a065a27386d443957cd (diff)
downloadHexBoard-a79c6aa7a6eaaac246b775bbd34c1e1e8378fac8.tar.gz
Firmware 1.1 BETA 1
Diffstat (limited to 'src')
-rw-r--r--src/HexBoard.ino4764
1 files changed, 4764 insertions, 0 deletions
diff --git a/src/HexBoard.ino b/src/HexBoard.ino
new file mode 100644
index 0000000..a4881b5
--- /dev/null
+++ b/src/HexBoard.ino
@@ -0,0 +1,4764 @@
+// @readme
+  /*
+    HexBoard
+    Copyright 2022-2025 Jared DeCook and Zach DeCook
+    with help from Nicholas Fox
+    Licensed under the GNU GPL Version 3.
+
+    Hardware information:
+      Generic RP2040 running at 133MHz with 16MB of flash
+        https://github.com/earlephilhower/arduino-pico
+      Additional board manager URL:
+        https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
+      Tools > USB Stack > (Adafruit TinyUSB)
+      Sketch > Export Compiled Binary
+
+    Compilation instructions:
+      Using arduino-cli...
+        # Download the board index
+        arduino-cli --additional-urls=https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json core update-index
+        # Install the core for rp2040
+        arduino-cli --additional-urls=https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json core download rp2040:rp2040
+        arduino-cli --additional-urls=https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json core install rp2040:rp2040
+        # Install libraries
+        arduino-cli lib install "MIDI library"
+        arduino-cli lib install "Adafruit NeoPixel"
+        arduino-cli lib install "U8g2" # dependency for GEM
+        arduino-cli lib install "Adafruit GFX Library" # dependency for GEM
+        arduino-cli lib install "GEM"
+        sed -i 's@#include "config/enable-glcd.h"@//\0@g' ~/Arduino/libraries/GEM/src/config.h # remove dependency from GEM - I think this is unnecessary now.
+        # Run Make to build the firmware
+        make
+    ---------------------------
+    New to programming Arduino?
+    ---------------------------
+    Coding the Hexboard is, basically, done in C++.
+
+    When the HexBoard is plugged in, it runs
+    void setup() and void setup1(), then
+    runs void loop() and void loop1() on an
+    infinite loop until the HexBoard powers down.
+    There are two cores running independently.
+    You can pretend that the compiler tosses
+    these two routines inside an int main() for
+    each processor.
+
+    To #include libraries, the Arduino
+    compiler expects them to be installed from
+    a centralized repository. You can also bring
+    your own .h / .cpp code but it must be saved
+    in "/src/____/___.h" to be valid.
+
+    We found this really annoying so to the
+    extent possible we have consolidated
+    this code into one single .ino sketch file.
+    However, the code is sectioned into something
+    like a library format for each feature
+    of the HexBoard, so that if the code becomes
+    too long to manage in a single file in the
+    future, it is easier to air-lift parts of
+    the code into a library at that point.
+  */
+
+// @init
+  #include <Arduino.h>            // this is necessary to talk to the Hexboard!
+  #include <Wire.h>               // this is necessary to connect with I2C devices (such as the oled display)
+  #define SDAPIN 16
+  #define SCLPIN 17
+  #include <GEM_u8g2.h>           // library of code to create menu objects on the B&W display
+  #include <numeric>              // need that GCD function, son
+  #include <string>               // standard C++ library string classes (use "std::string" to invoke it); these do not cause the memory corruption that Arduino::String does.
+  #include <queue>                // standard C++ library construction to store open channels in microtonal mode (use "std::queue" to invoke it)
+// Software-detected hardware revision
+  #define HARDWARE_UNKNOWN 0
+  #define HARDWARE_V1_1 1
+  #define HARDWARE_V1_2 2
+  byte Hardware_Version = 0;       // 0 = unknown, 1 = v1.1 board. 2 = v1.2 board.
+
+
+
+/////////      global variables and defines     ///////////////////////////////////////////////
+
+  bool forceEnableMPE = false;
+  byte defaultMidiChannel = 1;
+  byte layoutRotation = 0;
+
+  //  Keyboard layout swapping
+  bool mirrorLeftRight = false;
+  bool mirrorUpDown = false;
+
+
+  // Helper, might be redundant
+  std::vector<byte> pressedKeyIDs = {};
+
+
+  //  Just Intonation related global variables
+  byte justIntonationBPM = 60;
+  byte justIntonationBPM_Multiplier = 1;
+  bool useJustIntonationBPM = false;
+  bool useDynamicJustIntonation = false;
+
+/////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+
+// @helpers
+  /*
+    C++ returns a negative value for
+    negative N % D. This function
+    guarantees the mod value is always
+    positive.
+  */
+  int positiveMod(int n, int d) {
+    return (((n % d) + d) % d);
+  }
+  /*
+    There may already exist linear interpolation
+    functions in the standard library. This one is helpful
+    because it will do the weighting division for you.
+    It only works on byte values since it's intended
+    to blend color values together. A better C++
+    coder may be able to allow automatic type casting here.
+  */
+  byte byteLerp(byte xOne, byte xTwo, float yOne, float yTwo, float y) {
+    float weight = (y - yOne) / (yTwo - yOne);
+    int temp = xOne + ((xTwo - xOne) * weight);
+    if (temp < xOne) {temp = xOne;}
+    if (temp > xTwo) {temp = xTwo;}
+    return temp;
+  }
+
+// @defaults
+  /*
+    This section sets default values
+    for user-editable options
+  */
+  int transposeSteps = 0;
+  bool scaleLock = 0;
+  bool perceptual = 1;
+  bool paletteBeginsAtKeyCenter = 1;
+  byte animationFPS = 32;             // actually frames per 2^20 microseconds. close enough to 30fps
+
+  byte wheelMode = 0;                 // standard vs. fine tune mode
+  byte modSticky = 0;
+  byte pbSticky = 0;
+  byte velSticky = 1;
+  int modWheelSpeed = 8;
+  int pbWheelSpeed = 1024;
+  int velWheelSpeed = 8;
+
+  #define SYNTH_OFF 0
+  #define SYNTH_MONO 1
+  #define SYNTH_ARPEGGIO 2
+  #define SYNTH_POLY 3
+  byte playbackMode = SYNTH_OFF;
+
+  #define WAVEFORM_SINE 0
+  #define WAVEFORM_STRINGS 1
+  #define WAVEFORM_CLARINET 2
+  #define WAVEFORM_HYBRID 7
+  #define WAVEFORM_SQUARE 8
+  #define WAVEFORM_SAW 9
+  #define WAVEFORM_TRIANGLE 10
+  byte currWave = WAVEFORM_HYBRID;
+
+  #define RAINBOW_MODE 0
+  #define TIERED_COLOR_MODE 1
+  #define ALTERNATE_COLOR_MODE 2
+  #define RAINBOW_OF_FIFTHS_MODE 3
+  #define PIANO_ALT_COLOR_MODE 4
+  #define PIANO_COLOR_MODE 5
+  #define PIANO_INCANDESCENT_COLOR_MODE 6
+  byte colorMode = RAINBOW_MODE;
+
+  #define ANIMATE_NONE 0
+  #define ANIMATE_STAR 1
+  #define ANIMATE_SPLASH 2
+  #define ANIMATE_ORBIT 3
+  #define ANIMATE_OCTAVE 4
+  #define ANIMATE_BY_NOTE 5
+  #define ANIMATE_BEAMS 6
+  #define ANIMATE_SPLASH_REVERSE 7
+  #define ANIMATE_STAR_REVERSE 8
+  byte animationType = ANIMATE_NONE;
+
+  #define BRIGHT_MAX 255
+  #define BRIGHT_HIGH 210
+  #define BRIGHT_MID 180
+  #define BRIGHT_LOW 150
+  #define BRIGHT_DIM 110
+  #define BRIGHT_DIMMER 70
+  #define BRIGHT_DARK 50    // BRIGHT_DIMMEST
+  #define BRIGHT_DARKER 34  // Lowest brightness before backlight shuts down
+  #define BRIGHT_FAINT 33   // Highest brightness before backlight turns on
+  #define BRIGHT_FAINTER 24 // Lowest brightness before any highlighted button is lit in all color modes
+  #define BRIGHT_OFF 0
+  byte globalBrightness = BRIGHT_DIM;
+
+// @microtonal
+  /*
+    Most users will stick to playing in standard Western
+    tuning, but for those looking to play microtonally,
+    the Hexboard accommodates equal step tuning systems
+    of any arbitrary size.
+  */
+  /*
+    Each tuning system needs to be
+    pre-defined, pre-counted, and enumerated as below.
+    Future editions of this sketch may enable free
+    definition and smart pointer references to tuning
+    presets without requiring an enumeration.
+  */
+  #define TUNING_12EDO 0
+  #define TUNING_12EDO_ZETA 1
+  #define TUNING_17EDO 2
+  #define TUNING_19EDO 3
+  #define TUNING_22EDO 4
+  #define TUNING_24EDO 5
+  #define TUNING_31EDO 6
+  #define TUNING_31EDO_ZETA 7
+  #define TUNING_41EDO 8
+  #define TUNING_43EDO 9
+  #define TUNING_46EDO 10
+  #define TUNING_53EDO 11
+  #define TUNING_58EDO 12
+  #define TUNING_58EDO_ZETA 13
+  #define TUNING_72EDO 14
+  #define TUNING_72EDO_ZETA 15
+  #define TUNING_80EDO 16
+  #define TUNING_87EDO 17
+  #define TUNING_BP    18
+  #define TUNING_ALPHA 19
+  #define TUNING_BETA  20
+  #define TUNING_GAMMA 21
+  #define TUNINGCOUNT  22
+  /*
+    Note names and palette arrays are allocated in memory
+    at runtime. Their usable size is based on the number
+    of steps (in standard tuning, semitones) in a tuning
+    system before a new period is reached (in standard
+    tuning, the octave). This value provides a maximum
+    array size that handles almost all useful tunings
+    without wasting much space.
+  */
+  #define MAX_SCALE_DIVISIONS 87
+  /*
+    A dictionary of musical scales is defined in the code.
+    A scale is tied to one tuning system, with the exception
+    of "no scale" (i.e. every note is part of the scale).
+    "No scale" is tied to this value "ALL_TUNINGS" so it can
+    always be chosen in the menu.
+  */
+  #define ALL_TUNINGS 255
+  /*
+    MIDI notes are enumerated 0-127 (7 bits).
+    Values of 128-255 can be used to indicate
+    command instructions for non-note buttons.
+    These definitions support this function.
+  */
+  #define CMDB 192
+  #define UNUSED_NOTE 255
+  /*
+    When sending smoothly-varying pitch bend
+    or modulation messages over MIDI, the
+    code uses a cool-down period of about
+    1/30 of a second in between messages, enough
+    for changes to sound continuous without
+    overloading the MIDI message queue.
+  */
+  #define CC_MSG_COOLDOWN_MICROSECONDS 32768
+  /*
+    This class provides the seed values
+    needed to map buttons to note frequencies
+    and palette colors, and to populate
+    the menu with correct key names and
+    scale choices, for a given equal step
+    tuning system.
+  */
+  class tuningDef {
+  public:
+    std::string name;         // limit is 17 characters for GEM menu
+    byte cycleLength;         // steps before period/cycle/octave repeats
+    float stepSize;           // in cents, 100 = "normal" semitone.
+    SelectOptionInt keyChoices[MAX_SCALE_DIVISIONS];
+    int spanCtoA() {
+      return keyChoices[0].val_int;
+    }
+  };
+  /*
+    Note that for all practical musical purposes,
+    expressing step sizes to six significant figures is
+    sufficient to eliminate any detectable tuning artifacts
+    due to rounding.
+
+    The note names are formatted in an array specifically to
+    match the format needed for the GEM Menu to accept directly
+    as a spinner selection item. The number next to the note name
+    is the number of steps from the anchor note A that key is.
+
+    There are other ways the tuning could be calculated.
+    Some microtonal players choose an anchor note
+    other than A 440. Future versions will allow for
+    more flexibility in anchor selection, which will also
+    change the implementation of key options.
+  */
+
+  /*
+    Sko: I felt like maximizing precision for just intonation purposes.
+    Values are precalculated by compiler, and MIDI 2.0 or later might benefit from it
+  */
+  tuningDef tuningOptions[] = {
+    { "12 EDO", 12, 100.000,
+      {{"C" ,-9},{"C#",-8},{"D" ,-7},{"Eb",-6},{"E" ,-5},{"F",-4}
+      ,{"F#",-3},{"G" ,-2},{"G#",-1},{"A" , 0},{"Bb", 1},{"B", 2}
+    }},
+    { "12 EDO Zeta peak", 12, 99.8071515654111465,
+      {{"C" ,-9},{"C#",-8},{"D" ,-7},{"Eb",-6},{"E" ,-5},{"F",-4}
+      ,{"F#",-3},{"G" ,-2},{"G#",-1},{"A" , 0},{"Bb", 1},{"B", 2}
+    }},
+    { "17 EDO", 17, 1200.0/17.0,
+      {{"C",-13},{"Db",-12},{"C#",-11},{"D",-10},{"Eb",-9},{"D#",-8}
+      ,{"E", -7},{"F" , -6},{"Gb", -5},{"F#",-4},{"G", -3},{"Ab",-2}
+      ,{"G#",-1},{"A" ,  0},{"Bb",  1},{"A#", 2},{"B",  3}
+    }},
+    { "19 EDO", 19, 1200.0/19.0,
+      {{"C" ,-14},{"C#",-13},{"Db",-12},{"D",-11},{"D#",-10},{"Eb",-9},{"E",-8}
+      ,{"E#", -7},{"F" , -6},{"F#", -5},{"Gb",-4},{"G",  -3},{"G#",-2}
+      ,{"Ab", -1},{"A" ,  0},{"A#",  1},{"Bb", 2},{"B",   3},{"Cb", 4}
+    }},
+    { "22 EDO", 22, 1200.0/22.0,
+      {{" C", -17},{"^C",-16},{"vC#",-15},{"vD",-14},{" D",-13},{"^D",-12}
+      ,{"^Eb",-11},{"vE",-10},{" E",  -9},{" F", -8},{"^F", -7},{"vF#",-6}
+      ,{"vG",  -5},{" G", -4},{"^G",  -3},{"vG#",-2},{"vA", -1},{" A",  0}
+      ,{"^A",   1},{"^Bb", 2},{"vB",   3},{" B",  4}
+    }},
+    { "24 EDO", 24, 1200.0/24.0,
+      {{"C", -18},{"C+",-17},{"C#",-16},{"Dd",-15},{"D",-14},{"D+",-13}
+      ,{"Eb",-12},{"Ed",-11},{"E", -10},{"E+", -9},{"F", -8},{"F+", -7}
+      ,{"F#", -6},{"Gd", -5},{"G",  -4},{"G+", -3},{"G#",-2},{"Ad", -1}
+      ,{"A",   0},{"A+",  1},{"Bb",  2},{"Bd",  3},{"B",  4},{"Cd",  5}
+    }},
+    { "31 EDO", 31, 1200.0/31.0,
+      {{"C",-23},{"C+",-22},{"C#",-21},{"Db",-20},{"Dd",-19}
+      ,{"D",-18},{"D+",-17},{"D#",-16},{"Eb",-15},{"Ed",-14}
+      ,{"E",-13},{"E+",-12}                      ,{"Fd",-11}
+      ,{"F",-10},{"F+", -9},{"F#", -8},{"Gb", -7},{"Gd", -6}
+      ,{"G", -5},{"G+", -4},{"G#", -3},{"Ab", -2},{"Ad", -1}
+      ,{"A",  0},{"A+",  1},{"A#",  2},{"Bb",  3},{"Bd",  4}
+      ,{"B",  5},{"B+",  6}                      ,{"Cd",  7}
+    }},
+    { "31 EDO Zeta peak", 31, 1200.0/30.9783818789525220,
+      {{"C",-23},{"C+",-22},{"C#",-21},{"Db",-20},{"Dd",-19}
+      ,{"D",-18},{"D+",-17},{"D#",-16},{"Eb",-15},{"Ed",-14}
+      ,{"E",-13},{"E+",-12}                      ,{"Fd",-11}
+      ,{"F",-10},{"F+", -9},{"F#", -8},{"Gb", -7},{"Gd", -6}
+      ,{"G", -5},{"G+", -4},{"G#", -3},{"Ab", -2},{"Ad", -1}
+      ,{"A",  0},{"A+",  1},{"A#",  2},{"Bb",  3},{"Bd",  4}
+      ,{"B",  5},{"B+",  6}                      ,{"Cd",  7}
+    }},
+    { "41 EDO", 41, 1200.0/41.0,
+      {{" C",-31},{"^C",-30},{" C+",-29},{" Db",-28},{" C#",-27},{" Dd",-26},{"vD",-24}
+      ,{" D",-24},{"^D",-23},{" D+",-22},{" Eb",-21},{" D#",-20},{" Ed",-19},{"vE",-18}
+      ,{" E",-17},{"^E",-16}                                                ,{"vF",-15}
+      ,{" F",-14},{"^F",-13},{" F+",-12},{" Gb",-11},{" F#",-10},{" Gd", -9},{"vG", -8}
+      ,{" G", -7},{"^G", -6},{" G+", -5},{" Ab", -4},{" G#", -3},{" Ad", -2},{"vA", -1}
+      ,{" A",  0},{"^A",  1},{" A+",  2},{" Bb",  3},{" A#",  4},{" Bd",  5},{"vB",  6}
+      ,{" B",  7},{"^B",  8}                                                ,{"vC",  9}
+    }},
+    { "43 EDO", 43, 1200.0/43.0,
+      {{" C",-32},{"C+1",-31},{"C+2",-30},{"C+3",-29},{"C+4",-28},{"C+5",-27},{"C+6",-26}
+      ,{" D",-25},{"D+1",-24},{"D+2",-23},{"D+3",-22},{"D+4",-21},{"D+5",-20},{"D+6",-19}
+      ,{" E",-18},{"E+1",-17},{"E+2",-16}                                    ,{"E+3",-15}
+      ,{" F",-14},{"F+1",-13},{"F+2",-12},{"F+3",-11},{"F+4",-10},{"F+5", -9},{"F+6", -8}
+      ,{" G", -7},{"G+1", -6},{"G+2", -5},{"G+3", -4},{"G+4", -3},{"G+5", -2},{"G+6", -1}
+      ,{" A",  0},{"A+1",  1},{"A+2",  2},{"A+3",  3},{"A+4",  4},{"A+5",  5},{"A+6",  6}
+      ,{" B",  7},{"B+1",  8},{"B+2",  9},{"B+3", 10},                        {"B+4", 11}
+    }},
+    { "46 EDO", 46, 1200.0/46.0,
+      {{" C",-35},{"C+1",-34},{"C+2",-33},{"C+3",-32},{"C+4",-31},{"C+5",-30},{"C+6",-29},{"C+7",-28}
+      ,{" D",-27},{"D+1",-26},{"D+2",-25},{"D+3",-24},{"D+4",-23},{"D+5",-22},{"D+6",-21},{"D+7",-20}
+      ,{" E",-19},{"E+1",-18},{"E+2",-17}
+      ,{" F",-16},{"F+1",-15},{"F+2",-14},{"F+3",-13},{"F+4",-12},{"F+5",-11},{"F+6",-10},{"F+7", -9}
+      ,{" G", -8},{"G+1", -7},{"G+2", -6},{"G+3", -5},{"G+4", -4},{"G+5", -3},{"G+6", -2},{"G+7", -1}
+      ,{" A",  0},{"A+1",  1},{"A+2",  2},{"A+3",  3},{"A+4",  4},{"A+5",  5},{"A+6",  6},{"A+7",  7}
+      ,{" B",  8},{"B+1",  9},{"B+2", 10}
+    }},
+    { "53 EDO", 53, 1200.0/53.0,
+      {{" C", -40},{"^C", -39},{">C",-38},{"vDb",-37},{"Db",-36}
+      ,{" C#",-35},{"^C#",-34},{"<D",-33},{"vD", -32}
+      ,{" D", -31},{"^D", -30},{">D",-29},{"vEb",-28},{"Eb",-27}
+      ,{" D#",-26},{"^D#",-25},{"<E",-24},{"vE", -23}
+      ,{" E", -22},{"^E", -21},{">E",-20},{"vF", -19}
+      ,{" F", -18},{"^F", -17},{">F",-16},{"vGb",-15},{"Gb",-14}
+      ,{" F#",-13},{"^F#",-12},{"<G",-11},{"vG", -10}
+      ,{" G",  -9},{"^G",  -8},{">G", -7},{"vAb", -6},{"Ab", -5}
+      ,{" G#", -4},{"^G#", -3},{"<A", -2},{"vA",  -1}
+      ,{" A",   0},{"^A",   1},{">A",  2},{"vBb",  3},{"Bb",  4}
+      ,{" A#",  5},{"^A#",  6},{"<B",  7},{"vB",   8}
+      ,{" B",   9},{"^B",  10},{"<C", 11},{"vC",  12}
+    }},
+    { "58 EDO", 58, 1200.0/58.0,
+      {{" C",-44},{"C+1",-43},{"C+2",-42},{"C+3",-41},{"C+4",-40},{"C+5",-39},{"C+6",-38},{"C+7",-37},{"C+8",-36},{"C+8",-35}
+      ,{" D",-34},{"D+1",-33},{"D+2",-32},{"D+3",-31},{"D+4",-30},{"D+5",-29},{"D+6",-28},{"D+7",-27},{"D+8",-26},{"D+8",-25}
+      ,{" E",-24},{"E+1",-23},{"E+2",-22},{"E+3",-21}
+      ,{" F",-20},{"F+1",-19},{"F+2",-18},{"F+3",-17},{"F+4",-16},{"F+5",-15},{"F+6",-14},{"F+7",-13},{"F+8",-12},{"F+9",-11}
+      ,{" G",-10},{"G+1", -9},{"G+2", -8},{"G+3", -7},{"G+4", -6},{"G+5", -5},{"G+6", -4},{"G+7", -3},{"G+8", -2},{"G+9", -1}
+      ,{" A",  0},{"A+1",  1},{"A+2",  2},{"A+3",  3},{"A+4",  4},{"A+5",  5},{"A+6",  6},{"A+7",  7},{"A+8",  7},{"A+9",  7}
+      ,{" B", 10},{"B+1", 11},{"B+2", 12}
+    }},
+    { "58 EDO Zeta peak", 58, 1200.0/58.066718758225889,
+      {{" C",-44},{"C+1",-43},{"C+2",-42},{"C+3",-41},{"C+4",-40},{"C+5",-39},{"C+6",-38},{"C+7",-37},{"C+8",-36},{"C+8",-35}
+      ,{" D",-34},{"D+1",-33},{"D+2",-32},{"D+3",-31},{"D+4",-30},{"D+5",-29},{"D+6",-28},{"D+7",-27},{"D+8",-26},{"D+8",-25}
+      ,{" E",-24},{"E+1",-23},{"E+2",-22},{"E+3",-21}
+      ,{" F",-20},{"F+1",-19},{"F+2",-18},{"F+3",-17},{"F+4",-16},{"F+5",-15},{"F+6",-14},{"F+7",-13},{"F+8",-12},{"F+9",-11}
+      ,{" G",-10},{"G+1", -9},{"G+2", -8},{"G+3", -7},{"G+4", -6},{"G+5", -5},{"G+6", -4},{"G+7", -3},{"G+8", -2},{"G+9", -1}
+      ,{" A",  0},{"A+1",  1},{"A+2",  2},{"A+3",  3},{"A+4",  4},{"A+5",  5},{"A+6",  6},{"A+7",  7},{"A+8",  7},{"A+9",  7}
+      ,{" B", 10},{"B+1", 11},{"B+2", 12}
+    }},
+    { "72 EDO", 72, 1200.0/72.0,
+      {{" C", -54},{"^C", -53},{">C", -52},{" C+",-51},{"<C#",-50},{"vC#",-49}
+      ,{" C#",-48},{"^C#",-47},{">C#",-46},{" Dd",-45},{"<D" ,-44},{"vD" ,-43}
+      ,{" D", -42},{"^D", -41},{">D", -40},{" D+",-39},{"<Eb",-38},{"vEb",-37}
+      ,{" Eb",-36},{"^Eb",-35},{">Eb",-34},{" Ed",-33},{"<E" ,-32},{"vE" ,-31}
+      ,{" E", -30},{"^E", -29},{">E", -28},{" E+",-27},{"<F" ,-26},{"vF" ,-25}
+      ,{" F", -24},{"^F", -23},{">F", -22},{" F+",-21},{"<F#",-20},{"vF#",-19}
+      ,{" F#",-18},{"^F#",-17},{">F#",-16},{" Gd",-15},{"<G" ,-14},{"vG" ,-13}
+      ,{" G", -12},{"^G", -11},{">G", -10},{" G+", -9},{"<G#", -8},{"vG#", -7}
+      ,{" G#", -6},{"^G#", -5},{">G#", -4},{" Ad", -3},{"<A" , -2},{"vA" , -1}
+      ,{" A",   0},{"^A",   1},{">A",   2},{" A+",  3},{"<Bb",  4},{"vBb",  5}
+      ,{" Bb",  6},{"^Bb",  7},{">Bb",  8},{" Bd",  9},{"<B" , 10},{"vB" , 11}
+      ,{" B",  12},{"^B",  13},{">B",  14},{" Cd", 15},{"<C" , 16},{"vC" , 17}
+    }},
+    { "72 EDO Zeta peak", 72, 1200.0/71.9506066608606432,
+      {{" C", -54},{"^C", -53},{">C", -52},{" C+",-51},{"<C#",-50},{"vC#",-49}
+      ,{" C#",-48},{"^C#",-47},{">C#",-46},{" Dd",-45},{"<D" ,-44},{"vD" ,-43}
+      ,{" D", -42},{"^D", -41},{">D", -40},{" D+",-39},{"<Eb",-38},{"vEb",-37}
+      ,{" Eb",-36},{"^Eb",-35},{">Eb",-34},{" Ed",-33},{"<E" ,-32},{"vE" ,-31}
+      ,{" E", -30},{"^E", -29},{">E", -28},{" E+",-27},{"<F" ,-26},{"vF" ,-25}
+      ,{" F", -24},{"^F", -23},{">F", -22},{" F+",-21},{"<F#",-20},{"vF#",-19}
+      ,{" F#",-18},{"^F#",-17},{">F#",-16},{" Gd",-15},{"<G" ,-14},{"vG" ,-13}
+      ,{" G", -12},{"^G", -11},{">G", -10},{" G+", -9},{"<G#", -8},{"vG#", -7}
+      ,{" G#", -6},{"^G#", -5},{">G#", -4},{" Ad", -3},{"<A" , -2},{"vA" , -1}
+      ,{" A",   0},{"^A",   1},{">A",   2},{" A+",  3},{"<Bb",  4},{"vBb",  5}
+      ,{" Bb",  6},{"^Bb",  7},{">Bb",  8},{" Bd",  9},{"<B" , 10},{"vB" , 11}
+      ,{" B",  12},{"^B",  13},{">B",  14},{" Cd", 15},{"<C" , 16},{"vC" , 17}
+    }},
+    { "80 EDO", 80, 1200.0/80.0,
+      {{" C",-61},{"C+1",-60},{"C+2",-59},{"C+3",-58},{"C+4",-57},{"C+5",-56},{"C+6",-55},{"C+7",-54},{"C+8",-53},{"C+9",-52},{"C+10",-51},{"C+11",-50},{"C+12",-49},{"C+13",-48}
+      ,{" D",-47},{"D+1",-46},{"D+2",-45},{"D+3",-44},{"D+4",-43},{"D+5",-42},{"D+6",-41},{"D+7",-40},{"D+8",-39},{"D+9",-38},{"D+11",-37},{"D+12",-36},{"D+13",-35},{"D+14",-34}
+      ,{" E",-33},{"E+1",-32},{"E+2",-31},{"E+3",-30},{"E+4",-29}
+      ,{" F",-28},{"F+1",-27},{"F+2",-26},{"F+3",-25},{"F+4",-24},{"F+5",-23},{"F+6",-22},{"F+7",-21},{"F+8",-20},{"F+9",-19},{"F+11",-18},{"F+12",-17},{"F+13",-16},{"F+14",-15}
+      ,{" G",-14},{"G+1",-13},{"G+2",-12},{"G+3",-11},{"G+4",-10},{"G+5",-9},{"G+6",-8},{"G+7",-7},{"G+8",-6},{"G+9",-5},{"G+11",-4},{"G+12",-3},{"G+13",-2},{"G+14",-1}
+      ,{" A",  0},{"A+1",  1},{"A+2",  2},{"A+3",  3},{"A+4",  4},{"A+5",  5},{"A+6",  6},{"A+7",  7},{"A+8",  8},{"A+9",  9},{"A+10",  10},{"A+11",  11},{"A+12",  12},{"A+13",  13}
+      ,{" B", 14},{"B+1", 15},{"B+2", 16},{"B+3", 17},{"B+4", 18}
+    }},
+    { "87 EDO", 87, 1200.0/87.0,
+      {{" C",-66},{"C+1",-65},{"C+2",-64},{"C+3",-63},{"C+4",-62},{"C+5",-61},{"C+6",-60},{"C+7",-59},{"C+8",-58},{"C+9",-57},{"C10",-57},{"C+11",-56},{"C+12",-55},{"C+13",-54},{"C+14",-53},{"C+15",-52}
+      ,{" D",-51},{"D+1",-50},{"D+2",-49},{"D+3",-48},{"D+4",-47},{"D+5",-46},{"D+6",-45},{"D+7",-44},{"D+8",-43},{"D+9",-42},{"D+10",-41},{"D+11",-40},{"D+12",-39},{"D+13",-38},{"D+14",-37}
+      ,{" E",-36},{"E+1",-35},{"E+2",-34},{"E+3",-33}
+      ,{" F",-30},{"F+1",-29},{"F+2",-28},{"F+3",-27},{"F+4",-26},{"F+5",-25},{"F+6",-24},{"F+7",-23},{"F+8",-22},{"F+9",-21},{"F+10",-20},{"F+11",-19},{"F+12",-18},{"F+13",-17},{"F+14",-16}
+      ,{" G",-15},{"G+1",-14},{"G+2",-13},{"G+3",-12},{"G+4",-11},{"G+5",-10},{"G+6",-9},{"G+7",-8},{"G+8",-7},{"G+9",-6},{"G+10",-5},{"G+11",-4},{"G+12",-3},{"G+13",-2},{"G+14",-1}
+      ,{" A",  0},{"A+1",  1},{"A+2",  2},{"A+3",  3},{"A+4",  4},{"A+5",  5},{"A+6",  6},{"A+7",  7},{"A+8",  8},{"A+9",  9},{"A+10",  10},{"A+11",  11},{"A+12",  12},{"A+13",  13},{"A+14",  14}
+      ,{" B", 15},{"B+1", 16},{"B+2", 17},{"B+3", 18}
+    }},
+    { "Bohlen-Pierce", 13, (1200.0 * (log(3.0/1.0) / log(2.0)))/13.0,
+      {{"C",-10},{"Db",-9},{"D",-8},{"E",-7},{"F",-6},{"Gb",-5}
+      ,{"G",-4},{"H",-3},{"Jb",-2},{"J",-1},{"A",0},{"Bb",1},{"B",2}
+    }},
+    { "Carlos Alpha", 9, 77.964990,
+      {{"I",0},{"I#",1},{"II-",2},{"II+",3},{"III",4}
+      ,{"III#",5},{"IV-",6},{"IV+",7},{"Ib",8}
+    }},
+    { "Carlos Beta", 11, 63.832933,
+      {{"I",0},{"I#",1},{"IIb",2},{"II",3},{"II#",4},{"III",5}
+      ,{"III#",6},{"IVb",7},{"IV",8},{"IV#",9},{"Ib",10}
+    }},
+    { "Carlos Gamma", 20, 35.0985422804,
+      {{" I",  0},{"^I",  1},{" IIb", 2},{"^IIb", 3},{" I#",   4},{"^I#",   5}
+      ,{" II", 6},{"^II", 7}
+      ,{" III",8},{"^III",9},{" IVb",10},{"^IVb",11},{" III#",12},{"^III#",13}
+      ,{" IV",14},{"^IV",15},{" Ib", 16},{"^Ib", 17},{" IV#", 18},{"^IV#", 19}
+    }},
+  };
+
+// @layout
+  /*
+    This section defines the different
+    preset note layout options.
+  */
+  /*
+    This class provides the seed values
+    needed to implement a given isomorphic
+    note layout. From it, the map of buttons
+    to note frequencies can be calculated.
+
+    A layout is tied to a specific tuning.
+  */
+  class layoutDef {
+  public:
+    std::string name;    // limit is 17 characters for GEM menu
+    bool isPortrait;     // affects orientation of the GEM menu only.
+    byte hexMiddleC;     // instead of "what note is button 1", "what button is the middle"
+    int8_t acrossSteps;  // defined this way to be compatible with original v1.1 firmare
+    int8_t dnLeftSteps;  // defined this way to be compatible with original v1.1 firmare
+    byte tuning;         // index of the tuning that this layout is designed for
+  };
+  /*
+    Isomorphic layouts are defined by
+    establishing where the center of the
+    layout is, and then the number of tuning
+    steps to go up or down for the hex button
+    across or down diagonally.
+  */
+
+    // NOTE: Aside from adding new layouts,
+    // I have also rearranged them for personal use:
+    // - Wicki-Hayden first, if it manages to map all notes;
+    // - Compressed Janko second, if it maps all notes;
+    // - Full Janko otherwise;
+    // You might want to arrange them as seems fit for release,
+    // including all other layouts as I didn't put them in any particular order
+  layoutDef layoutOptions[] = {
+    { "Wicki-Hayden",      1, 64,   2,  -7, TUNING_12EDO },
+    { "Harmonic Table",    0, 75,  -7,   3, TUNING_12EDO },
+    { "Janko",             0, 65,   1,  -2, TUNING_12EDO },
+    { "Bosanquet-Wilson",  0, 65,  -1,  -1, TUNING_12EDO },
+    { "Compressed Janko",  0, 65,  -1,  -2, TUNING_12EDO },
+    { "Compr. Bosanquet",  0, 65,  -1,   3, TUNING_12EDO },
+    { "Gerhard",           0, 65,  -1,  -3, TUNING_12EDO },
+    { "Accordion C-sys.",  1, 75,   2,  -3, TUNING_12EDO },
+    { "Accordion B-sys.",  1, 64,   1,  -3, TUNING_12EDO },
+    { "Chromatic",         0, 75,  12,  -1, TUNING_12EDO },
+    { "Full Gamut",        1, 75,   1,  -9, TUNING_12EDO },
+    { "31 EDO Janko",      0, 65,   2,  -5, TUNING_12EDO }, // These layouts are meant for
+    { "31 EDO Compr.Janko",0, 65,  -3,  -5, TUNING_12EDO }, // synths that do not support MPE
+    { "31 EDO Bos.Wilson", 0, 65,  -2,  -3, TUNING_12EDO }, // but support scala/tun files
+    { "31 EDO Wicki-Hayd.",1, 64,   5, -18, TUNING_12EDO },
+    { "31 EDO Harm. Table",0, 75, -18,   8, TUNING_12EDO }, // I did not add many, as it would get VERY cluttered
+    { "41 EDO Janko",      0, 65,   4,  -7, TUNING_12EDO },
+    { "53 EDO Janko",      0, 65,   5,  -9, TUNING_12EDO }, // A different microtuning option in firmware
+    { "58 EDO Janko",      0, 65,   3, -10, TUNING_12EDO }, // should mitigate the need for these layouts
+
+    { "Wicki-Hayden",      1, 64,   2,  -7, TUNING_12EDO_ZETA },
+    { "Harmonic Table",    0, 75,  -7,   3, TUNING_12EDO_ZETA },
+    { "Janko",             0, 65,   1,  -2, TUNING_12EDO_ZETA },
+    { "Bosanquet-Wilson",  0, 65,  -1,  -1, TUNING_12EDO_ZETA },
+    { "Compressed Janko",  0, 65,  -1,  -2, TUNING_12EDO_ZETA },
+    { "Compr. Bosanquet",  0, 65,  -1,   3, TUNING_12EDO_ZETA },
+    { "Gerhard",           0, 65,  -1,  -3, TUNING_12EDO_ZETA },
+    { "Accordion C-sys.",  1, 75,   2,  -3, TUNING_12EDO_ZETA },
+    { "Accordion B-sys.",  1, 64,   1,  -3, TUNING_12EDO_ZETA },
+    { "Chromatic",         0, 75,  12,  -1, TUNING_12EDO_ZETA },
+    { "Full Gamut",        1, 75,   1,  -9, TUNING_12EDO_ZETA },
+
+    { "Compressed Janko",  0, 65,  -1,  -3, TUNING_17EDO },
+    { "Compr. Bosanquet",  0, 65,  -2,  -1, TUNING_17EDO },
+    { "Janko",             0, 65,   2,  -3, TUNING_17EDO },
+    { "Bosanquet-Wilson",  0, 65,  -2,  -1, TUNING_17EDO },
+    { "Neutral Thirds A",  0, 65,  -1,  -2, TUNING_17EDO },
+    { "Neutral Thirds B",  0, 65,   1,  -3, TUNING_17EDO },
+    { "Full Gamut",        1, 75,   1,  -9, TUNING_17EDO },
+
+    { "Wicki-Hayden",      1, 65,   3, -11, TUNING_19EDO },
+    { "Compressed Janko",  0, 65,  -2,  -3, TUNING_19EDO },
+    { "Compr. Bosanquet",  0, 65,  -2,   5, TUNING_19EDO },
+    { "Janko",             0, 65,   1,  -3, TUNING_19EDO },
+    { "Bosanquet-Wilson",  0, 65,  -1,  -2, TUNING_19EDO },
+    { "Harmonic Table",    0, 75, -11,   5, TUNING_19EDO },
+    { "Kleismic",          0, 65,  -1,  -4, TUNING_19EDO },
+    { "Full Gamut",        1, 75,   1,  -9, TUNING_19EDO },
+
+    { "Compressed Janko",  0, 65,  -1,  -4, TUNING_22EDO },
+    { "Compr. Bosanquet",  0, 65,  -1,   5, TUNING_22EDO },
+    { "Janko",             0, 65,   3,  -4, TUNING_22EDO },
+    { "Bosanquet-Wilson",  0, 65,  -3,  -1, TUNING_22EDO },
+    { "Wicki-Hayden",      1, 64,   4, -13, TUNING_22EDO },
+    { "Porcupine",         0, 65,   1,  -4, TUNING_22EDO },
+    { "Full Gamut",        1, 75,   1,  -9, TUNING_22EDO },
+
+    { "Janko",             0, 65,   1,  -4, TUNING_24EDO }, // Maybe call it "Quartertone Janko"?
+    { "Bosanquet-Wilson",  0, 65,  -1,  -3, TUNING_24EDO }, // Maybe call it "1/4 tone Bosanquet"?
+    { "Full Gamut",        1, 75,   1,  -9, TUNING_24EDO },
+
+    { "Compressed Janko",  0, 65,  -3,  -5, TUNING_31EDO },
+    { "Compr. Bosanquet",  0, 65,  -3,   8, TUNING_31EDO },
+    { "Janko",             0, 65,   2,  -5, TUNING_31EDO },
+    { "Bosanquet-Wilson",  0, 65,  -2,  -3, TUNING_31EDO },
+    { "Wicki-Hayden",      1, 64,   5, -18, TUNING_31EDO },
+    { "5X -13Y",           1, 64,   5, -13, TUNING_31EDO }, // Unnamed layout, between Wicki-Hayd. and compressed Janko
+    { "Harmonic Table",    0, 75, -18,   8, TUNING_31EDO },
+    { "Double Bosanquet",  0, 65,  -1,  -4, TUNING_31EDO },
+    { "Anti-Double Bos.",  0, 65,   1,  -5, TUNING_31EDO },
+    { "Full Gamut",        1, 75,   1,  -9, TUNING_31EDO },
+
+    { "Compressed Janko",  0, 65,  -3,  -5, TUNING_31EDO_ZETA },
+    { "Compr. Bosanquet",  0, 65,  -3,   8, TUNING_31EDO_ZETA },
+    { "Janko",             0, 65,   2,  -5, TUNING_31EDO_ZETA },
+    { "Bosanquet-Wilson",  0, 65,  -2,  -3, TUNING_31EDO_ZETA },
+    { "Wicki-Hayden",      1, 64,   5, -18, TUNING_31EDO_ZETA },
+    { "5X -13Y",           1, 64,   5, -13, TUNING_31EDO_ZETA },  // Unnamed layout, between Wicki-Hayd. and compressed Janko
+    { "Harmonic Table",    0, 75, -18,   8, TUNING_31EDO_ZETA },
+    { "Double Bosanquet",  0, 65,  -1,  -4, TUNING_31EDO_ZETA },
+    { "Anti-Double Bos.",  0, 65,   1,  -5, TUNING_31EDO_ZETA },
+    { "Full Gamut",        1, 75,   1,  -9, TUNING_31EDO_ZETA },
+
+    { "Compressed Janko",  0, 65,  -3,  -7, TUNING_41EDO },
+    { "Compr. Bosanquet",  0, 65,  -3,  10, TUNING_41EDO },
+    { "Janko",             0, 65,   4,  -7, TUNING_41EDO },
+    { "Bosanquet-Wilson",  0, 65,  -4,  -3, TUNING_41EDO },  // forty-one #1
+    { "Harmonic Table",    0, 75, -24,  11, TUNING_41EDO },
+    { "Wicki-Hayden",      1, 64,   7, -24, TUNING_41EDO },
+    { "Gerhard",           0, 65,   3, -10, TUNING_41EDO },  // forty-one #2
+    { "Baldy",             0, 65,  -1,  -6, TUNING_41EDO },
+    { "Rodan",             1, 65,  -1,  -7, TUNING_41EDO },
+    { "Full Gamut",        1, 75,   1,  -9, TUNING_41EDO },  // forty-one #3
+
+    { "Janko",             0, 65,   3,  -7, TUNING_43EDO },
+    { "Bosanquet-Wilson",  0, 65,  -3,  -4, TUNING_43EDO },
+    { "Wicki-Hayden",      1, 64,   7, -25, TUNING_43EDO },
+    { "Harmonic Table",    0, 75, -25,  11, TUNING_43EDO },
+    { "Full Gamut",        0, 75,   1,  -9, TUNING_43EDO },
+
+    { "Janko",             0, 65,   5,  -8, TUNING_46EDO },
+    { "Bosanquet-Wilson",  0, 65,  -5,  -3, TUNING_46EDO },
+    { "Harmonic Table",    0, 75, -27,  12, TUNING_46EDO },
+    { "Echidnic",          0, 65,   5,  -9, TUNING_46EDO },
+    { "Full Gamut",        1, 75,   1,  -9, TUNING_46EDO },
+
+    { "Janko",             0, 65,   5,  -9, TUNING_53EDO },
+    { "Bosanquet-Wilson",  0, 65,  -5,  -4, TUNING_53EDO },
+    { "Harmonic Table",    0, 75, -31,  14, TUNING_53EDO },
+    { "Wicki-Hayden",      1, 64,   9, -31, TUNING_53EDO },
+    { "Kleismic A",        0, 65,  -8,  -3, TUNING_53EDO },
+    { "Kleismic B",        0, 65,  -5,  -3, TUNING_53EDO },
+    { "Buzzard",           0, 65,  -9,  -1, TUNING_53EDO },
+    { "Compressed Janko",  1, 65,   9, -13, TUNING_53EDO }, // Can only fit vertically
+    { "Compr. Bosanquet",  1, 65,   9,   4, TUNING_53EDO }, // Can only fit vertically
+    { "Full Gamut",        1, 75,   1,  -9, TUNING_53EDO },
+
+    { "Janko",             0, 64,   3, -10, TUNING_58EDO }, // Maybe call it "Quartertone Janko"?
+    { "Bosanquet-Wilson",  0, 64,   3,   7, TUNING_58EDO }, // Maybe call it "Quartertone Bosanquet"?
+    { "Hemififths",        0, 64,   4,  -7, TUNING_58EDO },
+    { "Hemififths mirror.",0, 64,  -4,  -3, TUNING_58EDO },
+    { "Chromatic",         0, 64,  -7,  -5, TUNING_58EDO },
+    { "Harmonic Table",    0, 75, -34,  15, TUNING_58EDO },
+    { "Septimal H.T.",     0, 75, -34,  13, TUNING_58EDO },
+    { "Diaschismic",       0, 64,   4,  -9, TUNING_58EDO },
+    { "4X -19Y",           0, 64,   4, -19, TUNING_58EDO }, // unnamed layout, efficient for major 7ths, 9s, #11s and so on 
+    { "-27X 10Y",          1, 64, -27,  10, TUNING_58EDO }, // weird but efficient layout
+    { "Wicki-Hayd.(29EDO)",1, 64,  10, -34, TUNING_58EDO },
+    { "Bos.Wilson (29EDO)",0, 65,  -6,  -4, TUNING_58EDO },
+    { "Janko      (29EDO)",0, 65,   6, -10, TUNING_58EDO }, // 29 EDO subset, each for two rings of fifths
+    { "Tridec.H.T.(29EDO)",0, 75, -34,  14, TUNING_58EDO },
+    { "Full Gamut",        1, 75,   1,  -9, TUNING_58EDO },
+
+    { "Janko",             0, 64,   3, -10, TUNING_58EDO_ZETA }, // Maybe call it "Quartertone Janko"?
+    { "Bosanquet-Wilson",  0, 64,   3,   7, TUNING_58EDO_ZETA }, // Maybe call it "1/4 tone Bosanquet"?
+    { "Hemififths",        0, 64,   4,  -7, TUNING_58EDO_ZETA },
+    { "Hemififths mirror.",0, 64,  -4,  -3, TUNING_58EDO_ZETA },
+    { "Chromatic",         0, 64,  -7,  -5, TUNING_58EDO_ZETA },
+    { "Harmonic Table",    0, 75, -34,  15, TUNING_58EDO_ZETA },
+    { "Septimal H.T.",     0, 75, -34,  13, TUNING_58EDO_ZETA },
+    { "Diaschismic",       0, 64,   4,  -9, TUNING_58EDO_ZETA },
+    { "4X -19Y",           0, 64,   4, -19, TUNING_58EDO_ZETA }, // unnamed layout, efficient for major 7ths, 9s, #11s and so on 
+    { "-27X 10Y",          1, 64, -27,  10, TUNING_58EDO_ZETA }, // weird but efficient layout
+    { "Wicki-Hayd.(29EDO)",1, 64,  10, -34, TUNING_58EDO_ZETA },
+    { "Bos.Wilson (29EDO)",0, 65,  -6,  -4, TUNING_58EDO_ZETA },
+    { "Janko      (29EDO)",0, 65,   6, -10, TUNING_58EDO_ZETA }, // 29 EDO subset, each for two rings of fifths
+    { "Tridec.H.T.(29EDO)",0, 75, -34,  14, TUNING_58EDO_ZETA },
+    { "Full Gamut",        1, 75,   1,  -9, TUNING_58EDO_ZETA },
+
+    { "Harmonic Table",    0, 75, -42,  19, TUNING_72EDO },
+    { "-30X 19Y",          0, 75, -30,  19, TUNING_72EDO }, // unnamed layout. Like harmonic table but with fourths instead of fifths
+    { "Miracle mapping",   0, 65,  -7,  -2, TUNING_72EDO },
+    { "Sept.H.T.(36EDO)",  0, 75, -42,  16, TUNING_72EDO }, // 36 EDO subset
+    { "Expanded Janko",    0, 65,  -1,  -6, TUNING_72EDO },
+    { "Full Gamut",        1, 65,   1,  -9, TUNING_72EDO },
+
+    { "Harmonic Table",    0, 75, -42,  19, TUNING_72EDO_ZETA },
+    { "-30X 19Y",          0, 75, -30,  19, TUNING_72EDO_ZETA }, // unnamed layout. Like harmonic table but with fourths instead of fifths
+    { "Miracle mapping",   0, 65,  -7,  -2, TUNING_72EDO_ZETA },
+    { "Sept.H.T.(36EDO)",  0, 75, -42,  16, TUNING_72EDO_ZETA }, // 36 EDO subset
+    { "Expanded Janko",    0, 65,  -1,  -6, TUNING_72EDO_ZETA },
+    { "Full Gamut",        1, 65,   1,  -9, TUNING_72EDO_ZETA },
+
+    { "Janko",             0, 65,   9, -14, TUNING_80EDO }, // Janko mapping is still too large to map all notes (same for 87 EDO)
+    { "Bosanquet-Wilson",  0, 65,  -9,  -5, TUNING_80EDO }, // Same for Bosanquet-Wilson. Still usable
+    { "Compressed Janko",  0, 65,  -5, -14, TUNING_80EDO },
+    { "Full Gamut",        1, 75,   1,  -9, TUNING_80EDO }, // So far this is the only one layout that maps every note
+
+    { "Harmonic Table",    0, 75, -51,  23, TUNING_87EDO },
+    { "Janko (good 3/2)",  0, 65,   5, -14, TUNING_87EDO },
+    { "Bos.W.(good 3/2)",  0, 65,  -5,  -9, TUNING_87EDO },
+    { "Wic.Hayd.nooctave", 1, 64,  14, -51, TUNING_87EDO }, // perfect thirds at the cost of losing an octave
+    { "Wic.Hayd. pyth.",   1, 64,  15, -51, TUNING_87EDO }, // pythagorean thirds, octave is preserved, note variety decreased
+    { "Janko (good 4/3)",  0, 65,   6, -14, TUNING_87EDO }, // Less efficient but allows perfect chord inversions
+    { "Bos.W.(good 4/3)",  0, 65,  -6,  -8, TUNING_87EDO },
+    { "Bos.W.(26EDO)",     0, 65,  -9,  -6, TUNING_87EDO },
+    { "Janko (26EDO)",     0, 65,   9, -15, TUNING_87EDO },
+    { "Full Gamut",        1, 75,   1,  -9, TUNING_87EDO },
+
+    { "Standard",          0, 65,  -2,  -1, TUNING_BP },
+    { "Full Gamut",        1, 65,   1,  -9, TUNING_BP },
+
+    { "Harmonic Table",    0, 75,  -9,   5, TUNING_ALPHA },
+    { "Compressed",        0, 65,  -2,  -1, TUNING_ALPHA },
+    { "Full Gamut",        1, 65,   1,  -9, TUNING_ALPHA },
+
+    { "Wicki-Hayden",      1, 65,   3, -11, TUNING_BETA }, // Carlos Beta has the same mappings as 19 EDO
+    { "Compressed Janko",  0, 65,  -2,  -3, TUNING_BETA },
+    { "Compr. Bosanquet",  0, 65,  -2,   5, TUNING_BETA },
+    { "Janko",             0, 65,   1,  -3, TUNING_BETA },
+    { "Bosanquet-Wilson",  0, 65,  -1,  -2, TUNING_BETA },
+    { "Harmonic Table",    0, 75, -11,   5, TUNING_BETA },
+    { "Kleismic",          0, 65,  -1,  -4, TUNING_BETA },
+    { "Full Gamut",        1, 75,   1,  -9, TUNING_BETA },
+
+    { "Harmonic Table",    0, 75, -20,   9, TUNING_GAMMA }, // Same mappings as for 34 EDO
+    { "Compressed",        0, 65,  -2,  -1, TUNING_GAMMA }, // Difficult to map, has two rings of fifths
+    { "Full Gamut",        1, 65,   1,  -9, TUNING_GAMMA }
+  };
+  const byte layoutCount = sizeof(layoutOptions) / sizeof(layoutDef);
+// @scales
+  /*
+    This class defines a scale pattern
+    for a given tuning. It is basically
+    an array with the number of steps in
+    between each degree of the scale. For
+    example, the major scale in 12EDO
+    is 2, 2, 1, 2, 2, 2, 1.
+
+    A scale is tied to a specific tuning.
+  */
+  class scaleDef {
+  public:
+    std::string name;
+    byte tuning;
+    byte pattern[MAX_SCALE_DIVISIONS];
+  };
+  scaleDef scaleOptions[] = {
+    { "None",              ALL_TUNINGS,      { 0 } },
+    // 12 EDO
+    { "Major",             TUNING_12EDO,     { 2,2,1,2,2,2,1 } },
+    { "Minor, natural",    TUNING_12EDO,     { 2,1,2,2,1,2,2 } },
+    { "Minor, melodic",    TUNING_12EDO,     { 2,1,2,2,2,2,1 } },
+    { "Minor, harmonic",   TUNING_12EDO,     { 2,1,2,2,1,3,1 } },
+    { "Pentatonic, major", TUNING_12EDO,     { 2,2,3,2,3 } },
+    { "Pentatonic, minor", TUNING_12EDO,     { 3,2,2,3,2 } },
+    { "Blues",             TUNING_12EDO,     { 3,1,1,1,1,3,2 } },
+    { "Double Harmonic",   TUNING_12EDO,     { 1,3,1,2,1,3,1 } },
+    { "Phrygian",          TUNING_12EDO,     { 1,2,2,2,1,2,2 } },
+    { "Phrygian Dominant", TUNING_12EDO,     { 1,3,1,2,1,2,2 } },
+    { "Dorian",            TUNING_12EDO,     { 2,1,2,2,2,1,2 } },
+    { "Lydian",            TUNING_12EDO,     { 2,2,2,1,2,2,1 } },
+    { "Lydian Dominant",   TUNING_12EDO,     { 2,2,2,1,2,1,2 } },
+    { "Mixolydian",        TUNING_12EDO,     { 2,2,1,2,2,1,2 } },
+    { "Locrian",           TUNING_12EDO,     { 1,2,2,1,2,2,2 } },
+    { "Whole tone",        TUNING_12EDO,     { 2,2,2,2,2,2 } },
+    { "Octatonic",         TUNING_12EDO,     { 2,1,2,1,2,1,2,1 } },
+    // 17 EDO; for more: https://en.xen.wiki/w/17edo#Scales
+    { "Diatonic",          TUNING_17EDO,  { 3,3,1,3,3,3,1 } },
+    { "Pentatonic",        TUNING_17EDO,  { 3,3,4,3,4 } },
+    { "Harmonic",          TUNING_17EDO,  { 3,2,3,2,2,2,3 } },
+    { "Husayni maqam",     TUNING_17EDO,  { 2,2,3,3,2,1,1,3 } },
+    { "Blues",             TUNING_17EDO,  { 4,3,1,1,1,4,3 } },
+    { "Hydra",             TUNING_17EDO,  { 3,3,1,1,2,3,2,1,1 } },
+    // 19 EDO; for more: https://en.xen.wiki/w/19edo#Scales
+    { "Diatonic",          TUNING_19EDO,   { 3,3,2,3,3,3,2 } },
+    { "Pentatonic",        TUNING_19EDO,   { 3,3,5,3,5 } },
+    { "Semaphore",         TUNING_19EDO,   { 3,1,3,1,3,3,1,3,1 } },
+    { "Negri",             TUNING_19EDO,   { 2,2,2,2,2,1,2,2,2,2 } },
+    { "Sensi",             TUNING_19EDO,   { 2,2,1,2,2,2,1,2,2,2,1 } },
+    { "Kleismic",          TUNING_19EDO,   { 1,3,1,1,3,1,1,3,1,3,1 } },
+    { "Magic",             TUNING_19EDO,   { 3,1,1,1,3,1,1,1,3,1,1,1,1 } },
+    { "Kind of blues",     TUNING_19EDO,   { 4,4,1,2,4,4 } },
+    // 22 EDO; for more: https://en.xen.wiki/w/22edo_modes
+    { "Diatonic",          TUNING_22EDO,  { 4,4,1,4,4,4,1 } },
+    { "Pentatonic",        TUNING_22EDO,  { 4,4,5,4,5 } },
+    { "Orwell",            TUNING_22EDO,  { 3,2,3,2,3,2,3,2,2 } },
+    { "Porcupine",         TUNING_22EDO,  { 4,3,3,3,3,3,3 } },
+    { "Pajara",            TUNING_22EDO,  { 2,2,3,2,2,2,3,2,2,2 } },
+    // 24 EDO; for more: https://en.xen.wiki/w/24edo_scales
+    { "Diatonic 12",       TUNING_24EDO, { 4,4,2,4,4,4,2 } },
+    { "Diatonic Soft",     TUNING_24EDO, { 3,5,2,3,5,4,2 } },
+    { "Diatonic Neutral",  TUNING_24EDO, { 4,3,3,4,3,4,3 } },
+    { "Pentatonic (12)",   TUNING_24EDO, { 4,4,6,4,6 } },
+    { "Pentatonic (Haba)", TUNING_24EDO, { 5,5,5,5,4 } },
+    { "Invert Pentatonic", TUNING_24EDO, { 6,3,6,6,3 } },
+    { "Rast maqam",        TUNING_24EDO, { 4,3,3,4,4,2,1,3 } },
+    { "Bayati maqam",      TUNING_24EDO, { 3,3,4,4,2,1,3,4 } },
+    { "Hijaz maqam",       TUNING_24EDO, { 2,6,2,4,2,1,3,4 } },
+    { "8-EDO",             TUNING_24EDO, { 3,3,3,3,3,3,3,3 } },
+    { "Wyschnegradsky",    TUNING_24EDO, { 2,2,2,2,2,1,2,2,2,2,2,2,1 } },
+    // 31 EDO; for more: https://en.xen.wiki/w/31edo#Scales
+    { "Diatonic",          TUNING_31EDO,  { 5,5,3,5,5,5,3 } },
+    { "Pentatonic",        TUNING_31EDO,  { 5,5,8,5,8 } },
+    { "Harmonic",          TUNING_31EDO,  { 5,5,4,4,4,3,3,3 } },
+    { "Mavila",            TUNING_31EDO,  { 5,3,3,3,5,3,3,3,3 } },
+    { "Quartal",           TUNING_31EDO,  { 2,2,7,2,2,7,2,7 } },
+    { "Orwell",            TUNING_31EDO,  { 4,3,4,3,4,3,4,3,3 } },
+    { "Neutral",           TUNING_31EDO,  { 4,4,4,4,4,4,4,3 } },
+    { "Miracle",           TUNING_31EDO,  { 4,3,3,3,3,3,3,3,3,3 } },
+    // 31 EDO ZETA PEAK;
+    { "Diatonic",          TUNING_31EDO_ZETA,  { 5,5,3,5,5,5,3 } },
+    { "Pentatonic",        TUNING_31EDO_ZETA,  { 5,5,8,5,8 } },
+    { "Harmonic",          TUNING_31EDO_ZETA,  { 5,5,4,4,4,3,3,3 } },
+    { "Mavila",            TUNING_31EDO_ZETA,  { 5,3,3,3,5,3,3,3,3 } },
+    { "Quartal",           TUNING_31EDO_ZETA,  { 2,2,7,2,2,7,2,7 } },
+    { "Orwell",            TUNING_31EDO_ZETA,  { 4,3,4,3,4,3,4,3,3 } },
+    { "Neutral",           TUNING_31EDO_ZETA,  { 4,4,4,4,4,4,4,3 } },
+    { "Miracle",           TUNING_31EDO_ZETA,  { 4,3,3,3,3,3,3,3,3,3 } },
+    // 41 EDO; for more: https://en.xen.wiki/w/41edo#Scales_and_modes
+    { "Diatonic",          TUNING_41EDO,   { 7,7,3,7,7,7,3 } },
+    { "Pentatonic",        TUNING_41EDO,   { 7,7,10,7,10 } },
+    { "Pure major",        TUNING_41EDO,   { 7,6,4,7,6,7,4 } },
+    { "5-limit chromatic", TUNING_41EDO,   { 4,3,4,2,4,3,4,4,2,4,3,4 } },
+    { "7-limit chromatic", TUNING_41EDO,   { 3,4,2,4,4,3,4,2,4,3,3,4 } },
+    { "Harmonic",          TUNING_41EDO,   { 5,4,4,4,4,3,3,3,3,3,2,3 } },
+    { "Middle East-ish",   TUNING_41EDO,   { 7,5,7,5,5,7,5 } },
+    { "Thai",              TUNING_41EDO,   { 6,6,6,6,6,6,5 } },
+    { "Slendro",           TUNING_41EDO,   { 8,8,8,8,9 } },
+    { "Pelog / Mavila",    TUNING_41EDO,   { 8,5,5,8,5,5,5 } },
+    // 53 EDO
+    { "Diatonic",          TUNING_53EDO, { 9,9,4,9,9,9,4 } },
+    { "Pentatonic",        TUNING_53EDO, { 9,9,13,9,13 } },
+    { "Rast makam",        TUNING_53EDO, { 9,8,5,9,9,4,4,5 } },
+    { "Usshak makam",      TUNING_53EDO, { 7,6,9,9,4,4,5,9 } },
+    { "Hicaz makam",       TUNING_53EDO, { 5,12,5,9,4,9,9 } },
+    { "Orwell",            TUNING_53EDO, { 7,5,7,5,7,5,7,5,5 } },
+    { "Sephiroth",         TUNING_53EDO, { 6,5,5,6,5,5,6,5,5,5 } },
+    { "Smitonic",          TUNING_53EDO, { 11,11,3,11,3,11,3 } },
+    { "Slendric",          TUNING_53EDO, { 7,3,7,3,7,3,7,3,7,3,3 } },
+    { "Semiquartal",       TUNING_53EDO, { 9,2,9,2,9,2,9,2,9 } },
+    // 72 EDO
+    { "Diatonic",          TUNING_72EDO, { 12,12,6,12,12,12,6 } },
+    { "Pentatonic",        TUNING_72EDO, { 12,12,18,12,18 } },
+    { "Ben Johnston",      TUNING_72EDO, { 6,6,6,5,5,5,9,8,4,4,7,7 } },
+    { "18-EDO",            TUNING_72EDO, { 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4 } },
+    { "Miracle",           TUNING_72EDO, { 5,2,5,2,5,2,2,5,2,5,2,5,2,5,2,5,2,5,2,5,2 } },
+    { "Marvolo",           TUNING_72EDO, { 5,5,5,5,5,5,5,2,5,5,5,5,5,5 } },
+    { "Catakleismic",      TUNING_72EDO, { 4,7,4,4,4,7,4,4,4,7,4,4,4,7,4 } },
+    { "Palace",            TUNING_72EDO, { 10,9,11,12,10,9,11 } },
+    // BP
+    { "Lambda",            TUNING_BP, { 2,1,2,1,2,1,2,1,1 } },
+    // Alpha
+    { "Super Meta Lydian", TUNING_ALPHA, { 3,2,2,2 } },
+    // Beta
+    { "Super Meta Lydian", TUNING_BETA,  { 3,3,3,2 } },
+    // Gamma
+    { "Super Meta Lydian", TUNING_GAMMA, { 6,5,5,4 } }
+  };
+  const byte scaleCount = sizeof(scaleOptions) / sizeof(scaleDef);
+
+// @palettes
+  /*
+    This section defines the code needed
+    to determine colors for each hex.
+  */
+  /*
+    LED colors are defined in the code
+    on a perceptual basis. Instead of
+    calculating RGB codes, the program
+    uses an artist's color wheel approach.
+
+    For value / brightness, two sets of
+    named constants are defined. The BRIGHT_
+    series (see the defaults section above)
+    corresponds to the overall
+    level of lights from the HexBoard, from
+    dim to maximum. The VALUE_ series
+    is used to differentiate light and dark
+    colors in a palette. The BRIGHT and VALUE
+    are multiplied together (and normalized)
+    to get the output brightness.
+  */
+  #define VALUE_BLACK 0
+  #define VALUE_LOW   80
+  #define VALUE_SHADE 164
+  #define VALUE_NORMAL 180
+  #define VALUE_FULL  255
+  /*
+    Saturation is zero for black and white, and 255
+    for fully chromatic color. Value is the
+    brightness level of the LED, from 0 = off
+    to 255 = max.
+  */
+  #define SAT_BW 0
+  #define SAT_TINT 32
+  #define SAT_DULL 85
+  #define SAT_MODERATE 120
+  #define SAT_VIVID 255
+  /*
+    Hues are angles from 0 to 360, starting
+    at red and towards yellow->green->blue
+    when the hue angle increases.
+  */
+  #define HUE_NONE 0.0
+  #define HUE_RED 0.0
+  #define HUE_ORANGE 36.0
+  #define HUE_YELLOW 72.0
+  #define HUE_LIME 108.0
+  #define HUE_GREEN 144.0
+  #define HUE_CYAN 180.0
+  #define HUE_BLUE 216.0
+  #define HUE_INDIGO 252.0
+  #define HUE_PURPLE 288.0
+  #define HUE_MAGENTA 324.0
+  /*
+    This class is a basic hue, saturation,
+    and value triplet, with some limited
+    transformation functions. Rather than
+    load a full color space library, this
+    program uses non-class procedures to
+    perform conversions to and from LED-
+    friendly color codes.
+  */
+  class colorDef {
+  public:
+    float hue;
+    byte sat;
+    byte val;
+    colorDef tint() {
+      colorDef temp;
+      temp.hue = this->hue;
+      temp.sat = ((this->sat > SAT_MODERATE) ? SAT_MODERATE : this->sat);
+      temp.val = VALUE_FULL;
+      return temp;
+    }
+    colorDef shade() {
+      colorDef temp;
+      temp.hue = this->hue;
+      temp.sat = ((this->sat > SAT_MODERATE) ? SAT_MODERATE : this->sat);
+      temp.val = VALUE_LOW;
+      return temp;
+    }
+  };
+  /*
+    This class defines a palette, which is
+    a map of musical scale degrees to
+    colors. A palette is tied to a specific
+    tuning but not to a specific layout.
+  */
+  class paletteDef {
+  public:
+    colorDef swatch[MAX_SCALE_DIVISIONS]; // the different colors used in this palette
+    byte colorNum[MAX_SCALE_DIVISIONS];   // map key (c,d...) to swatches
+    colorDef getColor(byte givenStepFromC) {
+      return swatch[colorNum[givenStepFromC] - 1];
+    }
+    float getHue(byte givenStepFromC) {
+      return getColor(givenStepFromC).hue;
+    }
+    byte getSat(byte givenStepFromC) {
+      return getColor(givenStepFromC).sat;
+    }
+    byte getVal(byte givenStepFromC) {
+      return getColor(givenStepFromC).val;
+    }
+  };
+  /*
+    Palettes are defined by creating
+    a set of colors, and then making
+    an array of numbers that map the
+    intervals of that tuning to the
+    chosen colors. It's like paint
+    by numbers! Note that the indexes
+    start with 1, because the arrays are
+    padded with 0 for entries after
+    those intialized.
+  */
+  paletteDef palette[] = {
+    // 12 EDO
+      {{{HUE_NONE,    SAT_BW,    64}
+      , {200,    60,  VALUE_SHADE }
+      , {HUE_BLUE,    SAT_VIVID,  VALUE_SHADE}
+      , {230,  240, VALUE_NORMAL}
+      , {HUE_PURPLE, SAT_VIVID, VALUE_NORMAL}
+      , {270, SAT_VIVID, VALUE_NORMAL}
+      },{6,1,2,1,2,2,1,4,1,2,1,2}},
+    // 17 EDO
+      {{{HUE_NONE,    SAT_BW,    VALUE_NORMAL}
+      , {HUE_INDIGO,  SAT_VIVID, VALUE_NORMAL}
+      , {HUE_RED,     SAT_VIVID, VALUE_NORMAL}
+      },{1,2,3,1,2,3,1,1,2,3,1,2,3,1,2,3,1}},
+    // 19 EDO
+      {{{HUE_NONE,    SAT_BW,    VALUE_NORMAL} // n
+      , {HUE_YELLOW,  SAT_VIVID, VALUE_NORMAL} //  #
+      , {HUE_BLUE,    SAT_VIVID, VALUE_NORMAL} //  b
+      , {HUE_MAGENTA, SAT_VIVID, VALUE_NORMAL} // enh
+      },{1,2,3,1,2,3,1,4,1,2,3,1,2,3,1,2,3,1,4}},
+    // 22 EDO
+      {{{HUE_NONE,    SAT_BW,    VALUE_NORMAL} // n
+      , {HUE_BLUE,    SAT_VIVID, VALUE_NORMAL} // ^
+      , {HUE_MAGENTA, SAT_VIVID, VALUE_NORMAL} // mid
+      , {HUE_YELLOW,  SAT_VIVID, VALUE_NORMAL} // v
+      },{1,2,3,4,1,2,3,4,1,1,2,3,4,1,2,3,4,1,2,3,4,1}},
+    // 24 EDO
+      {{{HUE_NONE,    SAT_BW,    VALUE_NORMAL} // n
+      , {HUE_LIME,    SAT_DULL,  VALUE_SHADE } //  +
+      , {HUE_CYAN,    SAT_VIVID, VALUE_NORMAL} //  #/b
+      , {HUE_INDIGO,  SAT_DULL,  VALUE_SHADE } //  d
+      , {HUE_CYAN,    SAT_DULL,  VALUE_SHADE } // enh
+      },{1,2,3,4,1,2,3,4,1,5,1,2,3,4,1,2,3,4,1,2,3,4,1,5}},
+    // 31 EDO
+      {{{HUE_NONE,    SAT_BW,    VALUE_NORMAL} // n
+      , {HUE_RED,     SAT_DULL,  VALUE_NORMAL} //  +
+      , {HUE_YELLOW,  SAT_DULL,  VALUE_SHADE } //  #
+      , {HUE_CYAN,    SAT_DULL,  VALUE_SHADE } //  b
+      , {HUE_INDIGO,  SAT_DULL,  VALUE_NORMAL} //  d
+      , {HUE_RED,     SAT_DULL,  VALUE_SHADE } //  enh E+ Fb
+      , {HUE_INDIGO,  SAT_DULL,  VALUE_SHADE } //  enh E# Fd
+      },{1,2,3,4,5,1,2,3,4,5,1,6,7,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,6,7}},
+    // 41 EDO
+      {{{HUE_NONE,    SAT_BW,    VALUE_NORMAL} // n
+      , {HUE_RED,     SAT_DULL,  VALUE_NORMAL} //  ^
+      , {HUE_BLUE,    SAT_VIVID, VALUE_NORMAL} //  +
+      , {HUE_CYAN,    SAT_DULL,  VALUE_SHADE } //  b
+      , {HUE_GREEN,   SAT_DULL,  VALUE_SHADE } //  #
+      , {HUE_MAGENTA, SAT_DULL,  VALUE_NORMAL} //  d
+      , {HUE_YELLOW,  SAT_VIVID, VALUE_NORMAL} //  v
+      },{1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,1,2,3,4,5,6,7,
+         1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,6,7}},
+    // 43 EDO
+      {{{HUE_NONE,    SAT_BW,    VALUE_NORMAL} // n
+      , {HUE_RED,     SAT_DULL,  VALUE_NORMAL} //  ^
+      , {HUE_BLUE,    SAT_VIVID, VALUE_NORMAL} //  +
+      , {HUE_CYAN,    SAT_DULL,  VALUE_SHADE } //  b
+      , {HUE_GREEN,   SAT_DULL,  VALUE_SHADE } //  #
+      , {HUE_MAGENTA, SAT_DULL,  VALUE_NORMAL} //  d
+      , {HUE_YELLOW,  SAT_VIVID, VALUE_NORMAL} //  v
+      },{1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,1,2,3,4,5,6,7,
+         1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,6,7}},
+    // 53 EDO
+      {{{HUE_NONE,    SAT_BW,    VALUE_NORMAL} // n
+      , {HUE_ORANGE,  SAT_VIVID, VALUE_NORMAL} //  ^
+      , {HUE_MAGENTA, SAT_DULL,  VALUE_NORMAL} //  L
+      , {HUE_INDIGO,  SAT_VIVID, VALUE_NORMAL} // bv
+      , {HUE_GREEN,   SAT_VIVID, VALUE_SHADE } // b
+      , {HUE_YELLOW,  SAT_VIVID, VALUE_SHADE } // #
+      , {HUE_RED,     SAT_VIVID, VALUE_NORMAL} // #^
+      , {HUE_PURPLE,  SAT_DULL,  VALUE_NORMAL} //  7
+      , {HUE_CYAN,    SAT_VIVID, VALUE_SHADE } //  v
+      },{1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7,8,9,1,2,3,9,1,2,3,4,5,6,7,8,9,
+         1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7,8,9,1,2,3,9}},
+    // 72 EDO
+      {{{HUE_NONE,    SAT_BW,    VALUE_NORMAL} // n
+      , {HUE_GREEN,   SAT_DULL,  VALUE_SHADE } // ^
+      , {HUE_RED,     SAT_DULL,  VALUE_SHADE } // L
+      , {HUE_PURPLE,  SAT_DULL,  VALUE_SHADE } // +/d
+      , {HUE_BLUE,    SAT_DULL,  VALUE_SHADE } // 7
+      , {HUE_YELLOW,  SAT_DULL,  VALUE_SHADE } // v
+      , {HUE_INDIGO,  SAT_VIVID, VALUE_SHADE } // #/b
+      },{1,2,3,4,5,6,7,2,3,4,5,6,1,2,3,4,5,6,7,2,3,4,5,6,1,2,3,4,5,6,1,2,3,4,5,6,
+         7,2,3,4,5,6,1,2,3,4,5,6,7,2,3,4,5,6,1,2,3,4,5,6,7,2,3,4,5,6,1,2,3,4,5,6}},
+    // BOHLEN PIERCE
+      {{{HUE_NONE,    SAT_BW,    VALUE_NORMAL}
+      , {HUE_INDIGO,  SAT_VIVID, VALUE_NORMAL}
+      , {HUE_RED,     SAT_VIVID, VALUE_NORMAL}
+      },{1,2,3,1,2,3,1,1,2,3,1,2,3}},
+    // ALPHA
+      {{{HUE_NONE,    SAT_BW,    VALUE_NORMAL} // n
+      , {HUE_YELLOW,  SAT_VIVID, VALUE_NORMAL} // #
+      , {HUE_INDIGO,  SAT_VIVID, VALUE_NORMAL} // d
+      , {HUE_LIME,    SAT_VIVID, VALUE_NORMAL} // +
+      , {HUE_RED,     SAT_VIVID, VALUE_NORMAL} // enharmonic
+      , {HUE_CYAN,    SAT_VIVID, VALUE_NORMAL} // b
+      },{1,2,3,4,1,2,3,5,6}},
+    // BETA
+      {{{HUE_NONE,    SAT_BW,    VALUE_NORMAL} // n
+      , {HUE_INDIGO,  SAT_VIVID, VALUE_NORMAL} // #
+      , {HUE_RED,     SAT_VIVID, VALUE_NORMAL} // b
+      , {HUE_MAGENTA, SAT_DULL,  VALUE_NORMAL} // enharmonic
+      },{1,2,3,1,4,1,2,3,1,2,3}},
+    // GAMMA
+      {{{HUE_NONE,    SAT_BW,    VALUE_NORMAL} // n
+      , {HUE_RED,     SAT_VIVID, VALUE_NORMAL} // b
+      , {HUE_BLUE,    SAT_VIVID, VALUE_NORMAL} // #
+      , {HUE_YELLOW,  SAT_VIVID, VALUE_NORMAL} // n^
+      , {HUE_PURPLE,  SAT_VIVID, VALUE_NORMAL} // b^
+      , {HUE_GREEN,   SAT_VIVID, VALUE_NORMAL} // #^
+      }, {1,4,2,5,3,6,1,4,1,4,2,5,3,6,1,4,2,5,3,6}},
+  };
+
+// @presets
+  /*
+    This section of the code defines
+    a "preset" as a collection of
+    parameters that control how the
+    hexboard is operating and playing.
+
+    In the long run this will serve as
+    a foundation for saving and loading
+    preferences / settings through the
+    file system.
+  */
+
+  class presetDef {
+    public:
+      std::string presetName;
+      int tuningIndex;     // instead of using pointers, i chose to store index value of each option, to be saved to a .pref or .ini or something
+      int layoutIndex;
+      int scaleIndex;
+      int keyStepsFromA; // what key the scale is in, where zero equals A.
+      int transpose;
+      // define simple recall functions
+      tuningDef tuning() {
+        return tuningOptions[tuningIndex];
+      }
+      layoutDef layout() {
+        return layoutOptions[layoutIndex];
+      }
+      scaleDef scale() {
+        return scaleOptions[scaleIndex];
+      }
+      int layoutsBegin() {
+        if (tuningIndex == TUNING_12EDO) {
+          return 0;
+        } else {
+          int temp = 0;
+          while (layoutOptions[temp].tuning < tuningIndex) {
+            temp++;
+          }
+          return temp;
+        }
+      }
+      int keyStepsFromC() {
+        return tuning().spanCtoA() - keyStepsFromA;
+      }
+      int pitchRelToA4(int givenStepsFromC) {
+        return givenStepsFromC + tuning().spanCtoA() + transpose;
+      }
+      int keyDegree(int givenStepsFromC) {
+        return positiveMod(givenStepsFromC + keyStepsFromC(), tuning().cycleLength);
+      }
+    };
+
+    presetDef current = {
+      "Default",      // name
+      TUNING_12EDO,   // tuning
+      0,              // default to the first layout, wicki hayden
+      0,              // default to using no scale (chromatic)
+      -9,             // default to the key of C, which in 12EDO is -9 steps from A.
+      0               // default to no transposition
+    };
+
+// @diagnostics
+  /*
+    This section of the code handles
+    optional sending of log messages
+    to the Serial port
+  */
+  #define DIAGNOSTICS_ON true
+  void sendToLog(std::string msg) {
+    if (DIAGNOSTICS_ON) {
+      Serial.println(msg.c_str());
+    }
+  }
+
+// @timing
+  /*
+    This section of the code handles basic
+    timekeeping stuff
+  */
+  #include "hardware/timer.h"     // library of code to access the processor's clock functions
+  uint64_t runTime = 0;                // Program loop consistent variable for time in microseconds since power on
+  uint64_t lapTime = 0;                // Used to keep track of how long each loop takes. Useful for rate-limiting.
+  uint64_t loopTime = 0;               // Used to check speed of the loop
+  uint64_t readClock() {
+    uint64_t temp = timer_hw->timerawh;
+    return (temp << 32) | timer_hw->timerawl;
+  }
+  void timeTracker() {
+    lapTime = runTime - loopTime;
+    loopTime = runTime;                                 // Update previousTime variable to give us a reference point for next loop
+    runTime = readClock();   // Store the current time in a uniform variable for this program loop
+  }
+
+// @fileSystem
+  /*
+    This section of the code handles the
+    file system. There isn't much being
+    done with it yet, per se.
+    If so, this section might be relocated
+  */
+  #include "LittleFS.h"       // code to use flash drive space as a file system -- not implemented yet, as of May 2024
+  void setupFileSystem() {
+    Serial.begin(115200);     // Set serial to make uploads work without bootsel button
+    LittleFSConfig cfg;       // Configure file system defaults
+    cfg.setAutoFormat(true);  // Formats file system if it cannot be mounted.
+    LittleFS.setConfig(cfg);
+    LittleFS.begin();         // Mounts file system.
+    if (!LittleFS.begin()) {
+      sendToLog("An Error has occurred while mounting LittleFS");
+    } else {
+      sendToLog("LittleFS mounted OK");
+    }
+  }
+
+// @gridSystem
+  /*
+    This section of the code handles the hex grid
+       Hexagonal coordinates
+         https://www.redblobgames.com/grids/hexagons/
+         http://ondras.github.io/rot.js/manual/#hex/indexing
+    The HexBoard contains a grid of 140 buttons with
+    hexagonal keycaps. The processor has 10 pins connected
+    to a multiplexing unit, which hotswaps between the 14 rows
+    of ten buttons to allow all 140 inputs to be read in one
+    program read cycle.
+  */
+  #define MPLEX_1_PIN 4
+  #define MPLEX_2_PIN 5
+  #define MPLEX_4_PIN 2
+  #define MPLEX_8_PIN 3
+  #define COLUMN_PIN_0 6
+  #define COLUMN_PIN_1 7
+  #define COLUMN_PIN_2 8
+  #define COLUMN_PIN_3 9
+  #define COLUMN_PIN_4 10
+  #define COLUMN_PIN_5 11
+  #define COLUMN_PIN_6 12
+  #define COLUMN_PIN_7 13
+  #define COLUMN_PIN_8 14
+  #define COLUMN_PIN_9 15
+  /*
+    There are 140 LED pixels on the Hexboard.
+    LED instructions all go through the LED_PIN.
+    It so happens that each LED pixel corresponds
+    to one and only one hex button, so both a LED
+    and its button can have the same index from 0-139.
+    Since these parameters are pre-defined by the
+    hardware build, the dimensions of the grid
+    are therefore constants.
+  */
+  #define LED_COUNT 140
+  #define COLCOUNT 10
+  #define ROWCOUNT 16
+  #define BTN_COUNT COLCOUNT*ROWCOUNT
+  /*
+    Of the 140 buttons, 7 are offset to the bottom left
+    quadrant of the Hexboard and are reserved as command
+    buttons. Their LED reference is pre-defined here.
+    If you want those seven buttons remapped to play
+    notes, you may wish to change or remove these
+    variables and alter the value of CMDCOUNT to agree
+    with how many buttons you reserve for non-note use.
+  */
+  #define CMDBTN_0 0
+  #define CMDBTN_1 20
+  #define CMDBTN_2 40
+  #define CMDBTN_3 60
+  #define CMDBTN_4 80
+  #define CMDBTN_5 100
+  #define CMDBTN_6 120
+  #define CMDCOUNT 7
+  /*
+    This class defines the hexagon button
+    as an object. It stores all real-time
+    properties of the button -- its coordinates,
+    its current pressed state, the color
+    codes to display based on what action is
+    taken, what note and frequency is assigned,
+    whether the button is a command or not,
+    whether the note is in the selected scale,
+    whether the button is flagged to be animated,
+    and whether the note is currently
+    sounding on MIDI / the synth.
+
+    Needless to say, this is an important class.
+  */
+  class buttonDef {
+  public:
+    #define BTN_STATE_OFF 0
+    #define BTN_STATE_NEWPRESS 1
+    #define BTN_STATE_RELEASED 2
+    #define BTN_STATE_HELD 3
+    byte     btnState = 0;        // binary 00 = off, 01 = just pressed, 10 = just released, 11 = held
+    void interpBtnPress(bool isPress) {
+      btnState = (((btnState << 1) + isPress) & 3);
+    }
+    int8_t   coordRow = 0;        // hex coordinates
+    int8_t   coordCol = 0;        // hex coordinates
+    uint64_t timePressed = 0;     // timecode of last press
+    uint32_t LEDcodeAnim = 0;     // calculate it once and store value, to make LED playback snappier
+    uint32_t LEDcodePlay = 0;     // calculate it once and store value, to make LED playback snappier
+    uint32_t LEDcodeRest = 0;     // calculate it once and store value, to make LED playback snappier
+    uint32_t LEDcodeOff = 0;      // calculate it once and store value, to make LED playback snappier
+    uint32_t LEDcodeDim = 0;      // calculate it once and store value, to make LED playback snappier
+    bool     animate = 0;         // hex is flagged as part of the animation in this frame, helps make animations smoother
+    int16_t  stepsFromC = 0;      // number of steps from C4 (semitones in 12EDO; microtones if >12EDO)
+    bool     isCmd = 0;           // 0 if it's a MIDI note; 1 if it's a MIDI control cmd
+    bool     inScale = 0;         // 0 if it's not in the selected scale; 1 if it is
+    byte     note = UNUSED_NOTE;  // MIDI note or control parameter corresponding to this hex
+    int16_t  bend = 0;            // in microtonal mode, the pitch bend for this note needed to be tuned correctly
+    byte     MIDIch = 0;          // what MIDI channel this note is playing on
+    byte     synthCh = 0;         // what synth polyphony ch this is playing on
+    float    frequency = 0.0;     // what frequency to ring on the synther
+  };
+  /*
+    This class is like a virtual wheel.
+    It takes references / pointers to
+    the state of three command buttons,
+    translates presses of those buttons
+    into wheel turns, and converts
+    these movements into corresponding
+    values within a range.
+
+    This lets us generalize the
+    behavior of a virtual pitch bend
+    wheel or mod wheel using the same
+    code, only needing to modify the
+    range of output and the connected
+    buttons to operate it.
+  */
+  class wheelDef {
+  public:
+    byte* alternateMode; // two ways to control
+    byte* isSticky;      // TRUE if you leave value unchanged when no buttons pressed
+    byte* topBtn;        // pointer to the key Status of the button you use as this button
+    byte* midBtn;
+    byte* botBtn;
+    int16_t minValue;
+    int16_t maxValue;
+    int* stepValue;      // this can be changed via GEM menu
+    int16_t defValue;    // snapback value
+    int16_t curValue;
+    int16_t targetValue;
+    uint64_t timeLastChanged;
+    void setTargetValue() {
+      if (*alternateMode) {
+        if (*midBtn >> 1) { // middle button toggles target (0) vs. step (1) mode
+          int16_t temp = curValue;
+              if (*topBtn == 1)     {temp += *stepValue;} // tap button
+              if (*botBtn == 1)     {temp -= *stepValue;} // tap button
+              if (temp > maxValue)  {temp  = maxValue;}
+          else if (temp <= minValue) {temp  = minValue;}
+          targetValue = temp;
+        } else {
+          switch (((*topBtn >> 1) << 1) + (*botBtn >> 1)) {
+            case 0b10:   targetValue = maxValue;     break;
+            case 0b11:   targetValue = defValue;     break;
+            case 0b01:   targetValue = minValue;     break;
+            default:     targetValue = curValue;     break;
+          }
+        }
+      } else {
+        switch (((*topBtn >> 1) << 2) + ((*midBtn >> 1) << 1) + (*botBtn >> 1)) {
+          case 0b100:  targetValue = maxValue;                         break;
+          case 0b110:  targetValue = (3 * maxValue + minValue) / 4;    break;
+          case 0b010:
+          case 0b111:
+          case 0b101:  targetValue = (maxValue + minValue) / 2;        break;
+          case 0b011:  targetValue = (maxValue + 3 * minValue) / 4;    break;
+          case 0b001:  targetValue = minValue;                         break;
+          case 0b000:  targetValue = (*isSticky ? curValue : defValue); break;
+          default: break;
+        }
+      }
+    }
+    bool updateValue(uint64_t givenTime) {
+      int16_t temp = targetValue - curValue;
+      if (temp != 0) {
+        if ((givenTime - timeLastChanged) >= CC_MSG_COOLDOWN_MICROSECONDS ) {
+          timeLastChanged = givenTime;
+          if (abs(temp) < *stepValue) {
+            curValue = targetValue;
+          } else {
+            curValue = curValue + (*stepValue * (temp / abs(temp)));
+          }
+          return 1;
+        } else {
+          return 0;
+        }
+      } else {
+        return 0;
+      }
+    }
+  };
+  const byte mPin[] = {
+    MPLEX_1_PIN, MPLEX_2_PIN, MPLEX_4_PIN, MPLEX_8_PIN
+  };
+  const byte cPin[] = {
+    COLUMN_PIN_0, COLUMN_PIN_1, COLUMN_PIN_2, COLUMN_PIN_3,
+    COLUMN_PIN_4, COLUMN_PIN_5, COLUMN_PIN_6,
+    COLUMN_PIN_7, COLUMN_PIN_8, COLUMN_PIN_9
+  };
+  const byte assignCmd[] = {
+    CMDBTN_0, CMDBTN_1, CMDBTN_2, CMDBTN_3,
+    CMDBTN_4, CMDBTN_5, CMDBTN_6
+  };
+
+  /*
+    define h, which is a collection of all the
+    buttons from 0 to 139. h[i] refers to the
+    button with the LED address = i.
+  */
+  buttonDef h[BTN_COUNT];
+
+  wheelDef modWheel = { &wheelMode, &modSticky,
+    &h[assignCmd[4]].btnState, &h[assignCmd[5]].btnState, &h[assignCmd[6]].btnState,
+    0, 127, &modWheelSpeed, 0, 0, 0, 0
+  };
+  wheelDef pbWheel =  { &wheelMode, &pbSticky,
+    &h[assignCmd[4]].btnState, &h[assignCmd[5]].btnState, &h[assignCmd[6]].btnState,
+    -8192, 8191, &pbWheelSpeed, 0, 0, 0, 0
+  };
+  wheelDef velWheel = { &wheelMode, &velSticky,
+    &h[assignCmd[0]].btnState, &h[assignCmd[1]].btnState, &h[assignCmd[2]].btnState,
+    0, 127, &velWheelSpeed, 96, 96, 96, 0
+  };
+
+  bool toggleWheel = 0; // 0 for mod, 1 for pb
+
+  void setupPins() {
+    for (byte p = 0; p < sizeof(cPin); p++) { // For each column pin...
+      pinMode(cPin[p], INPUT_PULLUP);         // set the pinMode to INPUT_PULLUP (+3.3V / HIGH).
+    }
+    for (byte p = 0; p < sizeof(mPin); p++) { // For each column pin...
+      pinMode(mPin[p], OUTPUT);               // Setting the row multiplexer pins to output.
+    }
+    sendToLog("Pins mounted");
+  }
+
+  void setupGrid() {
+    for (byte i = 0; i < BTN_COUNT; i++) {
+      h[i].coordRow = (i / 10);
+      h[i].coordCol = (2 * (i % 10)) + (h[i].coordRow & 1);
+      h[i].isCmd = 0;
+      h[i].note = UNUSED_NOTE;
+      h[i].btnState = 0;
+    }
+    for (byte c = 0; c < CMDCOUNT; c++) {
+      h[assignCmd[c]].isCmd = 1;
+      h[assignCmd[c]].note = CMDB + c;
+    }
+    // "flag" buttons
+    for (byte i = 140; i < BTN_COUNT; i++) {
+      h[i].isCmd = 1;
+    }
+    // On version 1.2, "button" 140 is shorted (always connected)
+    h[140].note = HARDWARE_V1_2;
+  }
+
+// @LED
+  /*
+    This section of the code handles sending
+    color data to the LED pixels underneath
+    the hex buttons.
+  */
+  #include <Adafruit_NeoPixel.h>  // library of code to interact with the LED array
+  #define LED_PIN 22
+  Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
+  int32_t rainbowDegreeTime = 65'536; // microseconds to go through 1/360 of rainbow
+  /*
+    This is actually a hacked together approximation
+    of the color space OKLAB. A true conversion would
+    take the hue, saturation, and value bits and
+    turn them into linear RGB to feed directly into
+    the LED class. This conversion is... not very OK...
+    but does the job for now. A proper implementation
+    of OKLAB is in the works.
+
+    For transforming hues, the okLAB hue degree (0-360) is
+    mapped to the RGB hue degree from 0 to 65535, using
+    simple linear interpolation I created by hand comparing
+    my HexBoard outputs to a Munsell color chip book.
+  */
+  int16_t transformHue(float h) {
+    float D = fmod(h,360);
+    if (!perceptual) {
+      return 65536 * D / 360;
+    } else {
+      //                red            yellow             green        cyan         blue
+      int hueIn[] =  {    0,    9,   18,  102,  117,  135,  142,  155,  203,  240,  252,  261,  306,  333,  360};
+      //              #ff0000          #ffff00           #00ff00      #00ffff     #0000ff     #ff00ff
+      int hueOut[] = {    0, 3640, 5861,10922,12743,16384,21845,27306,32768,38229,43690,49152,54613,58254,65535};
+      byte B = 0;
+      while (D - hueIn[B] > 0) {
+        B++;
+      }
+      float T = (D - hueIn[B - 1]) / (float)(hueIn[B] - hueIn[B - 1]);
+      return (hueOut[B - 1] * (1 - T)) + (hueOut[B] * T);
+    }
+  }
+
+  namespace incandescence
+  {
+  /*
+  const int fixed_shift = 16;
+  const int fixed_scale = (1 << fixed_shift);
+
+  constexpr int32_t lambda_r = 700*256;
+  constexpr int32_t lambda_g = 550*256;
+  constexpr int32_t lambda_b = 450*256;
+
+  constexpr uint32_t C1 = 374183; // W*m^2
+  constexpr uint32_t C2 = 14388;   // m*K
+
+  int32_t fixed_exp(int32_t x)
+  {
+    return (fixed_scale + x + ((x*x) >> 1) + ((x*x*x)/6));
+  }
+  int32_t planckRadiation(int32_t lambda, int32_t temp)
+  {
+    int32_t denom = (C2 / (lambda*temp >> fixed_shift));
+    return (C1 / (pow(lambda,5))) / (fixed_exp(denom));
+  }
+  */
+
+  constexpr float lambda_r = 625e-9; // average wavelengths of LED diodes
+  constexpr float lambda_g = 525e-9;
+  constexpr float lambda_b = 460e-9;
+  
+  constexpr float C1 = 3.74183e-16; // W*m^2
+  constexpr float C2 = 1.4388e-2;   // m*K
+
+  float maxTemperature = 2400;
+  float brightnessCoefficient = 745000000.0f;
+
+  float planckRadiation(float lambda, float temp)
+  {
+    return (C1 / (pow(lambda,5))) / (exp(C2/(lambda*temp))-1);
+  }
+
+  float getCoefficient(float lambda, float maxTemperature)
+  {
+    float radiation = planckRadiation(lambda, maxTemperature);
+    return radiation/256.0f;
+  }
+
+  float getTemperatureFromV(float value)
+  {
+    return value;
+  }
+
+  colorDef getColor(int32_t temp)
+  {
+    float r = planckRadiation(lambda_r,temp);
+    float g = planckRadiation(lambda_g,temp);
+    float b = planckRadiation(lambda_b,temp);
+
+    float maxVal = max(max(r,g),b);
+
+    float minVal = min(min(r,g),b);
+    float delta = maxVal - minVal;
+    float h = 0, s = 0, v = 0;
+
+    if(delta > 0.00001)
+    {
+      s = delta/maxVal;
+      if(maxVal == r)
+      {
+        h=60.0*fmodf(((g-b)/delta),6.0);
+        v = r / getCoefficient(lambda_r,maxTemperature);
+      }
+      else if(maxVal == g)
+      {
+        h=60.0*(((g-b)/delta)+2.0);
+        v = g / getCoefficient(lambda_g,maxTemperature);
+      }
+      else
+      {
+        h=60.0*(((g-b)/delta)+4.0);
+        v = b / getCoefficient(lambda_b,maxTemperature);
+      }
+      v=min(max(v,0),255);
+    }
+
+    if(h < 0.0) h += 360.0;
+    return colorDef{h,(byte)(s*255),(byte)(v)};
+  }
+  }
+
+  /*
+    Saturation and Brightness are taken as is (already in a 0-255 range).
+    The global brightness / 255 attenuates the resulting color for the
+    user's brightness selection. Then the resulting RGB (HSV) color is
+    "un-gamma'd" to be converted to the LED strip color.
+  */
+  uint32_t getLEDcode(colorDef c) {
+    return strip.gamma32(strip.ColorHSV(transformHue(c.hue),c.sat,c.val * globalBrightness / 255));
+  }
+  /*
+    This function cycles through each button, and based on what color
+    palette is active, it calculates the LED color code in the palette,
+    plus its variations for being animated, played, or out-of-scale, and
+    stores it for recall during playback and animation. The color
+    codes remain in the object until this routine is called again.
+  */
+  void setLEDcolorCodes() {
+    for (byte i = 0; i < LED_COUNT; i++) {
+      if (!(h[i].isCmd)) {
+        colorDef setColor;
+        byte paletteIndex = positiveMod(h[i].stepsFromC,current.tuning().cycleLength);
+        if (paletteBeginsAtKeyCenter) {
+          paletteIndex = current.keyDegree(paletteIndex);
+        }
+        switch (colorMode) {
+          case TIERED_COLOR_MODE: // This mode sets the color based on the palettes defined above.
+            setColor = palette[current.tuningIndex].getColor(paletteIndex);
+            break;
+          case RAINBOW_MODE:      // This mode assigns the root note as red, and the rest as saturated spectrum colors across the rainbow.
+            setColor =
+              { 360 * ((float)paletteIndex / (float)current.tuning().cycleLength)
+              , SAT_VIVID
+              , VALUE_NORMAL
+              };
+            break;
+          case RAINBOW_OF_FIFTHS_MODE:      // This mode assigns the root note as red, and the rest as saturated spectrum colors across the rainbow.
+          {
+          float stepSize = current.tuning().stepSize;
+          float octaveCycleLength = 1200.0/current.tuning().stepSize; // This is to prevent non-octave colouring weirdness
+          float semipaletteIndex = fmodf(h[i].stepsFromC+(octaveCycleLength*256.0),octaveCycleLength);
+          float keyDegree = fmodf(semipaletteIndex + (current.tuning().spanCtoA() - current.keyStepsFromA), octaveCycleLength);
+          float fifthSize = ((ratioToCents(3.0/2.0))/stepSize);
+          float reverseFifth = fifthSize;
+          switch (current.tuningIndex)
+          {
+            case TUNING_17EDO:      { reverseFifth = 12 ;}break;  // reverse hash of (10*x)%17=x where 10 steps is a 17EDO fifth
+            case TUNING_19EDO:      { reverseFifth = 7  ;}break;  // reverse hash of (11*x)%19=x where 11 steps is a 19EDO fifth
+            case TUNING_22EDO:      { reverseFifth = 17 ;}break;  // reverse hash of (13*x)%22=x where 13 steps is a 22EDO fifth
+            case TUNING_24EDO:      { reverseFifth = 11 ;}break;  // hand-picked best-fit value. This tuning is very unruly
+            case TUNING_31EDO:      { reverseFifth = 19 ;}break;  // reverse hash of (18*x)%31=x where 18 steps is a 31EDO fifth
+            case TUNING_31EDO_ZETA: { reverseFifth = 19 ;}break;
+            case TUNING_41EDO:      { reverseFifth = 12 ;}break;  // reverse hash of (24*x)%41=x where 24 steps is a 41EDO fifth
+            case TUNING_43EDO:      { reverseFifth = 31 ;}break;  // reverse hash of (25*x)%43=x where 25 steps is a 43EDO fifth
+            case TUNING_46EDO:      { reverseFifth = 29 ;}break;  // reverse hash of (27*x)%46=x where 27 steps is a 46EDO fifth
+            case TUNING_53EDO:      { reverseFifth = 12 ;}break;  // reverse hash of (31*x)%53=x where 31 steps is a 53EDO fifth
+            case TUNING_58EDO:      { reverseFifth = 12 ;}break;  // reverse hash for 29EDO (2 chains of 29 EDO fifths in 58 EDO)
+            case TUNING_58EDO_ZETA: { reverseFifth = 12 ;}break;
+            case TUNING_72EDO:      { reverseFifth = 7  ;}break;  // reverse hash for 12EDO (6 chains of 12 EDO fifths in 72 EDO)
+            case TUNING_72EDO_ZETA: { reverseFifth = 7  ;}break;
+            case TUNING_80EDO:      { reverseFifth = 63 ;}break;  // reverse hash of (47*x)%80=x where 47 steps is an 80EDO fifth
+            case TUNING_87EDO:      { reverseFifth = 41 ;}break;  // A hand-picked value, seems to work. 46 also works
+            case TUNING_BP:         { reverseFifth = 5  ;}break;  // A hand-picked value; 23 and 64 also work
+            case TUNING_ALPHA:      { reverseFifth = 5  ;}break;  // A hand-picked value
+            case TUNING_BETA:       { reverseFifth = 7  ;}break;  // reverse hash of (11*x)%19=x where 11 steps is a 19EDO equivalent fifth
+            case TUNING_GAMMA:      { reverseFifth = 12 ;}break;  // reverse hash for 17EDO(2 chains of 17 EDO fifths in 34 EDO equivalent)
+            default:                { reverseFifth = fifthSize;}  // either the tuning has no fifths or scrambling colors using fifths works
+          }
+
+          float paletteIndexOfFifths = fmodf((keyDegree*reverseFifth),octaveCycleLength);
+            setColor =
+              { 360.0f * (paletteIndexOfFifths/(1200.0f/stepSize))
+              , SAT_VIVID
+              , VALUE_NORMAL
+              };
+          }
+            break;
+          case PIANO_ALT_COLOR_MODE:
+          {
+          float octaveCycleLength = 1200.0/current.tuning().stepSize; // This is to prevent non-octave colouring weirdness
+          float semipaletteIndex = fmodf(h[i].stepsFromC+(octaveCycleLength*256.0),octaveCycleLength);
+          float keyDegree = (12.0f/octaveCycleLength)*semipaletteIndex;
+          if((int)round(keyDegree)%12 == 1 || (int)round(keyDegree)%12 == 3 || (int)round(keyDegree)%12 == 6 || (int)round(keyDegree)%12 == 8 || (int)round(keyDegree)%12 == 10)
+          {
+              float deviationFromDiatonic = (float)((int)round(keyDegree) - keyDegree)*180.0; // range from 180 to 360
+              // +360 for proper fmodf; 180 is the opposite tint of 0; 30 is midway between yellow and red;
+              setColor = {fmodf(360.0+180.0+30.0+deviationFromDiatonic,360.0f),SAT_VIVID,VALUE_NORMAL}; 
+          }
+          else // White key
+          {
+              float deviationFromDiatonic = (((float)((int)round(keyDegree))) - (keyDegree))*180.0; // from -60 to 120
+              setColor = {fmodf(360.0+0.0+30.0+deviationFromDiatonic,360.0f),SAT_VIVID,VALUE_NORMAL};
+          }
+          }
+            break;
+          case PIANO_COLOR_MODE:
+          {
+          float octaveCycleLength = 1200.0/current.tuning().stepSize; // This is to prevent non-octave colouring weirdness
+          float semipaletteIndex = fmodf(h[i].stepsFromC+(octaveCycleLength*256.0),octaveCycleLength);
+          float keyDegree = (12.0f/octaveCycleLength)*semipaletteIndex;
+          if((int)round(keyDegree)%12 == 1 || (int)round(keyDegree)%12 == 3 || (int)round(keyDegree)%12 == 6 || (int)round(keyDegree)%12 == 8 || (int)round(keyDegree)%12 == 10)
+          {
+              float deviationFromDiatonic = ((float)((int)round(keyDegree) - keyDegree) * 3072.0f)/12.0;
+              uint8_t tint = (uint8_t)(abs(round(deviationFromDiatonic)));
+              tint = strip.gamma8(tint);
+              setColor = {360 * (fmodf(round(keyDegree),12.0f) / 12.0f),SAT_TINT,VALUE_BLACK};
+          }
+          else // White key
+          {
+              float deviationFromDiatonic = ((((float)((int)round(keyDegree))) - (keyDegree)) * 3072.0f)/12.0;
+              uint8_t tint = 255 - (uint8_t)(abs(round(deviationFromDiatonic)));
+              tint = strip.gamma8(tint);
+              setColor = {360 * (fmodf(round(keyDegree),12.0f) / 12.0f),SAT_TINT,VALUE_NORMAL};
+          }
+          }
+            break;
+          case PIANO_INCANDESCENT_COLOR_MODE:
+          {
+          float octaveCycleLength = 1200.0/current.tuning().stepSize; // This is to prevent non-octave colouring weirdness
+          float semipaletteIndex = fmodf(h[i].stepsFromC+(octaveCycleLength*256.0),octaveCycleLength);
+          float keyDegree = (12.0f/octaveCycleLength)*semipaletteIndex;
+          float tint, deviationFromDiatonic;
+          if((int)round(keyDegree)%12 == 1 || (int)round(keyDegree)%12 == 3 || (int)round(keyDegree)%12 == 6 || (int)round(keyDegree)%12 == 8 || (int)round(keyDegree)%12 == 10)
+          {
+              deviationFromDiatonic = (round(keyDegree) - keyDegree);
+              deviationFromDiatonic = (abs(deviationFromDiatonic)); // from 0 to 0.5
+          }
+          else // White key
+          {
+              deviationFromDiatonic = (round(keyDegree) - keyDegree);
+              deviationFromDiatonic = 1.0-abs(deviationFromDiatonic); // from 1 to 0.5
+          }
+          auto baseTemperature = 800;
+          tint = ((sqrt(deviationFromDiatonic))) * (incandescence::maxTemperature-baseTemperature) + baseTemperature;
+          
+          setColor = incandescence::getColor(tint);
+          }
+            break;
+          case ALTERNATE_COLOR_MODE:
+            // This mode assigns each note a color based on the interval it forms with the root note.
+            // This is an adaptation of an algorithm developed by Nicholas Fox and Kite Giedraitis.
+            float cents = current.tuning().stepSize * paletteIndex;
+            bool perf = 0;
+            float center = 0.0;
+                   if                    (cents <   50)  {perf = 1; center =    0.0;}
+              else if ((cents >=  50) && (cents <  250)) {          center =  147.1;}
+              else if ((cents >= 250) && (cents <  450)) {          center =  351.0;}
+              else if ((cents >= 450) && (cents <  600)) {perf = 1; center =  498.0;}
+              else if ((cents >= 600) && (cents <= 750)) {perf = 1; center =  702.0;}
+              else if ((cents >  750) && (cents <= 950)) {          center =  849.0;}
+              else if ((cents >  950) && (cents <=1150)) {          center = 1053.0;}
+              else if ((cents > 1150) && (cents < 1250)) {perf = 1; center = 1200.0;}
+              else if ((cents >=1250) && (cents < 1450)) {          center = 1347.1;}
+              else if ((cents >=1450) && (cents < 1650)) {          center = 1551.0;}
+              else if ((cents >=1650) && (cents < 1850)) {perf = 1; center = 1698.0;}
+              else if ((cents >=1800) && (cents <=1950)) {perf = 1; center = 1902.0;}
+            float offCenter = cents - center;
+            int16_t altHue = positiveMod((int)(150 + (perf * ((offCenter > 0) ? -72 : 72)) - round(1.44 * offCenter)), 360);
+            float deSaturate = perf * (abs(offCenter) < 20) * (1 - (0.02 * abs(offCenter)));
+            setColor = {
+              (float)altHue,
+              (byte)(255 - round(255 * deSaturate)),
+              (byte)(cents ? VALUE_SHADE : VALUE_NORMAL) };
+            break;
+        }
+        h[i].LEDcodeRest   = getLEDcode(setColor);
+        h[i].LEDcodePlay = getLEDcode(setColor.tint());
+        h[i].LEDcodeDim  = getLEDcode(setColor.shade());
+        setColor = {HUE_NONE,SAT_BW,VALUE_BLACK};
+        h[i].LEDcodeOff  = getLEDcode(setColor);                // turn off entirely
+        h[i].LEDcodeAnim = h[i].LEDcodePlay;
+      }
+    }
+    sendToLog("LED codes re-calculated.");
+  }
+
+  void resetVelocityLEDs() {
+    colorDef tempColor =
+      { (runTime % (rainbowDegreeTime * 360)) / (float)rainbowDegreeTime
+      , SAT_MODERATE
+      , byteLerp(0,255,85,127,velWheel.curValue)
+      };
+    strip.setPixelColor(assignCmd[0], getLEDcode(tempColor));
+
+    tempColor.val = byteLerp(0,255,42,85,velWheel.curValue);
+    strip.setPixelColor(assignCmd[1], getLEDcode(tempColor));
+
+    tempColor.val = byteLerp(0,255,0,42,velWheel.curValue);
+    strip.setPixelColor(assignCmd[2], getLEDcode(tempColor));
+  }
+  void resetWheelLEDs() {
+    // middle button
+    byte tempSat = SAT_BW;
+    colorDef tempColor = {HUE_NONE, tempSat, (byte)(toggleWheel ? VALUE_SHADE : VALUE_LOW)};
+    strip.setPixelColor(assignCmd[3], getLEDcode(tempColor));
+    if (toggleWheel) {
+      // pb red / green
+      tempSat = byteLerp(SAT_BW,SAT_VIVID,0,8192,abs(pbWheel.curValue));
+      tempColor = {(float)((pbWheel.curValue > 0) ? HUE_RED : HUE_CYAN), tempSat, VALUE_FULL};
+      strip.setPixelColor(assignCmd[5], getLEDcode(tempColor));
+
+      tempColor.val = tempSat * (pbWheel.curValue > 0);
+      strip.setPixelColor(assignCmd[4], getLEDcode(tempColor));
+
+      tempColor.val = tempSat * (pbWheel.curValue < 0);
+      strip.setPixelColor(assignCmd[6], getLEDcode(tempColor));
+    } else {
+      // mod blue / yellow
+      tempSat = byteLerp(SAT_BW,SAT_VIVID,0,64,abs(modWheel.curValue - 63));
+      tempColor = {
+        (float)((modWheel.curValue > 63) ? HUE_YELLOW : HUE_INDIGO),
+        tempSat,
+        (byte)(127 + (tempSat / 2))
+      };
+      strip.setPixelColor(assignCmd[6], getLEDcode(tempColor));
+
+      if (modWheel.curValue <= 63) {
+        tempColor.val = 127 - (tempSat / 2);
+      }
+      strip.setPixelColor(assignCmd[5], getLEDcode(tempColor));
+
+      tempColor.val = tempSat * (modWheel.curValue > 63);
+      strip.setPixelColor(assignCmd[4], getLEDcode(tempColor));
+    }
+  }
+  uint32_t applyNotePixelColor(byte x) {
+           if (h[x].animate) { return h[x].LEDcodeAnim;
+    } else if (h[x].MIDIch)  { return h[x].LEDcodePlay;
+    } else if (h[x].inScale) { return h[x].LEDcodeRest;
+    } else if (scaleLock)    { return h[x].LEDcodeOff;
+    } else                   { return h[x].LEDcodeDim;
+    }
+  }
+  void setupLEDs() {
+    strip.begin();    // INITIALIZE NeoPixel strip object
+    strip.show();     // Turn OFF all pixels ASAP
+    sendToLog("LEDs started...");
+    setLEDcolorCodes();
+  }
+  void lightUpLEDs() {
+    for (byte i = 0; i < LED_COUNT; i++) {
+      if (!(h[i].isCmd)) {
+        strip.setPixelColor(i,applyNotePixelColor(i));
+      }
+    }
+    resetVelocityLEDs();
+    resetWheelLEDs();
+    strip.show();
+  }
+
+// @MIDI
+  /*
+    This section of the code handles all
+    things related to MIDI messages.
+  */
+  #include <Adafruit_TinyUSB.h>   // library of code to get the USB port working
+  #include <MIDI.h>               // library of code to send and receive MIDI messages
+  /*
+    These values support correct MIDI output.
+    Note frequencies are converted to MIDI note
+    and pitch bend messages assuming note 69
+    equals concert A4, as defined below.
+  */
+  #define CONCERT_A_HZ 440.0
+  /*
+    Pitch bend messages are calibrated
+    to a pitch bend range where
+    -8192 to 8191 = -200 to +200 cents,
+    or two semitones.
+  */
+  #define PITCH_BEND_SEMIS 2
+  /*
+    We use pitch bends to retune notes in MPE mode.
+    Some setups can adjust to fit this, but some need us to adjust it.
+  */
+  byte MPEpitchBendSemis = 48;
+  /*
+    Create a new instance of the Arduino MIDI Library,
+    and attach usb_midi as the transport.
+  */
+  Adafruit_USBD_MIDI usb_midi;
+  MIDI_CREATE_INSTANCE(Adafruit_USBD_MIDI, usb_midi, UMIDI);
+  MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, SMIDI);
+  // midiD takes the following bitwise flags
+  #define MIDID_NONE 0
+  #define MIDID_USB 1
+  #define MIDID_SER 2
+  #define MIDID_BOTH 3
+  byte midiD = MIDID_USB | MIDID_SER;
+
+  // What program change number we last sent (General MIDI/Roland MT-32)
+  byte programChange = 0;
+
+  std::queue<byte> MPEchQueue;
+  byte MPEpitchBendsNeeded;
+
+  float freqToMIDI(float Hz) {             // formula to convert from Hz to MIDI note
+    return 69.0 + 12.0 * log2f(Hz / 440.0);
+  }
+  float MIDItoFreq(float midi) {           // formula to convert from MIDI note to Hz
+    return 440.0 * exp2((midi - 69.0) / 12.0);
+  }
+  float stepsToMIDI(int16_t stepsFromA) {  // return the MIDI pitch associated
+    return freqToMIDI(CONCERT_A_HZ) + ((float)stepsFromA * (float)current.tuning().stepSize / 100.0);
+  }
+
+  void setPitchBendRange(byte Ch, byte semitones) {
+    if (midiD&MIDID_USB) {
+        UMIDI.beginRpn(0, Ch);
+        UMIDI.sendRpnValue(semitones << 7, Ch);
+        UMIDI.endRpn(Ch);
+    }
+    if (midiD&MIDID_SER) {
+        SMIDI.beginRpn(0, Ch);
+        SMIDI.sendRpnValue(semitones << 7, Ch);
+        SMIDI.endRpn(Ch);
+    }
+    sendToLog(
+      "set pitch bend range on ch " +
+      std::to_string(Ch) + " to be " +
+      std::to_string(semitones) + " semitones"
+    );
+  }
+
+  void setMPEzone(byte masterCh, byte sizeOfZone) {
+    if (midiD&MIDID_USB) {
+        UMIDI.beginRpn(6, masterCh);
+        UMIDI.sendRpnValue(sizeOfZone << 7, masterCh);
+        UMIDI.endRpn(masterCh);
+    }
+    if (midiD&MIDID_SER) {
+        SMIDI.beginRpn(6, masterCh);
+        SMIDI.sendRpnValue(sizeOfZone << 7, masterCh);
+        SMIDI.endRpn(masterCh);
+    }
+    sendToLog(
+      "tried sending MIDI msg to set MPE zone, master ch " +
+      std::to_string(masterCh) + ", zone of this size: " + std::to_string(sizeOfZone)
+    );
+  }
+
+  void resetTuningMIDI() {
+    /*
+      currently the only way that microtonal
+      MIDI works is via MPE (MIDI polyphonic expression).
+      This assigns re-tuned notes to an independent channel
+      so they can be pitched separately.
+
+      if operating in a standard 12-EDO tuning, or in a
+      tuning with steps that are all exact multiples of
+      100 cents, then MPE is not necessary.
+    */
+    if (current.tuning().stepSize == 100.0 && !useDynamicJustIntonation && !useJustIntonationBPM && !forceEnableMPE) {
+       MPEpitchBendsNeeded = 1;  // Standard 12EDO, single-channel mode
+    /*  this was an attempt to allow unlimited polyphony for certain EDOs. doesn't work in Logic Pro.
+    } else if (round(current.tuning().cycleLength * current.tuning().stepSize) == 1200) {
+      MPEpitchBendsNeeded = current.tuning().cycleLength / std::gcd(12, current.tuning().cycleLength);
+    */
+    } else {
+      MPEpitchBendsNeeded = 255;  // Enables MPE mode when in Just Intonation or microtonal tuning
+    }
+    if (MPEpitchBendsNeeded > 15) {
+      setMPEzone(1, 15);   // MPE zone 1 = ch 2 thru 16
+      while (!MPEchQueue.empty()) {     // empty the channel queue
+        MPEchQueue.pop();
+      }
+      for (byte i = 2; i <= 16; i++) {
+        MPEchQueue.push(i);           // fill the channel queue
+        sendToLog("pushed ch " + std::to_string(i) + " to the open channel queue");
+      }
+    } else {
+      setMPEzone(1, 0);
+    }
+    // force pitch bend back to the expected range of 2 semitones.
+    for (byte i = 1; i <= 16; i++) {
+      if(midiD&MIDID_USB)UMIDI.sendControlChange(123, 0, i);
+      if(midiD&MIDID_SER)SMIDI.sendControlChange(123, 0, i);
+      setPitchBendRange(i, MPEpitchBendSemis);
+    }
+  }
+
+  void sendMIDImodulationToCh1() {
+    if(midiD&MIDID_USB)UMIDI.sendControlChange(1, modWheel.curValue, 1);
+    if(midiD&MIDID_SER)SMIDI.sendControlChange(1, modWheel.curValue, 1);
+    sendToLog("sent mod value " + std::to_string(modWheel.curValue) + " to ch 1");
+  }
+
+  void sendMIDIpitchBendToCh1() {
+    if(midiD&MIDID_USB)UMIDI.sendPitchBend(pbWheel.curValue, 1);
+    if(midiD&MIDID_SER)SMIDI.sendPitchBend(pbWheel.curValue, 1);
+    sendToLog("sent pb wheel value " + std::to_string(pbWheel.curValue) + " to ch 1");
+  }
+
+//////////////////////////////////////////////////////////////////
+//  Dynamic just intonation code start
+
+
+  // HOW BPM SYNC WORKS:
+  // The idea is to round off the note frequency to a certain precision.
+  // If you round the note frequencies of a C-E-G chord to integer values (261.626Hz / 329.628Hz / 391.995Hz) -
+  // you'll get a chord with ratio of 262/294/392.
+  // As a result, because these frequency values are always a multiple of 1 Hz -
+  // they all will be guaranteed to finish their wave cycle in 1 second.
+  // Thus, this chord will beat at 1 Hz if not faster.
+
+  // By knowing the pressed keys it is possible to pick better ratios, ideally having 262/327.5/393 (4/5/6) in this example
+
+  // TODO: make BPM sync work with dynamic just intonation to make pure just intonation achieveable.
+  // Without it - this implementation provides you with n-EDO-sized independent JI rings, unconnected to eachother;
+  // TODO: replace floating point math with integer math;
+  // TODO: replace std::pair<byte,byte> ratios with precomputed floating(or fixed) point ratios;
+  // TODO: generate the table of ratios with a constexpr function rather than holding a huge block of hardcoded values in the code;
+  // TODO: It is a good idea to octave-reduce the ratios, and adjust the code to calculate pitchbend against the octave reduced set of ratios for significant performance improvement;
+  // TODO: implement dynamic just intonation for buzzer
+  // TODO: There is an issue with tuning error in this line:
+  // UMIDI.sendPitchBend(h[x].bend + justIntonationRetune(x), h[x].MIDIch);
+  // The retuning is done twice, and rounding errors will add up. This can be heard if you play a unison with different pitches that collapse into 1/1 by dynamic just intonation (or if you use BPM sync with a really high frequency). You will sometimes hear extremely slow beating despite both notes being forced into the same pitch
+
+
+  // This is a list of ratios sorted from the simplest ones to the most complex ones. The code searches for a first match that's good enough within 1/4 of an EDO step, literally bruteforcing through the list. As a result - the simplest ratio is chosen before more comples ones, prioritising consonant ratios first. In case not a single good ratio is found - the best one found so far is chosen instead
+  
+  // byte pair was chosen to preserve space. The ratio is "unpacked" later
+  std::vector<std::pair<byte,byte>> ratios =
+  {
+    {1,1},
+    {1,2},
+    {2,1},
+    {3,1},
+    {1,3},
+    {1,4},
+    {2,3},
+    {1,4},
+    {4,1},
+    {3,2},
+    {1,5},
+    {5,1},
+    {5,2},
+    {1,6},
+    {3,4},
+    {5,2},
+    {4,3},
+    {6,1},
+    {2,5},
+    {5,3},
+    {1,7},
+    {7,1},
+    {3,5},
+    {2,7},
+    {8,1},
+    {5,4},
+    {1,8},
+    {4,5},
+    {7,2},
+    {9,1},
+    {7,3},
+    {1,9},
+    {3,7},
+    {1,9},
+    {1,10},
+    {10,1},
+    {7,4},
+    {3,8},
+    {8,3},
+    {6,5},
+    {1,10},
+    {8,3},
+    {4,7},
+    {2,9},
+    {9,2},
+    {5,6},
+    {11,1},
+    {7,5},
+    {1,11},
+    {5,7},
+    {5,8},
+    {3,10},
+    {4,9},
+    {3,10},
+    {2,11},
+    {11,2},
+    {12,1},
+    {1,12},
+    {9,4},
+    {5,8},
+    {1,12},
+    {8,5},
+    {10,3},
+    {6,7},
+    {7,6},
+    {12,1},
+    {9,5},
+    {1,13},
+    {3,11},
+    {11,3},
+    {9,5},
+    {5,9},
+    {13,1},
+    {14,1},
+    {13,2},
+    {11,4},
+    {1,14},
+    {2,13},
+    {8,7},
+    {7,8},
+    {4,11},
+    {9,7},
+    {11,5},
+    {7,9},
+    {5,11},
+    {13,3},
+    {3,13},
+    {15,1},
+    {1,15},
+    {4,13},
+    {2,15},
+    {10,7},
+    {2,15},
+    {11,6},
+    {8,9},
+    {16,1},
+    {12,5},
+    {3,14},
+    {7,10},
+    {5,12},
+    {14,3},
+    {9,8},
+    {15,2},
+    {13,4},
+    {1,16},
+    {6,11},
+    {17,1},
+    {1,17},
+    {5,13},
+    {13,5},
+    {4,15},
+    {17,2},
+    {9,10},
+    {2,17},
+    {9,10},
+    {12,7},
+    {10,9},
+    {11,8},
+    {16,3},
+    {3,16},
+    {13,6},
+    {14,5},
+    {15,4},
+    {18,1},
+    {8,11},
+    {1,18},
+    {4,15},
+    {5,14},
+    {6,13},
+    {7,12},
+    {19,1},
+    {11,9},
+    {17,3},
+    {3,17},
+    {9,11},
+    {1,19},
+    {5,16},
+    {20,1},
+    {8,13},
+    {10,11},
+    {20,1},
+    {19,2},
+    {1,20},
+    {11,10},
+    {2,19},
+    {13,8},
+    {17,4},
+    {4,17},
+    {16,5},
+    {1,20},
+    {13,9},
+    {21,1},
+    {7,15},
+    {9,13},
+    {19,3},
+    {17,5},
+    {3,19},
+    {5,17},
+    {15,7},
+    {1,21},
+    {13,10},
+    {3,20},
+    {12,11},
+    {21,2},
+    {18,5},
+    {6,17},
+    {15,8},
+    {3,20},
+    {20,3},
+    {19,4},
+    {5,18},
+    {14,9},
+    {9,14},
+    {8,15},
+    {2,21},
+    {1,22},
+    {17,6},
+    {22,1},
+    {10,13},
+    {11,12},
+    {4,19},
+    {5,19},
+    {1,23},
+    {19,5},
+    {23,1},
+    {18,7},
+    {8,17},
+    {21,4},
+    {22,3},
+    {3,22},
+    {7,18},
+    {6,19},
+    {12,13},
+    {19,6},
+    {2,23},
+    {9,16},
+    {17,8},
+    {24,1},
+    {13,12},
+    {1,24},
+    {23,2},
+    {4,21},
+    {16,9},
+    {9,17},
+    {1,25},
+    {5,21},
+    {25,1},
+    {15,11},
+    {17,9},
+    {3,23},
+    {23,3},
+    {11,15},
+    {21,5},
+    {17,10},
+    {10,17},
+    {19,8},
+    {5,22},
+    {20,7},
+    {22,5},
+    {23,4},
+    {7,20},
+    {1,26},
+    {8,19},
+    {25,2},
+    {26,1},
+    {2,25},
+    {4,23},
+    {5,23},
+    {9,19},
+    {1,27},
+    {13,15},
+    {3,25},
+    {15,13},
+    {23,5},
+    {19,9},
+    {27,1},
+    {25,3},
+    {25,4},
+    {14,15},
+    {27,2},
+    {9,20},
+    {2,27},
+    {26,3},
+    {20,9},
+    {17,12},
+    {1,28},
+    {24,5},
+    {10,19},
+    {12,17},
+    {23,6},
+    {21,8},
+    {11,18},
+    {19,10},
+    {5,24},
+    {4,25},
+    {5,24},
+    {3,26},
+    {18,11},
+    {28,1},
+    {8,21},
+    {6,23},
+    {15,14},
+    {1,29},
+    {29,1},
+    {23,8},
+    {24,7},
+    {7,24},
+    {6,25},
+    {10,21},
+    {30,1},
+    {5,26},
+    {25,6},
+    {11,20},
+    {30,1},
+    {1,30},
+    {16,15},
+    {8,23},
+    {4,27},
+    {2,29},
+    {26,5},
+    {9,22},
+    {29,2},
+    {27,4},
+    {28,3},
+    {15,16},
+    {20,11},
+    {18,13},
+    {22,9},
+    {21,10},
+    {13,18},
+    {12,19},
+    {19,12},
+    {3,28},
+    {17,15},
+    {15,17},
+    {29,3},
+    {31,1},
+    {27,5},
+    {3,29},
+    {9,23},
+    {23,9},
+    {1,31},
+    {5,27},
+    {13,20},
+    {2,31},
+    {28,5},
+    {1,32},
+    {29,4},
+    {25,8},
+    {20,13},
+    {4,29},
+    {8,25},
+    {23,10},
+    {10,23},
+    {5,28},
+    {31,2},
+    {32,1},
+    {33,1},
+    {3,31},
+    {1,33},
+    {5,29},
+    {15,19},
+    {9,25},
+    {31,3},
+    {19,15},
+    {25,9},
+    {29,5},
+    {33,2},
+    {6,29},
+    {17,18},
+    {34,1},
+    {2,33},
+    {32,3},
+    {26,9},
+    {31,4},
+    {27,8},
+    {1,34},
+    {4,31},
+    {18,17},
+    {29,6},
+    {8,27},
+    {12,23},
+    {11,24},
+    {3,32},
+    {9,26},
+    {23,12},
+    {24,11},
+    {5,31},
+    {31,5},
+    {35,1},
+    {1,35},
+    {1,36},
+    {30,7},
+    {24,13},
+    {18,19},
+    {36,1},
+    {6,31},
+    {28,9},
+    {34,3},
+    {36,1},
+    {15,22},
+    {7,30},
+    {8,29},
+    {1,36},
+    {17,20},
+    {29,8},
+    {4,33},
+    {12,25},
+    {10,27},
+    {32,5},
+    {20,17},
+    {3,34},
+    {25,12},
+    {5,32},
+    {2,35},
+    {33,4},
+    {22,15},
+    {9,28},
+    {13,24},
+    {27,10},
+    {35,2},
+    {19,18},
+    {31,6},
+    {9,29},
+    {35,3},
+    {29,9},
+    {5,33},
+    {23,15},
+    {33,5},
+    {15,23},
+    {37,1},
+    {3,35},
+    {1,37},
+    {10,29},
+    {31,8},
+    {5,34},
+    {4,35},
+    {1,38},
+    {35,4},
+    {29,10},
+    {34,5},
+    {37,2},
+    {2,37},
+    {19,20},
+    {8,31},
+    {20,19},
+    {38,1},
+    {31,9},
+    {39,1},
+    {37,3},
+    {3,37},
+    {9,31},
+    {1,39},
+    {38,3},
+    {11,30},
+    {9,32},
+    {26,15},
+    {31,10},
+    {29,12},
+    {32,9},
+    {20,21},
+    {2,39},
+    {35,6},
+    {33,8},
+    {5,36},
+    {37,4},
+    {21,20},
+    {15,26},
+    {40,1},
+    {8,33},
+    {10,31},
+    {30,11},
+    {12,29},
+    {23,18},
+    {17,24},
+    {36,5},
+    {40,1},
+    {1,40},
+    {4,37},
+    {24,17},
+    {39,2},
+    {6,35},
+    {18,23},
+    {3,38},
+    {41,1},
+    {37,5},
+    {5,37},
+    {1,41},
+    {33,10},
+    {3,40},
+    {4,39},
+    {1,42},
+    {37,6},
+    {13,30},
+    {12,31},
+    {42,1},
+    {10,33},
+    {7,36},
+    {36,7},
+    {9,34},
+    {41,2},
+    {35,8},
+    {40,3},
+    {8,35},
+    {5,38},
+    {2,41},
+    {39,4},
+    {38,5}
+  };
+
+  int16_t centsToRelativePitchBend(float cents){
+    return round(cents * (8192.0/(100.0*MPEpitchBendSemis)));
+  }
+
+  float ratioToCents(float ratio){
+    return 1200.0 * (std::log(ratio) / std::log(2.0));
+  }
+
+  int16_t justIntonationRetune(byte x)
+  {
+    if(useDynamicJustIntonation == false && useJustIntonationBPM == false)
+    {
+      return 0;
+    }
+    int16_t pitchAdjustment = 0;
+    float pitchAdjustmentCents = 0;
+    float basePitchOffset = 0;
+    //int16_t degree = (current.keyDegree(h[x].stepsFromC + current.transpose + current.tuning().spanCtoA()));
+    //float buttonStepsFromA = degree;
+    if(useJustIntonationBPM)
+    {
+      float buttonStepsFromA = -current.tuning().spanCtoA() - h[x].stepsFromC;
+      // It was planned to use integer math but floating point arithmetics works fast enough so far
+      float rounding = ((float)justIntonationBPM / 60.0 * justIntonationBPM_Multiplier);
+      pitchAdjustmentCents = (buttonStepsFromA * current.tuning().stepSize) -
+      ratioToCents(round(440.0 / rounding) / round(h[x].frequency / rounding));
+
+      if(pressedKeyIDs.size() > 1 && useDynamicJustIntonation)
+      {
+        basePitchOffset = ((-current.tuning().spanCtoA() - h[pressedKeyIDs[0]].stepsFromC) * current.tuning().stepSize) -
+        ratioToCents(round(440.0 / rounding) / round(h[pressedKeyIDs[0]].frequency / rounding));
+      }
+      else
+      {
+        pitchAdjustment += centsToRelativePitchBend(pitchAdjustmentCents);
+      }
+    }
+    if(useDynamicJustIntonation && pressedKeyIDs.size() > 1)
+    {
+      //bool ratioFound = false;  // I might need this one later
+      bool preferSmallRatios = true;  // if false - the closest found ratio will be chosen from the ratio table
+
+      // detune within a 1/4 of a step, avoid wild detuning but cover the entire pitch range
+      float errorThreshold = current.tuning().stepSize / 4.0;
+      float deviation = INFINITY;
+      float EDOCents = ratioToCents(h[pressedKeyIDs[0]].frequency / h[x].frequency);
+      std::pair<byte,byte> selectedRatio;
+
+      for(int i = 0; i < ratios.size();i++)
+      {
+        auto ratio = ratios[i];
+        float ratio0 = ratio.first;
+        float ratio1 = ratio.second;
+        //if(h[pressedKeyIDs[0]].note < h[x].note)
+        //{
+        //  std::swap(ratio1,ratio0);
+        //}
+        float ratioCents = ratioToCents(ratio0/ratio1);
+
+        if(std::abs(deviation) > std::abs(ratioCents - EDOCents))
+        {
+          deviation = (EDOCents - ratioCents);
+          selectedRatio.first = ratio0;
+          selectedRatio.second = ratio1;
+          if(preferSmallRatios && std::abs(deviation) < errorThreshold)
+          {
+            //ratioFound = true;
+            break;
+          }
+        }
+      }
+      //if(ratioFound)
+      {
+        pitchAdjustment += centsToRelativePitchBend(deviation + basePitchOffset);
+      }
+    }
+    return pitchAdjustment;
+  }
+
+
+void tryMIDInoteOn(byte x) {
+    // This gets called on any non-command hex that is not scale-locked.
+    if (!(h[x].MIDIch)) {
+        if (MPEpitchBendsNeeded == 1) {
+            h[x].MIDIch = defaultMidiChannel;
+        } else if (MPEpitchBendsNeeded <= 15) {
+            h[x].MIDIch = 2 + positiveMod(h[x].stepsFromC, MPEpitchBendsNeeded);
+        } else {
+            if (MPEchQueue.empty()) {   // If there aren't any open channels
+                sendToLog("MPE queue was empty so did not play a MIDI note");
+            } else {
+                h[x].MIDIch = MPEchQueue.front();   // Value in MIDI terms (1-16)
+                MPEchQueue.pop();
+                sendToLog("Popped " + std::to_string(h[x].MIDIch) + " off the MPE queue");
+            }
+        }
+
+        if (h[x].MIDIch) {
+            pressedKeyIDs.push_back(x); // Dynamic JI pressed key tracking
+            // First, send the pitch bend (if applicable)
+            if (MPEpitchBendsNeeded != 1) {
+                if (midiD & MIDID_USB) UMIDI.sendPitchBend(h[x].bend + justIntonationRetune(x), h[x].MIDIch); // ch 1-16
+                if (midiD & MIDID_SER) SMIDI.sendPitchBend(h[x].bend + justIntonationRetune(x), h[x].MIDIch); // ch 1-16
+            }
+
+            // Then, send the note-on message
+            if (midiD & MIDID_USB) UMIDI.sendNoteOn(h[x].note, velWheel.curValue, h[x].MIDIch); // ch 1-16
+            if (midiD & MIDID_SER) SMIDI.sendNoteOn(h[x].note, velWheel.curValue, h[x].MIDIch); // ch 1-16
+
+            sendToLog(
+                "Sent MIDI pitch bend: " + std::to_string((MPEpitchBendsNeeded != 1) ? h[x].bend + justIntonationRetune(x) : 0) +
+                " to ch " + std::to_string(h[x].MIDIch)
+            );
+            sendToLog(
+                "Sent MIDI noteOn: " + std::to_string(h[x].note) +
+                " vel " + std::to_string(velWheel.curValue) +
+                " ch "  + std::to_string(h[x].MIDIch)
+            );
+        }
+    }
+}
+
+  void tryMIDInoteOff(byte x) {
+    // this gets called on any non-command hex
+    // that is not scale-locked.
+    if (h[x].MIDIch) {    // but just in case, check
+      if(midiD&MIDID_USB)UMIDI.sendNoteOff(h[x].note, velWheel.curValue, h[x].MIDIch);
+      if(midiD&MIDID_SER)SMIDI.sendNoteOff(h[x].note, velWheel.curValue, h[x].MIDIch);
+      pressedKeyIDs.pop_back(); // Dynamic JI pressed key tracking
+      sendToLog(
+        "sent note off: " + std::to_string(h[x].note) +
+        " pb " + std::to_string(h[x].bend) +
+        " vel " + std::to_string(velWheel.curValue) +
+        " ch " + std::to_string(h[x].MIDIch)
+      );
+      if (MPEpitchBendsNeeded > 15 && h[x].MIDIch > 1) {
+        MPEchQueue.push(h[x].MIDIch);
+        sendToLog("pushed " + std::to_string(h[x].MIDIch) + " on the MPE queue");
+      }
+      h[x].MIDIch = 0;
+    }
+  }
+
+  void setupMIDI() {
+    usb_midi.setStringDescriptor("HexBoard MIDI");  // Initialize MIDI, and listen to all MIDI channels
+    UMIDI.begin(MIDI_CHANNEL_OMNI);                 // This will also call usb_midi's begin()
+    SMIDI.begin(MIDI_CHANNEL_OMNI);
+    resetTuningMIDI();
+    sendToLog("setupMIDI okay");
+  }
+
+// @synth
+  /*
+    This section of the code handles audio
+    output via the piezo buzzer and/or the
+    headphone jack (on hardware v1.2 only)
+  */
+  #include "hardware/pwm.h"       // library of code to access the processor's built in pulse wave modulation features
+  #include "hardware/irq.h"       // library of code to let you interrupt code execution to run something of higher priority
+  /*
+    It is more convenient to pre-define the correct
+    pulse wave modulation slice and channel associated
+    with the PIEZO_PIN on this processor (see RP2040
+    manual) than to have it looked up each time.
+  */
+  #define PIEZO_PIN 23
+  #define PIEZO_SLICE 3
+  #define PIEZO_CHNL 1
+  #define AJACK_PIN 25
+  #define AJACK_SLICE 4
+  #define AJACK_CHNL 1
+  // midiD takes the following bitwise flags
+  #define AUDIO_NONE 0
+  #define AUDIO_PIEZO 1
+  #define AUDIO_AJACK 2
+  #define AUDIO_BOTH 3
+  byte audioD = AUDIO_PIEZO | AUDIO_AJACK;
+  /*
+    These definitions provide 8-bit samples to emulate.
+    You can add your own as desired; it must
+    be an array of 256 values, each from 0 to 255.
+    Ideally the waveform is normalized so that the
+    peaks are at 0 to 255, with 127 representing
+    no wave movement.
+  */
+  byte sine[] = {
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   1,   1,   2,   3,   3,
+      4,   5,   6,   7,   8,   9,  10,  12,  13,  15,  16,  18,  19,  21,  23,  25,
+      27,  29,  31,  33,  35,  37,  39,  42,  44,  46,  49,  51,  54,  56,  59,  62,
+      64,  67,  70,  73,  76,  79,  81,  84,  87,  90,  93,  96,  99, 103, 106, 109,
+    112, 115, 118, 121, 124, 127, 131, 134, 137, 140, 143, 146, 149, 152, 156, 159,
+    162, 165, 168, 171, 174, 176, 179, 182, 185, 188, 191, 193, 196, 199, 201, 204,
+    206, 209, 211, 213, 216, 218, 220, 222, 224, 226, 228, 230, 232, 234, 236, 237,
+    239, 240, 242, 243, 245, 246, 247, 248, 249, 250, 251, 252, 252, 253, 254, 254,
+    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 254, 253, 252, 252,
+    251, 250, 249, 248, 247, 246, 245, 243, 242, 240, 239, 237, 236, 234, 232, 230,
+    228, 226, 224, 222, 220, 218, 216, 213, 211, 209, 206, 204, 201, 199, 196, 193,
+    191, 188, 185, 182, 179, 176, 174, 171, 168, 165, 162, 159, 156, 152, 149, 146,
+    143, 140, 137, 134, 131, 127, 124, 121, 118, 115, 112, 109, 106, 103,  99,  96,
+      93,  90,  87,  84,  81,  79,  76,  73,  70,  67,  64,  62,  59,  56,  54,  51,
+      49,  46,  44,  42,  39,  37,  35,  33,  31,  29,  27,  25,  23,  21,  19,  18,
+      16,  15,  13,  12,  10,   9,   8,   7,   6,   5,   4,   3,   3,   2,   1,   1
+  };
+  byte strings[] = {
+      0,   0,   0,   1,   3,   6,  10,  14,  20,  26,  33,  41,  50,  59,  68,  77,
+      87,  97, 106, 115, 124, 132, 140, 146, 152, 157, 161, 164, 166, 167, 167, 167,
+    165, 163, 160, 157, 153, 149, 144, 140, 135, 130, 126, 122, 118, 114, 111, 109,
+    106, 104, 103, 101, 101, 100, 100, 100, 100, 101, 101, 102, 103, 103, 104, 105,
+    106, 107, 108, 109, 110, 111, 113, 114, 115, 116, 117, 119, 120, 121, 123, 124,
+    126, 127, 129, 131, 132, 134, 135, 136, 138, 139, 140, 141, 142, 144, 145, 146,
+    147, 148, 149, 150, 151, 152, 152, 153, 154, 154, 155, 155, 155, 155, 154, 154,
+    152, 151, 149, 146, 144, 140, 137, 133, 129, 125, 120, 115, 111, 106, 102,  98,
+      95,  92,  90,  88,  88,  88,  89,  91,  94,  98, 103, 109, 115, 123, 131, 140,
+    149, 158, 168, 178, 187, 196, 205, 214, 222, 229, 235, 241, 245, 249, 252, 254,
+    255, 255, 255, 254, 253, 250, 248, 245, 242, 239, 236, 233, 230, 227, 224, 222,
+    220, 218, 216, 215, 214, 213, 212, 211, 210, 210, 209, 208, 207, 206, 205, 203,
+    201, 199, 197, 194, 191, 188, 184, 180, 175, 171, 166, 161, 156, 150, 145, 139,
+    133, 127, 122, 116, 110, 105,  99,  94,  89,  84,  80,  75,  71,  67,  64,  61,
+      58,  56,  54,  52,  50,  49,  48,  47,  46,  45,  45,  44,  43,  42,  41,  40,
+      39,  37,  35,  33,  31,  28,  25,  22,  19,  16,  13,  10,   7,   5,   2,   1
+  };
+  byte clarinet[] = {
+      0,   0,   2,   7,  14,  21,  30,  38,  47,  54,  61,  66,  70,  72,  73,  74,
+      73,  73,  72,  71,  70,  71,  72,  74,  76,  80,  84,  88,  93,  97, 101, 105,
+    109, 111, 113, 114, 114, 114, 113, 112, 111, 110, 109, 109, 109, 110, 112, 114,
+    116, 118, 121, 123, 126, 127, 128, 129, 128, 127, 126, 123, 121, 118, 116, 114,
+    112, 110, 109, 109, 109, 110, 111, 112, 113, 114, 114, 114, 113, 111, 109, 105,
+    101,  97,  93,  88,  84,  80,  76,  74,  72,  71,  70,  71,  72,  73,  73,  74,
+      73,  72,  70,  66,  61,  54,  47,  38,  30,  21,  14,   7,   2,   0,   0,   2,
+      9,  18,  31,  46,  64,  84, 105, 127, 150, 171, 191, 209, 224, 237, 246, 252,
+    255, 255, 253, 248, 241, 234, 225, 217, 208, 201, 194, 189, 185, 183, 182, 181,
+    182, 182, 183, 184, 185, 184, 183, 181, 179, 175, 171, 167, 162, 158, 154, 150,
+    146, 144, 142, 141, 141, 141, 142, 143, 144, 145, 146, 146, 146, 145, 143, 141,
+    139, 136, 134, 132, 129, 128, 127, 126, 127, 128, 129, 132, 134, 136, 139, 141,
+    143, 145, 146, 146, 146, 145, 144, 143, 142, 141, 141, 141, 142, 144, 146, 150,
+    154, 158, 162, 167, 171, 175, 179, 181, 183, 184, 185, 184, 183, 182, 182, 181,
+    182, 183, 185, 189, 194, 201, 208, 217, 225, 234, 241, 248, 253, 255, 255, 252,
+    246, 237, 224, 209, 191, 171, 150, 127, 105,  84,  64,  46,  31,  18,   9,   2,
+  };
+  /*
+    The hybrid synth sound blends between
+    square, saw, and triangle waveforms
+    at different frequencies. Said frequencies
+    are controlled via constants here.
+  */
+    #define TRANSITION_SQUARE    220.0
+    #define TRANSITION_SAW_LOW   440.0
+    #define TRANSITION_SAW_HIGH  880.0
+    #define TRANSITION_TRIANGLE 1760.0
+  /*
+    The poll interval represents how often a
+    new sample value is emulated on the PWM
+    hardware. It is the inverse of the digital
+    audio sample rate. 24 microseconds has been
+    determined to be the sweet spot, and corresponds
+    to approximately 41 kHz, which is close to
+    CD-quality (44.1 kHz). A shorter poll interval
+    may produce more pleasant tones, but if the
+    poll is too short then the code will not have
+    enough time to calculate the new sample and
+    the resulting audio becomes unstable and
+    inaccurate.
+  */
+  #define POLL_INTERVAL_IN_MICROSECONDS 24
+  /*
+    Eight voice polyphony can be simulated.
+    Any more voices and the
+    resolution is too low to distinguish;
+    also, the code becomes too slow to keep
+    up with the poll interval. This value
+    can be safely reduced below eight if
+    there are issues.
+
+    Note this is NOT the same as the MIDI
+    polyphony limit, which is 15 (based
+    on using channel 2 through 16 for
+    polyphonic expression mode).
+  */
+  #define POLYPHONY_LIMIT 8
+  /*
+    This defines which hardware alarm
+    and interrupt address are used
+    to time the call of the poll() function.
+  */
+  #define ALARM_NUM 2
+  #define ALARM_IRQ TIMER_IRQ_2
+  /*
+    A basic EQ level can be stored to perform
+    simple loudness adjustments at certain
+    frequencies where human hearing is sensitive.
+
+    By default it's off but you can change this
+    flag to "true" to enable it. This may also
+    be moved to a Advanced menu option.
+  */
+  #define EQUAL_LOUDNESS_ADJUST true
+  /*
+    This class defines a virtual oscillator.
+    It stores an oscillation frequency in
+    the form of an increment value, which is
+    how much a counter would have to be increased
+    every time the poll() interval is reached,
+    such that a counter overflows from 0 to 65,535
+    back to zero at some frequency per second.
+
+    The value of the counter is useful for reading
+    a waveform sample, so that an analog signal
+    can be emulated by reading the sample at each
+    poll() based on how far the counter has moved
+    towards 65,536.
+  */
+  class oscillator {
+  public:
+    uint16_t increment = 0;
+    uint16_t counter = 0;
+    byte a = 127;
+    byte b = 128;
+    byte c = 255;
+    uint16_t ab = 0;
+    uint16_t cd = 0;
+    byte eq = 0;
+  };
+  oscillator synth[POLYPHONY_LIMIT];          // maximum polyphony
+  std::queue<byte> synthChQueue;
+  const byte attenuation[] = {64,24,17,14,12,11,10,9,8}; // full volume in mono mode; equalized volume in poly.
+
+  byte arpeggiatingNow = UNUSED_NOTE;         // if this is 255, set to off (0% duty cycle)
+  uint64_t arpeggiateTime = 0;                // Used to keep track of when this note started playing in ARPEG mode
+  uint64_t arpeggiateLength = 65536;         // in microseconds. approx a 1/32 note at 114 BPM
+
+  // RUN ON CORE 2
+  void poll() {
+    hw_clear_bits(&timer_hw->intr, 1u << ALARM_NUM);
+    timer_hw->alarm[ALARM_NUM] = readClock() + POLL_INTERVAL_IN_MICROSECONDS;
+    uint32_t mix = 0;
+    byte voices = POLYPHONY_LIMIT;
+    uint16_t p;
+    byte t;
+    byte level = 0;
+    for (byte i = 0; i < POLYPHONY_LIMIT; i++) {
+      if (synth[i].increment) {
+        synth[i].counter += synth[i].increment; // should loop from 65536 -> 0
+        p = synth[i].counter;
+        t = p >> 8;
+        switch (currWave) {
+          case WAVEFORM_SAW:                                                            break;
+          case WAVEFORM_TRIANGLE: p = 2 * ((p >> 15) ? p : (65535 - p));                break;
+          case WAVEFORM_SQUARE:   p = 0 - (p > (32768 - modWheel.curValue * 7 * 16));   break;
+          case WAVEFORM_HYBRID:   if (t <= synth[i].a) {
+                                    p = 0;
+                                  } else if (t < synth[i].b) {
+                                    p = (t - synth[i].a) * synth[i].ab;
+                                  } else if (t <= synth[i].c) {
+                                    p = 65535;
+                                  } else {
+                                    p = (256 - t) * synth[i].cd;
+                                  };                                                  break;
+          case WAVEFORM_SINE:     p = sine[t] << 8;                                   break;
+          case WAVEFORM_STRINGS:  p = strings[t] << 8;                                break;
+          case WAVEFORM_CLARINET: p = clarinet[t] << 8;                               break;
+          default:                                                                  break;
+        }
+        mix += (p * synth[i].eq);  // P[16bit] * EQ[3bit] =[19bit]
+      } else {
+        --voices;
+      }
+    }
+    mix *= attenuation[(playbackMode == SYNTH_POLY) * voices]; // [19bit]*atten[6bit] = [25bit]
+    mix *= velWheel.curValue; // [25bit]*vel[7bit]=[32bit], poly+
+    level = mix >> 24;  // [32bit] - [8bit] = [24bit]
+    if(audioD&AUDIO_PIEZO)pwm_set_chan_level(PIEZO_SLICE, PIEZO_CHNL, level);
+    if(audioD&AUDIO_AJACK)pwm_set_chan_level(AJACK_SLICE, AJACK_CHNL, level);
+  }
+  // RUN ON CORE 1
+  byte isoTwoTwentySix(float f) {
+    /*
+      a very crude implementation of ISO 226
+      equal loudness curves
+        Hz dB  Amp ~ sqrt(10^(dB/10))
+       200  0  8
+       800 -3  6
+      1500  0  8
+      3250 -6  4
+      5000  0  8
+    */
+    if ((f < 8.0) || (f > 12500.0)) {   // really crude low- and high-pass
+      return 0;
+    } else {
+      if (EQUAL_LOUDNESS_ADJUST) {
+        if ((f <= 200.0) || (f >= 5000.0)) {
+          return 8;
+        } else {
+          if (f < 1500.0) {
+            return 6 + 2 * (float)(abs(f-800) / 700);
+          } else {
+            return 4 + 4 * (float)(abs(f-3250) / 1750);
+          }
+        }
+      } else {
+        return 8;
+      }
+    }
+  }
+  void setSynthFreq(float frequency, byte channel) {
+    byte c = channel - 1;
+    float f = frequency * exp2(pbWheel.curValue * PITCH_BEND_SEMIS / 98304.0);
+    synth[c].counter = 0;
+    synth[c].increment = round(f * POLL_INTERVAL_IN_MICROSECONDS * 0.065536);   // cycle 0-65535 at resultant frequency
+    synth[c].eq = isoTwoTwentySix(f);
+    if (currWave == WAVEFORM_HYBRID) {
+      if (f < TRANSITION_SQUARE) {
+        synth[c].b = 128;
+      } else if (f < TRANSITION_SAW_LOW) {
+        synth[c].b = (byte)(128 + 127 * (f - TRANSITION_SQUARE) / (TRANSITION_SAW_LOW - TRANSITION_SQUARE));
+      } else if (f < TRANSITION_SAW_HIGH) {
+        synth[c].b = 255;
+      } else if (f < TRANSITION_TRIANGLE) {
+        synth[c].b = (byte)(127 + 128 * (TRANSITION_TRIANGLE - f) / (TRANSITION_TRIANGLE - TRANSITION_SAW_HIGH));
+      } else {
+        synth[c].b = 127;
+      }
+      if (f < TRANSITION_SAW_LOW) {
+        synth[c].a = 255 - synth[c].b;
+        synth[c].c = 255;
+      } else {
+        synth[c].a = 0;
+        synth[c].c = synth[c].b;
+      }
+      if (synth[c].a > 126) {
+        synth[c].ab = 65535;
+      } else {
+        synth[c].ab = 65535 / (synth[c].b - synth[c].a - 1);
+      }
+      synth[c].cd = 65535 / (256 - synth[c].c);
+    }
+  }
+
+  // USE THIS IN MONO OR ARPEG MODE ONLY
+
+  byte findNextHeldNote() {
+    byte n = UNUSED_NOTE;
+    for (byte i = 1; i <= BTN_COUNT; i++) {
+      byte j = positiveMod(arpeggiatingNow + i, BTN_COUNT);
+      if ((h[j].MIDIch) && (!h[j].isCmd)) {
+        n = j;
+        break;
+      }
+    }
+    return n;
+  }
+  void replaceMonoSynthWith(byte x) {
+    if (arpeggiatingNow == x) return;
+    h[arpeggiatingNow].synthCh = 0;
+    arpeggiatingNow = x;
+    if (arpeggiatingNow != UNUSED_NOTE) {
+      h[arpeggiatingNow].synthCh = 1;
+      setSynthFreq(h[arpeggiatingNow].frequency, 1);
+    } else {
+      setSynthFreq(0, 1);
+    }
+  }
+
+  void resetSynthFreqs() {
+    while (!synthChQueue.empty()) {
+      synthChQueue.pop();
+    }
+    for (byte i = 0; i < POLYPHONY_LIMIT; i++) {
+      synth[i].increment = 0;
+      synth[i].counter = 0;
+    }
+    for (byte i = 0; i < BTN_COUNT; i++) {
+      h[i].synthCh = 0;
+    }
+    if (playbackMode == SYNTH_POLY) {
+      for (byte i = 0; i < POLYPHONY_LIMIT; i++) {
+        synthChQueue.push(i + 1);
+      }
+    }
+  }
+  void sendProgramChange() {
+    if(midiD&MIDID_USB)UMIDI.sendProgramChange(programChange - 1, 1);
+    if(midiD&MIDID_SER)SMIDI.sendProgramChange(programChange - 1, 1);
+  }
+
+  void updateSynthWithNewFreqs() {
+    if(midiD&MIDID_USB)UMIDI.sendPitchBend(pbWheel.curValue, 1);
+    if(midiD&MIDID_SER)SMIDI.sendPitchBend(pbWheel.curValue, 1);
+    for (byte i = 0; i < BTN_COUNT; i++) {
+      if (!(h[i].isCmd)) {
+        if (h[i].synthCh) {
+          setSynthFreq(h[i].frequency,h[i].synthCh);           // pass all notes thru synth again if the pitch bend changes
+        }
+      }
+    }
+  }
+
+  void trySynthNoteOn(byte x) {
+    if (playbackMode != SYNTH_OFF) {
+      if (playbackMode == SYNTH_POLY) {
+        // operate independently of MIDI
+        if (synthChQueue.empty()) {
+          sendToLog("synth channels all firing, so did not add one");
+        } else {
+          h[x].synthCh = synthChQueue.front();
+          synthChQueue.pop();
+          sendToLog("popped " + std::to_string(h[x].synthCh) + " off the synth queue");
+          setSynthFreq(h[x].frequency, h[x].synthCh);
+        }
+      } else {
+        // operate in lockstep with MIDI
+        if (h[x].MIDIch) {
+          replaceMonoSynthWith(x);
+        }
+      }
+    }
+  }
+
+  void trySynthNoteOff(byte x) {
+    if (playbackMode && (playbackMode != SYNTH_POLY)) {
+      if (arpeggiatingNow == x) {
+        replaceMonoSynthWith(findNextHeldNote());
+      }
+    }
+    if (playbackMode == SYNTH_POLY) {
+      if (h[x].synthCh) {
+        setSynthFreq(0, h[x].synthCh);
+        synthChQueue.push(h[x].synthCh);
+        h[x].synthCh = 0;
+      }
+    }
+  }
+
+  void setupSynth(byte pin, byte slice) {
+    gpio_set_function(pin, GPIO_FUNC_PWM);      // set that pin as PWM
+    pwm_set_phase_correct(slice, true);           // phase correct sounds better
+    pwm_set_wrap(slice, 254);                     // 0 - 254 allows 0 - 255 level
+    pwm_set_clkdiv(slice, 1.0f);                  // run at full clock speed
+    pwm_set_chan_level(slice, PIEZO_CHNL, 0);        // initialize at zero to prevent whining sound
+    pwm_set_enabled(slice, true);                 // ENGAGE!
+    hw_set_bits(&timer_hw->inte, 1u << ALARM_NUM);  // initialize the timer
+    irq_set_exclusive_handler(ALARM_IRQ, poll);     // function to run every interrupt
+    irq_set_enabled(ALARM_IRQ, true);               // ENGAGE!
+    timer_hw->alarm[ALARM_NUM] = readClock() + POLL_INTERVAL_IN_MICROSECONDS;
+    resetSynthFreqs();
+    sendToLog("synth is ready.");
+  }
+
+  void arpeggiate() {
+    if (playbackMode == SYNTH_ARPEGGIO) {
+      if (runTime - arpeggiateTime > arpeggiateLength) {
+        arpeggiateTime = runTime;
+        replaceMonoSynthWith(findNextHeldNote());
+      }
+    }
+  }
+
+// @animate
+  /*
+    This section of the code handles
+    LED animation responsive to key
+    presses
+  */
+  /*
+    The coordinate system used to locate hex buttons
+    a certain distance and direction away relies on
+    a preset array of coordinate offsets corresponding
+    to each of the six linear directions on the hex grid.
+    These cardinal directions are enumerated to make
+    the code more legible for humans.
+  */
+  #define HEX_DIRECTION_EAST 0
+  #define HEX_DIRECTION_NE   1
+  #define HEX_DIRECTION_NW   2
+  #define HEX_DIRECTION_WEST 3
+  #define HEX_DIRECTION_SW   4
+  #define HEX_DIRECTION_SE   5
+  // animation variables  E NE NW  W SW SE
+  int8_t vertical[] =   { 0,-1,-1, 0, 1, 1};
+  int8_t horizontal[] = { 2, 1,-1,-2,-1, 1};
+
+  uint64_t animFrame(byte x) {
+    if (h[x].timePressed) {          // 2^20 microseconds is close enough to 1 second
+      return 1 + (((runTime - h[x].timePressed) * animationFPS) >> 20);
+    } else {
+      return 0;
+    }
+  }
+  void flagToAnimate(int8_t r, int8_t c) {
+    if (!
+      (    ( r < 0 ) || ( r >= ROWCOUNT )
+        || ( c < 0 ) || ( c >= (2 * COLCOUNT) )
+        || ( ( c + r ) & 1 )
+      )
+    ) {
+      h[(10 * r) + (c / 2)].animate = 1;
+    }
+  }
+  void animateMirror() {
+    for (byte i = 0; i < LED_COUNT; i++) {                      // check every hex
+      if ((!(h[i].isCmd)) && (h[i].MIDIch)) {                   // that is a held note
+        for (byte j = 0; j < LED_COUNT; j++) {                  // compare to every hex
+          if ((!(h[j].isCmd)) && (!(h[j].MIDIch))) {            // that is a note not being played
+            int16_t temp = h[i].stepsFromC - h[j].stepsFromC;   // look at difference between notes
+            if (animationType == ANIMATE_OCTAVE) {              // set octave diff to zero if need be
+              temp = positiveMod(temp, current.tuning().cycleLength);
+            }
+            if (temp == 0) {                                    // highlight if diff is zero
+              h[j].animate = 1;
+            }
+          }
+        }
+      }
+    }
+  }
+/*
+  void animateOrbit() {
+    for (byte i = 0; i < LED_COUNT; i++) {                               // check every hex
+      if ((!(h[i].isCmd)) && (h[i].MIDIch) && ((h[i].inScale) || (!scaleLock))) {    // that is a held note
+        byte tempDir = (animFrame(i) % 6);
+        flagToAnimate(h[i].coordRow + vertical[tempDir], h[i].coordCol + horizontal[tempDir]);       // different neighbor each frame
+      }
+    }
+  }
+*/
+  void animateOrbit() { //BETTER ORBIT
+  const byte ORBIT_RADIUS = 2;               // Radius of the orbit
+  const byte SLOW_FACTOR = 1;                // Slowdown factor for animation
+
+  for (byte i = 0; i < LED_COUNT; i++) {     // Check every hex
+    if ((!(h[i].isCmd)) && (h[i].MIDIch) &&  // That is a held note
+        ((h[i].inScale) || (!scaleLock))) {  // And is in scale or scale is unlocked
+
+      byte frame = animFrame(i) / SLOW_FACTOR;  // Slow down the animation
+      byte currentStep = frame % 12;            // Determine position in the 12-light orbit
+
+      // Determine row and column adjustments for the 12 possible directions
+      int8_t rowOffsets[12];
+      int8_t colOffsets[12];
+
+      // Fill offsets for the 6 primary directions
+      for (byte dir = 0; dir < 6; dir++) {
+        rowOffsets[dir * 2]     = ORBIT_RADIUS * vertical[dir];
+        colOffsets[dir * 2]     = ORBIT_RADIUS * horizontal[dir];
+
+        // Fill the intermediate (diagonal) positions
+        rowOffsets[dir * 2 + 1] = ORBIT_RADIUS * (vertical[dir] + vertical[(dir + 1) % 6]) / 2;
+        colOffsets[dir * 2 + 1] = ORBIT_RADIUS * (horizontal[dir] + horizontal[(dir + 1) % 6]) / 2;
+      }
+
+      // Calculate light positions
+      int8_t light1Row = h[i].coordRow + rowOffsets[currentStep];
+      int8_t light1Col = h[i].coordCol + colOffsets[currentStep];
+
+      byte oppositeStep = (currentStep + 6) % 12;  // Opposite position in the 12-light ring
+      int8_t light2Row = h[i].coordRow + rowOffsets[oppositeStep];
+      int8_t light2Col = h[i].coordCol + colOffsets[oppositeStep];
+
+      // Flag both lights for animation
+      flagToAnimate(light1Row, light1Col);
+      flagToAnimate(light2Row, light2Col);
+    }
+  }
+}
+
+void animateStaticBeams() {
+  const byte MAX_BEAM_LENGTH = 13;  // Maximum distance the beam can travel
+  static byte lastDirection[LED_COUNT] = {255};  // Track the last direction for each button (255 = uninitialized)
+
+  for (byte i = 0; i < LED_COUNT; i++) {  // Check every hex
+    // Skip buttons that are not in the playable area
+    if (h[i].isCmd || (!h[i].inScale && scaleLock)) {
+      continue;
+    }
+
+    if (h[i].btnState == BTN_STATE_NEWPRESS) {  // Button was just pressed
+      uint64_t clockValue = readClock();  // Get system clock
+
+      // Choose a new random direction, excluding the last one
+      byte newDirection;
+      do {
+        newDirection = clockValue % 3;  // Randomly pick 0, 1, or 2
+        clockValue /= 3;  // Update clockValue for a new seed
+      } while (newDirection == lastDirection[i]);  // Exclude last direction
+
+      lastDirection[i] = newDirection;  // Store new direction
+    }
+
+    if (h[i].btnState == BTN_STATE_HELD || h[i].btnState == BTN_STATE_NEWPRESS) {  // Active button
+      byte baseDirection = lastDirection[i] * 2;  // Convert to hex direction (0, 2, or 4)
+      byte oppositeDirection = (baseDirection + 3) % 6;  // Opposite direction
+
+      // Light up the entire beam in both directions
+      for (byte length = 1; length <= MAX_BEAM_LENGTH; length++) {
+        // Beam in primary direction
+        int8_t beam1Row = h[i].coordRow + (length * vertical[baseDirection]);
+        int8_t beam1Col = h[i].coordCol + (length * horizontal[baseDirection]);
+
+        // Beam in opposite direction
+        int8_t beam2Row = h[i].coordRow + (length * vertical[oppositeDirection]);
+        int8_t beam2Col = h[i].coordCol + (length * horizontal[oppositeDirection]);
+
+        // Flag both beams for animation
+        flagToAnimate(beam1Row, beam1Col);
+        flagToAnimate(beam2Row, beam2Col);
+      }
+    }
+  }
+}
+
+  void animateRadial() {
+    for (byte i = 0; i < LED_COUNT; i++) {                                // check every hex
+      if (!(h[i].isCmd) && (h[i].inScale || !scaleLock)) {                                                // that is a note
+        uint64_t radius = animFrame(i);
+        if ((radius > 0) && (radius < 16)) {                              // played in the last 16 frames
+          byte steps = ((animationType == ANIMATE_SPLASH) ? radius : 1);  // star = 1 step to next corner; ring = 1 step per hex
+          int8_t turtleRow = h[i].coordRow + (radius * vertical[HEX_DIRECTION_SW]);
+          int8_t turtleCol = h[i].coordCol + (radius * horizontal[HEX_DIRECTION_SW]);
+          for (byte dir = HEX_DIRECTION_EAST; dir < 6; dir++) {           // walk along the ring in each of the 6 hex directions
+            for (byte i = 0; i < steps; i++) {                            // # of steps to the next corner
+              flagToAnimate(turtleRow,turtleCol);                         // flag for animation
+              turtleRow += (vertical[dir] * (radius / steps));
+              turtleCol += (horizontal[dir] * (radius / steps));
+            }
+          }
+        }
+      }
+    }
+  }
+
+  void animateRadialReverse() { //inverted splash/star
+    #define MAX_RADIUS 5
+  for (byte i = 0; i < LED_COUNT; i++) {                                   // Check every hex
+    if (!(h[i].isCmd) && (h[i].inScale || !scaleLock)) {                   // That is a note
+      uint64_t frame = animFrame(i);                                       // Current animation frame
+      if ((frame > 0) && (frame < MAX_RADIUS)) {                                   // Played in the last X frames
+        uint8_t reverseRadius = MAX_RADIUS - frame;                        // Calculate reverse radius
+        byte steps = ((animationType == ANIMATE_SPLASH_REVERSE) ? reverseRadius : 1);  // Steps depend on animation type
+        int8_t turtleRow = h[i].coordRow + (reverseRadius * vertical[HEX_DIRECTION_SW]);
+        int8_t turtleCol = h[i].coordCol + (reverseRadius * horizontal[HEX_DIRECTION_SW]);
+        for (byte dir = HEX_DIRECTION_EAST; dir < 6; dir++) {              // Walk along the ring in 6 hex directions
+          for (byte j = 0; j < steps; j++) {                               // Steps to the next corner
+            flagToAnimate(turtleRow, turtleCol);                           // Flag for animation
+            turtleRow += (vertical[dir] * (reverseRadius / steps));
+            turtleCol += (horizontal[dir] * (reverseRadius / steps));
+          }
+        }
+      }
+    }
+  }
+}
+
+  void animateLEDs() {
+    for (byte i = 0; i < LED_COUNT; i++) {
+      h[i].animate = 0;
+    }
+    if (animationType) {
+      switch (animationType) {
+        case ANIMATE_STAR: case ANIMATE_SPLASH:
+          animateRadial();
+          break;
+        case ANIMATE_ORBIT:
+          animateOrbit();
+          break;
+        case ANIMATE_OCTAVE: case ANIMATE_BY_NOTE:
+          animateMirror();
+          break;
+        case ANIMATE_BEAMS:
+          animateStaticBeams();
+          break;
+        case ANIMATE_SPLASH_REVERSE: case ANIMATE_STAR_REVERSE:
+          animateRadialReverse();
+          break;
+        default:
+          break;
+      }
+    }
+  }
+
+// @assignment
+  /*
+    This section of the code contains broad
+    procedures for assigning musical notes
+    and related values to each button
+    of the hex grid.
+  */
+  // run this if the layout, key, or transposition changes, but not if color or scale changes
+  void assignPitches() {
+    sendToLog("assignPitch was called:");
+    for (byte i = 0; i < LED_COUNT; i++) {
+      if (!(h[i].isCmd)) {
+        // steps is the distance from C
+        // the stepsToMIDI function needs distance from A4
+        // it also needs to reflect any transposition, but
+        // NOT the key of the scale.
+        float N = stepsToMIDI(current.pitchRelToA4(h[i].stepsFromC));
+        if (N < 0 || N >= 128) {
+          h[i].note = UNUSED_NOTE;
+          h[i].bend = 0;
+          h[i].frequency = 0.0;
+        } else {
+          h[i].note = ((N >= 127) ? 127 : round(N));
+          h[i].bend = (ldexp(N - h[i].note, 13) / MPEpitchBendSemis);
+          h[i].frequency = MIDItoFreq(N);
+        }
+        sendToLog(
+          "hex #" + std::to_string(i) + ", " +
+          "steps=" + std::to_string(h[i].stepsFromC) + ", " +
+          "isCmd? " + std::to_string(h[i].isCmd) + ", " +
+          "note=" + std::to_string(h[i].note) + ", " +
+          "bend=" + std::to_string(h[i].bend) + ", " +
+          "freq=" + std::to_string(h[i].frequency) + ", " +
+          "inScale? " + std::to_string(h[i].inScale) + "."
+        );
+      }
+    }
+    sendToLog("assignPitches complete.");
+  }
+  void applyScale() {
+    sendToLog("applyScale was called:");
+    for (byte i = 0; i < LED_COUNT; i++) {
+      if (!(h[i].isCmd)) {
+        if (current.scale().tuning == ALL_TUNINGS) {
+          h[i].inScale = 1;
+        } else {
+          byte degree = current.keyDegree(h[i].stepsFromC);
+          if (degree == 0) {
+            h[i].inScale = 1;    // the root is always in the scale
+          } else {
+            byte tempSum = 0;
+            byte iterator = 0;
+            while (degree > tempSum) {
+              tempSum += current.scale().pattern[iterator];
+              iterator++;
+            }  // add the steps in the scale, and you're in scale
+            h[i].inScale = (tempSum == degree);   // if the note lands on one of those sums
+          }
+        }
+        sendToLog(
+          "hex #" + std::to_string(i) + ", " +
+          "steps=" + std::to_string(h[i].stepsFromC) + ", " +
+          "isCmd? " + std::to_string(h[i].isCmd) + ", " +
+          "note=" + std::to_string(h[i].note) + ", " +
+          "inScale? " + std::to_string(h[i].inScale) + "."
+        );
+      }
+    }
+    setLEDcolorCodes();
+    sendToLog("applyScale complete.");
+  }
+  void applyLayout() {       // call this function when the layout changes
+    sendToLog("buildLayout was called:");
+///////////////////////////////////////////////////////////////////////////////////////
+        int8_t acrossSteps = current.layout().acrossSteps; // x
+        int8_t dnLeftSteps = current.layout().dnLeftSteps; // y
+        if(mirrorUpDown)
+        {
+          dnLeftSteps = -(acrossSteps + dnLeftSteps); // y = -(x + y)
+        }
+        if(mirrorLeftRight)
+        {
+          dnLeftSteps = acrossSteps + dnLeftSteps;    // y = x + y
+          acrossSteps = -acrossSteps;                  // x = -x
+        }
+        for(byte rotations = 0; rotations < layoutRotation; rotations++)
+        {
+          byte keyOffsetY = dnLeftSteps;
+          byte keyOffsetX = acrossSteps;
+          dnLeftSteps = keyOffsetX + keyOffsetY;
+          keyOffsetY = dnLeftSteps;
+          dnLeftSteps = -acrossSteps;
+          acrossSteps = keyOffsetY;
+        }
+////////////////////////////////////////////////////////////////////////////////////////
+    for (byte i = 0; i < LED_COUNT; i++) {
+      if (!(h[i].isCmd)) {
+        int8_t distCol = h[i].coordCol - h[current.layout().hexMiddleC].coordCol;
+        int8_t distRow = h[i].coordRow - h[current.layout().hexMiddleC].coordRow;
+        h[i].stepsFromC = (
+          (distCol * acrossSteps) +
+          (distRow * (
+            acrossSteps +
+            (2 * dnLeftSteps)
+          ))
+        ) / 2;
+        sendToLog(
+          "hex #" + std::to_string(i) + ", " +
+          "steps from C4=" + std::to_string(h[i].stepsFromC) + "."
+        );
+      }
+    }
+    applyScale();        // when layout changes, have to re-apply scale and re-apply LEDs
+    assignPitches();     // same with pitches
+    sendToLog("buildLayout complete.");
+  }
+  void cmdOn(byte x) {   // volume and mod wheel read all current buttons
+    switch (h[x].note) {
+      case CMDB + 3:
+        toggleWheel = !toggleWheel;
+        break;
+      case HARDWARE_V1_2:
+        Hardware_Version = h[x].note;
+        setupHardware();
+        break;
+      default:
+        // the rest should all be taken care of within the wheelDef structure
+        break;
+    }
+  }
+  void cmdOff(byte x) {   // pitch bend wheel only if buttons held.
+    switch (h[x].note) {
+      default:
+        break;  // nothing; should all be taken care of within the wheelDef structure
+    }
+  }
+
+// @menu
+  /*
+    This section of the code handles the
+    dot matrix screen and, most importantly,
+    the menu system display and controls.
+
+    The following library is used: documentation
+    is also available here.
+      https://github.com/Spirik/GEM
+  */
+  #define GEM_DISABLE_GLCD       // this line is needed to get the B&W display to work
+  /*
+    The GEM menu library accepts initialization
+    values to set the width of various components
+    of the menu display, as below.
+  */
+  #define MENU_ITEM_HEIGHT 10
+  #define MENU_PAGE_SCREEN_TOP_OFFSET 10
+  #define MENU_VALUES_LEFT_OFFSET 78
+  #define CONTRAST_AWAKE 63
+  #define CONTRAST_SCREENSAVER 1
+  // Create an instance of the U8g2 graphics library.
+  U8G2_SH1107_SEEED_128X128_F_HW_I2C u8g2(U8G2_R2, /* reset=*/ U8X8_PIN_NONE);
+  // Create menu object of class GEM_u8g2. Supply its constructor with reference to u8g2 object we created earlier
+  GEM_u8g2 menu(
+    u8g2, GEM_POINTER_ROW, GEM_ITEMS_COUNT_AUTO,
+    MENU_ITEM_HEIGHT, MENU_PAGE_SCREEN_TOP_OFFSET, MENU_VALUES_LEFT_OFFSET
+  );
+  bool screenSaverOn = 0;
+  uint64_t screenTime = 0;                        // GFX timer to count if screensaver should go on
+  const uint64_t screenSaverTimeout = (1u << 23); // 2^23 microseconds ~ 8 seconds
+  /*
+    Create menu page object of class GEMPage.
+    Menu page holds menu items (GEMItem) and represents menu level.
+    Menu can have multiple menu pages (linked to each other) with multiple menu items each.
+
+    GEMPage constructor creates each page with the associated label.
+    GEMItem constructor can create many different sorts of menu items.
+    The items here are navigation links.
+    The first parameter is the item label.
+    The second parameter is the destination page when that item is selected.
+  */
+  GEMPage  menuPageMain("HexBoard MIDI Controller");
+  GEMPage  menuPageTuning("Tuning");
+  GEMItem  menuGotoTuning("Tuning", menuPageTuning);
+  GEMItem  menuTuningBack("<< Back", menuPageMain);
+  GEMPage  menuPageLayout("Layout");
+  GEMItem  menuGotoLayout("Layout", menuPageLayout);
+  GEMItem  menuLayoutBack("<< Back", menuPageMain);
+  GEMPage  menuPageScales("Scales");
+  GEMItem  menuGotoScales("Scales", menuPageScales);
+  GEMItem  menuScalesBack("<< Back", menuPageMain);
+  GEMPage  menuPageColors("Color options");
+  GEMItem  menuGotoColors("Color options", menuPageColors);
+  GEMItem  menuColorsBack("<< Back", menuPageMain);
+  GEMPage  menuPageSynth("Synth options");
+  GEMItem  menuGotoSynth("Synth options", menuPageSynth);
+  GEMItem  menuSynthBack("<< Back", menuPageMain);
+  GEMPage  menuPageMIDI("MIDI options");
+  GEMItem  menuGotoMIDI("MIDI options", menuPageMIDI);
+  GEMItem  menuMIDIBack("<< Back", menuPageMain);
+  GEMPage  menuPageControl("Control wheel");
+  GEMItem  menuGotoControl("Control wheel", menuPageControl);
+  GEMItem  menuControlBack("<< Back", menuPageMain);
+  GEMPage  menuPageAdvanced("Advanced");
+  GEMItem  menuGotoAdvanced("Advanced", menuPageAdvanced);
+  GEMItem  menuAdvancedBack("<< Back", menuPageMain);
+  GEMPage  menuPageReboot("Ready to flash firmware!");
+  /*
+    We haven't written the code for some procedures,
+    but the menu item needs to know the address
+    of procedures it has to run when it's selected.
+    So we forward-declare a placeholder for the
+    procedure like this, so that the menu item
+    can be built, and then later we will define
+    this procedure in full.
+  */
+  void changeTranspose();
+  void rebootToBootloader();
+  /*
+    This GEMItem is meant to just be a read-only text label.
+    To be honest I don't know how to get just a plain text line to show here other than this!
+  */
+  void fakeButton() {}
+  GEMItem  menuItemVersion("1.1 beta", fakeButton);
+  SelectOptionByte optionByteHardware[] =  {
+    { "V1.1", HARDWARE_UNKNOWN }, { "V1.1" , HARDWARE_V1_1 },
+    { "V1.2", HARDWARE_V1_2 }
+  };
+  GEMSelect selectHardware( sizeof(optionByteHardware)  / sizeof(SelectOptionByte), optionByteHardware);
+  GEMItem  menuItemHardware("HexBoard", Hardware_Version, selectHardware, GEM_READONLY);
+  /*
+    This GEMItem runs a given procedure when you select it.
+    We must declare or define that procedure first.
+  */
+  GEMItem  menuItemUSBBootloader("Update Firmware", rebootToBootloader);
+  /*
+    Tunings, layouts, scales, and keys are defined
+    earlier in this code. We should not have to
+    manually type in menu objects for those
+    pre-loaded values. Instead, we will use routines to
+    construct menu items automatically.
+
+    These lines are forward declarations for
+    the menu objects we will make later.
+    This allocates space in memory with
+    enough size to procedurally fill
+    the objects based on the contents of
+    the pre-loaded tuning/layout/etc. definitions
+    we defined above.
+  */
+  GEMItem* menuItemTuning[TUNINGCOUNT];
+  GEMItem* menuItemLayout[layoutCount];
+  GEMItem* menuItemScales[scaleCount];
+  GEMSelect* selectKey[TUNINGCOUNT];
+  GEMItem* menuItemKeys[TUNINGCOUNT];
+  /*
+    We are now creating some GEMItems that let you
+    1) select a value from a list of options,
+    2) update a given variable based on what was chosen,
+    3) if necessary, run a procedure as well once the value's chosen.
+
+    The list of options is in the form of a 2-d array.
+    There are A arrays, one for each option.
+    Each is 2 entries long. First entry is the label
+    for that choice, second entry is the value associated.
+
+    These arrays go into a typedef that depends on the type of the variable
+    being selected (i.e. Byte for small positive integers; Int for
+    sign-dependent and large integers).
+
+    Then that typeDef goes into a GEMSelect object, with parameters
+    equal to the number of entries in the array, and the storage size of one element
+    in the array. The GEMSelect object is basically just a pointer to the
+    array of choices. The GEMItem then takes the GEMSelect pointer as a parameter.
+
+    The fact that GEM expects pointers and references makes it tricky
+    to work with if you are new to C++.
+  */
+  SelectOptionByte optionByteMPEpitchBend[] = { { "2", 2}, {"12", 12}, {"24", 24}, {"48", 48}, {"96", 96} };
+  GEMSelect selectMPEpitchBend( sizeof(optionByteMPEpitchBend) / sizeof(SelectOptionByte), optionByteMPEpitchBend);
+  GEMItem menuItemMPEpitchBend( "MPE Bend:", MPEpitchBendSemis, selectMPEpitchBend, assignPitches);
+
+  SelectOptionByte optionByteYesOrNo[] =  { { "No", 0 }, { "Yes" , 1 } };
+  GEMSelect selectYesOrNo( sizeof(optionByteYesOrNo)  / sizeof(SelectOptionByte), optionByteYesOrNo);
+  GEMItem  menuItemScaleLock( "Scale lock?", scaleLock);
+  GEMItem  menuItemPercep( "Fix color:", perceptual, setLEDcolorCodes);
+  GEMItem  menuItemShiftColor( "ColorByKey", paletteBeginsAtKeyCenter, setLEDcolorCodes);
+  GEMItem  menuItemWheelAlt( "Alt wheel?", wheelMode, selectYesOrNo);
+
+  bool rotaryInvert = false;
+  GEMItem  menuItemRotary( "Invert Encoder:", rotaryInvert);
+
+  SelectOptionByte optionByteWheelType[] = { { "Springy", 0 }, { "Sticky", 1} };
+  GEMSelect selectWheelType( sizeof(optionByteWheelType) / sizeof(SelectOptionByte), optionByteWheelType);
+  GEMItem  menuItemPBBehave( "Pitch bend", pbSticky, selectWheelType);
+  GEMItem  menuItemModBehave( "Mod wheel", modSticky, selectWheelType);
+
+  SelectOptionByte optionBytePlayback[] = { { "Off", SYNTH_OFF }, { "Mono", SYNTH_MONO }, { "Arp'gio", SYNTH_ARPEGGIO }, { "Poly", SYNTH_POLY } };
+  GEMSelect selectPlayback(sizeof(optionBytePlayback) / sizeof(SelectOptionByte), optionBytePlayback);
+  GEMItem  menuItemPlayback(  "Synth mode:",       playbackMode,  selectPlayback, resetSynthFreqs);
+
+  // Hardware V1.2-only
+  SelectOptionByte optionByteAudioD[] =  {
+    { "Buzzer", AUDIO_PIEZO }, { "Jack" , AUDIO_AJACK }, { "Both", AUDIO_BOTH }
+  };
+  GEMSelect selectAudioD( sizeof(optionByteAudioD)  / sizeof(SelectOptionByte), optionByteAudioD);
+  GEMItem  menuItemAudioD("SynthOutput:", audioD, selectAudioD);
+
+////////////////////////////////////////////////////////////////
+
+  SelectOptionByte optionByteBPM[] = {
+    {"1 BPM", 1},
+    {"2 BPM", 2},
+    {"3 BPM", 3},
+    {"4 BPM", 4},
+    {"5 BPM", 5},
+    {"6 BPM", 6},
+    {"7 BPM", 7},
+    {"8 BPM", 8},
+    {"9 BPM", 9},
+    {"10 BPM", 10},
+    {"11 BPM", 11},
+    {"12 BPM", 12},
+    {"13 BPM", 13},
+    {"14 BPM", 14},
+    {"15 BPM", 15},
+    {"16 BPM", 16},
+    {"17 BPM", 17},
+    {"18 BPM", 18},
+    {"19 BPM", 19},
+    {"20 BPM", 20},
+    {"21 BPM", 21},
+    {"22 BPM", 22},
+    {"23 BPM", 23},
+    {"24 BPM", 24},
+    {"25 BPM", 25},
+    {"26 BPM", 26},
+    {"27 BPM", 27},
+    {"28 BPM", 28},
+    {"29 BPM", 29},
+    {"30 BPM", 30},
+    {"31 BPM", 31},
+    {"32 BPM", 32},
+    {"33 BPM", 33},
+    {"34 BPM", 34},
+    {"35 BPM", 35},
+    {"36 BPM", 36},
+    {"37 BPM", 37},
+    {"38 BPM", 38},
+    {"39 BPM", 39},
+    {"40 BPM", 40},
+    {"41 BPM", 41},
+    {"42 BPM", 42},
+    {"43 BPM", 43},
+    {"44 BPM", 44},
+    {"45 BPM", 45},
+    {"46 BPM", 46},
+    {"47 BPM", 47},
+    {"48 BPM", 48},
+    {"49 BPM", 49},
+    {"50 BPM", 50},
+    {"51 BPM", 51},
+    {"52 BPM", 52},
+    {"53 BPM", 53},
+    {"54 BPM", 54},
+    {"55 BPM", 55},
+    {"56 BPM", 56},
+    {"57 BPM", 57},
+    {"58 BPM", 58},
+    {"59 BPM", 59},
+    {"60 BPM", 60},
+    {"61 BPM", 61},
+    {"62 BPM", 62},
+    {"63 BPM", 63},
+    {"64 BPM", 64},
+    {"65 BPM", 65},
+    {"66 BPM", 66},
+    {"67 BPM", 67},
+    {"68 BPM", 68},
+    {"69 BPM", 69},
+    {"70 BPM", 70},
+    {"71 BPM", 71},
+    {"72 BPM", 72},
+    {"73 BPM", 73},
+    {"74 BPM", 74},
+    {"75 BPM", 75},
+    {"76 BPM", 76},
+    {"77 BPM", 77},
+    {"78 BPM", 78},
+    {"79 BPM", 79},
+    {"80 BPM", 80},
+    {"81 BPM", 81},
+    {"82 BPM", 82},
+    {"83 BPM", 83},
+    {"84 BPM", 84},
+    {"85 BPM", 85},
+    {"86 BPM", 86},
+    {"87 BPM", 87},
+    {"88 BPM", 88},
+    {"89 BPM", 89},
+    {"90 BPM", 90},
+    {"91 BPM", 91},
+    {"92 BPM", 92},
+    {"93 BPM", 93},
+    {"94 BPM", 94},
+    {"95 BPM", 95},
+    {"96 BPM", 96},
+    {"97 BPM", 97},
+    {"98 BPM", 98},
+    {"99 BPM", 99},
+    {"100 BPM", 100},
+    {"101 BPM", 101},
+    {"102 BPM", 102},
+    {"103 BPM", 103},
+    {"104 BPM", 104},
+    {"105 BPM", 105},
+    {"106 BPM", 106},
+    {"107 BPM", 107},
+    {"108 BPM", 108},
+    {"109 BPM", 109},
+    {"110 BPM", 110},
+    {"111 BPM", 111},
+    {"112 BPM", 112},
+    {"113 BPM", 113},
+    {"114 BPM", 114},
+    {"115 BPM", 115},
+    {"116 BPM", 116},
+    {"117 BPM", 117},
+    {"118 BPM", 118},
+    {"119 BPM", 119},
+    {"120 BPM", 120},
+    {"121 BPM", 121},
+    {"122 BPM", 122},
+    {"123 BPM", 123},
+    {"124 BPM", 124},
+    {"125 BPM", 125},
+    {"126 BPM", 126},
+    {"127 BPM", 127},
+    {"128 BPM", 128},
+    {"129 BPM", 129},
+    {"130 BPM", 130},
+    {"131 BPM", 131},
+    {"132 BPM", 132},
+    {"133 BPM", 133},
+    {"134 BPM", 134},
+    {"135 BPM", 135},
+    {"136 BPM", 136},
+    {"137 BPM", 137},
+    {"138 BPM", 138},
+    {"139 BPM", 139},
+    {"140 BPM", 140},
+    {"141 BPM", 141},
+    {"142 BPM", 142},
+    {"143 BPM", 143},
+    {"144 BPM", 144},
+    {"145 BPM", 145},
+    {"146 BPM", 146},
+    {"147 BPM", 147},
+    {"148 BPM", 148},
+    {"149 BPM", 149},
+    {"150 BPM", 150},
+    {"151 BPM", 151},
+    {"152 BPM", 152},
+    {"153 BPM", 153},
+    {"154 BPM", 154},
+    {"155 BPM", 155},
+    {"156 BPM", 156},
+    {"157 BPM", 157},
+    {"158 BPM", 158},
+    {"159 BPM", 159},
+    {"160 BPM", 160},
+    {"161 BPM", 161},
+    {"162 BPM", 162},
+    {"163 BPM", 163},
+    {"164 BPM", 164},
+    {"165 BPM", 165},
+    {"166 BPM", 166},
+    {"167 BPM", 167},
+    {"168 BPM", 168},
+    {"169 BPM", 169},
+    {"170 BPM", 170},
+    {"171 BPM", 171},
+    {"172 BPM", 172},
+    {"173 BPM", 173},
+    {"174 BPM", 174},
+    {"175 BPM", 175},
+    {"176 BPM", 176},
+    {"177 BPM", 177},
+    {"178 BPM", 178},
+    {"179 BPM", 179},
+    {"180 BPM", 180},
+    {"181 BPM", 181},
+    {"182 BPM", 182},
+    {"183 BPM", 183},
+    {"184 BPM", 184},
+    {"185 BPM", 185},
+    {"186 BPM", 186},
+    {"187 BPM", 187},
+    {"188 BPM", 188},
+    {"189 BPM", 189},
+    {"190 BPM", 190},
+    {"191 BPM", 191},
+    {"192 BPM", 192},
+    {"193 BPM", 193},
+    {"194 BPM", 194},
+    {"195 BPM", 195},
+    {"196 BPM", 196},
+    {"197 BPM", 197},
+    {"198 BPM", 198},
+    {"199 BPM", 199},
+    {"200 BPM", 200},
+    {"201 BPM", 201},
+    {"202 BPM", 202},
+    {"203 BPM", 203},
+    {"204 BPM", 204},
+    {"205 BPM", 205},
+    {"206 BPM", 206},
+    {"207 BPM", 207},
+    {"208 BPM", 208},
+    {"209 BPM", 209},
+    {"210 BPM", 210},
+    {"211 BPM", 211},
+    {"212 BPM", 212},
+    {"213 BPM", 213},
+    {"214 BPM", 214},
+    {"215 BPM", 215},
+    {"216 BPM", 216},
+    {"217 BPM", 217},
+    {"218 BPM", 218},
+    {"219 BPM", 219},
+    {"220 BPM", 220},
+    {"221 BPM", 221},
+    {"222 BPM", 222},
+    {"223 BPM", 223},
+    {"224 BPM", 224},
+    {"225 BPM", 225},
+    {"226 BPM", 226},
+    {"227 BPM", 227},
+    {"228 BPM", 228},
+    {"229 BPM", 229},
+    {"230 BPM", 230},
+    {"231 BPM", 231},
+    {"232 BPM", 232},
+    {"233 BPM", 233},
+    {"234 BPM", 234},
+    {"235 BPM", 235},
+    {"236 BPM", 236},
+    {"237 BPM", 237},
+    {"238 BPM", 238},
+    {"239 BPM", 239},
+    {"240 BPM", 240},
+    {"241 BPM", 241},
+    {"242 BPM", 242},
+    {"243 BPM", 243},
+    {"244 BPM", 244},
+    {"245 BPM", 245},
+    {"246 BPM", 246},
+    {"247 BPM", 247},
+    {"248 BPM", 248},
+    {"249 BPM", 249},
+    {"250 BPM", 250},
+    {"251 BPM", 251},
+    {"252 BPM", 252},
+    {"253 BPM", 253},
+    {"254 BPM", 254},
+    {"255 BPM", 255}
+  };
+
+    SelectOptionByte optionByteBPM_Multiplier[] = {
+    {"x1", 1},
+    {"x2", 2},
+    {"x3", 3},
+    {"x4", 4},
+    {"x5", 5},
+    {"x6", 6},
+    {"x7", 7},
+    {"x8", 8},
+    {"x9", 9},
+    {"x10", 10},
+    {"x11", 11},
+    {"x12", 12},
+    {"x13", 13},
+    {"x14", 14},
+    {"x15", 15},
+    {"x16", 16},
+    {"x17", 17},
+    {"x18", 18},
+    {"x19", 19},
+    {"x20", 20},
+    {"x21", 21},
+    {"x22", 22},
+    {"x23", 23},
+    {"x24", 24},
+    {"x25", 25},
+    {"x26", 26},
+    {"x27", 27},
+    {"x28", 28},
+    {"x29", 29},
+    {"x30", 30},
+    {"x31", 31},
+    {"x32", 32},
+    {"x33", 33},
+    {"x34", 34},
+    {"x35", 35},
+    {"x36", 36},
+    {"x37", 37},
+    {"x38", 38},
+    {"x39", 39},
+    {"x40", 40},
+    {"x41", 41},
+    {"x42", 42},
+    {"x43", 43},
+    {"x44", 44},
+    {"x45", 45},
+    {"x46", 46},
+    {"x47", 47},
+    {"x48", 48},
+    {"x49", 49},
+    {"x50", 50},
+    {"x51", 51},
+    {"x52", 52},
+    {"x53", 53},
+    {"x54", 54},
+    {"x55", 55},
+    {"x56", 56},
+    {"x57", 57},
+    {"x58", 58},
+    {"x59", 59},
+    {"x60", 60},
+    {"x61", 61},
+    {"x62", 62},
+    {"x63", 63},
+    {"x64", 64},
+    {"x65", 65},
+    {"x66", 66},
+    {"x67", 67},
+    {"x68", 68},
+    {"x69", 69},
+    {"x70", 70},
+    {"x71", 71},
+    {"x72", 72},
+    {"x73", 73},
+    {"x74", 74},
+    {"x75", 75},
+    {"x76", 76},
+    {"x77", 77},
+    {"x78", 78},
+    {"x79", 79},
+    {"x80", 80},
+    {"x81", 81},
+    {"x82", 82},
+    {"x83", 83},
+    {"x84", 84},
+    {"x85", 85},
+    {"x86", 86},
+    {"x87", 87},
+    {"x88", 88},
+    {"x89", 89},
+    {"x90", 90},
+    {"x91", 91},
+    {"x92", 92},
+    {"x93", 93},
+    {"x94", 94},
+    {"x95", 95},
+    {"x96", 96},
+    {"x97", 97},
+    {"x98", 98},
+    {"x99", 99},
+    {"x100", 100},
+    {"x101", 101},
+    {"x102", 102},
+    {"x103", 103},
+    {"x104", 104},
+    {"x105", 105},
+    {"x106", 106},
+    {"x107", 107},
+    {"x108", 108},
+    {"x109", 109},
+    {"x110", 110},
+    {"x111", 111},
+    {"x112", 112},
+    {"x113", 113},
+    {"x114", 114},
+    {"x115", 115},
+    {"x116", 116},
+    {"x117", 117},
+    {"x118", 118},
+    {"x119", 119},
+    {"x120", 120},
+    {"x121", 121},
+    {"x122", 122},
+    {"x123", 123},
+    {"x124", 124},
+    {"x125", 125},
+    {"x126", 126},
+    {"x127", 127},
+    {"x128", 128},
+    {"x129", 129},
+    {"x130", 130},
+    {"x131", 131},
+    {"x132", 132},
+    {"x133", 133},
+    {"x134", 134},
+    {"x135", 135},
+    {"x136", 136},
+    {"x137", 137},
+    {"x138", 138},
+    {"x139", 139},
+    {"x140", 140},
+    {"x141", 141},
+    {"x142", 142},
+    {"x143", 143},
+    {"x144", 144},
+    {"x145", 145},
+    {"x146", 146},
+    {"x147", 147},
+    {"x148", 148},
+    {"x149", 149},
+    {"x150", 150},
+    {"x151", 151},
+    {"x152", 152},
+    {"x153", 153},
+    {"x154", 154},
+    {"x155", 155},
+    {"x156", 156},
+    {"x157", 157},
+    {"x158", 158},
+    {"x159", 159},
+    {"x160", 160},
+    {"x161", 161},
+    {"x162", 162},
+    {"x163", 163},
+    {"x164", 164},
+    {"x165", 165},
+    {"x166", 166},
+    {"x167", 167},
+    {"x168", 168},
+    {"x169", 169},
+    {"x170", 170},
+    {"x171", 171},
+    {"x172", 172},
+    {"x173", 173},
+    {"x174", 174},
+    {"x175", 175},
+    {"x176", 176},
+    {"x177", 177},
+    {"x178", 178},
+    {"x179", 179},
+    {"x180", 180},
+    {"x181", 181},
+    {"x182", 182},
+    {"x183", 183},
+    {"x184", 184},
+    {"x185", 185},
+    {"x186", 186},
+    {"x187", 187},
+    {"x188", 188},
+    {"x189", 189},
+    {"x190", 190},
+    {"x191", 191},
+    {"x192", 192},
+    {"x193", 193},
+    {"x194", 194},
+    {"x195", 195},
+    {"x196", 196},
+    {"x197", 197},
+    {"x198", 198},
+    {"x199", 199},
+    {"x200", 200},
+    {"x201", 201},
+    {"x202", 202},
+    {"x203", 203},
+    {"x204", 204},
+    {"x205", 205},
+    {"x206", 206},
+    {"x207", 207},
+    {"x208", 208},
+    {"x209", 209},
+    {"x210", 210},
+    {"x211", 211},
+    {"x212", 212},
+    {"x213", 213},
+    {"x214", 214},
+    {"x215", 215},
+    {"x216", 216},
+    {"x217", 217},
+    {"x218", 218},
+    {"x219", 219},
+    {"x220", 220},
+    {"x221", 221},
+    {"x222", 222},
+    {"x223", 223},
+    {"x224", 224},
+    {"x225", 225},
+    {"x226", 226},
+    {"x227", 227},
+    {"x228", 228},
+    {"x229", 229},
+    {"x230", 230},
+    {"x231", 231},
+    {"x232", 232},
+    {"x233", 233},
+    {"x234", 234},
+    {"x235", 235},
+    {"x236", 236},
+    {"x237", 237},
+    {"x238", 238},
+    {"x239", 239},
+    {"x240", 240},
+    {"x241", 241},
+    {"x242", 242},
+    {"x243", 243},
+    {"x244", 244},
+    {"x245", 245},
+    {"x246", 246},
+    {"x247", 247},
+    {"x248", 248},
+    {"x249", 249},
+    {"x250", 250},
+    {"x251", 251},
+    {"x252", 252},
+    {"x253", 253},
+    {"x254", 254},
+    {"x255", 255}
+  };
+
+  GEMSelect selectFrequencyOfJI(sizeof(optionByteBPM) / sizeof(SelectOptionByte), optionByteBPM);
+  GEMSelect selectBPM_MultiplierOfJI(sizeof(optionByteBPM) / sizeof(SelectOptionByte), optionByteBPM_Multiplier);
+///////////////////////////////////////////////////////////////////
+
+  // Roland MT-32 mode (1987)
+  SelectOptionByte optionByteRolandMT32[] = {
+    // Piano
+    {"APiano1",  1}, {"APiano2",  2}, {"APiano3",  3},
+    {"EPiano1",  4}, {"EPiano2",  5}, {"EPiano3",  6}, {"EPiano4",  7},
+    {"HonkyTonk",8},
+    // Organ
+    {"EOrgan1",  9}, {"EOrgan2", 10}, {"EOrgan3", 11}, {"EOrgan4", 12},
+    {"POrgan2", 13}, {"POrgan3", 14}, {"POrgan4", 15},
+    {"Accordion",16},
+    // Keybrd
+    {"Harpsi1", 17}, {"Harpsi2", 18}, {"Harpsi3", 19},
+    {"Clavi 1", 20}, {"Clavi 2", 21}, {"Clavi 3", 22},
+    {"Celesta", 23}, {"Celest2", 24},
+    // S Brass
+    {"SBrass1", 25}, {"SBrass2", 26}, {"SBrass3", 27}, {"SBrass4", 28},
+    // SynBass
+    {"SynBass", 29}, {"SynBas2", 30}, {"SynBas3", 31}, {"SynBas4", 32},
+    // Synth 1
+    {"Fantasy", 33}, {"HarmoPan",34}, {"Chorale", 35}, {"Glasses", 36},
+    {"Soundtrack",37},{"Atmosphere",38},{"WarmBell",39},{"FunnyVox",40},
+    // Synth 2
+    {"EchoBell",41}, {"IceRain", 42}, {"Oboe2K1", 43}, {"EchoPan", 44},
+    {"Dr.Solo", 45}, {"SchoolDaze",46},{"BellSinger",47},{"SquareWave",48},
+    // Strings
+    {"StrSec1", 49}, {"StrSec2", 50}, {"StrSec3", 51}, {"Pizzicato", 52},
+    {"Violin1", 53}, {"Violin2", 54}, {"Cello 1", 55}, {"Cello 2", 56},
+    {"ContraBass",57}, {"Harp  1", 58}, {"Harp  2", 59},
+    // Guitar
+    {"Guitar1", 60}, {"Guitar2", 61}, {"EGuitr1", 62}, {"EGuitr2", 63},
+    {"Sitar", 64},
+    // Bass
+    {"ABass 1", 65}, {"ABass 2", 66}, {"EBass 1", 67}, {"EBass 2", 68},
+    {"SlapBass", 69},{"SlapBa2", 70}, {"Fretless", 71},{"Fretle2", 72},
+    // Wind
+    {"Flute 1", 73}, {"Flute 2", 74}, {"Piccolo", 75}, {"Piccol2", 76},
+    {"Recorder",77}, {"PanPipes",78},
+    {"Sax   1", 79}, {"Sax   2", 80}, {"Sax   3", 81}, {"Sax   4", 82},
+    {"Clarinet",83}, {"Clarin2", 84}, {"Oboe",    85}, {"EnglHorn", 86},
+    {"Bassoon", 87}, {"Harmonica",88},
+    // Brass
+    {"Trumpet", 89}, {"Trumpe2", 90}, {"Trombone",91}, {"Trombo2", 92},
+    {"FrHorn1", 93}, {"FrHorn2", 94},
+    {"Tuba", 95},    {"BrsSect", 96}, {"BrsSec2", 97},
+    // Mallet
+    {"Vibe  1", 98}, {"Vibe  2", 99},
+    {"SynMallet",100}, {"WindBell",101}, {"Glock",102}, {"TubeBell",103}, {"XyloPhone",104}, {"Marimba",105},
+    // Special
+    {"Koto", 106}, {"Sho", 107}, {"Shakuhachi",108},
+    {"Whistle",109}, {"Whistl2",110}, {"BottleBlow",111},{"BreathPipe",112},
+    // Percussion
+    {"Timpani",113}, {"MelTom", 114}, {"DeepSnare",115},
+    {"ElPerc1",116}, {"ElPerc2",117}, {"Taiko",  118}, {"TaikoRim",119},
+    {"Cymbal",120}, {"Castanets",121}, {"Triangle",122},
+    // Effects
+    {"OrchHit",123}, {"Telephone",124}, {"BirdTweet",125}, {"1NoteJam",126}, {"WaterBells",127}, {"JungleTune",128},
+  };
+  GEMSelect selectRolandMT32(sizeof(optionByteRolandMT32) / sizeof(SelectOptionByte), optionByteRolandMT32);
+  GEMItem  menuItemRolandMT32("RolandMT32:", programChange,  selectRolandMT32, sendProgramChange);
+
+  // General MIDI 1
+  SelectOptionByte optionByteGeneralMidi[] = {
+    // Piano
+    {"Piano 1", 1}, {"Piano 2", 2}, {"Piano 3", 3}, {"HonkyTonk", 4},
+    {"EPiano1", 5}, {"EPiano2", 6}, {"HarpsiChord", 7}, {"Clavinet", 8},
+    // Chromatic Percussion
+    {"Celesta", 9},  {"Glockenspiel", 10}, {"MusicBox", 11}, {"Vibraphone", 12},
+    {"Marimba", 13}, {"Xylophone", 14}, {"TubeBells", 15}, {"Dulcimer", 16},
+    // Organ
+    {"Organ 1", 17}, {"Organ 2", 18}, {"Organ 3", 19}, {"ChurchOrgan", 20},
+    {"ReedOrgan", 21}, {"Accordion", 22}, {"Harmonica", 23}, {"Bandoneon", 24},
+    // Guitar
+    {"AGtrNylon", 25}, {"AGtrSteel", 26},
+    {"EGtrJazz", 27}, {"EGtrClean", 28}, {"EGtrMuted", 29},
+    {"EGtrOverdrive", 30}, {"EGtrDistortion", 31}, {"EGtrHarmonics", 32},
+    // Bass
+    {"ABass", 33}, {"EBasFinger", 34}, {"EBasPicked", 35}, {"EBasFretless", 36},
+    {"SlpBass1", 37}, {"SlpBas2", 38}, {"SynBas1", 39}, {"SynBas2", 40},
+    // Strings
+    {"Violin", 41}, {"Viola", 42}, {"Cello", 43}, {"ContraBass", 44},
+    {"TremoloStrings", 45}, {"PizzicatoStrings", 46}, {"OrchHarp", 47}, {"Timpani", 48},
+    // Ensemble
+    {"StrEns1", 49}, {"StrEns2", 50}, {"SynStr1", 51}, {"SynStr2", 52},
+    {"ChoirAahs", 53}, {"VoiceOohs", 54}, {"SynVoice", 55}, {"OrchHit", 56},
+    // Brass
+    {"Trumpet", 57}, {"Trombone", 58}, {"Tuba", 59}, {"MutedTrumpet", 60},
+    {"FrenchHorn", 61}, {"BrassSection", 62}, {"SynBrs1", 63}, {"SynBrs2", 64},
+    // Reed
+    {"Sop Sax", 65}, {"AltoSax", 66}, {"Ten Sax", 67}, {"BariSax", 68},
+    {"Oboe", 69}, {"EnglHorn", 70}, {"Bassoon", 71}, {"Clarinet", 72},
+    // Pipe
+    {"Piccolo", 73}, {"Flute", 74}, {"Recorder", 75}, {"PanFlute", 76},
+    {"BlownBottle", 77}, {"Shakuhachi", 78}, {"Whistle", 79}, {"Ocarina", 80},
+    // Synth Lead
+    {"Ld1Square", 81}, {"Ld2Sawtooth", 82}, {"Ld3Calliope", 83}, {"Ld4Chiff", 84},
+    {"Ld5Charang", 85}, {"Ld6Voice", 86}, {"Ld7Fifths", 87}, {"Ld8Bass&Lead", 88},
+    // Synth Pad
+    {"Pd1NewAge", 89}, {"Pd2Warm", 90}, {"Pd3Polysynth", 91}, {"Pd4Choir", 92},
+    {"Pd5BowedGlass", 93}, {"Pd6Metallic", 94}, {"Pd7Halo", 95}, {"Pd8Sweep", 96},
+     // Synth Effects
+    {"FX1Rain", 97}, {"FX2Soundtrack", 98}, {"FX3Crystal", 99}, {"FX4Atmosphere", 100},
+    {"FX5Bright", 101}, {"FX6Goblins", 102}, {"FX7Echoes", 103}, {"FX8SciFi)", 104},
+    // Ethnic
+    {"Sitar", 105}, {"Banjo", 106}, {"Shamisen", 107}, {"Koto", 108},
+    {"Kalimba", 109}, {"BagPipe", 110}, {"Fiddle", 111}, {"Shanai", 112},
+    // Percussive
+    {"TinkleBell", 113}, {"Cowbell", 114}, {"SteelDrums", 115}, {"WoodBlock", 116},
+    {"TaikoDrum", 117}, {"MeloTom", 118}, {"SynDrum", 119}, {"RevCymbal", 120},
+    // Sound Effects
+    {"GtrFretNoise", 121}, {"BreathNoise", 122}, {"Seashore", 123}, {"BirdTweet", 124},
+    {"TelephoneRing", 125}, {"Helicopter", 126}, {"Applause", 127}, {"Gunshot", 128},
+  };
+  GEMSelect selectGeneralMidi(sizeof(optionByteGeneralMidi) / sizeof(SelectOptionByte), optionByteGeneralMidi);
+  GEMItem  menuItemGeneralMidi("GeneralMidi:", programChange,  selectGeneralMidi, sendProgramChange);
+
+
+  // doing this long-hand because the STRUCT has problems accepting string conversions of numbers for some reason
+  SelectOptionInt optionIntTransposeSteps[] = {
+    {"-127",-127},{"-126",-126},{"-125",-125},{"-124",-124},{"-123",-123},{"-122",-122},{"-121",-121},{"-120",-120},{"-119",-119},{"-118",-118},{"-117",-117},{"-116",-116},{"-115",-115},{"-114",-114},{"-113",-113},
+    {"-112",-112},{"-111",-111},{"-110",-110},{"-109",-109},{"-108",-108},{"-107",-107},{"-106",-106},{"-105",-105},{"-104",-104},{"-103",-103},{"-102",-102},{"-101",-101},{"-100",-100},{"- 99",- 99},{"- 98",- 98},
+    {"- 97",- 97},{"- 96",- 96},{"- 95",- 95},{"- 94",- 94},{"- 93",- 93},{"- 92",- 92},{"- 91",- 91},{"- 90",- 90},{"- 89",- 89},{"- 88",- 88},{"- 87",- 87},{"- 86",- 86},{"- 85",- 85},{"- 84",- 84},{"- 83",- 83},
+    {"- 82",- 82},{"- 81",- 81},{"- 80",- 80},{"- 79",- 79},{"- 78",- 78},{"- 77",- 77},{"- 76",- 76},{"- 75",- 75},{"- 74",- 74},{"- 73",- 73},{"- 72",- 72},{"- 71",- 71},{"- 70",- 70},{"- 69",- 69},{"- 68",- 68},
+    {"- 67",- 67},{"- 66",- 66},{"- 65",- 65},{"- 64",- 64},{"- 63",- 63},{"- 62",- 62},{"- 61",- 61},{"- 60",- 60},{"- 59",- 59},{"- 58",- 58},{"- 57",- 57},{"- 56",- 56},{"- 55",- 55},{"- 54",- 54},{"- 53",- 53},
+    {"- 52",- 52},{"- 51",- 51},{"- 50",- 50},{"- 49",- 49},{"- 48",- 48},{"- 47",- 47},{"- 46",- 46},{"- 45",- 45},{"- 44",- 44},{"- 43",- 43},{"- 42",- 42},{"- 41",- 41},{"- 40",- 40},{"- 39",- 39},{"- 38",- 38},
+    {"- 37",- 37},{"- 36",- 36},{"- 35",- 35},{"- 34",- 34},{"- 33",- 33},{"- 32",- 32},{"- 31",- 31},{"- 30",- 30},{"- 29",- 29},{"- 28",- 28},{"- 27",- 27},{"- 26",- 26},{"- 25",- 25},{"- 24",- 24},{"- 23",- 23},
+    {"- 22",- 22},{"- 21",- 21},{"- 20",- 20},{"- 19",- 19},{"- 18",- 18},{"- 17",- 17},{"- 16",- 16},{"- 15",- 15},{"- 14",- 14},{"- 13",- 13},{"- 12",- 12},{"- 11",- 11},{"- 10",- 10},{"-  9",-  9},{"-  8",-  8},
+    {"-  7",-  7},{"-  6",-  6},{"-  5",-  5},{"-  4",-  4},{"-  3",-  3},{"-  2",-  2},{"-  1",-  1},{"+/-0",   0},{"+  1",   1},{"+  2",   2},{"+  3",   3},{"+  4",   4},{"+  5",   5},{"+  6",   6},{"+  7",   7},
+    {"+  8",   8},{"+  9",   9},{"+ 10",  10},{"+ 11",  11},{"+ 12",  12},{"+ 13",  13},{"+ 14",  14},{"+ 15",  15},{"+ 16",  16},{"+ 17",  17},{"+ 18",  18},{"+ 19",  19},{"+ 20",  20},{"+ 21",  21},{"+ 22",  22},
+    {"+ 23",  23},{"+ 24",  24},{"+ 25",  25},{"+ 26",  26},{"+ 27",  27},{"+ 28",  28},{"+ 29",  29},{"+ 30",  30},{"+ 31",  31},{"+ 32",  32},{"+ 33",  33},{"+ 34",  34},{"+ 35",  35},{"+ 36",  36},{"+ 37",  37},
+    {"+ 38",  38},{"+ 39",  39},{"+ 40",  40},{"+ 41",  41},{"+ 42",  42},{"+ 43",  43},{"+ 44",  44},{"+ 45",  45},{"+ 46",  46},{"+ 47",  47},{"+ 48",  48},{"+ 49",  49},{"+ 50",  50},{"+ 51",  51},{"+ 52",  52},
+    {"+ 53",  53},{"+ 54",  54},{"+ 55",  55},{"+ 56",  56},{"+ 57",  57},{"+ 58",  58},{"+ 59",  59},{"+ 60",  60},{"+ 61",  61},{"+ 62",  62},{"+ 63",  63},{"+ 64",  64},{"+ 65",  65},{"+ 66",  66},{"+ 67",  67},
+    {"+ 68",  68},{"+ 69",  69},{"+ 70",  70},{"+ 71",  71},{"+ 72",  72},{"+ 73",  73},{"+ 74",  74},{"+ 75",  75},{"+ 76",  76},{"+ 77",  77},{"+ 78",  78},{"+ 79",  79},{"+ 80",  80},{"+ 81",  81},{"+ 82",  82},
+    {"+ 83",  83},{"+ 84",  84},{"+ 85",  85},{"+ 86",  86},{"+ 87",  87},{"+ 88",  88},{"+ 89",  89},{"+ 90",  90},{"+ 91",  91},{"+ 92",  92},{"+ 93",  93},{"+ 94",  94},{"+ 95",  95},{"+ 96",  96},{"+ 97",  97},
+    {"+ 98",  98},{"+ 99",  99},{"+100", 100},{"+101", 101},{"+102", 102},{"+103", 103},{"+104", 104},{"+105", 105},{"+106", 106},{"+107", 107},{"+108", 108},{"+109", 109},{"+110", 110},{"+111", 111},{"+112", 112},
+    {"+113", 113},{"+114", 114},{"+115", 115},{"+116", 116},{"+117", 117},{"+118", 118},{"+119", 119},{"+120", 120},{"+121", 121},{"+122", 122},{"+123", 123},{"+124", 124},{"+125", 125},{"+126", 126},{"+127", 127}
+  };
+  GEMSelect selectTransposeSteps( 255, optionIntTransposeSteps);
+  GEMItem  menuItemTransposeSteps( "Transpose:",   transposeSteps,  selectTransposeSteps, changeTranspose);
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////
+  // MIDI Channel selection
+  SelectOptionByte optionByteMIDIChannel[] = {{":  1",1},{":  2",2},{":  3",3},{":  4",4},{":  5",5},{":  6",6},{":  7",7},{":  8",8},{":  9",9},{":  10",10},{":  11",11},{":  12",12},{":  13",13},{":  14",14},{":  15",15},{":  16",16}};
+  GEMSelect selectMIDIchannel(16,optionByteMIDIChannel);
+  GEMItem menuItemSelectMIDIChannel( "MIDI Channel",   defaultMidiChannel,  selectMIDIchannel);
+
+  // MIDI force MPE option toggle
+  GEMItem menuItemToggleForceMPEChannels ("Force MPE", forceEnableMPE, resetTuningMIDI);
+
+  // Layout rotation selection
+  SelectOptionByte optionByteLayoutRotation[] = {{"0 deg",0},{"60 deg",1},{"120 deg",2},{"180 deg",3},{"240 deg",4},{"300 deg",5},{"360 deg",6}};
+  GEMSelect selectLayoutRotation(7,optionByteLayoutRotation);
+  GEMItem  menuItemSelectLayoutRotation( "Rotate: ",   layoutRotation,  selectLayoutRotation, updateLayoutAndRotate);
+    
+  // Layout mirroring toggles
+  GEMItem mirrorLeftRightGEMItem("Mirror vert.", mirrorLeftRight, updateLayoutAndRotate);
+  GEMItem mirrorUpDownGEMItem   ("Mirror hor." , mirrorUpDown, updateLayoutAndRotate);
+    
+  // Dynamic just intonation toggles and parameters
+  GEMItem menuItemToggleJI_BPM         ("JI align BPM", useJustIntonationBPM,resetTuningMIDI);
+  GEMItem menuItemSetJI_BPM            ("Beating BPM",justIntonationBPM,selectFrequencyOfJI);
+  GEMItem menuItemSetJI_BPM_Multiplier ("BPM mult.",justIntonationBPM_Multiplier,selectBPM_MultiplierOfJI);
+  GEMItem menuItemToggleDynamicJI      ("Dynamic JI", useDynamicJustIntonation, resetTuningMIDI);
+    
+//////////////////////////////////////////////////////////////////////////////////////////////////////
+
+  SelectOptionByte optionByteColor[] =    { { "Rainbow", RAINBOW_MODE }, { "Tiered" , TIERED_COLOR_MODE }, { "Alt", ALTERNATE_COLOR_MODE }, { "Fifths", RAINBOW_OF_FIFTHS_MODE }, { "Alt piano", PIANO_ALT_COLOR_MODE }, { "Piano", PIANO_COLOR_MODE }, { "Filament", PIANO_INCANDESCENT_COLOR_MODE } };
+  GEMSelect selectColor( sizeof(optionByteColor) / sizeof(SelectOptionByte), optionByteColor);
+  GEMItem  menuItemColor( "Color mode:", colorMode, selectColor, setLEDcolorCodes);
+
+  SelectOptionByte optionByteAnimate[] =  { { "None" , ANIMATE_NONE }, { "Octave", ANIMATE_OCTAVE },
+    { "By Note", ANIMATE_BY_NOTE }, { "Star", ANIMATE_STAR }, { "Splash" , ANIMATE_SPLASH }, { "Orbit", ANIMATE_ORBIT }, {"Beams", ANIMATE_BEAMS}, {"rSplash", ANIMATE_SPLASH_REVERSE}, {"rStar", ANIMATE_STAR_REVERSE} };
+  GEMSelect selectAnimate( sizeof(optionByteAnimate)  / sizeof(SelectOptionByte), optionByteAnimate);
+  GEMItem  menuItemAnimate( "Animation:", animationType, selectAnimate);
+
+  SelectOptionByte optionByteBright[] = { { "Off", BRIGHT_OFF}, {"the end", BRIGHT_FAINTER}, {"Blackout", BRIGHT_FAINT}, {"Darker", BRIGHT_DARKER}, {"Dark", BRIGHT_DARK}, {"Dimmer", BRIGHT_DIMMER}, {"Dim", BRIGHT_DIM}, {"Low", BRIGHT_LOW}, {"Normal", BRIGHT_MID}, {"High", BRIGHT_HIGH}, {"THE SUN", BRIGHT_MAX } };
+  GEMSelect selectBright( sizeof(optionByteBright) / sizeof(SelectOptionByte), optionByteBright);
+  GEMItem menuItemBright( "Brightness", globalBrightness, selectBright, setLEDcolorCodes);
+
+  SelectOptionByte optionByteWaveform[] = { { "Hybrid", WAVEFORM_HYBRID }, { "Square", WAVEFORM_SQUARE }, { "Saw", WAVEFORM_SAW },
+  {"Triangl", WAVEFORM_TRIANGLE}, {"Sine", WAVEFORM_SINE}, {"Strings", WAVEFORM_STRINGS}, {"Clrinet", WAVEFORM_CLARINET} };
+  GEMSelect selectWaveform(sizeof(optionByteWaveform) / sizeof(SelectOptionByte), optionByteWaveform);
+  GEMItem  menuItemWaveform( "Waveform:", currWave, selectWaveform, resetSynthFreqs);
+
+  SelectOptionInt optionIntModWheel[] = { { "too slo", 1 }, { "Turtle", 2 }, { "Slow", 4 },
+    { "Medium",    8 }, { "Fast",     16 }, { "Cheetah",  32 }, { "Instant", 127 } };
+  GEMSelect selectModSpeed(sizeof(optionIntModWheel) / sizeof(SelectOptionInt), optionIntModWheel);
+  GEMItem  menuItemModSpeed( "Mod wheel:", modWheelSpeed, selectModSpeed);
+  GEMItem  menuItemVelSpeed( "Vel wheel:", velWheelSpeed, selectModSpeed);
+
+  SelectOptionInt optionIntPBWheel[] =  { { "too slo", 128 }, { "Turtle", 256 }, { "Slow", 512 },
+    { "Medium", 1024 }, { "Fast", 2048 }, { "Cheetah", 4096 },  { "Instant", 16384 } };
+  GEMSelect selectPBSpeed(sizeof(optionIntPBWheel) / sizeof(SelectOptionInt), optionIntPBWheel);
+  GEMItem  menuItemPBSpeed( "PB wheel:", pbWheelSpeed, selectPBSpeed);
+
+  // Call this procedure to return to the main menu
+  void menuHome() {
+    menu.setMenuPageCurrent(menuPageMain);
+    menu.drawMenu();
+  }
+
+  void rebootToBootloader() {
+    menu.setMenuPageCurrent(menuPageReboot);
+    menu.drawMenu();
+    strip.clear();
+    strip.show();
+    rp2040.rebootToBootloader();
+  }
+  /*
+    This procedure sets each layout menu item to be either
+    visible if that layout is available in the current tuning,
+    or hidden if not.
+
+    It should run once after the layout menu items are
+    generated, and then once any time the tuning changes.
+  */
+  void showOnlyValidLayoutChoices() {
+    for (byte L = 0; L < layoutCount; L++) {
+      menuItemLayout[L]->hide((layoutOptions[L].tuning != current.tuningIndex));
+    }
+    sendToLog("menu: Layout choices were updated.");
+  }
+  /*
+    This procedure sets each scale menu item to be either
+    visible if that scale is available in the current tuning,
+    or hidden if not.
+
+    It should run once after the scale menu items are
+    generated, and then once any time the tuning changes.
+  */
+  void showOnlyValidScaleChoices() {
+    for (int S = 0; S < scaleCount; S++) {
+      menuItemScales[S]->hide((scaleOptions[S].tuning != current.tuningIndex) && (scaleOptions[S].tuning != ALL_TUNINGS));
+    }
+    sendToLog("menu: Scale choices were updated.");
+  }
+  /*
+    This procedure sets each key spinner menu item to be either
+    visible if the key names correspond to the current tuning,
+    or hidden if not.
+
+    It should run once after the key selectors are
+    generated, and then once any time the tuning changes.
+  */
+  void showOnlyValidKeyChoices() {
+    for (int T = 0; T < TUNINGCOUNT; T++) {
+      menuItemKeys[T]->hide((T != current.tuningIndex));
+    }
+    sendToLog("menu: Key choices were updated.");
+  }
+
+  void updateLayoutAndRotate() {
+    applyLayout();
+    u8g2.setDisplayRotation(current.layout().isPortrait ? U8G2_R2 : U8G2_R1);     // and landscape / portrait rotation
+  }
+  /*
+    This procedure is run when a layout is selected via the menu.
+    It sets the current layout to the selected value.
+    If it's different from the previous one, then
+    re-apply the layout to the grid. In any case, go to the
+    main menu when done.
+  */
+  void changeLayout(GEMCallbackData callbackData) {
+    byte selection = callbackData.valByte;
+    if (selection != current.layoutIndex) {
+      current.layoutIndex = selection;
+      updateLayoutAndRotate();
+    }
+    menuHome();
+  }
+  /*
+    This procedure is run when a scale is selected via the menu.
+    It sets the current scale to the selected value.
+    If it's different from the previous one, then
+    re-apply the scale to the grid. In any case, go to the
+    main menu when done.
+  */
+  void changeScale(GEMCallbackData callbackData) {   // when you change the scale via the menu
+    int selection = callbackData.valInt;
+    if (selection != current.scaleIndex) {
+      current.scaleIndex = selection;
+      applyScale();
+    }
+    menuHome();
+  }
+  /*
+    This procedure is run when the key is changed via the menu.
+    A key change results in a shift in the location of the
+    scale notes relative to the grid.
+    In this program, the only thing that occurs is that
+    the scale is reapplied to the grid.
+    The menu does not go home because the intent is to stay
+    on the scale/key screen.
+  */
+  void changeKey() {     // when you change the key via the menu
+    applyScale();
+  }
+  /*
+    This procedure was declared already and is being defined now.
+    It's run when the transposition is changed via the menu.
+    It sets the current transposition to the selected value.
+    The effect of transposition is to change the sounded
+    notes but not the layout or display.
+    The procedure to re-assign pitches is therefore called.
+    The menu doesn't change because the transpose is a spinner select.
+  */
+  void changeTranspose() {     // when you change the transpose via the menu
+    current.transpose = transposeSteps;
+    assignPitches();
+    updateSynthWithNewFreqs();
+  }
+  /*
+    This procedure is run when the tuning is changed via the menu.
+    It affects almost everything in the program, so
+    quite a few items are reset, refreshed, and redone
+    when the tuning changes.
+  */
+  void changeTuning(GEMCallbackData callbackData) {
+    byte selection = callbackData.valByte;
+    if (selection != current.tuningIndex) {
+      current.tuningIndex = selection;
+      current.layoutIndex = current.layoutsBegin();        // reset layout to first in list
+      current.scaleIndex = 0;                              // reset scale to "no scale"
+      current.keyStepsFromA = current.tuning().spanCtoA(); // reset key to C
+      showOnlyValidLayoutChoices();                        // change list of choices in GEM Menu
+      showOnlyValidScaleChoices();                         // change list of choices in GEM Menu
+      showOnlyValidKeyChoices();                           // change list of choices in GEM Menu
+      updateLayoutAndRotate();   // apply changes above
+      resetTuningMIDI();  // clear out MIDI queue
+      resetSynthFreqs();
+    }
+    menuHome();
+  }
+  /*
+    The procedure below builds menu items for tuning,
+    layout, scales, and keys based on what's preloaded.
+    We already declared arrays of menu item objects earlier.
+    Now we cycle through those arrays, and create GEMItem objects for
+    each index. What's nice about doing this in an array is,
+    we do not have to assign a variable name to each object; we just
+    refer to it by its index in the array.
+
+    The constructor "new GEMItem" is populated with the different
+    variables in the preset objects we defined earlier.
+    Then the menu item is added to the associated page.
+    The item must be entered with the asterisk operator
+    because an array index technically returns an address in memory
+    pointing to the object; the addMenuItem procedure wants
+    the contents of that item, which is what the * beforehand does.
+  */
+  void createTuningMenuItems() {
+    for (byte T = 0; T < TUNINGCOUNT; T++) {
+      menuItemTuning[T] = new GEMItem(tuningOptions[T].name.c_str(), changeTuning, T);
+      menuPageTuning.addMenuItem(*menuItemTuning[T]);
+    }
+  }
+  void createLayoutMenuItems() {
+    for (byte L = 0; L < layoutCount; L++) { // create pointers to all layouts
+      menuItemLayout[L] = new GEMItem(layoutOptions[L].name.c_str(), changeLayout, L);
+      menuPageLayout.addMenuItem(*menuItemLayout[L]);
+    }
+    showOnlyValidLayoutChoices();
+  }
+  void createKeyMenuItems() {
+    for (byte T = 0; T < TUNINGCOUNT; T++) {
+      selectKey[T] = new GEMSelect(tuningOptions[T].cycleLength, tuningOptions[T].keyChoices);
+      menuItemKeys[T] = new GEMItem("Key:", current.keyStepsFromA, *selectKey[T], changeKey);
+      menuPageScales.addMenuItem(*menuItemKeys[T]);
+    }
+    showOnlyValidKeyChoices();
+  }
+  void createScaleMenuItems() {
+    for (int S = 0; S < scaleCount; S++) {  // create pointers to all scale items, filter them as you go
+      menuItemScales[S] = new GEMItem(scaleOptions[S].name.c_str(), changeScale, S);
+      menuPageScales.addMenuItem(*menuItemScales[S]);
+    }
+    showOnlyValidScaleChoices();
+  }
+
+  void setupMenu() {
+    menu.setSplashDelay(0);
+    menu.init();
+    /*
+      addMenuItem procedure adds that GEM object to the given page.
+      The menu items appear in the order they are added,
+      so to change the order in the menu change the order in the code.
+    */
+    menuPageMain.addMenuItem(menuGotoTuning);
+      createTuningMenuItems();
+      menuPageTuning.addMenuItem(menuItemToggleJI_BPM);
+      menuPageTuning.addMenuItem(menuItemSetJI_BPM);
+      menuPageTuning.addMenuItem(menuItemSetJI_BPM_Multiplier);
+      menuPageTuning.addMenuItem(menuItemToggleDynamicJI);
+      menuPageTuning.addMenuItem(menuTuningBack);
+    menuPageMain.addMenuItem(menuGotoLayout);
+      createLayoutMenuItems();
+      menuPageLayout.addMenuItem(mirrorLeftRightGEMItem);
+      menuPageLayout.addMenuItem(mirrorUpDownGEMItem);
+      menuPageLayout.addMenuItem(menuItemSelectLayoutRotation);
+      menuPageLayout.addMenuItem(menuLayoutBack);
+    menuPageMain.addMenuItem(menuGotoScales);
+      createKeyMenuItems();
+      menuPageScales.addMenuItem(menuItemScaleLock);
+      createScaleMenuItems();
+      menuPageScales.addMenuItem(menuScalesBack);
+    menuPageMain.addMenuItem(menuGotoControl);
+      menuPageControl.addMenuItem(menuItemPBSpeed);
+      menuPageControl.addMenuItem(menuItemModSpeed);
+      menuPageControl.addMenuItem(menuItemVelSpeed);
+      menuPageControl.addMenuItem(menuControlBack);
+    menuPageMain.addMenuItem(menuGotoColors);
+      menuPageColors.addMenuItem(menuItemColor);
+      menuPageColors.addMenuItem(menuItemBright);
+      menuPageColors.addMenuItem(menuItemAnimate);
+      menuPageColors.addMenuItem(menuColorsBack);
+    menuPageMain.addMenuItem(menuGotoSynth);
+      menuPageSynth.addMenuItem(menuItemPlayback);
+      menuPageSynth.addMenuItem(menuItemWaveform);
+      // menuItemAudioD added here for hardware V1.2
+      menuPageSynth.addMenuItem(menuItemRolandMT32);
+      menuPageSynth.addMenuItem(menuItemGeneralMidi);
+      menuPageSynth.addMenuItem(menuSynthBack);
+    menuPageMain.addMenuItem(menuGotoMIDI);
+      menuPageMIDI.addMenuItem(menuItemSelectMIDIChannel);
+      menuPageMIDI.addMenuItem(menuItemMPEpitchBend);
+      menuPageMIDI.addMenuItem(menuItemToggleForceMPEChannels);
+      menuPageMIDI.addMenuItem(menuMIDIBack);
+    menuPageMain.addMenuItem(menuItemTransposeSteps);
+    menuPageMain.addMenuItem(menuGotoAdvanced);
+      menuPageAdvanced.addMenuItem(menuItemVersion);
+      menuPageAdvanced.addMenuItem(menuItemHardware);
+      menuPageAdvanced.addMenuItem(menuItemRotary);
+      menuPageAdvanced.addMenuItem(menuItemPercep);
+      menuPageAdvanced.addMenuItem(menuItemShiftColor);
+      menuPageAdvanced.addMenuItem(menuItemWheelAlt);
+      menuPageAdvanced.addMenuItem(menuItemPBBehave);
+      menuPageAdvanced.addMenuItem(menuItemModBehave);
+      menuPageAdvanced.addMenuItem(menuItemUSBBootloader);
+      menuPageAdvanced.addMenuItem(menuAdvancedBack);
+    menuHome();
+  }
+  void setupGFX() {
+    u8g2.begin();                       // Menu and graphics setup
+    u8g2.setBusClock(1000000);          // Speed up display
+    u8g2.setContrast(CONTRAST_AWAKE);   // Set contrast
+    sendToLog("U8G2 graphics initialized.");
+  }
+  void screenSaver() {
+    if (screenTime <= screenSaverTimeout) {
+      screenTime = screenTime + lapTime;
+      if (screenSaverOn) {
+        screenSaverOn = 0;
+        u8g2.setContrast(CONTRAST_AWAKE);
+      }
+    } else {
+      if (!screenSaverOn) {
+        screenSaverOn = 1;
+        u8g2.setContrast(CONTRAST_SCREENSAVER);
+        //if(globalBrightness == BRIGHT_OFF)
+        {
+          u8g2.clear();
+        }
+      }
+    }
+  }
+
+// @interface
+  /*
+    This section of the code handles reading
+    the rotary knob and physical hex buttons.
+
+    Documentation:
+      Rotary knob code derived from:
+        https://github.com/buxtronix/arduino/tree/master/libraries/Rotary
+    Copyright 2011 Ben Buxton. Licenced under the GNU GPL Version 3.
+    Contact: bb@cactii.net
+
+    when the mechanical rotary knob is turned,
+    the two pins go through a set sequence of
+    states during one physical "click", as follows:
+      Direction          Binary state of pin A\B
+      Counterclockwise = 1\1, 0\1, 0\0, 1\0, 1\1
+      Clockwise        = 1\1, 1\0, 0\0, 0\1, 1\1
+
+    The neutral state of the knob is 1\1; a turn
+    is complete when 1\1 is reached again after
+    passing through all the valid states above,
+    at which point action should be taken depending
+    on the direction of the turn.
+
+    The variable rotaryState stores all of this
+    data and refreshes it each loop of the 2nd processor.
+      Value    Meaning
+      0, 4     Knob is in neutral state
+      1, 2, 3  CCW turn state 1, 2, 3
+      5, 6, 7  CW  turn state 1, 2, 3
+      8, 16    Completed turn CCW, CW
+  */
+  #define ROT_PIN_A 20
+  #define ROT_PIN_B 21
+  #define ROT_PIN_C 24
+  byte rotaryState = 0;
+  const byte rotaryStateTable[8][4] = {
+    {0,5,1,0},{2,0,1,0},{2,3,1,0},{2,3,0,8},
+    {0,5,1,0},{6,5,0,0},{6,5,7,0},{6,0,7,16}
+  };
+  byte storeRotaryTurn = 0;
+  bool rotaryClicked = HIGH;
+
+  void readHexes() {
+    /* This is the original way of reading buttons. multiplexer is doing the least movement. May be faster?
+    for (byte r = 0; r < ROWCOUNT; r++) {      // Iterate through each of the row pins on the multiplexing chip.
+      for (byte d = 0; d < 4; d++) {
+        digitalWrite(mPin[d], (r >> d) & 1);
+      }
+      for (byte c = 0; c < COLCOUNT; c++) {    // Now iterate through each of the column pins that are connected to the current row pin.
+        byte p = cPin[c];                      // Hold the currently selected column pin in a variable.
+        pinMode(p, INPUT_PULLUP);              // Set that row pin to INPUT_PULLUP mode (+3.3V / HIGH).
+        byte i = c + (r * COLCOUNT);
+        delayMicroseconds(6);                  // delay while column pin mode
+        bool didYouPressHex = (digitalRead(p) == LOW);  // hex is pressed if it returns LOW. else not pressed
+        h[i].interpBtnPress(didYouPressHex);
+        if (h[i].btnState == BTN_STATE_NEWPRESS) {
+          h[i].timePressed = runTime;          // log the time
+        }
+        pinMode(p, INPUT);                     // Set the selected column pin back to INPUT mode (0V / LOW).
+       }
+    }*/
+    // trying out a new way which may reduce rf noise (and increase reliability) by reducing the ammount of times the columns get energized
+    for (byte c = 0; c < COLCOUNT; c++) {      // Iterate through each of the column pins.
+      byte p = cPin[c];                        // Hold the currently selected column pin in a variable.
+      pinMode(p, INPUT_PULLUP);                // Set that column pin to INPUT_PULLUP mode (+3.3V / HIGH).
+      delayMicroseconds(0);                    // delay to energize column and stabilize (may need adjustment)
+      for (byte r = 0; r < ROWCOUNT; r++) {    // Then iterate through each of the row pins on the multiplexing chip for the selected column.
+       for (byte d = 0; d < 4; d++) {
+          digitalWrite(mPin[d], (r >> d) & 1); // Selected multiplexer channel is pulled to ground.
+        }
+        byte i = c + (r * COLCOUNT);/*
+        byte tempSat = SAT_BW;
+        colorDef tempColor = {HUE_NONE, tempSat, (byte)(toggleWheel ? VALUE_SHADE : VALUE_LOW)};
+        strip.setPixelColor(i, getLEDcode(tempColor));
+        strip.show();*/
+        delayMicroseconds(14);                  // Delay to allow signal to settle and improve reliability (found this number by experimentation)
+        bool didYouPressHex = (digitalRead(p) == LOW);  // hex is pressed if it returns LOW. else not pressed
+        h[i].interpBtnPress(didYouPressHex);
+        if (h[i].btnState == BTN_STATE_NEWPRESS) {
+          h[i].timePressed = runTime;          // log the time
+        }
+      }
+      pinMode(p, INPUT);                     // Set the selected column pin back to INPUT mode (0V / LOW).
+    }
+    for (byte i = 0; i < BTN_COUNT; i++) {   // For all buttons in the deck
+      switch (h[i].btnState) {
+        case BTN_STATE_NEWPRESS: // just pressed
+          if (h[i].isCmd) {
+            cmdOn(i);
+          } else if (h[i].inScale || (!scaleLock)) {
+            tryMIDInoteOn(i);
+            trySynthNoteOn(i);
+          }
+          break;
+        case BTN_STATE_RELEASED: // just released
+          if (h[i].isCmd) {
+            cmdOff(i);
+          } else if (h[i].inScale || (!scaleLock)) {
+            tryMIDInoteOff(i);
+            trySynthNoteOff(i);
+          }
+          break;
+        case BTN_STATE_HELD: // held
+          break;
+        default: // inactive
+          break;
+      }
+    }
+  }
+  void updateWheels() {
+    velWheel.setTargetValue();
+    bool upd = velWheel.updateValue(runTime);
+    if (upd) {
+      sendToLog("vel became " + std::to_string(velWheel.curValue));
+    }
+    if (toggleWheel) {
+      pbWheel.setTargetValue();
+      upd = pbWheel.updateValue(runTime);
+      if (upd) {
+        sendMIDIpitchBendToCh1();
+        updateSynthWithNewFreqs();
+      }
+    } else {
+      modWheel.setTargetValue();
+      upd = modWheel.updateValue(runTime);
+      if (upd) {
+        sendMIDImodulationToCh1();
+      }
+    }
+  }
+  void setupRotary() {
+    pinMode(ROT_PIN_A, INPUT_PULLUP);
+    pinMode(ROT_PIN_B, INPUT_PULLUP);
+    pinMode(ROT_PIN_C, INPUT_PULLUP);
+  }
+  void readKnob() {
+    rotaryState = rotaryStateTable[rotaryState & 7][
+      (digitalRead(ROT_PIN_B) << 1) | digitalRead(ROT_PIN_A)
+    ];
+    if (rotaryState & 24) {
+      storeRotaryTurn = rotaryState;
+    }
+  }
+  void dealWithRotary() {
+    if (menu.readyForKey()) {
+      bool temp = digitalRead(ROT_PIN_C);
+      if (temp > rotaryClicked) {
+        menu.registerKeyPress(GEM_KEY_OK);
+        screenTime = 0;
+      }
+      rotaryClicked = temp;
+      if (storeRotaryTurn != 0) {
+        if (rotaryInvert == true) {
+          menu.registerKeyPress((storeRotaryTurn == 8) ? GEM_KEY_DOWN : GEM_KEY_UP);
+        } else {menu.registerKeyPress((storeRotaryTurn == 8) ? GEM_KEY_UP : GEM_KEY_DOWN);}
+        storeRotaryTurn = 0;
+        screenTime = 0;
+      }
+    }
+  }
+
+  void setupHardware() {
+    if (Hardware_Version == HARDWARE_V1_2) {
+        midiD = MIDID_USB | MIDID_SER;
+        audioD = AUDIO_PIEZO | AUDIO_AJACK;
+        menuPageSynth.addMenuItem(menuItemAudioD, 2);
+        globalBrightness = BRIGHT_DIM;
+        setLEDcolorCodes();
+        rotaryInvert = true;
+    }
+  }
+
+// @mainLoop
+  /*
+    An Arduino program runs
+    the setup() function once, then
+    runs the loop() function on repeat
+    until the machine is powered off.
+
+    The RP2040 has two identical cores.
+    Anything called from setup() and loop()
+    runs on the first core.
+    Anything called from setup1() and loop1()
+    runs on the second core.
+
+    On the HexBoard, the second core is
+    dedicated to two timing-critical tasks:
+    running the synth emulator, and tracking
+    the rotary knob inputs.
+    Everything else runs on the first core.
+  */
+  void setup() {
+    #if (defined(ARDUINO_ARCH_MBED) && defined(ARDUINO_ARCH_RP2040))
+    TinyUSB_Device_Init(0);  // Manual begin() is required on core without built-in support for TinyUSB such as mbed rp2040
+    #endif
+    setupMIDI();
+    setupFileSystem();
+    Wire.setSDA(SDAPIN);
+    Wire.setSCL(SCLPIN);
+    setupPins();
+    setupGrid();
+    applyLayout();
+    setupLEDs();
+    setupGFX();
+    setupRotary();
+    setupMenu();
+    for (byte i = 0; i < 5 && !TinyUSBDevice.mounted(); i++) {
+      delay(1);  // wait until device mounted, maybe
+    }
+  }
+  void loop() {   // run on first core
+    timeTracker();  // Time tracking functions
+    screenSaver();  // Reduces wear-and-tear on OLED panel
+    readHexes();       // Read and store the digital button states of the scanning matrix
+    arpeggiate();      // arpeggiate if synth mode allows it
+    updateWheels();   // deal with the pitch/mod wheel
+    animateLEDs();     // deal with animations
+    lightUpLEDs();      // refresh LEDs
+    dealWithRotary();  // deal with menu
+  }
+  void setup1() {  // set up on second core
+    setupSynth(PIEZO_PIN, PIEZO_SLICE);
+    setupSynth(AJACK_PIN, AJACK_SLICE);
+  }
+  void loop1() {  // run on second core
+    readKnob();
+  }