~earboxer/HexBoard

1

Hexperimental code, 2024-04-02

Details
Message ID
<CAJrt5V0saU_pr725i7pXtvv+9sid+EmYk2-5EVRgVHjAERMtFQ@mail.gmail.com>
DKIM signature
pass
Download raw message
// ====== Hexperiment
  // Sketch to program the hexBoard to handle microtones
  // March 2024, theHDM / Nicholas Fox
  // major thanks to Zach and Jared!
  // Arduino IDE setup:
  // Board = Generic RP2040 (use the following additional board manager repo:
  // https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json)
  // Patches needed for U8G2, Rotary.h
  // ==============================================================================
  // list of things remaining to do:
  // -- program the wheel -- OK!
  // -- put back the animations -- OK!
  // -- test MPE working on iPad garageband -- works on pianoteq
without MPE; need to get powered connection to iOS.
  // -- volume control test on buzzer
  // -- save and load presets
  // -- sequencer restore
  // ==============================================================================
  //
  #include <Arduino.h>
  #include <Wire.h>
  #include <LittleFS.h>
  #include <queue>              // std::queue construct to store open
channels in microtonal mode
  const byte diagnostics = 1;
// ====== initialize timers

  uint32_t runTime = 0;                 // Program loop consistent
variable for time in milliseconds since power on
  uint32_t lapTime = 0;                 // Used to keep track of how
long each loop takes. Useful for rate-limiting.
  uint32_t loopTime = 0;               // Used to check speed of the
loop in diagnostics mode 4

// ====== initialize SDA and SCL pins for hardware I/O

  const byte lightPinSDA = 16;
  const byte lightPinSCL = 17;

// ====== initialize MIDI

  #include <Adafruit_TinyUSB.h>
  #include <MIDI.h>
  Adafruit_USBD_MIDI usb_midi;
  MIDI_CREATE_INSTANCE(Adafruit_USBD_MIDI, usb_midi, MIDI);
  float concertA = 440.0;               // tuning of A4 in Hz
  byte MPE = 0; // microtonal mode. if zero then attempt to
self-manage multiple channels.
              // if one then on certain synths that are MPE compatible
will send in that mode.
  int16_t channelBend[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0 };  // what's the current note bend on this channel
  byte channelPoly[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0 };      // how many notes are playing on this channel
  std::queue<byte> openChannelQueue;
  const byte defaultPBRange = 2;

// ====== initialize LEDs

  #include <Adafruit_NeoPixel.h>
  const byte multiplexPins[] = { 4, 5, 2, 3 };  // m1p, m2p, m4p, m8p
  const byte rowCount = 14;                     // The number of rows
in the matrix
  const byte columnPins[] = { 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
  const byte colCount = sizeof(columnPins);  // The number of columns
in the matrix
  const byte hexCount = colCount * rowCount;  // The number of
elements in the matrix
  const byte LEDPin = 22;
  Adafruit_NeoPixel strip(hexCount, LEDPin, NEO_GRB + NEO_KHZ800);
  enum { NoAnim, StarAnim, SplashAnim, OrbitAnim, OctaveAnim, NoteAnim };
  byte animationType = 0;
  byte animationFPS = 32; // actually frames per 2^10 seconds. close
enough to 30fps
  int16_t rainbowDegreeTime = 64; // ms to go through 1/360 of rainbow.
// ====== initialize hex state object

  enum { Right, UpRight, UpLeft, Left, DnLeft, DnRight };
  typedef struct {
    int8_t row;
    int8_t col;
  } coordinates;
  typedef struct {
    byte keyState = 0;          // binary 00 = off, 01 = just pressed,
10 = just released, 11 = held
    coordinates coords = {0,0};
    uint32_t timePressed = 0;   // timecode of last press
    uint32_t LEDcolorAnim = 0;      //
    uint32_t LEDcolorPlay = 0;      //
    uint32_t LEDcolorOn = 0;   //
    uint32_t LEDcolorOff = 0;  //
    bool animate = 0;           // hex is flagged as part of the
animation in this frame
    int16_t steps = 0;          // number of steps from key center
(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 = 255;            // MIDI note or control parameter
corresponding to this hex
    int16_t bend;               // in microtonal mode, the pitch bend
for this note needed to be tuned correctly
    byte channel;               // what MIDI channel this note is playing on
    float frequency;            // what frequency to ring on the buzzer
    void updateKeyState(bool keyPressed) {
      keyState = (((keyState << 1) + keyPressed) & 3);
      if (keyState == 1) {
        timePressed = millis();  // log the time
      };
    };
    uint32_t animFrame() {
      if (timePressed) {    // 2^10 milliseconds is close enough to 1 second
        return 1 + (((runTime - timePressed) * animationFPS) >> 10);
      } else {
        return 0;
      };
    };
  } buttonDef;
  buttonDef h[hexCount]; // array of hex objects from 0 to 139
  const byte assignCmd[] = { 0, 20, 40, 60, 80, 100, 120 };
  const byte cmdCount = 7;
  const byte cmdCode = 192;

// ====== initialize wheel emulation

  const uint16_t ccMsgCoolDown = 20; // milliseconds between steps
  typedef struct {
    byte* topBtn;
    byte* midBtn;
    byte* botBtn;
    int16_t minValue;
    int16_t maxValue;
    uint16_t stepValue;
    int16_t defValue;
    int16_t curValue;
    int16_t targetValue;
    uint32_t timeLastChanged;
    void setTargetValue() {
      if (*midBtn >> 1) { // middle button toggles target (0) vs. step (1) mode
        int16_t temp = curValue;
             if (*topBtn == 1)     {temp += stepValue;};
             if (*botBtn == 1)     {temp -= stepValue;};
             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;
        };
      };
    };
    bool updateValue() {
      int16_t temp = targetValue - curValue;
      if (temp != 0) {
        if ((runTime - timeLastChanged) >= ccMsgCoolDown) {
          timeLastChanged = runTime;
          if (abs(temp) < stepValue) {
            curValue = targetValue;
          } else {
            curValue = curValue + (stepValue * (temp / abs(temp)));
          };
          return 1;
        } else {
          return 0;
        };
      } else {
        return 0;
      };
    };
  } wheelDef;
  wheelDef modWheel = {
    &h[assignCmd[4]].keyState,
    &h[assignCmd[5]].keyState,
    &h[assignCmd[6]].keyState,
    0, 127, 8,
    0, 0, 0
  };
  wheelDef pbWheel =  {
    &h[assignCmd[4]].keyState,
    &h[assignCmd[5]].keyState,
    &h[assignCmd[6]].keyState,
    -8192, 8192, 1024,
    0, 0, 0
  };
  wheelDef velWheel = {
    &h[assignCmd[0]].keyState,
    &h[assignCmd[1]].keyState,
    &h[assignCmd[2]].keyState,
    0, 127, 8,
    96, 96, 96
  };
  bool toggleWheel = 0; // 0 for mod, 1 for pb

// ====== initialize rotary knob

  #include <Rotary.h>
  const byte rotaryPinA = 20;
  const byte rotaryPinB = 21;
  const byte rotaryPinC = 24;
  Rotary rotary = Rotary(rotaryPinA, rotaryPinB);
  bool rotaryIsClicked = HIGH;          //
  bool rotaryWasClicked = HIGH;         //
  int8_t rotaryKnobTurns = 0;           //

// ====== initialize GFX display

  #include <GEM_u8g2.h>
  #define GEM_DISABLE_GLCD
  U8G2_SH1107_SEEED_128X128_F_HW_I2C u8g2(U8G2_R2, U8X8_PIN_NONE);
  GEM_u8g2 menu(u8g2, GEM_POINTER_ROW, GEM_ITEMS_COUNT_AUTO, 10, 10,
78); // menu item height; page screen top offset; menu values left
offset
  const byte defaultContrast = 63;           // GFX default contrast
  bool screenSaverOn = 0;                    //
  uint32_t screenTime = 0;                   // GFX timer to count if
screensaver should go on
  const uint32_t screenSaverMillis = 10'000; //

// ====== initialize piezo buzzer

  //#include "RP2040_Volume.h"    // simulated volume control on buzzer
  const byte tonePin = 23;
  //RP2040_Volume piezoBuzzer(tonePin, tonePin);
  byte buzzer = 0;                      // buzzer state
  byte currentBuzzNote = 255;           // need to work on this
  uint32_t currentBuzzTime = 0;         // Used to keep track of when
this note started buzzin
  uint32_t arpeggiateLength = 10;       //

// ====== initialize tuning (microtonal) presets

  typedef struct {
    char* name;
    byte cycleLength; // steps before repeat
    float stepSize;   // in cents, 100 = "normal" semitone.
  } tuningDef;
  enum {
    Twelve, Seventeen, Nineteen, TwentyTwo,
    TwentyFour, ThirtyOne, FortyOne, FiftyThree,
    SeventyTwo, BohlenPierce,
    CarlosA, CarlosB, CarlosG
  };
  tuningDef tuningOptions[] = {
    // replaces the idea of const byte EDO[] = { 12, 17, 19, 22, 24,
31, 41, 53, 72 };
    { (char*)"12 EDO",           12,  100.0 },
    { (char*)"17 EDO",           17, 1200.0 / 17 },
    { (char*)"19 EDO",           19, 1200.0 / 19 },
    { (char*)"22 EDO",           22, 1200.0 / 22 },
    { (char*)"24 EDO",           24,   50.0 },
    { (char*)"31 EDO",           31, 1200.0 / 31 },
    { (char*)"41 EDO",           41, 1200.0 / 41 },
    { (char*)"53 EDO",           53, 1200.0 / 53 },
    { (char*)"72 EDO",           72,  100.0 / 6 },
    { (char*)"Bohlen-Pierce",    13, 1901.955 / 13 }, //
    { (char*)"Carlos Alpha",      9, 77.965 }, //
    { (char*)"Carlos Beta",      11, 63.833 }, //
    { (char*)"Carlos Gamma",     20, 35.099 }
  };
  const byte tuningCount = sizeof(tuningOptions) / sizeof(tuningDef);

