From 763712de00ef88b95c7b4f308b96caebd705d50a Mon Sep 17 00:00:00 2001 From: Zach DeCook Date: Fri, 5 Apr 2024 21:52:13 -0400 Subject: Hexperiment: rename file --- HexBoard_V1.1.ino | 1648 ----------------------------------------------------- Hexperiment.ino | 1648 +++++++++++++++++++++++++++++++++++++++++++++++++++++ Makefile | 4 +- README.md | 7 + 4 files changed, 1657 insertions(+), 1650 deletions(-) delete mode 100644 HexBoard_V1.1.ino create mode 100644 Hexperiment.ino diff --git a/HexBoard_V1.1.ino b/HexBoard_V1.1.ino deleted file mode 100644 index d75c2f7..0000000 --- a/HexBoard_V1.1.ino +++ /dev/null @@ -1,1648 +0,0 @@ -// ====== 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 - #include - #include - #include // 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 - #include - 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 openChannelQueue; - const byte defaultPBRange = 2; - -// ====== initialize LEDs - - #include - 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 - 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 - #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 = 10000; // - -// ====== 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, 0b01010000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, - { (char*)"Minor, natural", Twelve, { 0b10110101, 0b10100000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, - { (char*)"Minor, melodic", Twelve, { 0b10110101, 0b01010000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, - { (char*)"Minor, harmonic", Twelve, { 0b10110101, 0b10010000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, - { (char*)"Pentatonic, major", Twelve, { 0b10101001, 0b01000000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, - { (char*)"Pentatonic, minor", Twelve, { 0b10010101, 0b00100000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, - { (char*)"Blues", Twelve, { 0b10010111, 0b00100000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, - { (char*)"Double Harmonic", Twelve, { 0b11001101, 0b10010000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, - { (char*)"Phrygian", Twelve, { 0b11010101, 0b10100000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, - { (char*)"Phrygian Dominant", Twelve, { 0b11001101, 0b10100000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, - { (char*)"Dorian", Twelve, { 0b10110101, 0b01100000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, - { (char*)"Lydian", Twelve, { 0b10101011, 0b01010000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, - { (char*)"Lydian Dominant", Twelve, { 0b10101011, 0b01100000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, - { (char*)"Mixolydian", Twelve, { 0b10101101, 0b01100000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, - { (char*)"Locrian", Twelve, { 0b11010110, 0b10100000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, - { (char*)"Whole tone", Twelve, { 0b10101010, 0b10100000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, - { (char*)"Octatonic", Twelve, { 0b10110110, 0b11010000, 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, 0b10000000, 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, -34, B }, - { (char*)" >C# / D / Eb", FiftyThree, -25, B }, - { (char*)" >D# / Fb) ^E", FiftyThree, -21, c }, - { (char*)" E", FiftyThree, -20, G }, - { (char*)" vF (F / Gb", FiftyThree, -12, B }, - { (char*)" >F# / G / Ab", FiftyThree, -3, B }, - { (char*)" >G# / A", FiftyThree, 2, l }, - { (char*)" vBb / Bb / ^A#", FiftyThree, 6, B }, - { (char*)" A#", FiftyThree, 7, g }, - { (char*)" Cb / vB", FiftyThree, 8, y }, - { (char*)"(^Cb) B", FiftyThree, 9, W }, - { (char*)"(>Cb) ^B", FiftyThree, 10, c }, - { (char*)" B", FiftyThree, 11, G }, - { (char*)" vC (= 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(); - } diff --git a/Hexperiment.ino b/Hexperiment.ino new file mode 100644 index 0000000..d75c2f7 --- /dev/null +++ b/Hexperiment.ino @@ -0,0 +1,1648 @@ +// ====== 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 + #include + #include + #include // 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 + #include + 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 openChannelQueue; + const byte defaultPBRange = 2; + +// ====== initialize LEDs + + #include + 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 + 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 + #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 = 10000; // + +// ====== 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, 0b01010000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Minor, natural", Twelve, { 0b10110101, 0b10100000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Minor, melodic", Twelve, { 0b10110101, 0b01010000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Minor, harmonic", Twelve, { 0b10110101, 0b10010000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Pentatonic, major", Twelve, { 0b10101001, 0b01000000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Pentatonic, minor", Twelve, { 0b10010101, 0b00100000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Blues", Twelve, { 0b10010111, 0b00100000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Double Harmonic", Twelve, { 0b11001101, 0b10010000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Phrygian", Twelve, { 0b11010101, 0b10100000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Phrygian Dominant", Twelve, { 0b11001101, 0b10100000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Dorian", Twelve, { 0b10110101, 0b01100000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Lydian", Twelve, { 0b10101011, 0b01010000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Lydian Dominant", Twelve, { 0b10101011, 0b01100000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Mixolydian", Twelve, { 0b10101101, 0b01100000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Locrian", Twelve, { 0b11010110, 0b10100000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Whole tone", Twelve, { 0b10101010, 0b10100000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Octatonic", Twelve, { 0b10110110, 0b11010000, 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, 0b10000000, 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, -34, B }, + { (char*)" >C# / D / Eb", FiftyThree, -25, B }, + { (char*)" >D# / Fb) ^E", FiftyThree, -21, c }, + { (char*)" E", FiftyThree, -20, G }, + { (char*)" vF (F / Gb", FiftyThree, -12, B }, + { (char*)" >F# / G / Ab", FiftyThree, -3, B }, + { (char*)" >G# / A", FiftyThree, 2, l }, + { (char*)" vBb / Bb / ^A#", FiftyThree, 6, B }, + { (char*)" A#", FiftyThree, 7, g }, + { (char*)" Cb / vB", FiftyThree, 8, y }, + { (char*)"(^Cb) B", FiftyThree, 9, W }, + { (char*)"(>Cb) ^B", FiftyThree, 10, c }, + { (char*)" B", FiftyThree, 11, G }, + { (char*)" vC (= 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(); + } diff --git a/Makefile b/Makefile index 6d377a9..9f7c1d3 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ # Copied fqbn from build.options.json build/build.ino.uf2: build/build.ino arduino-cli compile -b rp2040:rp2040:generic:flash=16777216_0,freq=133,opt=Small,rtti=Disabled,stackprotect=Disabled,exceptions=Disabled,dbgport=Disabled,dbglvl=None,usbstack=tinyusb,boot2=boot2_generic_03h_2_padded_checksum --output-dir build build -build/build.ino: HexBoard_V1.1.ino - cp HexBoard_V1.1.ino build/build.ino +build/build.ino: Hexperiment.ino + cp Hexperiment.ino build/build.ino /run/media/*/RPI-RP2/INFO_UF2.TXT: echo "Mounting device" diff --git a/README.md b/README.md index 4e03947..0a8f80f 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ or [contact Jared](mailto:jared@shapingthesilence.com) if you're interested in a ## The Team * Jared DeCook has been writing music, developing hardware, and performing as [Shaping The Silence](https://shapingthesilence.com/) for over a decade. * Zach DeCook has been listening to music, breaking hardware, and occasionally writing software since the former discovered his exploitable talents. +* Nicholas Fox has been 'hexperimenting' with the firmware since before receiving a HexBoard in the mail. ## HexBoard "Arduino" firmware @@ -23,6 +24,12 @@ The files are in the format `VX_Y_Z-model0.uf2`, where * `X_Y_Z` is the version number * `0` is the model number ("2" for production model, for sale since 2023. "1" for the 2022 development model with the RP2040 processor board). +## Hexperiment firmware + +Created by Nicholas Fox, sometimes available at https://git.sr.ht/~earboxer/Hexperiment + +[![Sourcehut Build status](https://builds.sr.ht/~earboxer/Hexperiment/commits/.svg)](https://builds.sr.ht/~earboxer/Hexperiment/commits/?) + ### Building the firmware If you want to build the firmware, -- cgit 1.4.1