Firmware for HexBoard MIDI controller
| -rw-r--r-- | HexBoard_V1.1.ino | 3403 |
1 files changed, 1708 insertions, 1695 deletions
diff --git a/HexBoard_V1.1.ino b/HexBoard_V1.1.ino index 014f1c5..fb03d9a 100644 --- a/HexBoard_V1.1.ino +++ b/HexBoard_V1.1.ino @@ -1,1772 +1,1785 @@ -// Copyright 2022-2023 Jared DeCook and Zach DeCook -// Licensed under the GNU GPL Version 3. -// Hardware Information: -// Generic RP2040 running at 133MHz with 16MB of flash -// https://github.com/earlephilhower/arduino-pico -// (Additional boards manager URL: https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json) -// Tools > USB Stack > (Adafruit TinyUSB) -// Sketch > Export Compiled Binary -// -// Brilliant resource for dealing with hexagonal coordinates. https://www.redblobgames.com/grids/hexagons/ -// Used this to get my hexagonal animations sorted. http://ondras.github.io/rot.js/manual/#hex/indexing - -// Menu library documentation https://github.com/Spirik/GEM - -#include <Arduino.h> -#include <Adafruit_TinyUSB.h> -#include "LittleFS.h" -#include <MIDI.h> -#include <Adafruit_NeoPixel.h> -#define GEM_DISABLE_GLCD -#include <GEM_u8g2.h> -#include <Wire.h> -#include <Rotary.h> - -// Change before compile depending on target hardware -// 1 = HexBoard 1.0 (dev unit) -// 2 = HexBoard 1.1 (first retail unit) -#define ModelNumber 2 - -// USB MIDI object // -Adafruit_USBD_MIDI usb_midi; -// Create a new instance of the Arduino MIDI Library, -// and attach usb_midi as the transport. -MIDI_CREATE_INSTANCE(Adafruit_USBD_MIDI, usb_midi, MIDI); - -// LED SETUP // -#define LED_PIN 22 -#define LED_COUNT 140 -#if ModelNumber == 1 -Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_RGB + NEO_KHZ800); -int stripBrightness = 110; -int defaultBrightness = 70; -int dimBrightness = 20; -int pressedBrightness = 255; -#elif ModelNumber == 2 -Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800); -int stripBrightness = 130; -int defaultBrightness = 100; -int dimBrightness = 26; -int pressedBrightness = 255; -#endif - - -// ENCODER SETUP // -#define ROTA 20 // Rotary encoder A -#define ROTB 21 // Rotary encoder B -Rotary rotary = Rotary(ROTA, ROTB); -const int encoderClick = 24; -int encoderState = 0; -int encoderLastState = 1; -int8_t encoder_val = 0; -uint8_t encoder_state; - -// Create an instance of the U8g2 graphics library. -#if ModelNumber == 1 -U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R2, /* reset=*/U8X8_PIN_NONE); -#elif ModelNumber == 2 -U8G2_SH1107_SEEED_128X128_F_HW_I2C u8g2(U8G2_R2, /* reset=*/U8X8_PIN_NONE); -#endif -int screenBrightness = stripBrightness / 2; - -// Button matrix and LED locations, Dev Unit Only. -// Prod unit has matrix columns swapped and LEDs in a sane order (see ROW_FLIP macro). -// Portrait orientation top view: -// 9 8 7 6 5 4 3 2 1 -// 20 19 18 17 16 15 14 13 12 11 -// 29 28 27 26 25 24 23 22 21 -// 40 39 38 37 36 35 34 33 32 31 -// 49 48 47 46 45 44 43 42 41 -// 60 59 58 57 56 55 54 53 52 51 -// 10 69 68 67 66 65 64 63 62 61 -// 30 80 79 78 77 76 75 74 73 72 71 -// 50 89 88 87 86 85 84 83 82 81 -// 70 100 99 98 97 96 95 94 93 92 91 -// 90 109 108 107 106 105 104 103 102 101 -//110 120 119 118 117 116 115 114 113 112 111 -// 130 129 128 127 126 125 124 123 122 121 -// 140 139 138 137 136 135 134 133 132 131 - -// DIAGNOSTICS // -// 1 = Full button test (1 and 0) -// 2 = Button test (button number) -// 3 = MIDI output test -// 4 = Loop timing readout in milliseconds -int diagnostics = 0; - -// BUTTON MATRIX PINS // -#if ModelNumber == 1 -const byte columns[] = { 14, 15, 13, 12, 11, 10, 9, 8, 7, 6 }; // Column pins in order from right to left -#elif ModelNumber == 2 -const byte columns[] = { 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; // New board revision -#endif -const int m1p = 4; // Multiplexing chip control pins -const int m2p = 5; -const int m4p = 2; -const int m8p = 3; -// 16 & 17 reserved for lights. -const byte columnCount = sizeof(columns); // The number of columns in the matrix -const byte rowCount = 14; // The number of rows in the matrix -const byte elementCount = columnCount * rowCount; // The number of elements in the matrix - -// Since MIDI only uses 7 bits, we can give greater values special meanings. -// (see commandPress) -const byte EXTR_1 = 128; -const byte EXTR_2 = 129; -const byte EXTR_3 = 130; -const byte EXTR_4 = 131; -const byte EXTR_5 = 132; -// start CMDB in a range that won't interfere with layouts. -const byte CMDB_1 = 201; -const byte CMDB_2 = 202; -const byte CMDB_3 = 203; -const byte CMDB_4 = 204; -const byte CMDB_5 = 205; -const byte CMDB_6 = 206; -const byte CMDB_7 = 207; -const byte UNUSED = 255; - -// LED addresses for CMD buttons. (consequencely, also the button address too) -#if ModelNumber == 1 -const byte cmdBtn1 = 10 - 1; -const byte cmdBtn2 = 30 - 1; -const byte cmdBtn3 = 50 - 1; -const byte cmdBtn4 = 70 - 1; -const byte cmdBtn5 = 90 - 1; -const byte cmdBtn6 = 110 - 1; -const byte cmdBtn7 = 130 - 1; -#else -const byte cmdBtn1 = 0; -const byte cmdBtn2 = 20; -const byte cmdBtn3 = 40; -const byte cmdBtn4 = 60; -const byte cmdBtn5 = 80; -const byte cmdBtn6 = 100; -const byte cmdBtn7 = 120; -#endif - -// MIDI NOTE LAYOUTS // -#if ModelNumber == 1 -#define ROW_FLIP(x, ix, viii, vii, vi, v, iv, iii, ii, i) i, ii, iii, iv, v, vi, vii, viii, ix, x -//hacky macro because I (Jared) messed up the board layout - I'll do better next time! xD -#else -#define ROW_FLIP(i, ii, iii, iv, v, vi, vii, viii, ix, x) i, ii, iii, iv, v, vi, vii, viii, ix, x -//fixed it the second time around! -#endif - -// MIDI note layout tables - -// ./makeLayout.py 90 2 -7 -const byte wickiHaydenLayout[elementCount] = { - ROW_FLIP(CMDB_1, 90, 92, 94, 96, 98, 100, 102, 104, 106), - ROW_FLIP(83, 85, 87, 89, 91, 93, 95, 97, 99, 101), - ROW_FLIP(CMDB_2, 78, 80, 82, 84, 86, 88, 90, 92, 94), - ROW_FLIP(71, 73, 75, 77, 79, 81, 83, 85, 87, 89), - ROW_FLIP(CMDB_3, 66, 68, 70, 72, 74, 76, 78, 80, 82), - ROW_FLIP(59, 61, 63, 65, 67, 69, 71, 73, 75, 77), - ROW_FLIP(CMDB_4, 54, 56, 58, 60, 62, 64, 66, 68, 70), - ROW_FLIP(47, 49, 51, 53, 55, 57, 59, 61, 63, 65), - ROW_FLIP(CMDB_5, 42, 44, 46, 48, 50, 52, 54, 56, 58), - ROW_FLIP(35, 37, 39, 41, 43, 45, 47, 49, 51, 53), - ROW_FLIP(CMDB_6, 30, 32, 34, 36, 38, 40, 42, 44, 46), - ROW_FLIP(23, 25, 27, 29, 31, 33, 35, 37, 39, 41), - ROW_FLIP(CMDB_7, 18, 20, 22, 24, 26, 28, 30, 32, 34), - ROW_FLIP(11, 13, 15, 17, 19, 21, 23, 25, 27, 29) -}; -// ./makeLayout.py 95 -7 3 -const byte harmonicTableLayout[elementCount] = { - ROW_FLIP(CMDB_1, 95, 88, 81, 74, 67, 60, 53, 46, 39), - ROW_FLIP(98, 91, 84, 77, 70, 63, 56, 49, 42, 35), - ROW_FLIP(CMDB_2, 94, 87, 80, 73, 66, 59, 52, 45, 38), - ROW_FLIP(97, 90, 83, 76, 69, 62, 55, 48, 41, 34), - ROW_FLIP(CMDB_3, 93, 86, 79, 72, 65, 58, 51, 44, 37), - ROW_FLIP(96, 89, 82, 75, 68, 61, 54, 47, 40, 33), - ROW_FLIP(CMDB_4, 92, 85, 78, 71, 64, 57, 50, 43, 36), - ROW_FLIP(95, 88, 81, 74, 67, 60, 53, 46, 39, 32), - ROW_FLIP(CMDB_5, 91, 84, 77, 70, 63, 56, 49, 42, 35), - ROW_FLIP(94, 87, 80, 73, 66, 59, 52, 45, 38, 31), - ROW_FLIP(CMDB_6, 90, 83, 76, 69, 62, 55, 48, 41, 34), - ROW_FLIP(93, 86, 79, 72, 65, 58, 51, 44, 37, 30), - ROW_FLIP(CMDB_7, 89, 82, 75, 68, 61, 54, 47, 40, 33), - ROW_FLIP(92, 85, 78, 71, 64, 57, 50, 43, 36, 29) -}; -// ./makeLayout.py 86 -1 -3 -const byte gerhardLayout[elementCount] = { - ROW_FLIP(CMDB_1, 86, 85, 84, 83, 82, 81, 80, 79, 78), - ROW_FLIP(83, 82, 81, 80, 79, 78, 77, 76, 75, 74), - ROW_FLIP(CMDB_2, 79, 78, 77, 76, 75, 74, 73, 72, 71), - ROW_FLIP(76, 75, 74, 73, 72, 71, 70, 69, 68, 67), - ROW_FLIP(CMDB_3, 72, 71, 70, 69, 68, 67, 66, 65, 64), - ROW_FLIP(69, 68, 67, 66, 65, 64, 63, 62, 61, 60), - ROW_FLIP(CMDB_4, 65, 64, 63, 62, 61, 60, 59, 58, 57), - ROW_FLIP(62, 61, 60, 59, 58, 57, 56, 55, 54, 53), - ROW_FLIP(CMDB_5, 58, 57, 56, 55, 54, 53, 52, 51, 50), - ROW_FLIP(55, 54, 53, 52, 51, 50, 49, 48, 47, 46), - ROW_FLIP(CMDB_6, 51, 50, 49, 48, 47, 46, 45, 44, 43), - ROW_FLIP(48, 47, 46, 45, 44, 43, 42, 41, 40, 39), - ROW_FLIP(CMDB_7, 44, 43, 42, 41, 40, 39, 38, 37, 36), - ROW_FLIP(41, 40, 39, 38, 37, 36, 35, 34, 33, 32) -}; -// ./makeLayout.py 74 -1 -1 -const byte bosanquetWilsonLayout[elementCount] = { - ROW_FLIP(CMDB_1, 74, 73, 72, 71, 70, 69, 68, 67, 66), - ROW_FLIP(73, 72, 71, 70, 69, 68, 67, 66, 65, 64), - ROW_FLIP(CMDB_2, 71, 70, 69, 68, 67, 66, 65, 64, 63), - ROW_FLIP(70, 69, 68, 67, 66, 65, 64, 63, 62, 61), - ROW_FLIP(CMDB_3, 68, 67, 66, 65, 64, 63, 62, 61, 60), - ROW_FLIP(67, 66, 65, 64, 63, 62, 61, 60, 59, 58), - ROW_FLIP(CMDB_4, 65, 64, 63, 62, 61, 60, 59, 58, 57), - ROW_FLIP(64, 63, 62, 61, 60, 59, 58, 57, 56, 55), - ROW_FLIP(CMDB_5, 62, 61, 60, 59, 58, 57, 56, 55, 54), - ROW_FLIP(61, 60, 59, 58, 57, 56, 55, 54, 53, 52), - ROW_FLIP(CMDB_6, 59, 58, 57, 56, 55, 54, 53, 52, 51), - ROW_FLIP(58, 57, 56, 55, 54, 53, 52, 51, 50, 49), - ROW_FLIP(CMDB_7, 56, 55, 54, 53, 52, 51, 50, 49, 48), - ROW_FLIP(55, 54, 53, 52, 51, 50, 49, 48, 47, 46) -}; -// This layout can't be created by makeLayout.py -const byte ezMajorLayout[elementCount] = { //Testing layout viability - probably will make this generative so we can easily add scales once figured out - ROW_FLIP(CMDB_1, 91, 93, 95, 96, 98, 100, 101, 103, 105), - ROW_FLIP(84, 86, 88, 89, 91, 93, 95, 96, 98, 100), - ROW_FLIP(CMDB_2, 79, 81, 83, 84, 86, 88, 89, 91, 93), - ROW_FLIP(72, 74, 76, 77, 79, 81, 83, 84, 86, 88), - ROW_FLIP(CMDB_3, 67, 69, 71, 72, 74, 76, 77, 79, 81), - ROW_FLIP(60, 62, 64, 65, 67, 69, 71, 72, 74, 76), - ROW_FLIP(CMDB_4, 55, 57, 59, 60, 62, 64, 65, 67, 69), - ROW_FLIP(48, 50, 52, 53, 55, 57, 59, 60, 62, 64), - ROW_FLIP(CMDB_5, 43, 45, 47, 48, 50, 52, 53, 55, 57), - ROW_FLIP(36, 38, 40, 41, 43, 45, 47, 48, 50, 52), - ROW_FLIP(CMDB_6, 31, 33, 35, 36, 38, 40, 41, 43, 45), - ROW_FLIP(24, 26, 28, 29, 31, 33, 35, 36, 38, 40), - ROW_FLIP(CMDB_7, 19, 21, 23, 24, 26, 28, 29, 31, 33), - ROW_FLIP(12, 14, 16, 17, 19, 21, 23, 24, 26, 28) -}; -// ./makeLayout.py 132 -1 -9 -const byte fullLayout[elementCount] = { - ROW_FLIP(CMDB_1, 132, 131, 130, 129, 128, 127, 126, 125, 124), - ROW_FLIP(123, 122, 121, 120, 119, 118, 117, 116, 115, 114), - ROW_FLIP(CMDB_2, 113, 112, 111, 110, 109, 108, 107, 106, 105), - ROW_FLIP(104, 103, 102, 101, 100, 99, 98, 97, 96, 95), - ROW_FLIP(CMDB_3, 94, 93, 92, 91, 90, 89, 88, 87, 86), - ROW_FLIP(85, 84, 83, 82, 81, 80, 79, 78, 77, 76), - ROW_FLIP(CMDB_4, 75, 74, 73, 72, 71, 70, 69, 68, 67), - ROW_FLIP(66, 65, 64, 63, 62, 61, 60, 59, 58, 57), - ROW_FLIP(CMDB_5, 56, 55, 54, 53, 52, 51, 50, 49, 48), - ROW_FLIP(47, 46, 45, 44, 43, 42, 41, 40, 39, 38), - ROW_FLIP(CMDB_6, 37, 36, 35, 34, 33, 32, 31, 30, 29), - ROW_FLIP(28, 27, 26, 25, 24, 23, 22, 21, 20, 19), - ROW_FLIP(CMDB_7, 18, 17, 16, 15, 14, 13, 12, 11, 10), - ROW_FLIP(9, 8, 7, 6, 5, 4, 3, 2, 1, 0) -}; - -// ./makeLayout.py 99 -4 -3 -const byte fourtyone1[elementCount] = { - ROW_FLIP(CMDB_1, 99, 95, 91, 87, 83, 79, 75, 71, 67), - ROW_FLIP(96, 92, 88, 84, 80, 76, 72, 68, 64, 60), - ROW_FLIP(CMDB_2, 89, 85, 81, 77, 73, 69, 65, 61, 57), - ROW_FLIP(86, 82, 78, 74, 70, 66, 62, 58, 54, 50), - ROW_FLIP(CMDB_3, 79, 75, 71, 67, 63, 59, 55, 51, 47), - ROW_FLIP(76, 72, 68, 64, 60, 56, 52, 48, 44, 40), - ROW_FLIP(CMDB_4, 69, 65, 61, 57, 53, 49, 45, 41, 37), - ROW_FLIP(66, 62, 58, 54, 50, 46, 42, 38, 34, 30), - ROW_FLIP(CMDB_5, 59, 55, 51, 47, 43, 39, 35, 31, 27), - ROW_FLIP(56, 52, 48, 44, 40, 36, 32, 28, 24, 20), - ROW_FLIP(CMDB_6, 49, 45, 41, 37, 33, 29, 25, 21, 17), - ROW_FLIP(46, 42, 38, 34, 30, 26, 22, 18, 14, 10), - ROW_FLIP(CMDB_7, 39, 35, 31, 27, 23, 19, 15, 11, 7), - ROW_FLIP(36, 32, 28, 24, 20, 16, 12, 8, 4, 0) -}; - -// TODO: Don't make this go outside of the normal range -// ./makeLayout.py 138 3 -10 -const byte fourtyone2[elementCount] = { - ROW_FLIP(CMDB_1, 138, 141, 144, 147, 150, 153, 156, 159, 162), - ROW_FLIP(128, 131, 134, 137, 140, 143, 146, 149, 152, 155), - ROW_FLIP(CMDB_2, 121, 124, 127, 130, 133, 136, 139, 142, 145), - ROW_FLIP(111, 114, 117, 120, 123, 126, 129, 132, 135, 138), - ROW_FLIP(CMDB_3, 104, 107, 110, 113, 116, 119, 122, 125, 128), - ROW_FLIP(94, 97, 100, 103, 106, 109, 112, 115, 118, 121), - ROW_FLIP(CMDB_4, 87, 90, 93, 96, 99, 102, 105, 108, 111), - ROW_FLIP(77, 80, 83, 86, 89, 92, 95, 98, 101, 104), - ROW_FLIP(CMDB_5, 70, 73, 76, 79, 82, 85, 88, 91, 94), - ROW_FLIP(60, 63, 66, 69, 72, 75, 78, 81, 84, 87), - ROW_FLIP(CMDB_6, 53, 56, 59, 62, 65, 68, 71, 74, 77), - ROW_FLIP(43, 46, 49, 52, 55, 58, 61, 64, 67, 70), - ROW_FLIP(CMDB_7, 36, 39, 42, 45, 48, 51, 54, 57, 60), - ROW_FLIP(26, 29, 32, 35, 38, 41, 44, 47, 50, 53) -}; - -// TODO: Don't make this go outside of the normal range -// ./makeLayout.py 152 -1 -8 -const byte fourtyone3[elementCount] = { - ROW_FLIP(CMDB_1, 152, 151, 150, 149, 148, 147, 146, 145, 144), - ROW_FLIP(144, 143, 142, 141, 140, 139, 138, 137, 136, 135), - ROW_FLIP(CMDB_2, 135, 134, 133, 132, 131, 130, 129, 128, 127), - ROW_FLIP(127, 126, 125, 124, 123, 122, 121, 120, 119, 118), - ROW_FLIP(CMDB_3, 118, 117, 116, 115, 114, 113, 112, 111, 110), - ROW_FLIP(110, 109, 108, 107, 106, 105, 104, 103, 102, 101), - ROW_FLIP(CMDB_4, 101, 100, 99, 98, 97, 96, 95, 94, 93), - ROW_FLIP(93, 92, 91, 90, 89, 88, 87, 86, 85, 84), - ROW_FLIP(CMDB_5, 84, 83, 82, 81, 80, 79, 78, 77, 76), - ROW_FLIP(76, 75, 74, 73, 72, 71, 70, 69, 68, 67), - ROW_FLIP(CMDB_6, 67, 66, 65, 64, 63, 62, 61, 60, 59), - ROW_FLIP(59, 58, 57, 56, 55, 54, 53, 52, 51, 50), - ROW_FLIP(CMDB_7, 50, 49, 48, 47, 46, 45, 44, 43, 42), - ROW_FLIP(42, 41, 40, 39, 38, 37, 36, 35, 34, 33) -}; - -const byte* currentLayout = wickiHaydenLayout; - -// These are for standard tuning only -// We start an octave above standard midi because integer hertz only have so much resolution. -const unsigned int pitches[128] = { - 16, 17, 18, 19, 21, 22, 23, 25, 26, 28, 29, 31, // Octave 0 - 33, 35, 37, 39, 41, 44, 46, 49, 52, 55, 58, 62, // Octave 1 - 65, 69, 73, 78, 82, 87, 93, 98, 104, 110, 117, 123, // Octave 2 - 131, 139, 147, 156, 165, 175, 185, 196, 208, 220, 233, 247, // Octave 3 - 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, // Octave 4 - 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, // Octave 5 - 1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, // Octave 6 - 2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951, // Octave 7 - 4186, 4435, 4699, 4978, 5274, 5588, 5920, 6272, 6645, 7040, 7459, 7902, // Octave 8 - 8372, 8870, 9397, 9956, 10548, 11175, 11840, 12544, 13290, 14080, 14917, 15804, //9 - 16744, // C10 - 17740, // C#10 - 18795, // D10 - 19912, // D#10 - 21096, // E10 - 22350, // F10 - 23680 // F#10 -}; -// ./makePitches.py 220 19 -14 -const unsigned int pitches19[128] = { -// start close to C3 - 132, 137, 142, 147, 153, 158, 164, 170, 177, 183, 190, 197, 205, 212, -220, 228, 237, 245, 255, 264, 274, 284, 295, 306, 317, 329, 341, 354, 367, 380, 394, 409, 424, -// Octave starting at A=440 -440, 456, 473, 491, 509, 528, 548, 568, 589, 611, 634, 657, 682, 707, 733, 761, 789, 818, 848, -880, 913, 947, 982, 1018, 1056, 1095, 1136, 1178, 1222, 1267, 1315, 1363, 1414, 1467, 1521, 1578, 1636, 1697, -1760, 1825, 1893, 1964, 2037, 2112, 2191, 2272, 2356, 2444, 2535, 2629, 2727, 2828, 2933, 3042, 3155, 3272, 3394, -3520, 3651, 3786, 3927, 4073, 4224, 4381, 4544, 4713, 4888, 5070, 5258, 5453, 5656, 5866, 6084, 6310, 6545, 6788, -7040, 7302, 7573, 7854, 8146, 8449, 8763, 9088, 9426, 9776, 10139, 10516, 10907, 11312, 11732, 12168, 12620, 13089, 13576 -}; -// ./makePitches.py 220 24 -18 -const unsigned int pitches24[128] = { -// start close to C3 - 131, 135, 139, 143, 147, 151, 156, 160, 165, 170, 175, 180, 185, 190, 196, 202, 208, 214, -220, 226, 233, 240, 247, 254, 262, 269, 277, 285, 294, 302, 311, 320, 330, 339, 349, 359, 370, 381, 392, 403, 415, 427, -// Octave starting at A=440 -440, 453, 466, 480, 494, 508, 523, 539, 554, 571, 587, 605, 622, 640, 659, 679, 698, 719, 740, 762, 784, 807, 831, 855, -880, 906, 932, 960, 988, 1017, 1047, 1077, 1109, 1141, 1175, 1209, 1245, 1281, 1319, 1357, 1397, 1438, 1480, 1523, 1568, 1614, 1661, 1710, -1760, 1812, 1865, 1919, 1976, 2033, 2093, 2154, 2217, 2282, 2349, 2418, 2489, 2562, 2637, 2714, 2794, 2876, 2960, 3047, 3136, 3228, 3322, 3420, -3520, 3623, 3729, 3839, 3951, 4067, 4186, 4309, 4435, 4565, 4699, 4836, 4978, 5124 -}; -// ./makePitches.py 220 31 -23 -const unsigned int pitches31[128] = { -// Start close to C - 132, 135, 138, 141, 144, 147, 150, 154, 157, 161, 165, 168, 172, 176, 180, 184, 188, 192, 197, 201, 206, 210, 215, -220, 225, 230, 235, 241, 246, 252, 257, 263, 269, 275, 281, 288, 294, 301, 308, 315, 322, 329, 336, 344, 352, 360, 368, 376, 385, 393, 402, 411, 421, 430, -440, 450, 460, 471, 481, 492, 503, 515, 526, 538, 550, 563, 575, 588, 602, 615, 629, 643, 658, 673, 688, 704, 720, 736, 753, 770, 787, 805, 823, 842, 861, -880, 900, 920, 941, 962, 984, 1006, 1029, 1052, 1076, 1100, 1125, 1151, 1177, 1203, 1231, 1258, 1287, 1316, 1346, 1376, 1407, 1439, 1472, 1505, 1539, 1574, 1609, 1646, 1683, 1721, -1760, 1800, 1840, 1882, 1925, 1968, 2013, 2058, 2105, 2152, 2201, 2251 -}; - -// 41-TET 41 equal temperament -// ./makePitches.py 220 41 +10 -const unsigned int pitches41[128] = { -// Start close to C4 -261, 265, 269, 274, 279, 284, 288, 293, 298, 303, 309, 314, 319, 325, 330, 336, 341, 347, 353, 359, 365, 372, 378, 384, 391, 398, 404, 411, 418, 425, 433, -// Octave starting at A=440 -440, 448, 455, 463, 471, 479, 487, 495, 504, 512, 521, 530, 539, 548, 557, 567, 577, 587, 597, 607, 617, 628, 638, 649, 660, 671, 683, 695, 706, 718, 731, 743, 756, 769, 782, 795, 809, 822, 836, 851, 865, -880, 895, 910, 926, 942, 958, 974, 991, 1007, 1025, 1042, 1060, 1078, 1096, 1115, 1134, 1153, 1173, 1193, 1213, 1234, 1255, 1276, 1298, 1320, 1343, 1366, 1389, 1413, 1437, 1461, 1486, 1512, 1537, 1564, 1590, 1617, 1645, 1673, 1701, 1730, -1760, 1790, 1821, 1852, 1883, 1915, 1948, 1981, 2015, 2049, 2084, 2120, 2156, 2193, 2230 -}; -// ./makePitches.py 220 72 18 -const unsigned int pitches72[128] = { -262, 264, 267, 269, 272, 275, 277, 280, 283, 285, 288, 291, 294, 297, 299, 302, 305, 308, 311, 314, 317, 320, 323, 326, 330, 333, 336, 339, 343, 346, 349, 353, 356, 359, 363, 366, 370, 374, 377, 381, 385, 388, 392, 396, 400, 403, 407, 411, 415, 419, 423, 427, 432, 436, 440, 444, 449, 453, 457, 462, 466, 471, 475, 480, 484, 489, 494, 499, 503, 508, 513, 518, 523, 528, 533, 539, 544, 549, 554, 560, 565, 571, 576, 582, 587, 593, 599, 605, 610, 616, 622, 628, 634, 640, 647, 653, 659, 666, 672, 679, 685, 692, 698, 705, 712, 719, 726, 733, 740, 747, 754, 762, 769, 776, 784, 792, 799, 807, 815, 823, 831, 839, 847, 855, 863, 872, 880, 889 -}; -#define TONEPIN 23 - -// Global time variables -unsigned long currentTime = 0; // Program loop consistent variable for time in milliseconds since power on -unsigned long previousTime = 0; // Used to check speed of the loop in diagnostics mode 4 -int loopTime = 0; // Used to keep track of how long each loop takes. Useful for rate-limiting. -int screenTime = 0; // Used to dim screen after a set time to prolong the lifespan of the OLED - -// Tone and Arpeggiator variables -int arpTime = 0; // Measures time per note -int arpThreshold = 20; // Set arp speed (in milliseconds) -byte curr_pitch = 128; -// Keep track of whether this note is held or not. -bool pitchRef[256]; - -// Pitch bend and mod wheel variables -int pitchBendNeutral = 0; // The center position for the pitch bend "wheel." Could be adjusted for global tuning? -int pitchBendPosition = 0; // The actual pitch bend variable used for sending MIDI -int pitchBendSpeed = 1024; // The amount the pitch bend moves every time modPitchTime hits it's limit. -int modPitchTime = 0; // Used to rate-limit pitch bend and mod wheel updates -byte modWheelPosition = 0; // Actual mod wheel variable used for sending MIDI -byte modWheelSpeed = 6; // The amount the mod wheel moves every time modPitchTime hits it's limit. -bool pitchModToggle = 1; // Used to toggle between pitch bend and mod wheel - -// Variables for holding digital button states and activation times -byte activeButtons[elementCount]; // Array to hold current note button states -byte previousActiveButtons[elementCount]; // Array to hold previous note button states for comparison -unsigned long activeButtonsTime[elementCount]; // Array to track last note button activation time for debounce -byte animationStep[elementCount]; // Array to track reactive lighting steps -byte cycleNumber = 0; // Used for animations that have a fixed cycle -int animationTime = 0; // Used for tracking how long since last lighting update - -// Variables for sequencer mode -// Sequencer mode probably needs some love before it will be useful/usable. -// The device is held vertically, and two rows create a "lane". -// the first 8 buttons from each row are the steps (giving you 4 measures with quarter-note precision) -// The extra 3 (4?) buttons are for bank switching, muting, and solo-ing -typedef struct { - // The first 16 are for bank 0, and the second 16 are for bank 1. - bool steps[32] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - bool bank = 0; - int state = 0; // TODO: change to enum: normal, mute, solo, mute&solo - int instrument = 0; // What midi note this lane will send to the computer. -} Lane; -#define STATE_MUTE 1 -#define STATE_SOLO 2 - -#define NLANES 7 -Lane lanes[NLANES]; - -int sequencerStep = 0; // 0 - 31 - -// You have to push a button to switch modes -bool sequencerMode = 0; - -/* -THESE CAN BE USED TO RESET THE SEQUENCE POSITION -void handleStart(void); -void handleContinue(void); -void handleStop(void); - -THIS WILL BE USED FOR THE SEQUENCER CLOCK (24 frames per quarter note) -void handleTimeCodeQuarterFrame(byte data); -We should be able to adjust the division in the menu to have different sequence speeds. - -*/ - -void handleNoteOn(byte channel, byte pitch, byte velocity) { - // Rosegarden sends its metronome this way. Using for testing... - if (1 == sequencerMode && 10 == channel && 100 == pitch) { - sequencerPlayNextNote(); - } -} - -// MENU SYSTEM SETUP // -// Create menu page object of class GEMPage. Menu page holds menu items (GEMItem) and represents menu level. -// Menu can have multiple menu pages (linked to each other) with multiple menu items each -GEMPage menuPageMain("HexBoard MIDI Controller"); -GEMPage menuPageLayout("Layout"); -GEMPage menuPageTesting("Unfinished Alpha Tests"); - -GEMItem menuItemLayout("Layout", menuPageLayout); -void wickiHayden(); //Forward declarations -void harmonicTable(); -void gerhard(); -void bosanquetWilson(); -void ezMajor(); -void full(); -void fortyone1(); -void fortyone2(); -void fortyone3(); -GEMItem menuItemWickiHayden("Wicki-Hayden", wickiHayden); -GEMItem menuItemHarmonicTable("Harmonic Table", harmonicTable); -GEMItem menuItemGerhard("Gerhard", gerhard); -GEMItem menuItemBosanquetWilson("Bosanquet-Wilson", bosanquetWilson); -GEMItem menuItemEzMajor("EZ Major", ezMajor); -GEMItem menuItemFull("Full", full); -GEMItem menuItem411("41-1", fortyone1); -GEMItem menuItem412("41-2", fortyone2); -GEMItem menuItem413("41-3", fortyone3); - -void setLayoutLEDs(); //Forward declaration -byte key = 0; -SelectOptionByte selectKeyOptions[] = { { "C", 0 }, { "C#", 1 }, { "D", 2 }, { "D#", 3 }, { "E", 4 }, { "F", 5 }, { "F#", 6 }, { "G", 7 }, { "G#", 8 }, { "A", 9 }, { "A#", 10 }, { "B", 11 } }; -GEMSelect selectKey(sizeof(selectKeyOptions) / sizeof(SelectOptionByte), selectKeyOptions); -GEMItem menuItemKey("Key:", key, selectKey, setLayoutLEDs); - -byte scale = 0; -SelectOptionByte selectScaleOptions[] = { - { "ALL", 0 }, { "Major", 1 }, { "HarMin", 2 }, { "MelMin", 3 }, { "NatMin", 4 }, { "PentMaj", 5 }, - { "PentMin", 6 }, { "Blues", 7 }, { "DoubHar", 8 }, { "Phrygia", 9 }, { "PhryDom", 10 }, { "Dorian", 11 }, - { "Lydian", 12 }, { "Mixolyd", 13 }, { "Locrian", 14 }, { "NONE", 15 } +// ====== Hexperiment + // Sketch to program the hexBoard to handle microtones + // March 2024, theHDM / Nicholas Fox + // major thanks to Zach and Jared! + // Arduino IDE setup: + // Board = Generic RP2040 (use the following additional board manager repo: + // https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json) + // Patches needed for U8G2, Rotary.h + // ============================================================================== + // list of things remaining to do: + // -- program the wheel -- OK! + // -- put back the animations -- OK! + // -- test MPE working on iPad garageband -- works on pianoteq +without MPE; need to get powered connection to iOS. + // -- volume control test on buzzer + // -- save and load presets + // -- sequencer restore + // ============================================================================== + // + #include <Arduino.h> + #include <Wire.h> + #include <LittleFS.h> + #include <queue> // std::queue construct to store open +channels in microtonal mode + const byte diagnostics = 1; +// ====== initialize timers + + uint32_t runTime = 0; // Program loop consistent +variable for time in milliseconds since power on + uint32_t lapTime = 0; // Used to keep track of how +long each loop takes. Useful for rate-limiting. + uint32_t loopTime = 0; // Used to check speed of the +loop in diagnostics mode 4 + +// ====== initialize SDA and SCL pins for hardware I/O + + const byte lightPinSDA = 16; + const byte lightPinSCL = 17; + +// ====== initialize MIDI + + #include <Adafruit_TinyUSB.h> + #include <MIDI.h> + Adafruit_USBD_MIDI usb_midi; + MIDI_CREATE_INSTANCE(Adafruit_USBD_MIDI, usb_midi, MIDI); + float concertA = 440.0; // tuning of A4 in Hz + byte MPE = 0; // microtonal mode. if zero then attempt to +self-manage multiple channels. + // if one then on certain synths that are MPE compatible +will send in that mode. + int16_t channelBend[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0 }; // what's the current note bend on this channel + byte channelPoly[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0 }; // how many notes are playing on this channel + std::queue<byte> openChannelQueue; + const byte defaultPBRange = 2; + +// ====== initialize LEDs + + #include <Adafruit_NeoPixel.h> + const byte multiplexPins[] = { 4, 5, 2, 3 }; // m1p, m2p, m4p, m8p + const byte rowCount = 14; // The number of rows +in the matrix + const byte columnPins[] = { 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; + const byte colCount = sizeof(columnPins); // The number of columns +in the matrix + const byte hexCount = colCount * rowCount; // The number of +elements in the matrix + const byte LEDPin = 22; + Adafruit_NeoPixel strip(hexCount, LEDPin, NEO_GRB + NEO_KHZ800); + enum { NoAnim, StarAnim, SplashAnim, OrbitAnim, OctaveAnim, NoteAnim }; + byte animationType = 0; + byte animationFPS = 32; // actually frames per 2^10 seconds. close +enough to 30fps + int16_t rainbowDegreeTime = 64; // ms to go through 1/360 of rainbow. +// ====== initialize hex state object + + enum { Right, UpRight, UpLeft, Left, DnLeft, DnRight }; + typedef struct { + int8_t row; + int8_t col; + } coordinates; + typedef struct { + byte keyState = 0; // binary 00 = off, 01 = just pressed, +10 = just released, 11 = held + coordinates coords = {0,0}; + uint32_t timePressed = 0; // timecode of last press + uint32_t LEDcolorAnim = 0; // + uint32_t LEDcolorPlay = 0; // + uint32_t LEDcolorOn = 0; // + uint32_t LEDcolorOff = 0; // + bool animate = 0; // hex is flagged as part of the +animation in this frame + int16_t steps = 0; // number of steps from key center +(semitones in 12EDO; microtones if >12EDO) + bool isCmd = 0; // 0 if it's a MIDI note; 1 if it's a +MIDI control cmd + bool inScale = 0; // 0 if it's not in the selected +scale; 1 if it is + byte note = 255; // MIDI note or control parameter +corresponding to this hex + int16_t bend; // in microtonal mode, the pitch bend +for this note needed to be tuned correctly + byte channel; // what MIDI channel this note is playing on + float frequency; // what frequency to ring on the buzzer + void updateKeyState(bool keyPressed) { + keyState = (((keyState << 1) + keyPressed) & 3); + if (keyState == 1) { + timePressed = millis(); // log the time + }; + }; + uint32_t animFrame() { + if (timePressed) { // 2^10 milliseconds is close enough to 1 second + return 1 + (((runTime - timePressed) * animationFPS) >> 10); + } else { + return 0; + }; + }; + } buttonDef; + buttonDef h[hexCount]; // array of hex objects from 0 to 139 + const byte assignCmd[] = { 0, 20, 40, 60, 80, 100, 120 }; + const byte cmdCount = 7; + const byte cmdCode = 192; + +// ====== initialize wheel emulation + + const uint16_t ccMsgCoolDown = 20; // milliseconds between steps + typedef struct { + byte* topBtn; + byte* midBtn; + byte* botBtn; + int16_t minValue; + int16_t maxValue; + uint16_t stepValue; + int16_t defValue; + int16_t curValue; + int16_t targetValue; + uint32_t timeLastChanged; + void setTargetValue() { + if (*midBtn >> 1) { // middle button toggles target (0) vs. step (1) mode + int16_t temp = curValue; + if (*topBtn == 1) {temp += stepValue;}; + if (*botBtn == 1) {temp -= stepValue;}; + if (temp > maxValue) {temp = maxValue;} + else if (temp <= minValue) {temp = minValue;}; + targetValue = temp; + } else { + switch (((*topBtn >> 1) << 1) + (*botBtn >> 1)) { + case 0b10: targetValue = maxValue; break; + case 0b11: targetValue = defValue; break; + case 0b01: targetValue = minValue; break; + default: targetValue = curValue; break; + }; + }; + }; + bool updateValue() { + int16_t temp = targetValue - curValue; + if (temp != 0) { + if ((runTime - timeLastChanged) >= ccMsgCoolDown) { + timeLastChanged = runTime; + if (abs(temp) < stepValue) { + curValue = targetValue; + } else { + curValue = curValue + (stepValue * (temp / abs(temp))); + }; + return 1; + } else { + return 0; + }; + } else { + return 0; + }; + }; + } wheelDef; + wheelDef modWheel = { + &h[assignCmd[4]].keyState, + &h[assignCmd[5]].keyState, + &h[assignCmd[6]].keyState, + 0, 127, 8, + 0, 0, 0 }; -GEMSelect selectScale(sizeof(selectScaleOptions) / sizeof(SelectOptionByte), selectScaleOptions); -GEMItem menuItemScale("Scale:", scale, selectScale, applySelectedScale); - -const bool (*selectedScale)[12]; -// Scale arrays of boolean (for O(1) access instead of O(12/2)) -// 0 1 2 3 4 5 6 7 8 9 X E -const bool noneScale[12] = {0,0,0,0,0,0,0,0,0,0,0,0}; -const bool chromaticScale[12] = {1,1,1,1,1,1,1,1,1,1,1,1}; -const bool majorScale[12] = {1,0,1,0,1,1,0,1,0,1,0,1}; -const bool harmonicMinorScale[12] = {1,0,1,1,0,1,0,1,1,0,0,1}; -const bool melodicMinorScale[12] = {1,0,1,1,0,1,0,1,0,1,0,1}; -const bool naturalMinorScale[12] = {1,0,1,1,0,1,0,1,1,0,1,0}; -const bool pentatonicMajorScale[12] = {1,0,1,0,1,0,0,1,0,1,0,0}; -const bool pentatonicMinorScale[12] = {1,0,0,1,0,1,0,1,0,0,1,0}; -const bool bluesScale[12] = {1,0,0,1,0,1,1,1,0,0,1,0}; -const bool doubleHarmonicScale[12] = {1,1,0,0,1,1,0,1,1,0,0,1}; -const bool phrygianScale[12] = {1,1,0,1,0,1,0,1,1,0,1,0}; -const bool phrygianDominantScale[12]= {1,1,0,0,1,1,0,1,1,0,1,0}; -const bool dorianScale[12] = {1,0,1,1,0,1,0,1,0,1,1,0}; -const bool lydianScale[12] = {1,0,1,0,1,0,1,1,0,1,0,1}; -const bool mixolydianScale[12] = {1,0,1,0,1,1,0,1,0,1,1,0}; -const bool locrianScale[12] = {1,1,0,1,0,1,1,0,1,0,1,0}; - -// Function to apply the selected scale -void applySelectedScale() { - switch (scale) { - case 0: // All notes - selectedScale = &chromaticScale; - break; - case 1: // Major scale - selectedScale = &majorScale; - break; - case 2: // Harmonic minor scale - selectedScale = &harmonicMinorScale; - break; - case 3: // Melodic minor scale - selectedScale = &melodicMinorScale; - break; - case 4: // Natural minor scale - selectedScale = &naturalMinorScale; - break; - case 5: // Pentatonic major scale - selectedScale = &pentatonicMajorScale; - break; - case 6: // Pentatonic minor scale - selectedScale = &pentatonicMinorScale; - break; - case 7: // Blues scale - selectedScale = &bluesScale; - break; - case 8: // Double Harmonic scale - selectedScale = &doubleHarmonicScale; - break; - case 9: // Phrygian scale - selectedScale = &phrygianScale; - break; - case 10: // Phrygian Dominant scale - selectedScale = &phrygianDominantScale; - break; - case 11: // Dorian scale - selectedScale = &dorianScale; - break; - case 12: // Lydian scale - selectedScale = &lydianScale; - break; - case 13: // Mixolydian scale - selectedScale = &mixolydianScale; - break; - case 14: // Locrian scale - selectedScale = &locrianScale; - break; - default: // Dim all LEDs - selectedScale = &noneScale; - break; - } - setLayoutLEDs(); -} - -bool scaleLock = false; // For disabling all keys not in the selected scale -GEMItem menuItemScaleLock("Scale Lock:", scaleLock, setLayoutLEDs); - -int transpose = 0; -SelectOptionInt selectTransposeOptions[] = { - { "-12", -12 }, { "-11", -11 }, { "-10", -10 }, { "-9", -9 }, { "-8", -8 }, { "-7", -7 }, { "-6", -6 }, { "-5", -5 }, { "-4", -4 }, { "-3", -3 }, { "-2", -2 }, { "-1", -1 }, { "0", 0 }, { "+1", 1 }, { "+2", 2 }, { "+3", 3 }, { "+4", 4 }, { "+5", 5 }, { "+6", 6 }, { "+7", 7 }, { "+8", 8 }, { "+9", 9 }, { "+10", 10 }, { "+11", 11 }, { "+12", 12 } -}; -GEMSelect selectTranspose(sizeof(selectTransposeOptions) / sizeof(SelectOptionByte), selectTransposeOptions); -void validateTranspose(); // Forward declaration -GEMItem menuItemTranspose("Transpose:", transpose, selectTranspose, validateTranspose); - -SelectOptionInt selectBendSpeedOptions[] = { { "too slo", 128 }, { "Turtle", 256 }, { "Slow", 512 }, { "Medium", 1024 }, { "Fast", 2048 }, { "Cheetah", 4096 }, { "Instant", 16384 } }; -GEMSelect selectBendSpeed(sizeof(selectBendSpeedOptions) / sizeof(SelectOptionInt), selectBendSpeedOptions); -GEMItem menuItemBendSpeed("Pitch Bend:", pitchBendSpeed, selectBendSpeed); - -SelectOptionByte selectModSpeedOptions[] = { { "too slo", 1 }, { "Turtle", 2 }, { "Slow", 3 }, { "Medium", 6 }, { "Fast", 12 }, { "Cheetah", 32 }, { "Instant", 127 } }; -GEMSelect selectModSpeed(sizeof(selectModSpeedOptions) / sizeof(SelectOptionByte), selectModSpeedOptions); -GEMItem menuItemModSpeed("Mod Wheel:", modWheelSpeed, selectModSpeed); - -void setBrightness(); //Forward declaration -#if ModelNumber == 1 -SelectOptionByte selectBrightnessOptions[] = { { "Night", 10 }, { "Dim", 30 }, { "Low", 70 }, { "Medium", 110 }, { "High", 160 }, { "Higher", 210 }, { "MAX(!!)", 255 } }; -#elif ModelNumber == 2 // Reducing options to simplify and because the lights aren't as bright. -SelectOptionByte selectBrightnessOptions[] = { { "Dim", 20 }, { "Low", 70 }, { "Medium", 130 }, { "High", 190 }, { "Max", 255 } }; -#endif -GEMSelect selectBrightness(sizeof(selectBrightnessOptions) / sizeof(SelectOptionByte), selectBrightnessOptions); -GEMItem menuItemBrightness("Brightness:", stripBrightness, selectBrightness, setBrightness); - -byte lightMode = 0; -SelectOptionByte selectLightingOptions[] = { { "Button", 0 }, { "Note", 1 }, { "Octave", 2 }, { "Splash", 3 }, { "Star", 4 }, { "Orbit", 5} }; -GEMSelect selectLighting(sizeof(selectLightingOptions) / sizeof(SelectOptionByte), selectLightingOptions); -GEMItem menuItemLighting("Lighting:", lightMode, selectLighting); - -byte colorMode = 0; -SelectOptionByte selectColorOptions[] = { { "Scale", 0 }, { "Tone", 1 }}; -GEMSelect selectColor(sizeof(selectColorOptions) / sizeof(SelectOptionByte), selectColorOptions); -GEMItem menuItemColor("Color:", colorMode, selectColor); - -int buzzer = 0; // For enabling built-in buzzer for sound generation without a computer -#define BUZZER_ARP_UP 2 -#define BUZZER_ARP_DOWN 3 -SelectOptionInt selectBuzzerOptions[] = {{"Off", 0}, {"Mono", 1}, {"Arp Up", BUZZER_ARP_UP}, {"Arp Dwn", BUZZER_ARP_DOWN}}; -GEMSelect selectBuzzer(sizeof(selectBuzzerOptions)/sizeof(SelectOptionInt), selectBuzzerOptions); -GEMItem menuItemBuzzer("Buzzer:", buzzer, selectBuzzer); - -// For use when testing out unfinished features -GEMItem menuItemTesting("Testing", menuPageTesting); -boolean release = false; // Whether this is a release or not -GEMItem menuItemVersion("V0.6.y ", release, GEM_READONLY); -void sequencerSetup(); //Forward declaration -// For enabling basic sequencer mode - not complete -GEMItem menuItemSequencer("Sequencer:", sequencerMode, sequencerSetup); - -int tones = 12; // Experimental microtonal support -// TODO: consider adding 17, 22, 53 -SelectOptionInt selectTonesOptions[] = {{"12", 12}, {"19", 19}, {"24", 24}, {"31", 31}, {"41", 41}, {"72", 72}}; -GEMSelect selectTones(sizeof(selectTonesOptions)/sizeof(SelectOptionInt), selectTonesOptions); -GEMItem menuItemTones("Tones:", tones, selectTones); - -SelectOptionInt selectArpOptions[] = {{"Fast", 20}, {"Medium", 40}, {"Slow", 80}, {"sloooow", 120}}; -GEMSelect selectArp(sizeof(selectArpOptions)/sizeof(SelectOptionInt), selectArpOptions); -GEMItem menuItemArp("Arp Speed:", arpThreshold, selectArp); - -// Create menu object of class GEM_u8g2. Supply its constructor with reference to u8g2 object we created earlier -byte menuItemHeight = 10; -byte menuPageScreenTopOffset = 10; -byte menuValuesLeftOffset = 78; -GEM_u8g2 menu(u8g2, GEM_POINTER_ROW, GEM_ITEMS_COUNT_AUTO, menuItemHeight, menuPageScreenTopOffset, menuValuesLeftOffset); - - -// MIDI channel assignment -byte midiChannel = 1; // Current MIDI channel (changed via user input) - -// Velocity levels -byte midiVelocity = 100; // Default velocity - -// END SETUP SECTION -// ------------------------------------------------------------------------------------------------------------------------------------------------------------ - -void setup() { - // sequencer mode midi instruments for each "lane". - lanes[0].instrument = 36; // Bass Drum 1 - lanes[1].instrument = 40; // Electric Snare - lanes[2].instrument = 46; // Open Hi-Hat - lanes[3].instrument = 42; // Closed Hi-Hat - lanes[4].instrument = 49; // Crash Cymbal 1 - lanes[5].instrument = 45; // Low Tom - lanes[6].instrument = 50; // Hi Tom - -#if defined(ARDUINO_ARCH_MBED) && defined(ARDUINO_ARCH_RP2040) - // Manual begin() is required on core without built-in support for TinyUSB such as mbed rp2040 - TinyUSB_Device_Init(0); -#endif - usb_midi.setStringDescriptor("HexBoard MIDI"); - // Initialize MIDI, and listen to all MIDI channels - // This will also call usb_midi's begin() - MIDI.begin(MIDI_CHANNEL_OMNI); - // Callback will be run by MIDI.read() - MIDI.setHandleNoteOn(handleNoteOn); - //MIDI.setHandleTimeCodeQuarterFrame(handleTimeCodeQuarterFrame); - - Wire.setSDA(16); - Wire.setSCL(17); - - pinMode(encoderClick, INPUT_PULLUP); - - 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"); - } + wheelDef pbWheel = { + &h[assignCmd[4]].keyState, + &h[assignCmd[5]].keyState, + &h[assignCmd[6]].keyState, + -8192, 8192, 1024, + 0, 0, 0 + }; + wheelDef velWheel = { + &h[assignCmd[0]].keyState, + &h[assignCmd[1]].keyState, + &h[assignCmd[2]].keyState, + 0, 127, 8, + 96, 96, 96 + }; + bool toggleWheel = 0; // 0 for mod, 1 for pb + +// ====== initialize rotary knob + + #include <Rotary.h> + const byte rotaryPinA = 20; + const byte rotaryPinB = 21; + const byte rotaryPinC = 24; + Rotary rotary = Rotary(rotaryPinA, rotaryPinB); + bool rotaryIsClicked = HIGH; // + bool rotaryWasClicked = HIGH; // + int8_t rotaryKnobTurns = 0; // + +// ====== initialize GFX display + + #include <GEM_u8g2.h> + #define GEM_DISABLE_GLCD + U8G2_SH1107_SEEED_128X128_F_HW_I2C u8g2(U8G2_R2, U8X8_PIN_NONE); + GEM_u8g2 menu(u8g2, GEM_POINTER_ROW, GEM_ITEMS_COUNT_AUTO, 10, 10, +78); // menu item height; page screen top offset; menu values left +offset + const byte defaultContrast = 63; // GFX default contrast + bool screenSaverOn = 0; // + uint32_t screenTime = 0; // GFX timer to count if +screensaver should go on + const uint32_t screenSaverMillis = 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, +0b0101'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Minor, natural", Twelve, { 0b10110101, +0b1010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Minor, melodic", Twelve, { 0b10110101, +0b0101'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Minor, harmonic", Twelve, { 0b10110101, +0b1001'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Pentatonic, major", Twelve, { 0b10101001, +0b0100'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Pentatonic, minor", Twelve, { 0b10010101, +0b0010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Blues", Twelve, { 0b10010111, +0b0010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Double Harmonic", Twelve, { 0b11001101, +0b1001'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Phrygian", Twelve, { 0b11010101, +0b1010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Phrygian Dominant", Twelve, { 0b11001101, +0b1010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Dorian", Twelve, { 0b10110101, +0b0110'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Lydian", Twelve, { 0b10101011, +0b0101'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Lydian Dominant", Twelve, { 0b10101011, +0b0110'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Mixolydian", Twelve, { 0b10101101, +0b0110'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Locrian", Twelve, { 0b11010110, +0b1010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Whole tone", Twelve, { 0b10101010, +0b1010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Octatonic", Twelve, { 0b10110110, +0b1101'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Rast maqam", TwentyFour, { 0b10001001, +0b00100010, 0b00101100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { (char*)"Rast makam", FiftyThree, { 0b10000000, +0b01000000, 0b01000010, 0b00000001, + 0b00000000, +0b10001000, 0b10000'000, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + }; + const byte scaleCount = sizeof(scaleOptions) / sizeof(scaleDef); + byte scaleLock = 0; // menu wants this to be an int, not a bool + +// ====== initialize key coloring routines + + enum colors { W, R, O, Y, L, G, C, B, +I, P, M, + r, o, y, l, g, c, b, i, + p, m }; + enum { DARK = 0, VeryDIM = 1, DIM = 32, BRIGHT = 127, VeryBRIGHT = 255 }; + enum { GRAY = 0, DULL = 127, VIVID = 255 }; + float hueCode[] = { 0.0, 0.0, 36.0, 72.0, 108.0, 144.0, 180.0, +216.0, 252.0, 288.0, 324.0, + 0.0, 36.0, 72.0, 108.0, 144.0, 180.0, +216.0, 252.0, 288.0, 324.0 }; + byte satCode[] = { GRAY, +VIVID,VIVID,VIVID,VIVID,VIVID,VIVID,VIVID,VIVID,VIVID,VIVID, + DULL, DULL, DULL, DULL, DULL, DULL, DULL, +DULL, DULL, DULL }; + byte colorMode = 0; + byte perceptual = 1; + enum {assignDefault = -1}; // auto-determine this component of color + +// ====== initialize note labels in each tuning, also used for key signature + + typedef struct { + char* name; + byte tuning; + int8_t offset; // steps from constant A4 to that key class + colors tierColor; + } keyDef; + keyDef keyOptions[] = { + // 12 EDO, whole tone = 2, #/b = 1 + { (char*)" C (B#)", Twelve, -9, W }, + { (char*)" C# / Db", Twelve, -8, I }, + { (char*)" D", Twelve, -7, W }, + { (char*)" D# / Eb", Twelve, -6, I }, + { (char*)" (Fb) E", Twelve, -5, W }, + { (char*)" F (E#)", Twelve, -4, W }, + { (char*)" Gb / F#", Twelve, -3, I }, + { (char*)" G", Twelve, -2, W }, + { (char*)" G# / Ab", Twelve, -1, I }, + { (char*)" A", Twelve, 0, W }, + { (char*)" A# / Bb", Twelve, 1, I }, + { (char*)"(Cb) B", Twelve, 2, W }, + // 17 EDO, whole tone = 3, #/b = 2, +/d = 1 + { (char*)" C (B+)", Seventeen, -13, W }, + { (char*)" C+ / Db / B#", Seventeen, -12, R }, + { (char*)" C# / Dd", Seventeen, -11, I }, + { (char*)" D", Seventeen, -10, W }, + { (char*)" D+ / Eb", Seventeen, -9, R }, + { (char*)" Fb / D# / Ed", Seventeen, -8, I }, + { (char*)"(Fd) E", Seventeen, -7, W }, + { (char*)" F (E+)", Seventeen, -6, W }, + { (char*)" F+ / Gb / E#", Seventeen, -5, R }, + { (char*)" F# / Gd", Seventeen, -4, I }, + { (char*)" G", Seventeen, -3, W }, + { (char*)" G+ / Ab", Seventeen, -2, R }, + { (char*)" G# / Ad", Seventeen, -1, I }, + { (char*)" A", Seventeen, 0, W }, + { (char*)" Bb / A+", Seventeen, 1, R }, + { (char*)" Cb / Bd / A#", Seventeen, 2, I }, + { (char*)"(Cd) B" , Seventeen, 3, W }, + // 19 EDO, whole tone = 3, #/b = 1 + { (char*)" C", Nineteen, -14, W }, + { (char*)" C#", Nineteen, -13, R }, + { (char*)" Db", Nineteen, -12, I }, + { (char*)" D", Nineteen, -11, W }, + { (char*)" D#", Nineteen, -10, R }, + { (char*)" Eb", Nineteen, -9, I }, + { (char*)" E", Nineteen, -8, W }, + { (char*)" E# / Fb", Nineteen, -7, m }, + { (char*)" F", Nineteen, -6, W }, + { (char*)" F#", Nineteen, -5, R }, + { (char*)" Gb", Nineteen, -4, I }, + { (char*)" G", Nineteen, -3, W }, + { (char*)" G#", Nineteen, -2, R }, + { (char*)" Ab", Nineteen, -1, I }, + { (char*)" A", Nineteen, 0, W }, + { (char*)" A#", Nineteen, 1, R }, + { (char*)" Bb", Nineteen, 2, I }, + { (char*)" B", Nineteen, 3, W }, + { (char*)" Cb / B#", Nineteen, 4, m }, + // 22 EDO, whole tone = 4, #/b = 3, ^/v = 1 + { (char*)" C (^B)", TwentyTwo, -17, W }, + { (char*)" ^C / Db / vB#", TwentyTwo, -16, l }, + { (char*)" vC# / ^Db / B#", TwentyTwo, -15, C }, + { (char*)" C# / vD", TwentyTwo, -14, i }, + { (char*)" D", TwentyTwo, -13, W }, + { (char*)" ^D / Eb", TwentyTwo, -12, l }, + { (char*)" Fb / vD# / ^Eb", TwentyTwo, -11, C }, + { (char*)" ^Fb / D# / vE", TwentyTwo, -10, i }, + { (char*)"(vF) E", TwentyTwo, -9, W }, + { (char*)" F (^E)", TwentyTwo, -8, W }, + { (char*)" ^F / Gb / vE#", TwentyTwo, -7, l }, + { (char*)" vF# / ^Gb / E#", TwentyTwo, -6, C }, + { (char*)" F# / vG", TwentyTwo, -5, i }, + { (char*)" G", TwentyTwo, -4, W }, + { (char*)" ^G / Ab", TwentyTwo, -3, l }, + { (char*)" vG# / ^Ab", TwentyTwo, -2, C }, + { (char*)" G# / vA", TwentyTwo, -1, i }, + { (char*)" A", TwentyTwo, 0, W }, + { (char*)" Bb / ^A", TwentyTwo, 1, l }, + { (char*)" Cb / ^Bb / vA#", TwentyTwo, 2, C }, + { (char*)" ^Cb / vB / A#", TwentyTwo, 3, i }, + { (char*)"(vC) B", TwentyTwo, 4, W }, + // 24 EDO, whole tone = 4, #/b = 2, +/d = 1 + { (char*)" C / B#", TwentyFour, -18, W }, + { (char*)" C+", TwentyFour, -17, r }, + { (char*)" C# / Db", TwentyFour, -16, I }, + { (char*)" Dd", TwentyFour, -15, g }, + { (char*)" D", TwentyFour, -14, W }, + { (char*)" D+", TwentyFour, -13, r }, + { (char*)" Eb / D#", TwentyFour, -12, I }, + { (char*)" Ed", TwentyFour, -11, g }, + { (char*)" E / Fb", TwentyFour, -10, W }, + { (char*)" E+ / Fd", TwentyFour, -9, y }, + { (char*)" E# / F", TwentyFour, -8, W }, + { (char*)" F+", TwentyFour, -7, r }, + { (char*)" Gb / F#", TwentyFour, -6, I }, + { (char*)" Gd", TwentyFour, -5, g }, + { (char*)" G", TwentyFour, -4, W }, + { (char*)" G+", TwentyFour, -3, r }, + { (char*)" G# / Ab", TwentyFour, -2, I }, + { (char*)" Ad", TwentyFour, -1, g }, + { (char*)" A", TwentyFour, 0, W }, + { (char*)" A+", TwentyFour, 1, r }, + { (char*)" Bb / A#", TwentyFour, 2, I }, + { (char*)" Bd", TwentyFour, 3, g }, + { (char*)" B / Cb", TwentyFour, 4, W }, + { (char*)" B+ / Cd", TwentyFour, 5, y }, + // 31 EDO, whole tone = 5, #/b = 2, +/d = 1 + { (char*)" C", ThirtyOne, -23, W }, + { (char*)" C+", ThirtyOne, -22, R }, + { (char*)" C#", ThirtyOne, -21, Y }, + { (char*)" Db", ThirtyOne, -20, C }, + { (char*)" Dd", ThirtyOne, -19, I }, + { (char*)" D", ThirtyOne, -18, W }, + { (char*)" D+", ThirtyOne, -17, R }, + { (char*)" D#", ThirtyOne, -16, Y }, + { (char*)" Eb", ThirtyOne, -15, C }, + { (char*)" Ed", ThirtyOne, -14, I }, + { (char*)" E", ThirtyOne, -13, W }, + { (char*)" E+ / Fb", ThirtyOne, -12, L }, + { (char*)" E# / Fd", ThirtyOne, -11, M }, + { (char*)" F", ThirtyOne, -10, W }, + { (char*)" F+", ThirtyOne, -9, R }, + { (char*)" F#", ThirtyOne, -8, Y }, + { (char*)" Gb", ThirtyOne, -7, C }, + { (char*)" Gd", ThirtyOne, -6, I }, + { (char*)" G", ThirtyOne, -5, W }, + { (char*)" G+", ThirtyOne, -4, R }, + { (char*)" G#", ThirtyOne, -3, Y }, + { (char*)" Ab", ThirtyOne, -2, C }, + { (char*)" Ad", ThirtyOne, -1, I }, + { (char*)" A", ThirtyOne, 0, W }, + { (char*)" A+", ThirtyOne, 1, R }, + { (char*)" A#", ThirtyOne, 2, Y }, + { (char*)" Bb", ThirtyOne, 3, C }, + { (char*)" Bd", ThirtyOne, 4, I }, + { (char*)" B", ThirtyOne, 5, W }, + { (char*)" Cb / B+", ThirtyOne, 6, L }, + { (char*)" Cd / B#", ThirtyOne, 7, M }, + // 41 EDO, whole tone = 7, #/b = 4, +/d = 2, ^/v = 1 + { (char*)" C (vB#)", FortyOne, -31, W }, + { (char*)" ^C / B#", FortyOne, -30, c }, + { (char*)" C+ ", FortyOne, -29, O }, + { (char*)" vC# / Db", FortyOne, -28, I }, + { (char*)" C# / ^Db", FortyOne, -27, R }, + { (char*)" Dd", FortyOne, -26, B }, + { (char*)" vD", FortyOne, -25, y }, + { (char*)" D", FortyOne, -24, W }, + { (char*)" ^D", FortyOne, -23, c }, + { (char*)" D+", FortyOne, -22, O }, + { (char*)" vD# / Eb", FortyOne, -21, I }, + { (char*)" D# / ^Eb", FortyOne, -20, R }, + { (char*)" Ed", FortyOne, -19, B }, + { (char*)" vE", FortyOne, -18, y }, + { (char*)" (^Fb) E", FortyOne, -17, W }, + { (char*)" Fd / ^E", FortyOne, -16, c }, + { (char*)" vF / E+", FortyOne, -15, y }, + { (char*)" F (vE#)", FortyOne, -14, W }, + { (char*)" ^F / E#", FortyOne, -13, c }, + { (char*)" F+", FortyOne, -12, O }, + { (char*)" Gb / vF#", FortyOne, -11, I }, + { (char*)" ^Gb / F#", FortyOne, -10, R }, + { (char*)" Gd", FortyOne, -9, B }, + { (char*)" vG", FortyOne, -8, y }, + { (char*)" G", FortyOne, -7, W }, + { (char*)" ^G", FortyOne, -6, c }, + { (char*)" G+", FortyOne, -5, O }, + { (char*)" vG# / Ab", FortyOne, -4, I }, + { (char*)" G# / ^Ab", FortyOne, -3, R }, + { (char*)" Ad", FortyOne, -2, B }, + { (char*)" vA", FortyOne, -1, y }, + { (char*)" A", FortyOne, 0, W }, + { (char*)" ^A", FortyOne, 1, c }, + { (char*)" A+", FortyOne, 2, O }, + { (char*)" vA# / Bb", FortyOne, 3, I }, + { (char*)" A# / ^Bb", FortyOne, 4, R }, + { (char*)" Bd", FortyOne, 5, B }, + { (char*)" vB", FortyOne, 6, y }, + { (char*)" (^Cb) B", FortyOne, 7, W }, + { (char*)" Cd / ^B", FortyOne, 8, c }, + { (char*)" vC / B+", FortyOne, 9, y }, + // 53 EDO, whole tone = 9, #/b = 5, >/< = 2, ^/v = 1 + { (char*)" C (vB#)", FiftyThree, -40, W }, + { (char*)" ^C / B#", FiftyThree, -39, c }, + { (char*)" >C / <Db", FiftyThree, -38, l }, + { (char*)" <C# / vDb", FiftyThree, -37, O }, + { (char*)" vC# / Db", FiftyThree, -36, I }, + { (char*)" C# / ^Db", FiftyThree, -35, R }, + { (char*)" ^C# / >Db", FiftyThree, -34, B }, + { (char*)" >C# / <D", FiftyThree, -33, g }, + { (char*)" vD", FiftyThree, -32, y }, + { (char*)" D", FiftyThree, -31, W }, + { (char*)" ^D", FiftyThree, -30, c }, + { (char*)" >D / <Eb", FiftyThree, -29, l }, + { (char*)" <D# / vEb", FiftyThree, -28, O }, + { (char*)" vD# / Eb", FiftyThree, -27, I }, + { (char*)" D# / ^Eb", FiftyThree, -26, R }, + { (char*)" ^D# / >Eb", FiftyThree, -25, B }, + { (char*)" >D# / <E", FiftyThree, -24, g }, + { (char*)" Fb / vE", FiftyThree, -23, y }, + { (char*)"(^Fb) E", FiftyThree, -22, W }, + { (char*)"(>Fb) ^E", FiftyThree, -21, c }, + { (char*)" <F / >E", FiftyThree, -20, G }, + { (char*)" vF (<E#)", FiftyThree, -19, y }, + { (char*)" F (vE#)", FiftyThree, -18, W }, + { (char*)" ^F / E#", FiftyThree, -17, c }, + { (char*)" >F / <Gb", FiftyThree, -16, l }, + { (char*)" <F# / vGb", FiftyThree, -15, O }, + { (char*)" vF# / Gb", FiftyThree, -14, I }, + { (char*)" F# / ^Gb", FiftyThree, -13, R }, + { (char*)" ^F# / >Gb", FiftyThree, -12, B }, + { (char*)" >F# / <G", FiftyThree, -11, g }, + { (char*)" vG", FiftyThree, -10, y }, + { (char*)" G", FiftyThree, -9, W }, + { (char*)" ^G", FiftyThree, -8, c }, + { (char*)" >G / <Ab", FiftyThree, -7, l }, + { (char*)" <G# / vAb", FiftyThree, -6, O }, + { (char*)" vG# / Ab", FiftyThree, -5, I }, + { (char*)" G# / ^Ab", FiftyThree, -4, R }, + { (char*)" ^G# / >Ab", FiftyThree, -3, B }, + { (char*)" >G# / <A", FiftyThree, -2, g }, + { (char*)" vA", FiftyThree, -1, y }, + { (char*)" A", FiftyThree, 0, W }, + { (char*)" ^A", FiftyThree, 1, c }, + { (char*)" <Bb / >A", FiftyThree, 2, l }, + { (char*)" vBb / <A#", FiftyThree, 3, O }, + { (char*)" Bb / vA#", FiftyThree, 4, I }, + { (char*)" ^Bb / A#", FiftyThree, 5, R }, + { (char*)" >Bb / ^A#", FiftyThree, 6, B }, + { (char*)" <B / >A#", FiftyThree, 7, g }, + { (char*)" Cb / vB", FiftyThree, 8, y }, + { (char*)"(^Cb) B", FiftyThree, 9, W }, + { (char*)"(>Cb) ^B", FiftyThree, 10, c }, + { (char*)" <C / >B", FiftyThree, 11, G }, + { (char*)" vC (<B#)", FiftyThree, 12, y }, + // 72 EDO, whole tone = 12, #/b = 6, +/d = 3, ^/v = 1 + { (char*)" C (B#)", SeventyTwo, -54, W }, + { (char*)" ^C", SeventyTwo, -53, g }, + { (char*)" vC+", SeventyTwo, -52, r }, + { (char*)" C+", SeventyTwo, -51, p }, + { (char*)" ^C+", SeventyTwo, -50, b }, + { (char*)" vC#", SeventyTwo, -49, y }, + { (char*)" C# / Db", SeventyTwo, -48, I }, + { (char*)" ^C# / ^Db", SeventyTwo, -47, g }, + { (char*)" vDd", SeventyTwo, -46, r }, + { (char*)" Dd", SeventyTwo, -45, p }, + { (char*)" ^Dd", SeventyTwo, -44, b }, + { (char*)" vD", SeventyTwo, -43, y }, + { (char*)" D", SeventyTwo, -42, W }, + { (char*)" ^D", SeventyTwo, -41, g }, + { (char*)" vD+", SeventyTwo, -40, r }, + { (char*)" D+", SeventyTwo, -39, p }, + { (char*)" ^D+", SeventyTwo, -38, b }, + { (char*)" vEb / vD#", SeventyTwo, -37, y }, + { (char*)" Eb / D#", SeventyTwo, -36, I }, + { (char*)" ^Eb / ^D#", SeventyTwo, -35, g }, + { (char*)" vEd", SeventyTwo, -34, r }, + { (char*)" Ed", SeventyTwo, -33, p }, + { (char*)" ^Ed", SeventyTwo, -32, b }, + { (char*)" vE (vFb)", SeventyTwo, -31, y }, + { (char*)" E (Fb)", SeventyTwo, -30, W }, + { (char*)" ^E (^Fb)", SeventyTwo, -29, g }, + { (char*)" vE+ / vFd", SeventyTwo, -28, r }, + { (char*)" E+ / Fd", SeventyTwo, -27, p }, + { (char*)" ^E+ / ^Fd", SeventyTwo, -26, b }, + { (char*)"(vE#) vF", SeventyTwo, -25, y }, + { (char*)" (E#) F", SeventyTwo, -24, W }, + { (char*)"(^E#) ^F", SeventyTwo, -23, g }, + { (char*)" vF+", SeventyTwo, -22, r }, + { (char*)" F+", SeventyTwo, -21, p }, + { (char*)" ^F+", SeventyTwo, -20, b }, + { (char*)" vGb / vF#", SeventyTwo, -19, y }, + { (char*)" Gb / F#", SeventyTwo, -18, I }, + { (char*)" ^Gb / ^F#", SeventyTwo, -17, g }, + { (char*)" vGd", SeventyTwo, -16, r }, + { (char*)" Gd", SeventyTwo, -15, p }, + { (char*)" ^Gd", SeventyTwo, -14, b }, + { (char*)" vG", SeventyTwo, -13, y }, + { (char*)" G", SeventyTwo, -12, W }, + { (char*)" ^G", SeventyTwo, -11, g }, + { (char*)" vG+", SeventyTwo, -10, r }, + { (char*)" G+", SeventyTwo, -9, p }, + { (char*)" ^G+", SeventyTwo, -8, b }, + { (char*)" vG# / vAb", SeventyTwo, -7, y }, + { (char*)" G# / Ab", SeventyTwo, -6, I }, + { (char*)" ^G# / ^Ab", SeventyTwo, -5, g }, + { (char*)" vAd", SeventyTwo, -4, r }, + { (char*)" Ad", SeventyTwo, -3, p }, + { (char*)" ^Ad", SeventyTwo, -2, b }, + { (char*)" vA", SeventyTwo, -1, y }, + { (char*)" A", SeventyTwo, 0, W }, + { (char*)" ^A", SeventyTwo, 1, g }, + { (char*)" vA+", SeventyTwo, 2, r }, + { (char*)" A+", SeventyTwo, 3, p }, + { (char*)" ^A+", SeventyTwo, 4, b }, + { (char*)" vBb / vA#", SeventyTwo, 5, y }, + { (char*)" Bb / A#", SeventyTwo, 6, I }, + { (char*)" ^Bb / ^A#", SeventyTwo, 7, g }, + { (char*)" vBd", SeventyTwo, 8, r }, + { (char*)" Bd", SeventyTwo, 9, p }, + { (char*)" ^Bd", SeventyTwo, 10, b }, + { (char*)" vB (vCb)", SeventyTwo, 11, y }, + { (char*)" B (Cb)", SeventyTwo, 12, W }, + { (char*)" ^B (^Cb)", SeventyTwo, 13, g }, + { (char*)" vB+ / vCd", SeventyTwo, 14, r }, + { (char*)" B+ / Cd", SeventyTwo, 15, p }, + { (char*)" ^B+ / ^Cd", SeventyTwo, 16, b }, + { (char*)"(vB#) vC", SeventyTwo, 17, y }, + // + { (char*)"n/a",BohlenPierce,0,W}, + { (char*)"n/a",BohlenPierce,0,W}, + { (char*)"n/a",BohlenPierce,0,W}, + { (char*)"n/a",BohlenPierce,0,W}, + { (char*)"n/a",BohlenPierce,0,W}, + { (char*)"n/a",BohlenPierce,0,W}, + { (char*)"n/a",BohlenPierce,0,W}, + { (char*)"n/a",BohlenPierce,0,W}, + { (char*)"n/a",BohlenPierce,0,W}, + { (char*)"n/a",BohlenPierce,0,W}, + { (char*)"n/a",BohlenPierce,0,W}, + { (char*)"n/a",BohlenPierce,0,W}, + { (char*)"n/a",BohlenPierce,0,W}, + // + { (char*)"n/a",CarlosA,0,W}, + { (char*)"n/a",CarlosA,0,W}, + { (char*)"n/a",CarlosA,0,W}, + { (char*)"n/a",CarlosA,0,W}, + { (char*)"n/a",CarlosA,0,W}, + { (char*)"n/a",CarlosA,0,W}, + { (char*)"n/a",CarlosA,0,W}, + { (char*)"n/a",CarlosA,0,W}, + { (char*)"n/a",CarlosA,0,W}, + // + { (char*)"n/a",CarlosB,0,W}, + { (char*)"n/a",CarlosB,0,W}, + { (char*)"n/a",CarlosB,0,W}, + { (char*)"n/a",CarlosB,0,W}, + { (char*)"n/a",CarlosB,0,W}, + { (char*)"n/a",CarlosB,0,W}, + { (char*)"n/a",CarlosB,0,W}, + { (char*)"n/a",CarlosB,0,W}, + { (char*)"n/a",CarlosB,0,W}, + { (char*)"n/a",CarlosB,0,W}, + { (char*)"n/a",CarlosB,0,W}, + // + { (char*)"n/a",CarlosG,0,W}, + { (char*)"n/a",CarlosG,0,W}, + { (char*)"n/a",CarlosG,0,W}, + { (char*)"n/a",CarlosG,0,W}, + { (char*)"n/a",CarlosG,0,W}, + { (char*)"n/a",CarlosG,0,W}, + { (char*)"n/a",CarlosG,0,W}, + { (char*)"n/a",CarlosG,0,W}, + { (char*)"n/a",CarlosG,0,W}, + { (char*)"n/a",CarlosG,0,W}, + { (char*)"n/a",CarlosG,0,W}, + { (char*)"n/a",CarlosG,0,W}, + { (char*)"n/a",CarlosG,0,W}, + { (char*)"n/a",CarlosG,0,W}, + { (char*)"n/a",CarlosG,0,W}, + { (char*)"n/a",CarlosG,0,W}, + { (char*)"n/a",CarlosG,0,W}, + { (char*)"n/a",CarlosG,0,W}, + { (char*)"n/a",CarlosG,0,W}, + { (char*)"n/a",CarlosG,0,W}, + }; + const int keyCount = sizeof(keyOptions) / sizeof(keyDef); +// ====== initialize structure to store and recall user preferences + + typedef struct { // put all user-selectable options into a class so +that down the line these can be saved and loaded. + char* presetName; + int tuningIndex; // instead of using pointers, i chose to +store index value of each option, to be saved to a .pref or .ini or +something + int layoutIndex; + int scaleIndex; + int keyIndex; + int transpose; + // define simple recall functions + tuningDef tuning() { + return tuningOptions[tuningIndex]; + }; + layoutDef layout() { + return layoutOptions[layoutIndex]; + }; + scaleDef scale() { + return scaleOptions[scaleIndex]; + }; + keyDef key() { + return keyOptions[keyIndex]; + }; + int layoutsBegin() { + if (tuningIndex == Twelve) { + return 0; + } else { + int temp = 0; + while (layoutOptions[temp].tuning < tuningIndex) { + temp++; + }; + return temp; + }; + }; + int keysBegin() { + if (tuningIndex == Twelve) { + return 0; + } else { + int temp = 0; + while (keyOptions[temp].tuning < tuningIndex) { + temp++; + }; + return temp; + }; + }; + int findC() { + return keyOptions[keysBegin()].offset; + }; + } presetDef; + presetDef current = { + (char*)"Default", + Twelve, // see the relevant enum{} statement + 0, // default to the first layout, wicki hayden + 0, // default to using no scale (chromatic) + 0, // default to the key of C + 0 // default to no transposition + }; +// ====== functions - // Set pinModes for the digital button matrix. - for (int pinNumber = 0; pinNumber < columnCount; pinNumber++) // For each column pin... - { - pinMode(columns[pinNumber], INPUT_PULLUP); // set the pinMode to INPUT_PULLUP (+3.3V / HIGH). + int positiveMod(int n, int d) { + return (((n % d) + d) % d); } - pinMode(m1p, OUTPUT); // Setting the row multiplexer pins to output. - pinMode(m2p, OUTPUT); - pinMode(m4p, OUTPUT); - pinMode(m8p, OUTPUT); - - strip.begin(); // INITIALIZE NeoPixel strip object - strip.show(); // Turn OFF all pixels ASAP - strip.setBrightness(stripBrightness); // Set BRIGHTNESS (max = 255) - setCMD_LEDs(); - strip.setPixelColor(cmdBtn1, strip.ColorHSV(65536 / 12, 255, pressedBrightness)); - selectedScale = &chromaticScale; // Set default scale - setLayoutLEDs(); - - u8g2.begin(); //Menu and graphics setup - u8g2.setBusClock(1000000); // Speed up display - u8g2.setContrast(stripBrightness / 2); - menu.setSplashDelay(0); - menu.init(); - setupMenu(); - menu.drawMenu(); - - // wait until device mounted, maybe - for (int i = 0; i < 5 && !TinyUSBDevice.mounted(); i++) delay(1); - - // Print diagnostic troubleshooting information to serial monitor - diagnosticTest(); -} - -void setup1() { //Second core exclusively runs encoder - //pinMode(ROTA, INPUT_PULLUP); - //pinMode(ROTB, INPUT_PULLUP); - //encoder_init(); -} - -// ------------------------------------------------------------------------------------------------------------------------------------------------------------ -// START LOOP SECTION -void loop() { - - // Time tracking function - timeTracker(); - - // Reduces wear-and-tear on OLED panel - screenSaver(); - - // Read and store the digital button states of the scanning matrix - readDigitalButtons(); - - if (sequencerMode) { - // Cause newpresses to change stuff - sequencerToggleThingies(); - - // If it's time to play notes, play them - sequencerMaybePlayNotes(); - } else { - // Act on those buttons - playNotes(); - if (buzzer == BUZZER_ARP_UP) { - arp(1); - } else if (buzzer == BUZZER_ARP_DOWN) { - arp(-1); - } - - if (pitchModToggle) { - // Pitch bend stuff - pitchBend(); - } else { - // mod wheel stuff - modWheel(); - } - - // Held buttons - heldButtons(); + coordinates indexToCoord(byte x) { + coordinates temp; + temp.row = (x / 10); + temp.col = (2 * (x % 10)) + (temp.row & 1); + return temp; } - - // Animations - reactiveLighting(); - - // Do the LEDS - strip.show(); - - // Read any new MIDI messages - MIDI.read(); - - // Read menu navigation functions - menuNavigation(); -} - -void loop1() { - rotate(); // Reads the encoder on the second core to avoid missed steps - //readEncoder(); -} -// END LOOP SECTION -// ------------------------------------------------------------------------------------------------------------------------------------------------------------ - - -// ------------------------------------------------------------------------------------------------------------------------------------------------------------ -// START FUNCTIONS SECTION - -void timeTracker() { - loopTime = currentTime - previousTime; - if (diagnostics == 4) { //Print out the time it takes to run each loop - Serial.println(loopTime); + bool hexOOB(coordinates c) { + return (c.row < 0) + || (c.row >= rowCount) + || (c.col < 0) + || (c.col >= (2 * colCount)) + || ((c.col + c.row) & 1); } - // Update previouTime variable to give us a reference point for next loop - previousTime = currentTime; - // Store the current time in a uniform variable for this program loop - currentTime = millis(); -} - -void diagnosticTest() { - if (diagnostics > 0) { - Serial.println("Zach was here"); + byte coordToIndex(coordinates c) { + if (hexOOB(c)) { + return 255; + } else { + return (10 * c.row) + (c.col / 2); + }; } -} - -void commandPress(byte command) { - // 14,15 undefined - // 20-31 undefined - // 35, 41 undefined - // 46-63 undefined - //84-90 undefined - // 102-119 undefined - if (command == EXTR_1) { - MIDI.sendControlChange(85, 127, midiChannel); - } else if (command == EXTR_2) { - MIDI.sendControlChange(86, 127, midiChannel); - } else if (command == EXTR_3) { - MIDI.sendControlChange(87, 127, midiChannel); - } else if (command == EXTR_4) { - MIDI.sendControlChange(88, 127, midiChannel); - } else if (command == EXTR_5) { - MIDI.sendControlChange(89, 127, midiChannel); - } else if (command == CMDB_1) { - midiVelocity = 100; - setCMD_LEDs(); - strip.setPixelColor(cmdBtn1, strip.ColorHSV(65536 / 12, 255, pressedBrightness)); - } else if (command == CMDB_2) { - midiVelocity = 60; - setCMD_LEDs(); - strip.setPixelColor(cmdBtn2, strip.ColorHSV(65536 / 3, 255, pressedBrightness)); - } else if (command == CMDB_3) { - midiVelocity = 20; - setCMD_LEDs(); - strip.setPixelColor(cmdBtn3, strip.ColorHSV(65536 / 2, 255, pressedBrightness)); - } else if (command == CMDB_4) { - pitchModToggle = !pitchModToggle; // Toggles between pitch bend and mod wheel - } else if (command == CMDB_5) { - } else if (command == CMDB_6) { - } else if (command == CMDB_7) { + 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; } -} -void commandRelease(byte command) { - switch(command) { - case EXTR_1: - MIDI.sendControlChange(85, 0, midiChannel); - break; - case EXTR_2: - MIDI.sendControlChange(86, 0, midiChannel); - break; - case EXTR_3: - MIDI.sendControlChange(87, 0, midiChannel); - break; - case EXTR_4: - MIDI.sendControlChange(88, 0, midiChannel); - break; - case EXTR_5: - MIDI.sendControlChange(89, 0, midiChannel); - break; + coordinates hexOffset(coordinates a, coordinates b) { + coordinates temp; + temp.row = a.row + b.row; + temp.col = a.col + b.col; + return temp; } -} - -void pitchBend() { - // Default: no pitch change - int pitchBendTarget = 0; - // Otherwise set the targetted value based on the buttons pressed. - if (activeButtons[cmdBtn5] && !activeButtons[cmdBtn6] && !activeButtons[cmdBtn7]) { - pitchBendTarget = 8191; // Whole pitch up - } else if (activeButtons[cmdBtn5] && activeButtons[cmdBtn6] && !activeButtons[cmdBtn7]) { - pitchBendTarget = 4096; // Half pitch up - } else if (!activeButtons[cmdBtn5] && activeButtons[cmdBtn6] && activeButtons[cmdBtn7]) { - pitchBendTarget = -4096; // Half pitch down - } else if (!activeButtons[cmdBtn5] && !activeButtons[cmdBtn6] && activeButtons[cmdBtn7]) { - pitchBendTarget = -8192; // Whole pitch down + coordinates hexDistance(coordinates origin, coordinates destination) { + coordinates temp; + temp.row = destination.row - origin.row; + temp.col = destination.col - origin.col; + return temp; } - - // Approach the target, sendPitchBend based on timing - if (pitchBendPosition != pitchBendTarget) { - modPitchTime = modPitchTime + loopTime; - if (modPitchTime >= 20) { // Only run this loop every 20 ms to avoid overrunning MIDI - modPitchTime = 0; // Reset clock. - // if distance between current value and target is less than the mod speed, - if (abs(pitchBendPosition - pitchBendTarget) < pitchBendSpeed) { - // don't go past the target. - pitchBendPosition = pitchBendTarget; - } else if (pitchBendPosition > pitchBendTarget) { - // otherwise, subtract (or add) the speed to approach the target. - pitchBendPosition = pitchBendPosition - pitchBendSpeed; - } else if (pitchBendPosition < pitchBendTarget) { - pitchBendPosition = pitchBendPosition + pitchBendSpeed; - } - // Always send the pitchBend because pitchBendPosition changed. - MIDI.sendPitchBend(pitchBendPosition, midiChannel); - } + float freqToMIDI(float Hz) { // formula to convert from +Hz to MIDI note + return 69.0 + 12.0 * log2f(Hz / 440.0); } - - if (pitchBendPosition == pitchBendTarget && modPitchTime != 200) { // When the pitchbend hits the target... - modPitchTime = 200; // ...reset the clock for instant responsiveness upon button change + float MIDItoFreq(float MIDI) { // formula to convert from +MIDI note to Hz + return 440.0 * exp2((MIDI - 69.0) / 12.0); } - // Set mode indicator button red if in pitch bend mode - strip.setPixelColor(cmdBtn4, strip.ColorHSV(0, 255, defaultBrightness)); - // Set the lights - if (pitchBendPosition > 0) { - strip.setPixelColor(cmdBtn5, strip.ColorHSV(0, 255, ((pitchBendPosition / 32) - 1))); - strip.setPixelColor(cmdBtn6, strip.ColorHSV(0, 255, (-pitchBendPosition / 32))); - strip.setPixelColor(cmdBtn7, strip.ColorHSV(0, 255, 0)); + float stepsToMIDI(int16_t stepsFromA) { // return the MIDI pitch associated + return freqToMIDI(concertA) + ((float)stepsFromA * +(float)current.tuning().stepSize / 100.0); } - if (pitchBendPosition == 0) { - strip.setPixelColor(cmdBtn5, strip.ColorHSV(0, 255, 0)); - strip.setPixelColor(cmdBtn6, strip.ColorHSV(0, 255, 255)); - strip.setPixelColor(cmdBtn7, strip.ColorHSV(0, 255, 0)); + +// ====== diagnostic wrapper + + void sendToLog(String msg) { + if (diagnostics) { + Serial.println(msg); + }; } - if (pitchBendPosition < 0) { - strip.setPixelColor(cmdBtn5, strip.ColorHSV(0, 255, 0)); - strip.setPixelColor(cmdBtn6, strip.ColorHSV(0, 255, (pitchBendPosition / 32))); - strip.setPixelColor(cmdBtn7, strip.ColorHSV(0, 255, ((-pitchBendPosition / 32) - 1))); + +// ====== 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 modWheel() { - //Set targetted value based on what keys are being pressed. - byte modWheelTarget = 0; - if (activeButtons[cmdBtn5] && !activeButtons[cmdBtn6] && !activeButtons[cmdBtn7]) { - modWheelTarget = 127; - } else if (activeButtons[cmdBtn5] && activeButtons[cmdBtn6] && !activeButtons[cmdBtn7]) { - modWheelTarget = 100; - } else if (!activeButtons[cmdBtn5] && activeButtons[cmdBtn6] && !activeButtons[cmdBtn7]) { - modWheelTarget = 75; - } else if (activeButtons[cmdBtn5] && activeButtons[cmdBtn7]) { - modWheelTarget = 75; - } else if (!activeButtons[cmdBtn5] && activeButtons[cmdBtn6] && activeButtons[cmdBtn7]) { - modWheelTarget = 50; - } else if (!activeButtons[cmdBtn5] && !activeButtons[cmdBtn6] && activeButtons[cmdBtn7]) { - modWheelTarget = 25; - } else if (!activeButtons[cmdBtn5] && !activeButtons[cmdBtn6] && !activeButtons[cmdBtn7]) { - modWheelTarget = 0; + 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 { + // + }; + }; } - if (modWheelPosition != modWheelTarget) { // Only runs the clock when mod target differs from actual - modPitchTime = modPitchTime + loopTime; - if (modPitchTime >= 20) { // If it has been over 20 ms from last run, - modPitchTime = 0; // reset clock. - // if distance between current value and target is less than the mod speed, - if (abs(modWheelPosition - modWheelTarget) < modWheelSpeed) { - // don't go past the target. - modWheelPosition = modWheelTarget; - } else if (modWheelPosition > modWheelTarget) { - // otherwise, subtract (or add) the speed to approach the target. - modWheelPosition = modWheelPosition - modWheelSpeed; - } else if (modWheelPosition < modWheelTarget) { - modWheelPosition = modWheelPosition + modWheelSpeed; - } - // Always send the control change because modWheelPosition changed. - MIDI.sendControlChange(1, modWheelPosition, midiChannel); - } +// ====== 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."); } - if (modWheelPosition == modWheelTarget && modPitchTime != 200) { - modPitchTime = 200; // Resets clock when modwheel hits target for instant responsiveness upon button change + 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."); } - // Set mode indicator button green if in mod wheel mode - strip.setPixelColor(cmdBtn4, strip.ColorHSV(21854, 255, defaultBrightness)); - // Set the pixel colors based on the (new) modWheelPosition. - if (modWheelPosition == 0) { - strip.setPixelColor(cmdBtn5, strip.ColorHSV(21854, 255, 0)); - strip.setPixelColor(cmdBtn6, strip.ColorHSV(21854, 255, 0)); - strip.setPixelColor(cmdBtn7, strip.ColorHSV(21854, 255, 0)); - } else if (modWheelPosition > 0 && modWheelPosition < 25) { - strip.setPixelColor(cmdBtn5, strip.ColorHSV(21854, 255, 0)); - strip.setPixelColor(cmdBtn6, strip.ColorHSV(21854, 255, 0)); - strip.setPixelColor(cmdBtn7, strip.ColorHSV(21854, 255, (modWheelPosition * 10))); - } else if (modWheelPosition == 25) { - strip.setPixelColor(cmdBtn5, strip.ColorHSV(21854, 255, 0)); - strip.setPixelColor(cmdBtn6, strip.ColorHSV(21854, 255, 0)); - strip.setPixelColor(cmdBtn7, strip.ColorHSV(21854, 255, 255)); - } else if (modWheelPosition > 25 && modWheelPosition < 75) { - strip.setPixelColor(cmdBtn5, strip.ColorHSV(21854, 255, 0)); - strip.setPixelColor(cmdBtn6, strip.ColorHSV(21854, 255, ((modWheelPosition - 25) * 5))); - strip.setPixelColor(cmdBtn7, strip.ColorHSV(21854, 255, 255)); - } else if (modWheelPosition == 75) { - strip.setPixelColor(cmdBtn5, strip.ColorHSV(21854, 255, 0)); - strip.setPixelColor(cmdBtn6, strip.ColorHSV(21854, 255, 255)); - strip.setPixelColor(cmdBtn7, strip.ColorHSV(21854, 255, 255)); - } else if (modWheelPosition > 75 && modWheelPosition < 125) { - strip.setPixelColor(cmdBtn5, strip.ColorHSV(21854, 255, ((modWheelPosition - 75) * 5))); - strip.setPixelColor(cmdBtn6, strip.ColorHSV(21854, 255, 255)); - strip.setPixelColor(cmdBtn7, strip.ColorHSV(21854, 255, 255)); - } else if (modWheelPosition >= 125) { - strip.setPixelColor(cmdBtn5, strip.ColorHSV(21854, 255, 255)); - strip.setPixelColor(cmdBtn6, strip.ColorHSV(21854, 255, 255)); - strip.setPixelColor(cmdBtn7, strip.ColorHSV(21854, 255, 255)); + 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."); } -} - -// BUTTONS // -void readDigitalButtons() { - if (diagnostics == 1) { - Serial.println(); +// ====== 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; } - // Button Deck - for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) // Iterate through each of the row pins on the multiplexing chip. - { - digitalWrite(m1p, rowIndex & 1); - digitalWrite(m2p, (rowIndex & 2) >> 1); - digitalWrite(m4p, (rowIndex & 4) >> 2); - digitalWrite(m8p, (rowIndex & 8) >> 3); - for (byte columnIndex = 0; columnIndex < columnCount; columnIndex++) // Now iterate through each of the column pins that are connected to the current row pin. - { - byte columnPin = columns[columnIndex]; // Hold the currently selected column pin in a variable. - pinMode(columnPin, INPUT_PULLUP); // Set that row pin to INPUT_PULLUP mode (+3.3V / HIGH). - byte buttonNumber = columnIndex + (rowIndex * columnCount); // Assign this location in the matrix a unique number. - delayMicroseconds(10); // Delay to give the pin modes time to change state (false readings are caused otherwise). - previousActiveButtons[buttonNumber] = activeButtons[buttonNumber]; // Track the "previous" variable for comparison. - byte buttonState = digitalRead(columnPin); // (don't)Invert reading due to INPUT_PULLUP, and store the currently selected pin state. - if (buttonState == LOW) { - if (diagnostics == 1) { - Serial.print("1"); - } else if (diagnostics == 2) { - Serial.println(buttonNumber); - } - if (!previousActiveButtons[buttonNumber]) { - // newpress time - activeButtonsTime[buttonNumber] = millis(); - } - activeButtons[buttonNumber] = 1; - } else { - // Otherwise, the button is inactive, write a 0. - if (diagnostics == 1) { - Serial.print("0"); - } - activeButtons[buttonNumber] = 0; - } - // Set the selected column pin back to INPUT mode (0V / LOW). - pinMode(columnPin, INPUT); - } + 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 + }; } -} - -// Function to check if a note is within the selected scale -bool isNotePlayable(byte note) { - if (!scaleLock) { - return true; // Return true unconditionally if the toggle is disabled +// ====== 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) + )); } - note = (note - key + transpose) % 12; - if(tones != 12 || (*selectedScale)[note]) { - return true; + 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; + }; } - return false; -} -// Used by things not affected by scaleLock -bool isNoteLit(byte note) { - note = (note - key + transpose) % 12; - if(tones != 12 || (*selectedScale)[note%12]){ - return true; + 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))); + }; + }; } - return false; -} -void playNotes() { - for (int i = 0; i < elementCount; i++) // For all buttons in the deck - { - if (activeButtons[i] != previousActiveButtons[i]) // If a change is detected - { - if (activeButtons[i] == 1) // If the button is active (newpress) - { - if (currentLayout[i] < 128) { - if (isNotePlayable(currentLayout[i])) { // If note is within the selected scale, light up and play - //strip.setPixelColor(i, keyColor(currentLayout[i], pressedBrightness)); - noteOn(midiChannel, (currentLayout[i] + transpose) % 128, midiVelocity); - } - } else { - commandPress(currentLayout[i]); - } - } else { - // If the button is inactive (released) - if (currentLayout[i] < 128) { - if (isNotePlayable(currentLayout[i])) { - //setLayoutLED(i); - noteOff(midiChannel, (currentLayout[i] + transpose) % 128, 0); - } + 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 { - commandRelease(currentLayout[i]); - } - } - } + temp = openChannelQueue.front(); + openChannelQueue.pop(); + sendToLog(String("popped " + String(temp) + " off the queue")); + }; + }; + return temp; + }; } -} - -void reactiveLighting() { - animationTime = animationTime + loopTime; - if (animationTime >= 33) { // If it has been at least 33 ms (30fps) from last run, - animationTime = 0; // reset clock. - cycleNumber++; - if (cycleNumber > 11) { // Set position in 6-step cycle - cycleNumber = 0; - } - setLayoutLEDs(); // Start by setting the lights to their defaults so we can "paint" on top of it. - for (int i = 0; i < elementCount; i++) { // Scanning through the buttons - if (isNotePlayable(currentLayout[i]) && currentLayout[i] < 128) { // (if they are playable) - switch (lightMode) { // and implementing the selected lighting pattern - case 0: - buttonPattern(i); // Lights up the button pressed. - break; + +// ====== 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: - notePattern(i); // Lights up the same exact notes as played across the array. - break; - case 2: - octavePattern(i); // Lights up the same notes as played in all octaves across the array. - break; - case 3: - splashPattern(i); // Creates an expanding ring around the pressed button. - break; - case 4: - starPattern(i); // Creates a starburst around the pressed button. + channelPoly[c - 1]--; + openChannelQueue.push(c); break; - case 5: - orbitPattern(i); // Lights orbit around the pressed button. + case 0: break; - default: // Just in case something goes wrong? - buttonPattern(i); + default: + channelPoly[c - 1]--; break; - } - } - } + }; + }; + if (buzzer) { + buzz(nextHeldNote()); + }; + }; } -} -uint16_t keyHue(byte note) { - // 60072 - return ((note - key + transpose) % tones) * ((65536)/tones); -} - -uint32_t keyColor(byte note, byte brightness) { - // The brightness being variable is a reason not to cache this value. - // TODO: generalize the brightness code. - if (colorMode == 0) { // "scale" - return strip.ColorHSV(keyHue(note), 255, brightness); - } else if (colorMode == 1) { // "tone" - if (tones == 12) { - switch (note%tones) { - // White keys - case 0: case 2: case 4: case 5: case 7: case 9: case 11: - return strip.ColorHSV(0, 0, brightness); - // Black keys - case 1: case 3: case 6: case 8: case 10: - return strip.ColorHSV(5461, 128, brightness); - } - } else if (tones == 24) { - return 3; - } else if (tones == 41) { - switch (note%tones) { - // TODO: Fix the brightness of these so contrast is better. - // C D E F G A B - case 0: case 7: case 14: case 17: case 24: case 31: case 38: - return strip.ColorHSV(0, 0, brightness); - // C^ D^ E^ F^ G^ A^ B^ - case 1: case 8: case 15: case 18: case 25: case 32: case 39: - return strip.ColorHSV(39*256, 0.44*256, brightness); - // C+ D+ F+ G+ A+ - case 2: case 9: case 19: case 26: case 33: - return strip.ColorHSV(230*256, 0.35*256, brightness); - // Db Eb Gb Ab Bb - case 3: case 10: case 20: case 27: case 34: - return strip.ColorHSV(166*256, 0.64*256, brightness); - // C# D# F# G# A# - case 4: case 11: case 21: case 28: case 35: - return strip.ColorHSV(66*256, 0.83*256, brightness); - // Dd Ed Gd Ad Bd - case 5: case 12: case 22: case 29: case 36: - return strip.ColorHSV(10*256, 0.52*256, brightness); - // Dv Ev Fv Gv Av Bv Cv - case 6: case 13: case 16: case 23: case 30: case 37: case 40: - return strip.ColorHSV(198*256, 0.43*256, brightness); - - } - } - } - return 0; -} - -void buttonPattern(int i) { - if (activeButtons[i] == 1) { // If it's an active button... - // ...then we light it up! - strip.setPixelColor(i, keyColor(currentLayout[i],pressedBrightness)); + 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 notePattern(int i) { - if (activeButtons[i] == 1) { // Check to see if the it's an active button. - for (int m = 0; m < elementCount; m++) { // Scanning through all the lights - if (currentLayout[m] < 128) { // Only runs on lights in the playable area - if (currentLayout[m] == currentLayout[i]) { // If it's the same note as the active button... - // ...then we light it up! - strip.setPixelColor(m, keyColor(currentLayout[m], pressedBrightness)); - } - } - } + void cmdOff(byte x) { // pitch bend wheel only if buttons held. + // nothing; should all be taken care of within the wheelDef structure } -} - -void octavePattern(int i) { - if (activeButtons[i] == 1) { // Check to see if the it's an active button. - for (int m = 0; m < elementCount; m++) { // Scanning through all the lights - if (currentLayout[m] < 128) { // Only runs on lights in the playable area - if (currentLayout[m] % tones == currentLayout[i] % tones) { // If it's in different octaves as the active button... - // ...then we light it up! - strip.setPixelColor(m, keyColor(currentLayout[m], pressedBrightness)); - } - } - } + +// ====== animations + + void flagToAnimate(coordinates C) { + if (!hexOOB(C)) { + h[coordToIndex(C)].animate = 1; + }; } -} - -/* Kinda inefficient - adds 3ms to the loop timer for every 4 buttons held, but does not affect playability yet. -If performance becomes an issue, this may be better handled by a lookup table or something like that. -Another thing I'm considering is having an array for the lights that is populated as it runs the loops and only does setPixelColor at the -end. This would allow me to have the brightness fade and only add to the array if the light is brighter than what's currently there.*/ -void splashPattern(int i) { - int x1 = i % 10; // Calculate the coordinates of the pressed button - int y1 = i / 10; - if (animationStep[i] > 0) { // Oh boy, animation time! Might be overcomplicating this... - for (int m = 0; m < elementCount; m++) { // Scanning through all the lights - if (currentLayout[m] < 128) { // Only runs on lights in the playable area - int x2 = m % 10; // Coordinates of lights - int y2 = m / 10; - int dx = x1 - x2; // Difference between light and button pressed - int dy = y1 - y2; -// Penalty int shifts the lights over depending on if they are on odd or even rows to correct for the staggered rows -#if ModelNumber == 1 - int penalty = (((y1 % 2 == 0) && (y2 % 2 != 0) && (x1 > x2)) || ((y2 % 2 == 0) && (y1 % 2 != 0) && (x2 > x1))) ? 1 : 0; -#elif ModelNumber == 2 - int penalty = (((y1 % 2 == 0) && (y2 % 2 != 0) && (x1 < x2)) || ((y2 % 2 == 0) && (y1 % 2 != 0) && (x2 < x1))) ? 1 : 0; -#endif - // If the light is the correct distance from the button... - if (max(abs(dy), abs(dx) + floor(abs(dy) / 2) + penalty) == animationStep[i]) { - // light it up! - strip.setPixelColor(m, keyColor(currentLayout[m], pressedBrightness)); - // or we could have it fade as it moves - //strip.setPixelColor(m, keyColor(currentLayout[m], (pressedBrightness - animationStep[i]*12))); - } - } - } + 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; + }; + }; + }; + }; + }; } - if (activeButtons[i] == 1) { // Check to see if the it's an active button. - // Then we light up the pressed button - strip.setPixelColor(i, keyColor(currentLayout[i], pressedBrightness)); - if (animationStep[i] < 16) { - animationStep[i]++; // Increment the animation to the next step for next time. - } - } else { - animationStep[i] = 0; // Stop the animation if the key is released + 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 starPattern(int i) { // This one is far more efficient with no noticeable performance hit when playing lots of notes at once. - int x1 = i % 10; // Calculate the coordinates of the pressed button - int y1 = i / 10; - // Define the relative offsets of neighboring buttons in the pattern -#if ModelNumber == 1 - int offsets[][2] = { - { 0, 1 }, // Left - { 0, -1 }, // Right - { -1, (y1 % 2 == 0) ? 0 : -1 }, // Top Left (adjusted based on row parity) - { -1, (y1 % 2 == 0) ? 1 : 0 }, // Top Right (adjusted based on row parity) - { 1, (y1 % 2 == 0) ? 1 : 0 }, // Bottom Right (adjusted based on row parity) - { 1, (y1 % 2 == 0) ? 0 : -1 } // Bottom Left (adjusted based on row parity) - }; -#elif ModelNumber == 2 - int offsets[][2] = { - { 0, -1 }, // Left - { 0, 1 }, // Right - { -1, (y1 % 2 == 0) ? 0 : 1 }, // Top Left (adjusted based on row parity) - { -1, (y1 % 2 == 0) ? -1 : 0 }, // Top Right (adjusted based on row parity) - { 1, (y1 % 2 == 0) ? -1 : 0 }, // Bottom Right (adjusted based on row parity) - { 1, (y1 % 2 == 0) ? 0 : 1 } // Bottom Left (adjusted based on row parity) - }; -#endif - - - if (animationStep[i] > 0) { // Oh boy, animation time! - for (const auto& offset : offsets) { - // Calculate the neighboring button coordinates - int y2 = y1 + offset[0] * animationStep[i]; -#if ModelNumber == 1 - int x2 = x1 + offset[1] * animationStep[i] + ((y1 % 2 == 0) ? -1 : 1) * ((y1 == y2) ? 0 : 1) * (animationStep[i] / 2); -#elif ModelNumber == 2 - int x2 = x1 + offset[1] * animationStep[i] + ((y1 % 2 == 0) ? 1 : -1) * ((y1 == y2) ? 0 : 1) * (animationStep[i] / 2); -#endif - // Check if the neighboring button is within the layout boundaries - if (y2 >= 0 && y2 < 14 && x2 >= 0 && x2 < 10) { - // Calculate the index of the neighboring button - int neighborIndex = y2 * 10 + x2; - if (currentLayout[neighborIndex] < 128) { // If it's in the playable area... - // ...set the color for the neighboring button - strip.setPixelColor(neighborIndex, keyColor(currentLayout[neighborIndex], pressedBrightness)); - } - } - } + 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 + }; + }; + }; + }; + }; } - - if (activeButtons[i] == 1) { // Check to see if the it's an active button. - // Then we light up the pressed button - strip.setPixelColor(i, keyColor(currentLayout[i], pressedBrightness)); - if (animationStep[i] < 16) { - animationStep[i]++; // Increment the animation to the next step for next time. - } - } else { - animationStep[i] = 0; // Stop the animation if the key is released +// ====== 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 orbitPattern(int i) { // Lights orbiting around the held note. - int x1 = i % 10; // Calculate the coordinates of the pressed button - int y1 = i / 10; - // Define the relative offsets of neighboring buttons in the pattern -#if ModelNumber == 1 - int offsets[][2] = { - { 0, 1 }, // Left - { -1, (y1 % 2 == 0) ? 1 : 0 }, // Top Left (adjusted based on row parity) - { -1, (y1 % 2 == 0) ? 0 : -1 }, // Top Right (adjusted based on row parity) - { 0, -1 }, // Right - { 1, (y1 % 2 == 0) ? 0 : -1 }, // Bottom Right (adjusted based on row parity) - { 1, (y1 % 2 == 0) ? 1 : 0 } // Bottom Left (adjusted based on row parity) - }; -#elif ModelNumber == 2 - int offsets[][2] = { - { 0, -1 }, // Left - { -1, (y1 % 2 == 0) ? -1 : 0 }, // Top Left (adjusted based on row parity) - { -1, (y1 % 2 == 0) ? 0 : 1 }, // Top Right (adjusted based on row parity) - { 0, 1 }, // Right - { 1, (y1 % 2 == 0) ? 0 : 1 }, // Bottom Right (adjusted based on row parity) - { 1, (y1 % 2 == 0) ? -1 : 0 } // Bottom Left (adjusted based on row parity) - }; -#endif - - if (activeButtons[i] == 1) { // Check to see if the it's an active button. - // Then we light up the pressed button - strip.setPixelColor(i, keyColor(currentLayout[i], pressedBrightness)); - // Calculate the neighboring button coordinates - int y2 = y1 + offsets[cycleNumber/2][0]; - int x2 = x1 + offsets[cycleNumber/2][1]; - // Check if the neighboring button is within the layout boundaries - if (y2 >= 0 && y2 < 14 && x2 >= 0 && x2 < 10) { - // Calculate the index of the neighboring button - int neighborIndex = y2 * 10 + x2; - if (currentLayout[neighborIndex] < 128) { // If it's in the playable area... - // ...set the color for the neighboring button - strip.setPixelColor(neighborIndex, keyColor(currentLayout[neighborIndex], pressedBrightness)); - } - } + 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 heldButtons() { - for (int i = 0; i < elementCount; i++) { - if (activeButtons[i]) { - //if ( - } + 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 sequencerToggleThingies() { - // For all buttons in the deck - for (int i = 0; i < elementCount; i++) { - // Some change was made - if (activeButtons[i] != previousActiveButtons[i]) { - // newpress - if (activeButtons[i]) { - int stripN = i / 20; - if (stripN >= NLANES) continue; // avoid an error. - int step = map2step(i % 20); - if (step >= 0) { - int offset = lanes[stripN].bank * 16; - lanes[stripN].steps[step + offset] = !lanes[stripN].steps[step + offset]; - int color = 0; - if (lanes[stripN].steps[step + offset]) color = 255; - strip.setPixelColor(i, color, color, color); - } else if (step == -1) { // switching banks - lanes[stripN].bank = !lanes[stripN].bank; - int offset = lanes[stripN].bank * 16; - for (int j = 0; j < 16; j++) { - int color = 0; - if (lanes[stripN].steps[j + offset]) color = 255; - strip.setPixelColor((stripN * 20) + step2map(j), color, color, color); - } - } - } - } + 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.")); } -} -// TODO: Redefine these for hexboard v2 -int map2step(int i) { - if (i >= 10) { - return (19 - i) * 2; + 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(); } - if (i <= 8) { - return ((8 - i) * 2) + 1; + 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(); } - return -1; -} -int step2map(int step) { - if (step % 2) { - return 8 - ((step - 1) / 2); + 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(); } - return 19 - (step / 2); -} - -void sequencerMaybePlayNotes() { - // TODO: sometimes call sequencerPlayNextNote(); -} - -// Do the next note, and increment the sequencer counter -void sequencerPlayNextNote() { - bool anySolo = false; - for (int i = 0; i < NLANES; i++) { - // bitwise check if it's soloed - if (lanes[i].state & STATE_SOLO) { - anySolo = true; - } + 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); - for (int i = 0; i < NLANES; i++) { - // If something is soloed (not this one), then don't do the thing - if (anySolo && !(lanes[i].state & STATE_SOLO)) { - continue; - } - // If this one was muted, don't do the thing. - if (lanes[i].state & STATE_MUTE) { - continue; - } - int offset = lanes[i].bank * 16; - if (lanes[i].steps[sequencerStep + offset]) { - // do the thing. - noteOn(midiChannel, lanes[i].instrument % 128, midiVelocity); - // TODO: Change when the noteoff is played? - noteOff(midiChannel, lanes[i].instrument % 128, 0); - } } - // increment and confine to limit - sequencerStep++; - sequencerStep %= 16; -} - -// Return the first note that is currently held. -byte getHeldNote() { - for (int i = 0; i < elementCount; i++) { - if (activeButtons[i]) { - if (currentLayout[i] < 128 && isNotePlayable(currentLayout[i])) { - return (currentLayout[i] + transpose) % 128; - } - } +// ====== 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() } - return 128; -} -byte getNextNote(int direction, byte note) { - // Find the notes after note in the direction which is held. - for (int i = 0; i < 127; i++) { - byte target = (128 + note + ((1 + i)*direction)) % 128; - if (pitchRef[target]) { - return target; + 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"); } } - return 128; -} - -void arp(int direction) { - if (currentTime - arpTime > arpThreshold){ - arpTime = millis(); - byte target = 128; - target = getNextNote(direction, curr_pitch); - if (target != 128) { - do_tone(target); - } - } -} - -void do_tone(byte pitch) { - curr_pitch = pitch; - if (pitch > 127 || pitch < 0) { - noTone(TONEPIN); - return; + 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). } - if (tones == 12) { - tone(TONEPIN, pitches[pitch]); - } else if (tones == 19) { - tone(TONEPIN, pitches19[pitch]); - } else if (tones == 24) { - tone(TONEPIN, pitches24[pitch]); - } else if (tones == 31) { - tone(TONEPIN, pitches31[pitch]); - } else if (tones == 41) { - tone(TONEPIN, pitches41[pitch]); - } else if (tones == 72) { - tone(TONEPIN, pitches72[pitch]); + for (byte p = 0; p < sizeof(multiplexPins); p++) // For each column pin... + { + pinMode(multiplexPins[p], OUTPUT); // Setting the row +multiplexer pins to output. } -} - -// MIDI AND OTHER OUTPUTS // -// Send Note On -void noteOn(byte channel, byte pitch, byte velocity) { - MIDI.sendNoteOn(pitch, velocity, channel); - pitchRef[pitch] = true; - if (diagnostics == 3) { - Serial.print(pitch); - Serial.print(", "); - Serial.print(velocity); - Serial.print(", "); - Serial.println(channel); + Wire.setSDA(lightPinSDA); + Wire.setSCL(lightPinSCL); + pinMode(rotaryPinC, INPUT_PULLUP); } - if (buzzer) { - do_tone(pitch); + 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(); } -} -// Send Note Off -void noteOff(byte channel, byte pitch, byte velocity) { - MIDI.sendNoteOff(pitch, velocity, channel); - pitchRef[pitch] = false; - noTone(TONEPIN); - if (buzzer) { - byte anotherPitch = getHeldNote(); - if (anotherPitch < 128) { - do_tone(anotherPitch); - } else { - } + void setupLEDs() { // need layout + strip.begin(); // INITIALIZE NeoPixel strip object + strip.show(); // Turn OFF all pixels ASAP + resetHexLEDs(); } -} - -// LEDS // -void setCMD_LEDs() { - strip.setPixelColor(cmdBtn1, strip.ColorHSV(65536 / 12, 255, dimBrightness)); - strip.setPixelColor(cmdBtn2, strip.ColorHSV(65536 / 3, 255, dimBrightness)); - strip.setPixelColor(cmdBtn3, strip.ColorHSV(65536 / 2, 255, dimBrightness)); - strip.setPixelColor(cmdBtn4, strip.ColorHSV(0, 255, defaultBrightness)); -} - -void setLayoutLEDs() { - for (int i = 0; i < elementCount; i++) { - if (currentLayout[i] <= 127) { - setLayoutLED(i); - } - } -} -void setLayoutLED(int i) { - if (scaleLock) { - strip.setPixelColor(i, keyColor(currentLayout[i], 0)); - } else { - strip.setPixelColor(i, keyColor(currentLayout[i], dimBrightness)); + void setupMenu() { // need menu + menu.setSplashDelay(0); + menu.init(); + buildMenu(); + menuHome(); } - - // Scale highlighting - if (isNoteLit(currentLayout[i])) { - strip.setPixelColor(i, keyColor(currentLayout[i], defaultBrightness)); + void setupGFX() { + u8g2.begin(); // Menu and graphics setup + u8g2.setBusClock(1000000); // Speed up display + u8g2.setContrast(defaultContrast); // Set contrast } -} - -// ENCODER // -// rotary encoder pin change interrupt handler -void readEncoder() { - encoder_state = (encoder_state << 4) | (digitalRead(ROTB) << 1) | digitalRead(ROTA); - Serial.println(encoder_val); - switch (encoder_state) { - case 0x23: encoder_val++; break; - case 0x32: encoder_val--; break; - default: break; + void testDiagnostics() { + sendToLog(String("theHDM was here")); } -} -void rotate() { - unsigned char result = rotary.process(); - if (result == DIR_CW) { - encoder_val++; - } else if (result == DIR_CCW) { - encoder_val--; + +// ====== 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 } -} -// rotary encoder init -void encoder_init() { - // enable pin change interrupts - attachInterrupt(digitalPinToInterrupt(ROTA), readEncoder, RISING); - attachInterrupt(digitalPinToInterrupt(ROTB), readEncoder, RISING); - encoder_state = (digitalRead(ROTB) << 1) | digitalRead(ROTA); - interrupts(); -} - -// MENU // -void menuNavigation() { - if (menu.readyForKey()) { - encoderState = digitalRead(encoderClick); - if (encoderState > encoderLastState) { - menu.registerKeyPress(GEM_KEY_OK); - screenTime = 0; - } - encoderLastState = encoderState; - if (encoder_val < 0) { - menu.registerKeyPress(GEM_KEY_UP); - encoder_val = 0; - screenTime = 0; - } - if (encoder_val > 0) { - menu.registerKeyPress(GEM_KEY_DOWN); - encoder_val = 0; - screenTime = 0; + void screenSaver() { + if (screenTime <= screenSaverMillis) { + screenTime = screenTime + lapTime; + if (screenSaverOn) { + screenSaverOn = 0; + u8g2.setContrast(defaultContrast); + } + } else { + if (!screenSaverOn) { + screenSaverOn = 1; + u8g2.setContrast(1); + } } } -} - -void setupMenu() { - // Add menu items to Main menu page - menuPageMain.addMenuItem(menuItemLayout); - menuPageMain.addMenuItem(menuItemKey); - menuPageMain.addMenuItem(menuItemScale); - menuPageMain.addMenuItem(menuItemScaleLock); - menuPageMain.addMenuItem(menuItemTranspose); - menuPageMain.addMenuItem(menuItemBendSpeed); - menuPageMain.addMenuItem(menuItemModSpeed); - menuPageMain.addMenuItem(menuItemBrightness); - menuPageMain.addMenuItem(menuItemLighting); - menuPageMain.addMenuItem(menuItemColor); - menuPageMain.addMenuItem(menuItemBuzzer); - menuPageMain.addMenuItem(menuItemTesting); - // Add menu items to Layout Select page - menuPageLayout.addMenuItem(menuItemWickiHayden); - menuPageLayout.addMenuItem(menuItemHarmonicTable); - menuPageLayout.addMenuItem(menuItemGerhard); - menuPageLayout.addMenuItem(menuItemBosanquetWilson); - // Add menu items to Testing page - menuPageTesting.addMenuItem(menuItemSequencer); - menuPageTesting.addMenuItem(menuItemEzMajor); - menuPageTesting.addMenuItem(menuItemFull); - menuPageTesting.addMenuItem(menuItem411); - menuPageTesting.addMenuItem(menuItem412); - menuPageTesting.addMenuItem(menuItem413); - menuPageTesting.addMenuItem(menuItemVersion); - menuPageTesting.addMenuItem(menuItemTones); - menuPageTesting.addMenuItem(menuItemArp); - // Specify parent menu page for the other menu pages - menuPageLayout.setParentMenuPage(menuPageMain); - menuPageTesting.setParentMenuPage(menuPageMain); - // Add menu page to menu and set it as current - menu.setMenuPageCurrent(menuPageMain); -} - -void wickiHayden() { - currentLayout = wickiHaydenLayout; - setLayoutLEDs(); - if (ModelNumber != 1) { - u8g2.setDisplayRotation(U8G2_R2); - } - menu.setMenuPageCurrent(menuPageMain); - menu.drawMenu(); -} -void harmonicTable() { - currentLayout = harmonicTableLayout; - setLayoutLEDs(); - if (ModelNumber != 1) { - u8g2.setDisplayRotation(U8G2_R1); - } - menu.setMenuPageCurrent(menuPageMain); - menu.drawMenu(); -} -void gerhard() { - currentLayout = gerhardLayout; - setLayoutLEDs(); - if (ModelNumber != 1) { - u8g2.setDisplayRotation(U8G2_R1); + 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). + } + } } - menu.setMenuPageCurrent(menuPageMain); - menu.drawMenu(); -} -void bosanquetWilson() { - currentLayout = bosanquetWilsonLayout; - setLayoutLEDs(); - if (ModelNumber != 1) { - u8g2.setDisplayRotation(U8G2_R1); + 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; + }; + }; } - menu.setMenuPageCurrent(menuPageMain); - menu.drawMenu(); -} -void full() { - currentLayout = fullLayout; - setLayoutLEDs(); - if (ModelNumber != 1) { - u8g2.setDisplayRotation(U8G2_R1); + void arpeggiate() { + if (buzzer > 1) { + if (runTime - currentBuzzTime > arpeggiateLength) { + currentBuzzTime = millis(); + byte nextNoteToBuzz = nextHeldNote(); + if (nextNoteToBuzz < cmdCode) { + buzz(nextNoteToBuzz); + }; + }; + }; } - menu.setMenuPageCurrent(menuPageMain); - menu.drawMenu(); -} -void fortyone1() { - currentLayout = fourtyone1; - setLayoutLEDs(); - if (ModelNumber != 1) { - u8g2.setDisplayRotation(U8G2_R1); + 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(); + }; + }; } - menu.setMenuPageCurrent(menuPageMain); - menu.drawMenu(); -} -void fortyone2() { - currentLayout = fourtyone2; - setLayoutLEDs(); - if (ModelNumber != 1) { - u8g2.setDisplayRotation(U8G2_R1); + 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; + }; + }; } - menu.setMenuPageCurrent(menuPageMain); - menu.drawMenu(); -} -void fortyone3() { - currentLayout = fourtyone3; - setLayoutLEDs(); - if (ModelNumber != 1) { - u8g2.setDisplayRotation(U8G2_R1); + 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; } - menu.setMenuPageCurrent(menuPageMain); - menu.drawMenu(); -} -void ezMajor() { - currentLayout = ezMajorLayout; - setLayoutLEDs(); - if (ModelNumber != 1) { - u8g2.setDisplayRotation(U8G2_R2); + 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(); } - menu.setMenuPageCurrent(menuPageMain); - menu.drawMenu(); -} - -void setBrightness() { - strip.setBrightness(stripBrightness); - setLayoutLEDs(); -} - -// Validation routine of transpose variable -void validateTranspose() { - //Need to add some code here to make sure transpose doesn't get out of hand - /*something like - if ((transpose + LOWEST NOTE IN ARRAY) < 0) { - transpose = 0; - } */ - setLayoutLEDs(); -} - -void screenSaver() { - if (screenTime <= 10000) { - screenTime = screenTime + loopTime; - if (screenBrightness != stripBrightness / 2) { - screenBrightness = stripBrightness / 2; - u8g2.setContrast(screenBrightness); + 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; + } } } - if (screenTime > 10000) - if (screenBrightness != 1) { - screenBrightness = 1; - u8g2.setContrast(screenBrightness); + void readMIDI() { + MIDI.read(); + } + void keepTrackOfRotaryKnobTurns() { + switch (rotary.process()) { + case DIR_CW: + rotaryKnobTurns++; + break; + case DIR_CCW: + rotaryKnobTurns--; + break; } -} -void sequencerSetup() { - if (sequencerMode) { - strip.clear(); - strip.show(); - } else { - setLayoutLEDs(); - setCMD_LEDs(); } -} -// END FUNCTIONS SECTION +// ====== 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(); + } |