// ====== initialize layout patterns

  typedef struct {
    char* name;
    bool isPortrait;
    byte rootHex;
    int8_t acrossSteps;
    int8_t dnLeftSteps;
    byte tuning;
  } layoutDef;
  layoutDef layoutOptions[] = {
    { (char*)"Wicki-Hayden",      1, 64,   2,  -7, Twelve     },
    { (char*)"Harmonic Table",    0, 75,  -7,   3, Twelve     },
    { (char*)"Janko",             0, 65,  -1,  -1, Twelve     },
    { (char*)"Gerhard",           0, 65,  -1,  -3, Twelve     },
    { (char*)"Accordion C-sys.",  1, 75,   2,  -3, Twelve     },
    { (char*)"Accordion B-sys.",  1, 64,   1,  -3, Twelve     },
    { (char*)"Full Layout",       1, 65,  -1,  -9, Twelve     },
    { (char*)"Bosanquet, 17",     0, 65,  -2,  -1, Seventeen  },
    { (char*)"Full Layout",       1, 65,  -1,  -9, Seventeen  },
    { (char*)"Bosanquet, 19",     0, 65,  -1,  -2, Nineteen   },
    { (char*)"Full Layout",       1, 65,  -1,  -9, Nineteen   },
    { (char*)"Bosanquet, 22",     0, 65,  -3,  -1, TwentyTwo  },
    { (char*)"Full Layout",       1, 65,  -1,  -9, TwentyTwo  },
    { (char*)"Bosanquet, 24",     0, 65,  -1,  -3, TwentyFour },
    { (char*)"Full Layout",       1, 65,  -1,  -9, TwentyFour },
    { (char*)"Bosanquet, 31",     0, 65,  -2,  -3, ThirtyOne  },
    { (char*)"Full Layout",       1, 65,  -1,  -9, ThirtyOne  },
    { (char*)"Bosanquet, 41",     0, 65,  -4,  -3, FortyOne   },  //
forty-one #1
    { (char*)"Gerhard, 41",       0, 65,   3, -10, FortyOne   },  //
forty-one #2
    { (char*)"Full Layout, 41",   0, 65,  -1,  -8, FortyOne   },  //
forty-one #3
    { (char*)"Wicki-Hayden, 53",  1, 64,   9, -31, FiftyThree },
    { (char*)"Harmonic Tbl, 53",  0, 75, -31,  14, FiftyThree },
    { (char*)"Bosanquet, 53",     0, 65,  -5,  -4, FiftyThree },
    { (char*)"Full Layout, 53",   0, 65,  -1,  -9, FiftyThree },
    { (char*)"Full Layout, 72",   0, 65,  -1,  -9, SeventyTwo },
    { (char*)"Full Layout",       1, 65,  -1,  -9, BohlenPierce },
    { (char*)"Full Layout",       1, 65,  -1,  -9, CarlosA    },
    { (char*)"Full Layout",       1, 65,  -1,  -9, CarlosB    },
    { (char*)"Full Layout",       1, 65,  -1,  -9, CarlosG    }
  };
  const byte layoutCount = sizeof(layoutOptions) / sizeof(layoutDef);

// ====== initialize list of supported scales / modes / raga / maqam

  typedef struct {
    char* name;
    byte tuning;
    byte step[16]; // 16 bytes = 128 bits, 1 = in scale; 0 = not
  } scaleDef;
  scaleDef scaleOptions[] = {
    { (char*)"None",              255,        { 255,        255,
  255,255,255,255,255,255,255,255,255,255,255,255,255,255} },
    { (char*)"Major",             Twelve,     { 0b10101101,
0b0101'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
    { (char*)"Minor, natural",    Twelve,     { 0b10110101,
0b1010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
    { (char*)"Minor, melodic",    Twelve,     { 0b10110101,
0b0101'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
    { (char*)"Minor, harmonic",   Twelve,     { 0b10110101,
0b1001'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
    { (char*)"Pentatonic, major", Twelve,     { 0b10101001,
0b0100'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
    { (char*)"Pentatonic, minor", Twelve,     { 0b10010101,
0b0010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
    { (char*)"Blues",             Twelve,     { 0b10010111,
0b0010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
    { (char*)"Double Harmonic",   Twelve,     { 0b11001101,
0b1001'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
    { (char*)"Phrygian",          Twelve,     { 0b11010101,
0b1010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
    { (char*)"Phrygian Dominant", Twelve,     { 0b11001101,
0b1010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
    { (char*)"Dorian",            Twelve,     { 0b10110101,
0b0110'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
    { (char*)"Lydian",            Twelve,     { 0b10101011,
0b0101'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
    { (char*)"Lydian Dominant",   Twelve,     { 0b10101011,
0b0110'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
    { (char*)"Mixolydian",        Twelve,     { 0b10101101,
0b0110'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
    { (char*)"Locrian",           Twelve,     { 0b11010110,
0b1010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
    { (char*)"Whole tone",        Twelve,     { 0b10101010,
0b1010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
    { (char*)"Octatonic",         Twelve,     { 0b10110110,
0b1101'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
    { (char*)"Rast maqam",        TwentyFour, { 0b10001001,
0b00100010, 0b00101100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
    { (char*)"Rast makam",        FiftyThree, { 0b10000000,
0b01000000, 0b01000010, 0b00000001,
                                                0b00000000,
0b10001000, 0b10000'000, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
  };
  const byte scaleCount = sizeof(scaleOptions) / sizeof(scaleDef);
  byte scaleLock = 0;    // menu wants this to be an int, not a bool

// ====== initialize key coloring routines

  enum colors      { W,    R,    O,    Y,    L,    G,    C,    B,
I,    P,    M,
                          r,    o,    y,    l,    g,    c,    b,    i,
   p,    m     };
  enum { DARK = 0, VeryDIM = 1, DIM = 32, BRIGHT = 127, VeryBRIGHT = 255 };
  enum { GRAY = 0, DULL = 127, VIVID = 255 };
  float hueCode[] = { 0.0, 0.0, 36.0, 72.0, 108.0, 144.0, 180.0,
216.0, 252.0, 288.0, 324.0,
                           0.0, 36.0, 72.0, 108.0, 144.0, 180.0,
216.0, 252.0, 288.0, 324.0  };
  byte satCode[] = { GRAY,
VIVID,VIVID,VIVID,VIVID,VIVID,VIVID,VIVID,VIVID,VIVID,VIVID,
                          DULL, DULL, DULL, DULL, DULL, DULL, DULL,
DULL, DULL, DULL  };
  byte colorMode = 0;
  byte perceptual = 1;
  enum {assignDefault = -1}; // auto-determine this component of color

// ====== initialize note labels in each tuning, also used for key signature

  typedef struct {
    char* name;
    byte tuning;
    int8_t offset;      // steps from constant A4 to that key class
    colors tierColor;
  } keyDef;
  keyDef keyOptions[] = {
    // 12 EDO, whole tone = 2, #/b = 1
      { (char*)" C        (B#)", Twelve, -9, W },
      { (char*)" C# / Db",       Twelve, -8, I },
      { (char*)"      D",        Twelve, -7, W },
      { (char*)"      D# / Eb",  Twelve, -6, I },
      { (char*)"     (Fb)  E",   Twelve, -5, W },
      { (char*)"      F   (E#)", Twelve, -4, W },
      { (char*)" Gb / F#",       Twelve, -3, I },
      { (char*)" G",             Twelve, -2, W },
      { (char*)" G# / Ab",       Twelve, -1, I },
      { (char*)"      A",        Twelve,  0, W },
      { (char*)"      A# / Bb",  Twelve,  1, I },
      { (char*)"(Cb)       B",   Twelve,  2, W },
    // 17 EDO, whole tone = 3, #/b = 2, +/d = 1
      { (char*)" C        (B+)", Seventeen,  -13, W },
      { (char*)" C+ / Db / B#",  Seventeen,  -12, R },
      { (char*)" C# / Dd",       Seventeen,  -11, I },
      { (char*)"      D",        Seventeen,  -10, W },
      { (char*)"      D+ / Eb",  Seventeen,   -9, R },
      { (char*)" Fb / D# / Ed",  Seventeen,   -8, I },
      { (char*)"(Fd)       E",   Seventeen,   -7, W },
      { (char*)" F        (E+)", Seventeen,   -6, W },
      { (char*)" F+ / Gb / E#",  Seventeen,   -5, R },
      { (char*)" F# / Gd",       Seventeen,   -4, I },
      { (char*)"      G",        Seventeen,   -3, W },
      { (char*)"      G+ / Ab",  Seventeen,   -2, R },
      { (char*)"      G# / Ad",  Seventeen,   -1, I },
      { (char*)"           A",   Seventeen,    0, W },
      { (char*)"      Bb / A+",  Seventeen,    1, R },
      { (char*)" Cb / Bd / A#",  Seventeen,    2, I },
      { (char*)"(Cd)  B"      ,  Seventeen,    3, W },
    // 19 EDO, whole tone = 3, #/b = 1
      { (char*)" C",       Nineteen, -14, W },
      { (char*)" C#",      Nineteen, -13, R },
      { (char*)" Db",      Nineteen, -12, I },
      { (char*)" D",       Nineteen, -11, W },
      { (char*)" D#",      Nineteen, -10, R },
      { (char*)" Eb",      Nineteen,  -9, I },
      { (char*)" E",       Nineteen,  -8, W },
      { (char*)" E# / Fb", Nineteen,  -7, m },
      { (char*)"      F",  Nineteen,  -6, W },
      { (char*)"      F#", Nineteen,  -5, R },
      { (char*)"      Gb", Nineteen,  -4, I },
      { (char*)"      G",  Nineteen,  -3, W },
      { (char*)"      G#", Nineteen,  -2, R },
      { (char*)"      Ab", Nineteen,  -1, I },
      { (char*)"      A",  Nineteen,   0, W },
      { (char*)"      A#", Nineteen,   1, R },
      { (char*)"      Bb", Nineteen,   2, I },
      { (char*)"      B",  Nineteen,   3, W },
      { (char*)" Cb / B#", Nineteen,   4, m },
    // 22 EDO, whole tone = 4, #/b = 3, ^/v = 1
      { (char*)"  C         (^B)", TwentyTwo, -17, W },
      { (char*)" ^C  /  Db / vB#", TwentyTwo, -16, l },
      { (char*)" vC# / ^Db /  B#", TwentyTwo, -15, C },
      { (char*)"  C# / vD",        TwentyTwo, -14, i },
      { (char*)"        D",        TwentyTwo, -13, W },
      { (char*)"       ^D  /  Eb", TwentyTwo, -12, l },
      { (char*)"  Fb / vD# / ^Eb", TwentyTwo, -11, C },
      { (char*)" ^Fb /  D# / vE",  TwentyTwo, -10, i },
      { (char*)"(vF)          E",  TwentyTwo,  -9, W },
      { (char*)"  F         (^E)", TwentyTwo,  -8, W },
      { (char*)" ^F  /  Gb / vE#", TwentyTwo,  -7, l },
      { (char*)" vF# / ^Gb /  E#", TwentyTwo,  -6, C },
      { (char*)"  F# / vG",        TwentyTwo,  -5, i },
      { (char*)"        G",        TwentyTwo,  -4, W },
      { (char*)"       ^G  /  Ab", TwentyTwo,  -3, l },
      { (char*)"       vG# / ^Ab", TwentyTwo,  -2, C },
      { (char*)"        G# / vA",  TwentyTwo,  -1, i },
      { (char*)"              A",  TwentyTwo,   0, W },
      { (char*)"        Bb / ^A",  TwentyTwo,   1, l },
      { (char*)"  Cb / ^Bb / vA#", TwentyTwo,   2, C },
      { (char*)" ^Cb / vB  /  A#", TwentyTwo,   3, i },
      { (char*)"(vC)    B",        TwentyTwo,   4, W },
    // 24 EDO, whole tone = 4, #/b = 2, +/d = 1
      { (char*)" C  / B#", TwentyFour, -18, W },
      { (char*)" C+",      TwentyFour, -17, r },
      { (char*)" C# / Db", TwentyFour, -16, I },
      { (char*)"      Dd", TwentyFour, -15, g },
      { (char*)"      D",  TwentyFour, -14, W },
      { (char*)"      D+", TwentyFour, -13, r },
      { (char*)" Eb / D#", TwentyFour, -12, I },
      { (char*)" Ed",      TwentyFour, -11, g },
      { (char*)" E  / Fb", TwentyFour, -10, W },
      { (char*)" E+ / Fd", TwentyFour,  -9, y },
      { (char*)" E# / F",  TwentyFour,  -8, W },
      { (char*)"      F+", TwentyFour,  -7, r },
      { (char*)" Gb / F#", TwentyFour,  -6, I },
      { (char*)" Gd",      TwentyFour,  -5, g },
      { (char*)" G",       TwentyFour,  -4, W },
      { (char*)" G+",      TwentyFour,  -3, r },
      { (char*)" G# / Ab", TwentyFour,  -2, I },
      { (char*)"      Ad", TwentyFour,  -1, g },
      { (char*)"      A",  TwentyFour,   0, W },
      { (char*)"      A+", TwentyFour,   1, r },
      { (char*)" Bb / A#", TwentyFour,   2, I },
      { (char*)" Bd",      TwentyFour,   3, g },
      { (char*)" B  / Cb", TwentyFour,   4, W },
      { (char*)" B+ / Cd", TwentyFour,   5, y },
    // 31 EDO, whole tone = 5, #/b = 2, +/d = 1
      { (char*)" C",       ThirtyOne, -23, W },
      { (char*)" C+",      ThirtyOne, -22, R },
      { (char*)" C#",      ThirtyOne, -21, Y },
      { (char*)" Db",      ThirtyOne, -20, C },
      { (char*)" Dd",      ThirtyOne, -19, I },
      { (char*)" D",       ThirtyOne, -18, W },
      { (char*)" D+",      ThirtyOne, -17, R },
      { (char*)" D#",      ThirtyOne, -16, Y },
      { (char*)" Eb",      ThirtyOne, -15, C },
      { (char*)" Ed",      ThirtyOne, -14, I },
      { (char*)" E",       ThirtyOne, -13, W },
      { (char*)" E+ / Fb", ThirtyOne, -12, L },
      { (char*)" E# / Fd", ThirtyOne, -11, M },
      { (char*)"      F",  ThirtyOne, -10, W },
      { (char*)"      F+", ThirtyOne,  -9, R },
      { (char*)"      F#", ThirtyOne,  -8, Y },
      { (char*)"      Gb", ThirtyOne,  -7, C },
      { (char*)"      Gd", ThirtyOne,  -6, I },
      { (char*)"      G",  ThirtyOne,  -5, W },
      { (char*)"      G+", ThirtyOne,  -4, R },
      { (char*)"      G#", ThirtyOne,  -3, Y },
      { (char*)"      Ab", ThirtyOne,  -2, C },
      { (char*)"      Ad", ThirtyOne,  -1, I },
      { (char*)"      A",  ThirtyOne,   0, W },
      { (char*)"      A+", ThirtyOne,   1, R },
      { (char*)"      A#", ThirtyOne,   2, Y },
      { (char*)"      Bb", ThirtyOne,   3, C },
      { (char*)"      Bd", ThirtyOne,   4, I },
      { (char*)"      B",  ThirtyOne,   5, W },
      { (char*)" Cb / B+", ThirtyOne,   6, L },
      { (char*)" Cd / B#", ThirtyOne,   7, M },
    // 41 EDO, whole tone = 7, #/b = 4, +/d = 2, ^/v = 1
      { (char*)"  C         (vB#)", FortyOne, -31, W },
      { (char*)" ^C        /  B#",  FortyOne, -30, c },
      { (char*)"  C+ ",             FortyOne, -29, O },
      { (char*)" vC# /  Db",        FortyOne, -28, I },
      { (char*)"  C# / ^Db",        FortyOne, -27, R },
      { (char*)"        Dd",        FortyOne, -26, B },
      { (char*)"       vD",         FortyOne, -25, y },
      { (char*)"        D",         FortyOne, -24, W },
      { (char*)"       ^D",         FortyOne, -23, c },
      { (char*)"        D+",        FortyOne, -22, O },
      { (char*)"       vD# /  Eb",  FortyOne, -21, I },
      { (char*)"        D# / ^Eb",  FortyOne, -20, R },
      { (char*)"              Ed",  FortyOne, -19, B },
      { (char*)"             vE",   FortyOne, -18, y },
      { (char*)"      (^Fb)   E",   FortyOne, -17, W },
      { (char*)"        Fd / ^E",   FortyOne, -16, c },
      { (char*)"       vF  /  E+",  FortyOne, -15, y },
      { (char*)"        F   (vE#)", FortyOne, -14, W },
      { (char*)"       ^F  /  E#",  FortyOne, -13, c },
      { (char*)"        F+",        FortyOne, -12, O },
      { (char*)"  Gb / vF#",        FortyOne, -11, I },
      { (char*)" ^Gb /  F#",        FortyOne, -10, R },
      { (char*)"  Gd",              FortyOne,  -9, B },
      { (char*)" vG",               FortyOne,  -8, y },
      { (char*)"  G",               FortyOne,  -7, W },
      { (char*)" ^G",               FortyOne,  -6, c },
      { (char*)"  G+",              FortyOne,  -5, O },
      { (char*)" vG# /  Ab",        FortyOne,  -4, I },
      { (char*)"  G# / ^Ab",        FortyOne,  -3, R },
      { (char*)"        Ad",        FortyOne,  -2, B },
      { (char*)"       vA",         FortyOne,  -1, y },
      { (char*)"        A",         FortyOne,   0, W },
      { (char*)"       ^A",         FortyOne,   1, c },
      { (char*)"        A+",        FortyOne,   2, O },
      { (char*)"       vA# /  Bb",  FortyOne,   3, I },
      { (char*)"        A# / ^Bb",  FortyOne,   4, R },
      { (char*)"              Bd",  FortyOne,   5, B },
      { (char*)"             vB",   FortyOne,   6, y },
      { (char*)"      (^Cb)   B",   FortyOne,   7, W },
      { (char*)"        Cd / ^B",   FortyOne,   8, c },
      { (char*)"       vC  /  B+",  FortyOne,   9, y },
    // 53 EDO, whole tone = 9, #/b = 5, >/< = 2, ^/v = 1
      { (char*)"  C         (vB#)", FiftyThree, -40, W },
      { (char*)" ^C     /     B#",  FiftyThree, -39, c },
      { (char*)" >C  / <Db",        FiftyThree, -38, l },
      { (char*)" <C# / vDb",        FiftyThree, -37, O },
      { (char*)" vC# /  Db",        FiftyThree, -36, I },
      { (char*)"  C# / ^Db",        FiftyThree, -35, R },
      { (char*)" ^C# / >Db",        FiftyThree, -34, B },
      { (char*)" >C# / <D",         FiftyThree, -33, g },
      { (char*)"       vD",         FiftyThree, -32, y },
      { (char*)"        D",         FiftyThree, -31, W },
      { (char*)"       ^D",         FiftyThree, -30, c },
      { (char*)"       >D  / <Eb",  FiftyThree, -29, l },
      { (char*)"       <D# / vEb",  FiftyThree, -28, O },
      { (char*)"       vD# /  Eb",  FiftyThree, -27, I },
      { (char*)"        D# / ^Eb",  FiftyThree, -26, R },
      { (char*)"       ^D# / >Eb",  FiftyThree, -25, B },
      { (char*)"       >D# / <E",   FiftyThree, -24, g },
      { (char*)"  Fb    /    vE",   FiftyThree, -23, y },
      { (char*)"(^Fb)         E",   FiftyThree, -22, W },
      { (char*)"(>Fb)        ^E",   FiftyThree, -21, c },
      { (char*)" <F     /    >E",   FiftyThree, -20, G },
      { (char*)" vF         (<E#)", FiftyThree, -19, y },
      { (char*)"  F         (vE#)", FiftyThree, -18, W },
      { (char*)" ^F     /     E#",  FiftyThree, -17, c },
      { (char*)" >F  / <Gb",        FiftyThree, -16, l },
      { (char*)" <F# / vGb",        FiftyThree, -15, O },
      { (char*)" vF# /  Gb",        FiftyThree, -14, I },
      { (char*)"  F# / ^Gb",        FiftyThree, -13, R },
      { (char*)" ^F# / >Gb",        FiftyThree, -12, B },
      { (char*)" >F# / <G",         FiftyThree, -11, g },
      { (char*)"       vG",         FiftyThree, -10, y },
      { (char*)"        G",         FiftyThree,  -9, W },
      { (char*)"       ^G",         FiftyThree,  -8, c },
      { (char*)"       >G  / <Ab",  FiftyThree,  -7, l },
      { (char*)"       <G# / vAb",  FiftyThree,  -6, O },
      { (char*)"       vG# /  Ab",  FiftyThree,  -5, I },
      { (char*)"        G# / ^Ab",  FiftyThree,  -4, R },
      { (char*)"       ^G# / >Ab",  FiftyThree,  -3, B },
      { (char*)"       >G# / <A",   FiftyThree,  -2, g },
      { (char*)"             vA",   FiftyThree,  -1, y },
      { (char*)"              A",   FiftyThree,   0, W },
      { (char*)"             ^A",   FiftyThree,   1, c },
      { (char*)"       <Bb / >A",   FiftyThree,   2, l },
      { (char*)"       vBb / <A#",  FiftyThree,   3, O },
      { (char*)"        Bb / vA#",  FiftyThree,   4, I },
      { (char*)"       ^Bb /  A#",  FiftyThree,   5, R },
      { (char*)"       >Bb / ^A#",  FiftyThree,   6, B },
      { (char*)"       <B  / >A#",  FiftyThree,   7, g },
      { (char*)"  Cb / vB",         FiftyThree,   8, y },
      { (char*)"(^Cb)   B",         FiftyThree,   9, W },
      { (char*)"(>Cb)  ^B",         FiftyThree,  10, c },
      { (char*)" <C  / >B",         FiftyThree,  11, G },
      { (char*)" vC   (<B#)",       FiftyThree,  12, y },
    // 72 EDO, whole tone = 12, #/b = 6, +/d = 3, ^/v = 1
      { (char*)"  C    (B#)", SeventyTwo, -54, W },
      { (char*)" ^C",         SeventyTwo, -53, g },
      { (char*)" vC+",        SeventyTwo, -52, r },
      { (char*)"  C+",        SeventyTwo, -51, p },
      { (char*)" ^C+",        SeventyTwo, -50, b },
      { (char*)" vC#",        SeventyTwo, -49, y },
      { (char*)"  C# /  Db",  SeventyTwo, -48, I },
      { (char*)" ^C# / ^Db",  SeventyTwo, -47, g },
      { (char*)"       vDd",  SeventyTwo, -46, r },
      { (char*)"        Dd",  SeventyTwo, -45, p },
      { (char*)"       ^Dd",  SeventyTwo, -44, b },
      { (char*)"       vD",   SeventyTwo, -43, y },
      { (char*)"        D",   SeventyTwo, -42, W },
      { (char*)"       ^D",   SeventyTwo, -41, g },
      { (char*)"       vD+",  SeventyTwo, -40, r },
      { (char*)"        D+",  SeventyTwo, -39, p },
      { (char*)"       ^D+",  SeventyTwo, -38, b },
      { (char*)" vEb / vD#",  SeventyTwo, -37, y },
      { (char*)"  Eb /  D#",  SeventyTwo, -36, I },
      { (char*)" ^Eb / ^D#",  SeventyTwo, -35, g },
      { (char*)" vEd",        SeventyTwo, -34, r },
      { (char*)"  Ed",        SeventyTwo, -33, p },
      { (char*)" ^Ed",        SeventyTwo, -32, b },
      { (char*)" vE   (vFb)", SeventyTwo, -31, y },
      { (char*)"  E    (Fb)", SeventyTwo, -30, W },
      { (char*)" ^E   (^Fb)", SeventyTwo, -29, g },
      { (char*)" vE+ / vFd",  SeventyTwo, -28, r },
      { (char*)"  E+ /  Fd",  SeventyTwo, -27, p },
      { (char*)" ^E+ / ^Fd",  SeventyTwo, -26, b },
      { (char*)"(vE#)  vF",   SeventyTwo, -25, y },
      { (char*)" (E#)   F",   SeventyTwo, -24, W },
      { (char*)"(^E#)  ^F",   SeventyTwo, -23, g },
      { (char*)"       vF+",  SeventyTwo, -22, r },
      { (char*)"        F+",  SeventyTwo, -21, p },
      { (char*)"       ^F+",  SeventyTwo, -20, b },
      { (char*)" vGb / vF#",  SeventyTwo, -19, y },
      { (char*)"  Gb /  F#",  SeventyTwo, -18, I },
      { (char*)" ^Gb / ^F#",  SeventyTwo, -17, g },
      { (char*)" vGd",        SeventyTwo, -16, r },
      { (char*)"  Gd",        SeventyTwo, -15, p },
      { (char*)" ^Gd",        SeventyTwo, -14, b },
      { (char*)" vG",         SeventyTwo, -13, y },
      { (char*)"  G",         SeventyTwo, -12, W },
      { (char*)" ^G",         SeventyTwo, -11, g },
      { (char*)" vG+",        SeventyTwo, -10, r },
      { (char*)"  G+",        SeventyTwo,  -9, p },
      { (char*)" ^G+",        SeventyTwo,  -8, b },
      { (char*)" vG# / vAb",  SeventyTwo,  -7, y },
      { (char*)"  G# /  Ab",  SeventyTwo,  -6, I },
      { (char*)" ^G# / ^Ab",  SeventyTwo,  -5, g },
      { (char*)"       vAd",  SeventyTwo,  -4, r },
      { (char*)"        Ad",  SeventyTwo,  -3, p },
      { (char*)"       ^Ad",  SeventyTwo,  -2, b },
      { (char*)"       vA",   SeventyTwo,  -1, y },
      { (char*)"        A",   SeventyTwo,   0, W },
      { (char*)"       ^A",   SeventyTwo,   1, g },
      { (char*)"       vA+",  SeventyTwo,   2, r },
      { (char*)"        A+",  SeventyTwo,   3, p },
      { (char*)"       ^A+",  SeventyTwo,   4, b },
      { (char*)" vBb / vA#",  SeventyTwo,   5, y },
      { (char*)"  Bb /  A#",  SeventyTwo,   6, I },
      { (char*)" ^Bb / ^A#",  SeventyTwo,   7, g },
      { (char*)" vBd",        SeventyTwo,   8, r },
      { (char*)"  Bd",        SeventyTwo,   9, p },
      { (char*)" ^Bd",        SeventyTwo,  10, b },
      { (char*)" vB   (vCb)", SeventyTwo,  11, y },
      { (char*)"  B    (Cb)", SeventyTwo,  12, W },
      { (char*)" ^B   (^Cb)", SeventyTwo,  13, g },
      { (char*)" vB+ / vCd",  SeventyTwo,  14, r },
      { (char*)"  B+ /  Cd",  SeventyTwo,  15, p },
      { (char*)" ^B+ / ^Cd",  SeventyTwo,  16, b },
      { (char*)"(vB#)  vC",   SeventyTwo,  17, y },
    //
      { (char*)"n/a",BohlenPierce,0,W},
      { (char*)"n/a",BohlenPierce,0,W},
      { (char*)"n/a",BohlenPierce,0,W},
      { (char*)"n/a",BohlenPierce,0,W},
      { (char*)"n/a",BohlenPierce,0,W},
      { (char*)"n/a",BohlenPierce,0,W},
      { (char*)"n/a",BohlenPierce,0,W},
      { (char*)"n/a",BohlenPierce,0,W},
      { (char*)"n/a",BohlenPierce,0,W},
      { (char*)"n/a",BohlenPierce,0,W},
      { (char*)"n/a",BohlenPierce,0,W},
      { (char*)"n/a",BohlenPierce,0,W},
      { (char*)"n/a",BohlenPierce,0,W},
    //
      { (char*)"n/a",CarlosA,0,W},
      { (char*)"n/a",CarlosA,0,W},
      { (char*)"n/a",CarlosA,0,W},
      { (char*)"n/a",CarlosA,0,W},
      { (char*)"n/a",CarlosA,0,W},
      { (char*)"n/a",CarlosA,0,W},
      { (char*)"n/a",CarlosA,0,W},
      { (char*)"n/a",CarlosA,0,W},
      { (char*)"n/a",CarlosA,0,W},
    //
      { (char*)"n/a",CarlosB,0,W},
      { (char*)"n/a",CarlosB,0,W},
      { (char*)"n/a",CarlosB,0,W},
      { (char*)"n/a",CarlosB,0,W},
      { (char*)"n/a",CarlosB,0,W},
      { (char*)"n/a",CarlosB,0,W},
      { (char*)"n/a",CarlosB,0,W},
      { (char*)"n/a",CarlosB,0,W},
      { (char*)"n/a",CarlosB,0,W},
      { (char*)"n/a",CarlosB,0,W},
      { (char*)"n/a",CarlosB,0,W},
    //
      { (char*)"n/a",CarlosG,0,W},
      { (char*)"n/a",CarlosG,0,W},
      { (char*)"n/a",CarlosG,0,W},
      { (char*)"n/a",CarlosG,0,W},
      { (char*)"n/a",CarlosG,0,W},
      { (char*)"n/a",CarlosG,0,W},
      { (char*)"n/a",CarlosG,0,W},
      { (char*)"n/a",CarlosG,0,W},
      { (char*)"n/a",CarlosG,0,W},
      { (char*)"n/a",CarlosG,0,W},
      { (char*)"n/a",CarlosG,0,W},
      { (char*)"n/a",CarlosG,0,W},
      { (char*)"n/a",CarlosG,0,W},
      { (char*)"n/a",CarlosG,0,W},
      { (char*)"n/a",CarlosG,0,W},
      { (char*)"n/a",CarlosG,0,W},
      { (char*)"n/a",CarlosG,0,W},
      { (char*)"n/a",CarlosG,0,W},
      { (char*)"n/a",CarlosG,0,W},
      { (char*)"n/a",CarlosG,0,W},

  };
  const int keyCount = sizeof(keyOptions) / sizeof(keyDef);
// ====== initialize structure to store and recall user preferences

  typedef struct { // put all user-selectable options into a class so
that down the line these can be saved and loaded.
    char* 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 keyIndex;
    int transpose;
    // define simple recall functions
    tuningDef tuning() {
      return tuningOptions[tuningIndex];
    };
    layoutDef layout() {
      return layoutOptions[layoutIndex];
    };
    scaleDef scale() {
      return scaleOptions[scaleIndex];
    };
    keyDef key() {
      return keyOptions[keyIndex];
    };
    int layoutsBegin() {
      if (tuningIndex == Twelve) {
        return 0;
      } else {
        int temp = 0;
        while (layoutOptions[temp].tuning < tuningIndex) {
          temp++;
        };
        return temp;
      };
    };
    int keysBegin() {
      if (tuningIndex == Twelve) {
        return 0;
      } else {
        int temp = 0;
        while (keyOptions[temp].tuning < tuningIndex) {
          temp++;
        };
        return temp;
      };
    };
    int findC() {
      return keyOptions[keysBegin()].offset;
    };
  } presetDef;
  presetDef current = {
    (char*)"Default",
    Twelve,     // see the relevant enum{} statement
    0,          // default to the first layout, wicki hayden
    0,          // default to using no scale (chromatic)
    0,          // default to the key of C
    0           // default to no transposition
  };
// ====== functions

  int positiveMod(int n, int d) {
    return (((n % d) + d) % d);
  }
  coordinates indexToCoord(byte x) {
    coordinates temp;
    temp.row = (x / 10);
    temp.col = (2 * (x % 10)) + (temp.row & 1);
    return temp;
  }
  bool hexOOB(coordinates c) {
    return (c.row < 0)
        || (c.row >= rowCount)
        || (c.col < 0)
        || (c.col >= (2 * colCount))
        || ((c.col + c.row) & 1);
  }
  byte coordToIndex(coordinates c) {
    if (hexOOB(c)) {
      return 255;
    } else {
      return (10 * c.row) + (c.col / 2);
    };
  }
  coordinates hexVector(byte direction, byte distance) {
    coordinates temp;
    int8_t vertical[]   = {0,-1,-1, 0, 1,1};
    int8_t horizontal[] = {2, 1,-1,-2,-1,1};
    temp.row = vertical[direction] * distance;
    temp.col = horizontal[direction] * distance;
    return temp;
  }
  coordinates hexOffset(coordinates a, coordinates b) {
    coordinates temp;
    temp.row = a.row + b.row;
    temp.col = a.col + b.col;
    return temp;
  }
  coordinates hexDistance(coordinates origin, coordinates destination) {
    coordinates temp;
    temp.row = destination.row - origin.row;
    temp.col = destination.col - origin.col;
    return temp;
  }
  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(concertA) + ((float)stepsFromA *
(float)current.tuning().stepSize / 100.0);
  }

// ====== diagnostic wrapper

  void sendToLog(String msg) {
    if (diagnostics) {
      Serial.println(msg);
    };
  }

// ====== LED routines

  int16_t transformHue(float D) {
    if ((!perceptual) || (D > 360.0)) {
      return 65536 * (D / 360.0);
    } else {
      //                red             yellow                 green
            blue
      int hueIn[] =  {    0,    9,   18,   90,  108,  126,  135,  150,
 198,  243,  252,  261,  306,  333,  360};
      //          #ff0000            #ffff00           #00ff00
#00ffff     #0000ff     #ff00ff
      int hueOut[] = {    0, 3640,
5461,10922,12743,16384,21845,27306,32768,38229,43690,49152,54613,58254,65535};
      byte B = 0;
      while (D - hueIn[B] > 0) {
        B++;
      };
      return hueOut[B - 1] + (hueOut[B] - hueOut[B - 1]) * ((D -
(float)hueIn[B - 1])/(float)(hueIn[B] - hueIn[B - 1]));
    };
  }
  void resetHexLEDs() { // calculate color codes for each hex, store
for playback
    int16_t hue;
    float hueDegrees;
    byte sat;
    colors c;
    for (byte i = 0; i < hexCount; i++) {
      if (!(h[i].isCmd)) {
        byte scaleDegree = positiveMod(h[i].steps +
current.key().offset - current.findC(),current.tuning().cycleLength);
        switch (colorMode) {
          case 1:
            c = keyOptions[current.keysBegin() + scaleDegree].tierColor;
            hueDegrees = hueCode[c];
            sat = satCode[c];
            break;
          default:
            hueDegrees = 360.0 * ((float)scaleDegree /
(float)current.tuning().cycleLength);
            sat = 255;
            break;
        };
        hue = transformHue(hueDegrees);
        h[i].LEDcolorPlay = strip.gamma32(strip.ColorHSV(hue,sat,VeryBRIGHT));
        h[i].LEDcolorOn = strip.gamma32(strip.ColorHSV(hue,sat,BRIGHT));
        h[i].LEDcolorOff = strip.gamma32(strip.ColorHSV(hue,sat,DIM));
        h[i].LEDcolorAnim = strip.ColorHSV(hue,0,255);
      } else {
        //
      };
    };
  }

// ====== layout routines

  void assignPitches() {     // run this if the layout, key, or
transposition changes, but not if color or scale changes
    sendToLog("assignPitch was called:");
    for (byte i = 0; i < hexCount; i++) {
      if (!(h[i].isCmd)) {
        float N = stepsToMIDI(h[i].steps + current.key().offset +
current.transpose);
        if (N < 0 || N >= 128) {
          h[i].note = 255;
          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) / defaultPBRange);
          h[i].frequency = MIDItoFreq(N);
        };
        sendToLog(String(
          "hex #" + String(i) + ", " +
          "steps=" + String(h[i].steps) + ", " +
          "isCmd? " + String(h[i].isCmd) + ", " +
          "note=" + String(h[i].note) + ", " +
          "bend=" + String(h[i].bend) + ", " +
          "freq=" + String(h[i].frequency) + ", " +
          "inScale? " + String(h[i].inScale) + "."
        ));
      };
    };
    sendToLog("assignPitches complete.");
  }
  void applyScale() {
    sendToLog("applyScale was called:");
    for (byte i = 0; i < hexCount; i++) {
      if (!(h[i].isCmd)) {
        byte degree = positiveMod(h[i].steps, current.tuning().cycleLength);
        byte whichByte = degree / 8;
        byte bitShift = 7 - (degree - (whichByte << 3));
        byte digitMask = 1 << bitShift;
        h[i].inScale = (current.scale().step[whichByte] & digitMask)
>> bitShift;
        sendToLog(String(
          "hex #" + String(i) + ", " +
          "steps=" + String(h[i].steps) + ", " +
          "isCmd? " + String(h[i].isCmd) + ", " +
          "note=" + String(h[i].note) + ", " +
          "inScale? " + String(h[i].inScale) + "."
        ));
      };
    };
    resetHexLEDs();
    sendToLog("applyScale complete.");
  }
  void applyLayout() {       // call this function when the layout changes
    sendToLog("buildLayout was called:");
    for (byte i = 0; i < hexCount; i++) {
      if (!(h[i].isCmd)) {
        coordinates dist =
hexDistance(h[current.layout().rootHex].coords, h[i].coords);
        h[i].steps = (
          (dist.col * current.layout().acrossSteps) +
          (dist.row * (
            current.layout().acrossSteps +
            (2 * current.layout().dnLeftSteps)
          ))
        ) / 2;
        sendToLog(String(
          "hex #" + String(i) + ", " +
          "steps=" + String(h[i].steps) + "."
        ));
      };
    };
    applyScale();        // when layout changes, have to re-apply
scale and re-apply LEDs
    assignPitches();     // same with pitches
    u8g2.setDisplayRotation(current.layout().isPortrait ? U8G2_R2 :
U8G2_R1);     // and landscape / portrait rotation
    sendToLog("buildLayout complete.");
  }
// ====== buzzer routines

  byte nextHeldNote() {
    byte n = 255;
    for (byte i = 1; i < hexCount; i++) {
      byte checkNote = positiveMod(currentBuzzNote + i, hexCount);
      if ((h[checkNote].channel) && (!h[checkNote].isCmd)) {
        n = checkNote;
        break;
      };
    };
    return n;
  }
  void buzz(byte x) {        // send 128 or larger to turn off tone
    currentBuzzNote = x;
    if ((!(h[x].isCmd)) && (h[x].note < 128) && (h[x].frequency < 32767)) {
      //piezoBuzzer.tone(h[x].frequency, (float)velWheel.curValue *
(100.0 / 128.0), 16384, TIME_MS);
      tone(tonePin, h[x].frequency);     // stock TONE library, but
frequency changed to float
    } else {
      //piezoBuzzer.stop_tone();
      noTone(tonePin);                     // stock TONE library
    };
  }
// ====== MIDI routines

  void setPitchBendRange(byte Ch, byte semitones) {
    MIDI.beginRpn(0, Ch);
    MIDI.sendRpnValue(semitones << 7, Ch);
    MIDI.endRpn(Ch);
    sendToLog(String(
      "set pitch bend range on ch " +
      String(Ch) + " to be " + String(semitones) + " semitones"
    ));
  }
  void setMPEzone(byte masterCh, byte sizeOfZone) {
    MIDI.beginRpn(6, masterCh);
    MIDI.sendRpnValue(sizeOfZone << 7, masterCh);
    MIDI.endRpn(masterCh);
    sendToLog(String(
      "tried sending MIDI msg to set MPE zone, master ch " +
      String(masterCh) + ", zone of this size: " + String(sizeOfZone)
    ));
  }
  void prepMIDIforMicrotones() {
    bool makeZone = (MPE && (current.tuningIndex != Twelve)); // if
MPE flag is on and tuning <> 12EDO
    setMPEzone(1, (8 * makeZone));   // MPE zone 1 = ch 2 thru 9 (or
reset if not using MPE)
    delay(ccMsgCoolDown);
    setMPEzone(16, (5 * makeZone));  // MPE zone 16 = ch 11 thru 15
(or reset if not using MPE)
    delay(ccMsgCoolDown);
    for (byte i = 1; i <= 16; i++) {
      setPitchBendRange(i, defaultPBRange);  // some synths try to set
PB range to 48 semitones.
      delay(ccMsgCoolDown);                  // this forces it back to
the expected range of 2 semitones.
      if ((i != 10) && ((!makeZone) || ((i > 1) && (i < 16)))) {
        openChannelQueue.push(i);
        sendToLog(String("pushed ch " + String(i) + " to the open
channel queue"));
      };
      channelBend[i - 1] = 0;
      channelPoly[i - 1] = 0;
    };
  }
  void chgModulation() {
  if (current.tuningIndex == Twelve) {
      MIDI.sendControlChange(1, modWheel.curValue, 1);
      sendToLog(String("sent mod value " + String(modWheel.curValue) +
" to ch 1"));
    } else if (MPE) {
      MIDI.sendControlChange(1, modWheel.curValue, 1);
      sendToLog(String("sent mod value " + String(modWheel.curValue) +
" to ch 1"));
      MIDI.sendControlChange(1, modWheel.curValue, 16);
      sendToLog(String("sent mod value " + String(modWheel.curValue) +
" to ch 16"));
    } else {
      for (byte i = 0; i < 16; i++) {
        MIDI.sendControlChange(1, modWheel.curValue, i + 1);
        sendToLog(String("sent mod value " + String(modWheel.curValue)
+ " to ch " + String(i+1)));
      };
    };
  };
  void chgUniversalPB() {
    if (current.tuningIndex == Twelve) {
      MIDI.sendPitchBend(pbWheel.curValue, 1);
      sendToLog(String("sent pb value " + String(pbWheel.curValue) + "
to ch 1"));
    } else if (MPE) {
      MIDI.sendPitchBend(pbWheel.curValue, 1);
      sendToLog(String("sent pb value " + String(pbWheel.curValue) + "
to ch 1"));
      MIDI.sendPitchBend(pbWheel.curValue, 16);
      sendToLog(String("sent pb value " + String(pbWheel.curValue) + "
to ch 16"));
    } else {
      for (byte i = 0; i < 16; i++) {
        MIDI.sendPitchBend(channelBend[i] + pbWheel.curValue, i + 1);
        sendToLog(String("sent pb value " + String(channelBend[i] +
pbWheel.curValue) + " to ch " + String(i+1)));
      };
    };
  }

  byte assignChannel(byte x) {
    if (current.tuningIndex == Twelve) {
      return 1;
    } else {
      byte temp = 17;
      for (byte c = MPE; c < (16 - MPE); c++) {  // MPE - look at ch 2
thru 15 [c 1-14]; otherwise ch 1 thru 16 [c 0-15]
        if ((c + 1 != 10) && (h[x].bend == channelBend[c])) {  // not
using drum channel ch 10 in either case
          temp = c + 1;
          sendToLog(String("found a matching channel: ch " +
String(temp) + " has pitch bend " + String(channelBend[c])));
          break;
        };
      };
      if (temp = 17) {
        if (openChannelQueue.empty()) {
          sendToLog(String("channel queue was empty so we didn't send
a note on"));
        } else {
          temp = openChannelQueue.front();
          openChannelQueue.pop();
          sendToLog(String("popped " + String(temp) + " off the queue"));
        };
      };
      return temp;
    };
  }

// ====== hex press routines

  void noteOn(byte x) {
    byte c = assignChannel(x);
    if (c <= 16) {
      h[x].channel = c;       // value is 1 - 16
      if (current.tuningIndex != Twelve) {
        MIDI.sendPitchBend(h[x].bend, c); // ch 1-16
      };
      MIDI.sendNoteOn(h[x].note, velWheel.curValue, c); // ch 1-16
      sendToLog(String(
        "sent note on: " + String(h[x].note) +
        " pb " + String(h[x].bend) +
        " vel " + String(velWheel.curValue) +
        " ch " + String(c)
      ));
      if (current.tuningIndex != Twelve) {
        channelPoly[c - 1]++;   // array is 0 - 15
      };
      if (buzzer) {
        buzz(x);
      };
    };
  }
  void noteOff(byte x) {
    byte c = h[x].channel;
    if (c) {
      h[x].channel = 0;
      MIDI.sendNoteOff(h[x].note, velWheel.curValue, c);
      sendToLog(String(
        "sent note off: " + String(h[x].note) +
        " pb " + String(h[x].bend) +
        " vel " + String(velWheel.curValue) +
        " ch " + String(c)
      ));
      if (current.tuningIndex != Twelve) {
        switch (channelPoly[c - 1]) {
          case 1:
            channelPoly[c - 1]--;
            openChannelQueue.push(c);
            break;
          case 0:
            break;
          default:
            channelPoly[c - 1]--;
            break;
        };
      };
      if (buzzer) {
        buzz(nextHeldNote());
      };
    };
  }
  void cmdOn(byte x) {   // volume and mod wheel read all current buttons
    switch (h[x].note) {
      case cmdCode + 3:
        toggleWheel = !toggleWheel;
        // recolorHex(x);
        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.
    // nothing; should all be taken care of within the wheelDef structure
  }

// ====== animations

  void flagToAnimate(coordinates C) {
    if (!hexOOB(C)) {
      h[coordToIndex(C)].animate = 1;
    };
  }
  void animateMirror() {
    for (byte i = 0; i < hexCount; i++) {                   // check every hex
      if ((!(h[i].isCmd)) && (h[i].channel)) {              // that is
a held note
        for (byte j = 0; j < hexCount; j++) {               // compare
to every hex
          if ((!(h[j].isCmd)) && (!(h[j].channel))) {       // that is
a note not being played
            int16_t temp = h[i].steps - h[j].steps;         // look at
difference between notes
            if (animationType == OctaveAnim) {              // 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 < hexCount; i++) {
 // check every hex
      if ((!(h[i].isCmd)) && (h[i].channel)) {
 // that is a held note
        flagToAnimate(hexOffset(h[i].coords,hexVector((h[i].animFrame()
% 6),1)));       // different neighbor each frame
      };
    };
  }
  void animateRadial() {
    for (byte i = 0; i < hexCount; i++) {
 // check every hex
      if (!(h[i].isCmd)) {
  // that is a note
        uint32_t radius = h[i].animFrame();
        if ((radius > 0) && (radius < 16)) {
   // played in the last 16 frames
          byte steps = ((animationType == SplashAnim) ? radius : 1);
 // star = 1 step to next corner; ring = 1 step per hex
          coordinates temp =
hexOffset(h[i].coords,hexVector(DnLeft,radius));  // start at one
corner of the ring
          for (byte dir = 0; 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(temp);
  // flag for animation
              temp = hexOffset(temp, hexVector(dir,radius / steps));
 // then next step
            };
          };
        };
      };
    };
  }

// ====== menu variables and routines

  // must declare these variables globally for some reason
  // doing so down here so we don't have to forward declare callback functions
  //
  SelectOptionByte optionByteYesOrNo[] = { { "No"     , 0 },
                                           { "Yes"    , 1 } };
  SelectOptionByte optionByteBuzzer[] =  { { "Off"    , 0 },
                                           { "Mono"   , 1 },
                                           { "Arp'gio", 2 } };
  SelectOptionByte optionByteColor[] =   { { "Rainbow", 0 },
                                           { "Tiered" , 1 } };
  SelectOptionByte optionByteAnimate[] = { { "None"   , NoAnim },
                                           { "Octave" , OctaveAnim},
                                           { "By Note", NoteAnim},
                                           { "Star"   , StarAnim},
                                           { "Splash" , SplashAnim},
                                           { "Orbit"  , OrbitAnim} };

  GEMSelect selectYesOrNo(sizeof(optionByteYesOrNo) /
sizeof(SelectOptionByte), optionByteYesOrNo);
  GEMSelect selectBuzzer( sizeof(optionByteBuzzer)  /
sizeof(SelectOptionByte), optionByteBuzzer);
  GEMSelect selectColor(  sizeof(optionByteColor)   /
sizeof(SelectOptionByte), optionByteColor);
  GEMSelect selectAnimate(sizeof(optionByteAnimate) /
sizeof(SelectOptionByte), optionByteAnimate);

  GEMPage  menuPageMain("HexBoard MIDI Controller");

  GEMPage  menuPageTuning("Tuning");
  GEMItem  menuGotoTuning("Tuning", menuPageTuning);
  GEMItem* menuItemTuning[tuningCount]; // dynamically generate item
based on tunings

  GEMPage  menuPageLayout("Layout");
  GEMItem  menuGotoLayout("Layout", menuPageLayout);
  GEMItem* menuItemLayout[layoutCount]; // dynamically generate item
based on presets

  GEMPage  menuPageScales("Scales");
  GEMItem  menuGotoScales("Scales", menuPageScales);
  GEMItem* menuItemScales[scaleCount];  // dynamically generate item
based on presets and if allowed in given EDO tuning

  GEMPage  menuPageKeys("Keys");
  GEMItem  menuGotoKeys("Keys",     menuPageKeys);
  GEMItem* menuItemKeys[keyCount];   // dynamically generate item
based on presets

  GEMItem  menuItemScaleLock( "Scale lock?",   scaleLock,     selectYesOrNo);
  GEMItem  menuItemMPE(       "MPE Mode:",     MPE,
selectYesOrNo, prepMIDIforMicrotones);
  GEMItem  menuItemBuzzer(    "Buzzer:",       buzzer,        selectBuzzer);
  GEMItem  menuItemColor(     "Color mode:",   colorMode,
selectColor,   resetHexLEDs);
  GEMItem  menuItemPercep(    "Adjust color:", perceptual,
selectYesOrNo, resetHexLEDs);
  GEMItem  menuItemAnimate(   "Animation:",    animationType, selectAnimate);

  void menuHome() {
    menu.setMenuPageCurrent(menuPageMain);
    menu.drawMenu();
  }
  void showOnlyValidLayoutChoices() { // re-run at setup and whenever
tuning changes
    for (byte L = 0; L < layoutCount; L++) {
      menuItemLayout[L]->hide((layoutOptions[L].tuning != current.tuningIndex));
    };
    sendToLog(String("menu: Layout choices were updated."));
  }
  void showOnlyValidScaleChoices() { // re-run at setup and whenever
tuning changes
    for (int S = 0; S < scaleCount; S++) {
      menuItemScales[S]->hide((scaleOptions[S].tuning !=
current.tuningIndex) && (scaleOptions[S].tuning != 255));
    };
    sendToLog(String("menu: Scale choices were updated."));
  }
  void showOnlyValidKeyChoices() { // re-run at setup and whenever
tuning changes
    for (int K = 0; K < keyCount; K++) {
      menuItemKeys[K]->hide((keyOptions[K].tuning != current.tuningIndex));
    };
    sendToLog(String("menu: Key choices were updated."));
  }
  void changeLayout(GEMCallbackData callbackData) {  // when you
change the layout via the menu
    byte selection = callbackData.valByte;
    if (selection != current.layoutIndex) {
      current.layoutIndex = selection;
      applyLayout();
    };
    menuHome();
  }
  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();
  }
  void changeKey(GEMCallbackData callbackData) {     // when you
change the key via the menu
    int selection = callbackData.valInt;
    if (selection != current.keyIndex) {
      current.keyIndex = selection;
      assignPitches();
    };
    menuHome();
  }
  void changeTuning(GEMCallbackData callbackData) { // not working yet
    byte selection = callbackData.valByte;
    if (selection != current.tuningIndex) {
      current.tuningIndex = selection;
      current.layoutIndex = current.layoutsBegin();
      current.scaleIndex = 0;
      current.keyIndex = current.keysBegin();
      showOnlyValidLayoutChoices();
      showOnlyValidScaleChoices();
      showOnlyValidKeyChoices();
      applyLayout();
      prepMIDIforMicrotones();
    };
    menuHome();
  }
  void buildMenu() {
    menuPageMain.addMenuItem(menuGotoTuning);
    for (byte T = 0; T < tuningCount; T++) { // create pointers to all
tuning choices
      menuItemTuning[T] = new GEMItem(tuningOptions[T].name, changeTuning, T);
      menuPageTuning.addMenuItem(*menuItemTuning[T]);
    };

    menuPageMain.addMenuItem(menuGotoLayout);
    for (byte L = 0; L < layoutCount; L++) { // create pointers to all layouts
      menuItemLayout[L] = new GEMItem(layoutOptions[L].name, changeLayout, L);
      menuPageLayout.addMenuItem(*menuItemLayout[L]);
    };
    showOnlyValidLayoutChoices();

    menuPageMain.addMenuItem(menuGotoScales);
    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, changeScale, S);
      menuPageScales.addMenuItem(*menuItemScales[S]);
    };
    showOnlyValidScaleChoices();

    menuPageMain.addMenuItem(menuGotoKeys);
    for (int K = 0; K < keyCount; K++) {
      menuItemKeys[K] = new GEMItem(keyOptions[K].name, changeKey, K);
      menuPageKeys.addMenuItem(*menuItemKeys[K]);
    };

    showOnlyValidKeyChoices();

    menuPageMain.addMenuItem(menuItemScaleLock);
    menuPageMain.addMenuItem(menuItemColor);
    menuPageMain.addMenuItem(menuItemBuzzer);
    menuPageMain.addMenuItem(menuItemAnimate);

    menuPageMain.addMenuItem(menuItemMPE);
    menuPageMain.addMenuItem(menuItemPercep);

  }

// ====== setup routines

  void setupMIDI() {
    usb_midi.setStringDescriptor("HexBoard MIDI");  // Initialize
MIDI, and listen to all MIDI channels
    MIDI.begin(MIDI_CHANNEL_OMNI);                  // This will also
call usb_midi's begin()
  }
  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()) {
      Serial.println("An Error has occurred while mounting LittleFS");
    }
  }
  void setupPins() {
    for (byte p = 0; p < sizeof(columnPins); p++)  // For each column pin...
    {
      pinMode(columnPins[p], INPUT_PULLUP);  // set the pinMode to
INPUT_PULLUP (+3.3V / HIGH).
    }
    for (byte p = 0; p < sizeof(multiplexPins); p++)  // For each column pin...
    {
      pinMode(multiplexPins[p], OUTPUT);  // Setting the row
multiplexer pins to output.
    }
    Wire.setSDA(lightPinSDA);
    Wire.setSCL(lightPinSCL);
    pinMode(rotaryPinC, INPUT_PULLUP);
  }
  void setupGrid() {
    sendToLog(String("initializing hex grid..."));
    for (byte i = 0; i < hexCount; i++) {
      h[i].coords = indexToCoord(i);
      h[i].isCmd = 0;
      h[i].note = 255;
      h[i].keyState = 0;
    };
    for (byte c = 0; c < cmdCount; c++) {
      h[assignCmd[c]].isCmd = 1;
      h[assignCmd[c]].note = cmdCode + c;
    };
    applyLayout();
  }
  void setupLEDs() {  // need layout
    strip.begin();    // INITIALIZE NeoPixel strip object
    strip.show();     // Turn OFF all pixels ASAP
    resetHexLEDs();
  }
  void setupMenu() {  // need menu
    menu.setSplashDelay(0);
    menu.init();
    buildMenu();
    menuHome();
  }
  void setupGFX() {
    u8g2.begin();                       // Menu and graphics setup
    u8g2.setBusClock(1000000);          // Speed up display
    u8g2.setContrast(defaultContrast);  // Set contrast
  }
  void testDiagnostics() {
    sendToLog(String("theHDM was here"));
  }

// ====== loop routines

  void timeTracker() {
    lapTime = runTime - loopTime;
    // sendToLog(String(lapTime));  // Print out the time it takes to
run each loop
    loopTime = runTime;  // Update previousTime variable to give us a
reference point for next loop
    runTime = millis();   // Store the current time in a uniform
variable for this program loop
  }
  void screenSaver() {
    if (screenTime <= screenSaverMillis) {
      screenTime = screenTime + lapTime;
      if (screenSaverOn) {
        screenSaverOn = 0;
        u8g2.setContrast(defaultContrast);
      }
    } else {
      if (!screenSaverOn) {
        screenSaverOn = 1;
        u8g2.setContrast(1);
      }
    }
  }
  void readHexes() {
    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(multiplexPins[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 = columnPins[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).
        delayMicroseconds(10);                // Delay to give the pin
modes time to change state (false readings are caused otherwise).
        bool didYouPressHex = (digitalRead(p) == LOW);  // hex is
pressed if it returns LOW. else not pressed
        h[c + (r * colCount)].updateKeyState(didYouPressHex);
        pinMode(p, INPUT);  // Set the selected column pin back to
INPUT mode (0V / LOW).
       }
    }
  }
  void actionHexes() {
    for (byte i = 0; i < hexCount; i++) {   // For all buttons in the deck
      switch (h[i].keyState) {
        case 1: // just pressed
          if (h[i].isCmd) {
            cmdOn(i);
          } else if (h[i].inScale || (!scaleLock)) {
            noteOn(i);
          };
          break;
        case 2: // just released
          if (h[i].isCmd) {
            cmdOff(i);
          } else if (h[i].inScale || (!scaleLock)) {
            noteOff(i);
          };
          break;
        case 3: // held
          break;
        default: // inactive
          break;
      };
    };
  }
  void arpeggiate() {
    if (buzzer > 1) {
      if (runTime - currentBuzzTime > arpeggiateLength) {
        currentBuzzTime = millis();
        byte nextNoteToBuzz = nextHeldNote();
        if (nextNoteToBuzz < cmdCode) {
          buzz(nextNoteToBuzz);
        };
      };
    };
  }
  void updateWheels() {
    velWheel.setTargetValue();
    bool upd = velWheel.updateValue(); // this function returns a
boolean, gotta put it somewhere even if it isn't being used
    if (upd) {
      sendToLog(String("vel became " + String(velWheel.curValue)));
    }
    if (toggleWheel) {
      pbWheel.setTargetValue();
      upd = pbWheel.updateValue();
      if (upd) {
        chgUniversalPB();
      };
    } else {
      modWheel.setTargetValue();
      upd = modWheel.updateValue();
      if (upd) {
        chgModulation();
      };
    };
  }
  void animateLEDs() {    // TBD
    for (byte i = 0; i < hexCount; i++) {
      h[i].animate = 0;
    };
    if (animationType) {
      switch (animationType) {
        case StarAnim: case SplashAnim:
          animateRadial();
          break;
        case OrbitAnim:
          animateOrbit();
          break;
        case OctaveAnim: case NoteAnim:
          animateMirror();
          break;
        default:
          break;
      };
    };
  }
  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;
  }
  void lightUpLEDs() {
    for (byte i = 0; i < hexCount; i++) {
      if (!(h[i].isCmd)) {
        if (h[i].animate) {
          strip.setPixelColor(i,h[i].LEDcolorAnim);
        } else if (h[i].channel) {
          strip.setPixelColor(i,h[i].LEDcolorPlay);
        } else if (h[i].inScale) {
          strip.setPixelColor(i,h[i].LEDcolorOn);
        } else {
          strip.setPixelColor(i,h[i].LEDcolorOff);
        };
      };
    };
    int16_t hueV = transformHue((runTime / rainbowDegreeTime) % 360);
    strip.setPixelColor(assignCmd[0],strip.gamma32(strip.ColorHSV(
      hueV,192,byteLerp(0,255,85,127,velWheel.curValue)
    )));
    strip.setPixelColor(assignCmd[1],strip.gamma32(strip.ColorHSV(
      hueV,192,byteLerp(0,255,42,85,velWheel.curValue)
    )));
    strip.setPixelColor(assignCmd[2],strip.gamma32(strip.ColorHSV(
      hueV,192,byteLerp(0,255,0,42,velWheel.curValue)
    )));
    if (toggleWheel) {
      // pb red / green
      int16_t hueP = transformHue((pbWheel.curValue > 0) ? 0 : 180);
      byte satP = byteLerp(0,255,0,8192,abs(pbWheel.curValue));
      strip.setPixelColor(assignCmd[3],strip.gamma32(strip.ColorHSV(
        0,0,64
      )));
      strip.setPixelColor(assignCmd[4],strip.gamma32(strip.ColorHSV(
        transformHue(0),satP * (pbWheel.curValue > 0),satP *
(pbWheel.curValue > 0)
      )));
      strip.setPixelColor(assignCmd[5],strip.gamma32(strip.ColorHSV(
        hueP,satP,255
      )));
      strip.setPixelColor(assignCmd[6],strip.gamma32(strip.ColorHSV(
        transformHue(180),satP * (pbWheel.curValue < 0),satP *
(pbWheel.curValue < 0)
      )));
    } else {
      // mod blue / yellow
      int16_t hueM = transformHue((modWheel.curValue > 63) ? 90 : 270);
      byte satM = byteLerp(0,255,0,64,abs(modWheel.curValue - 63));
      strip.setPixelColor(assignCmd[3],strip.gamma32(strip.ColorHSV(0,0,128)));
      strip.setPixelColor(assignCmd[4],strip.gamma32(strip.ColorHSV(
        hueM,satM,((modWheel.curValue > 63) ? satM : 0)
      )));
      strip.setPixelColor(assignCmd[5],strip.gamma32(strip.ColorHSV(
        hueM,satM,((modWheel.curValue > 63) ? 127 + (satM / 2) : 127 -
(satM / 2))
      )));
      strip.setPixelColor(assignCmd[6],strip.gamma32(strip.ColorHSV(
        hueM,satM,127 + (satM / 2)
      )));
    };
    strip.show();
  }
  void dealWithRotary() {
    if (menu.readyForKey()) {
      rotaryIsClicked = digitalRead(rotaryPinC);
      if (rotaryIsClicked > rotaryWasClicked) {
        menu.registerKeyPress(GEM_KEY_OK);
        screenTime = 0;
      }
      rotaryWasClicked = rotaryIsClicked;
      if (rotaryKnobTurns != 0) {
        for (byte i = 0; i < abs(rotaryKnobTurns); i++) {
          menu.registerKeyPress(rotaryKnobTurns < 0 ? GEM_KEY_UP :
GEM_KEY_DOWN);
        }
        rotaryKnobTurns = 0;
        screenTime = 0;
      }
    }
  }
  void readMIDI() {
    MIDI.read();
  }
  void keepTrackOfRotaryKnobTurns() {
    switch (rotary.process()) {
      case DIR_CW:
        rotaryKnobTurns++;
        break;
      case DIR_CCW:
        rotaryKnobTurns--;
        break;
    }
  }

// ====== setup() and loop()

  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();
    setupPins();
    testDiagnostics();  // Print diagnostic troubleshooting
information to serial monitor
    setupGrid();
    setupLEDs();
    setupGFX();
    setupMenu();
    for (byte i = 0; i < 5 && !TinyUSBDevice.mounted(); i++) {
      delay(1);  // wait until device mounted, maybe
    };
  }
  void setup1() {
    //
  };
  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
    actionHexes();       // actions on hexes
    arpeggiate();      // arpeggiate the buzzer
    updateWheels();   // deal with the pitch/mod wheel
    animateLEDs();     // deal with animations
    lightUpLEDs();      // refresh LEDs
    dealWithRotary();  // deal with menu
    readMIDI();
  }
  void loop1() {  // run on second core
    keepTrackOfRotaryKnobTurns();
  }
Details
Message ID
<D09ZJW88A3XV.2BKMDDR7P77V0@librem.one>
In-Reply-To
<CAJrt5V0saU_pr725i7pXtvv+9sid+EmYk2-5EVRgVHjAERMtFQ@mail.gmail.com> (view parent)
DKIM signature
pass
Download raw message
Thanks!  
I pushed this code to the hexperiment branch

https://git.sr.ht/~earboxer/HexBoard/tree/hexperiment

(had to fix some issues from the email -- some lines were breaking early, and there were some apostrophes where they shouldn't be... maybe sending it as an attachment instead of as the body would work better next time)

It's compiled at https://builds.sr.ht/~earboxer/job/1185290

I haven't tested it (my production hexboard is currently tied up working as a QMK keyboard on a computer with almost dayly use)

@Nicholas, if you want to say "I consent to license my hexperiment code under the GPLv3" (or some more permissive license),
(it's required by the Rotary library for the binary to be under that license).
Otherwise, the binary compiled at the link above could be illegal ;)

-Zach
Reply to thread Export thread (mbox)