Firmware for HexBoard MIDI controller
HexBoard Firmware Golden Master
I think that’s what they call it when the assumption is it’s ready to ship. Version 1.0.0 is ready!
| -rw-r--r-- | Classes.h | 158 | ||||
| -rw-r--r-- | Constants.h | 140 | ||||
| -rw-r--r-- | HexBoard.ino | 2719 | ||||
| -rw-r--r-- | Hexperiment.ino | 2033 |
4 files changed, 2719 insertions, 2331 deletions
diff --git a/Classes.h b/Classes.h deleted file mode 100644 index 133d3a5..0000000 --- a/Classes.h +++ /dev/null @@ -1,158 +0,0 @@ -// class definitions are in a header so that
-// they get read before compiling the main program.
-
-class tuningDef {
-public:
- std::string name; // limit is 17 characters for GEM menu
- byte cycleLength; // steps before period/cycle/octave repeats
- float stepSize; // in cents, 100 = "normal" semitone.
- SelectOptionInt keyChoices[MAX_SCALE_DIVISIONS];
- int spanCtoA() {
- return (- keyChoices[0].val_int);
- };
-};
-
-class layoutDef {
-public:
- std::string name; // limit is 17 characters for GEM menu
- bool isPortrait; // affects orientation of the GEM menu only.
- byte hexMiddleC; // instead of "what note is button 1", "what button is the middle"
- int8_t acrossSteps; // defined this way to be compatible with original v1.1 firmare
- int8_t dnLeftSteps; // defined this way to be compatible with original v1.1 firmare
- byte tuning; // index of the tuning that this layout is designed for
-};
-
-class colorDef {
-public:
- float hue;
- byte sat;
- byte val;
- colorDef mixWithWhite() {
- colorDef temp;
- temp.hue = this->hue;
- temp.sat = ((this->sat > SAT_TINT) ? SAT_TINT : this->sat);
- temp.val = VALUE_FULL;
- return temp;
- };
-};
-
-class paletteDef {
-public:
- colorDef swatch[MAX_SCALE_DIVISIONS]; // the different colors used in this palette
- byte colorNum[MAX_SCALE_DIVISIONS]; // map key (c,d...) to swatches
- colorDef getColor(byte givenStepFromC) {
- return swatch[colorNum[givenStepFromC] - 1];
- };
- float getHue(byte givenStepFromC) {
- return getColor(givenStepFromC).hue;
- };
- byte getSat(byte givenStepFromC) {
- return getColor(givenStepFromC).sat;
- };
- byte getVal(byte givenStepFromC) {
- return getColor(givenStepFromC).val;
- };
-};
-
-class buttonDef {
-public:
- byte btnState = 0; // binary 00 = off, 01 = just pressed, 10 = just released, 11 = held
- void interpBtnPress(bool isPress) {
- btnState = (((btnState << 1) + isPress) & 3);
- };
- int8_t coordRow = 0; // hex coordinates
- int8_t coordCol = 0; // hex coordinates
- uint32_t timePressed = 0; // timecode of last press
- uint32_t LEDcolorAnim = 0; // calculate it once and store value, to make LED playback snappier
- uint32_t LEDcolorPlay = 0; // calculate it once and store value, to make LED playback snappier
- uint32_t LEDcolorOn = 0; // calculate it once and store value, to make LED playback snappier
- uint32_t LEDcolorOff = 0; // calculate it once and store value, to make LED playback snappier
- bool animate = 0; // hex is flagged as part of the animation in this frame, helps make animations smoother
- int16_t stepsFromC = 0; // number of steps from C4 (semitones in 12EDO; microtones if >12EDO)
- bool isCmd = 0; // 0 if it's a MIDI note; 1 if it's a MIDI control cmd
- bool inScale = 0; // 0 if it's not in the selected scale; 1 if it is
- byte note = UNUSED_NOTE; // MIDI note or control parameter corresponding to this hex
- int16_t bend = 0; // in microtonal mode, the pitch bend for this note needed to be tuned correctly
- byte channel = 0; // what MIDI channel this note is playing on
- float frequency = 0.0; // what frequency to ring on the buzzer
-};
-
-class wheelDef {
-public:
- bool alternateMode; // two ways to control
- bool isSticky; // TRUE if you leave value unchanged when no buttons pressed
- byte* topBtn; // pointer to the key Status of the button you use as this button
- byte* midBtn;
- byte* botBtn;
- int16_t minValue;
- int16_t maxValue;
- int* stepValue; // this can be changed via GEM menu
- int16_t defValue; // snapback value
- int16_t curValue;
- int16_t targetValue;
- uint32_t timeLastChanged;
- void setTargetValue() {
- if (alternateMode) {
- if (*midBtn >> 1) { // middle button toggles target (0) vs. step (1) mode
- int16_t temp = curValue;
- if (*topBtn == 1) {temp += *stepValue;}; // tap button
- if (*botBtn == 1) {temp -= *stepValue;}; // tap button
- if (temp > maxValue) {temp = maxValue;}
- else if (temp <= minValue) {temp = minValue;};
- targetValue = temp;
- } else {
- switch (((*topBtn >> 1) << 1) + (*botBtn >> 1)) {
- case 0b10: targetValue = maxValue; break;
- case 0b11: targetValue = defValue; break;
- case 0b01: targetValue = minValue; break;
- default: targetValue = curValue; break;
- };
- };
- } else {
- switch (((*topBtn >> 1) << 2) + ((*midBtn >> 1) << 1) + (*botBtn >> 1)) {
- case 0b100: targetValue = maxValue; break;
- case 0b110: targetValue = (3 * maxValue + minValue) / 4; break;
- case 0b010:
- case 0b111:
- case 0b101: targetValue = (maxValue + minValue) / 2; break;
- case 0b011: targetValue = (maxValue + 3 * minValue) / 4; break;
- case 0b001: targetValue = minValue; break;
- case 0b000: targetValue = (isSticky ? curValue : defValue); break;
- default: break;
- };
- }
- };
- bool updateValue(uint32_t givenTime) {
- int16_t temp = targetValue - curValue;
- if (temp != 0) {
- if ((givenTime - timeLastChanged) >= CC_MSG_COOLDOWN_MICROSECONDS ) {
- timeLastChanged = givenTime;
- if (abs(temp) < *stepValue) {
- curValue = targetValue;
- } else {
- curValue = curValue + (*stepValue * (temp / abs(temp)));
- };
- return 1;
- } else {
- return 0;
- };
- } else {
- return 0;
- };
- };
-};
-// back button
-
-class scaleDef {
-public:
- std::string name;
- byte tuning;
- byte pattern[MAX_SCALE_DIVISIONS];
-};
-
-// this class should only be touched by the 2nd core
-class oscillator {
-public:
- uint16_t increment = 0;
- uint16_t counter = 0;
-};
\ No newline at end of file diff --git a/Constants.h b/Constants.h deleted file mode 100644 index a38732b..0000000 --- a/Constants.h +++ /dev/null @@ -1,140 +0,0 @@ -// hardware pins -#define SDAPIN 16 -#define SCLPIN 17 -#define LED_PIN 22 -#define ROT_PIN_A 20 -#define ROT_PIN_B 21 -#define ROT_PIN_C 24 -#define MPLEX_1_PIN 4 -#define MPLEX_2_PIN 5 -#define MPLEX_4_PIN 2 -#define MPLEX_8_PIN 3 -#define COLUMN_PIN_0 6 -#define COLUMN_PIN_1 7 -#define COLUMN_PIN_2 8 -#define COLUMN_PIN_3 9 -#define COLUMN_PIN_4 10 -#define COLUMN_PIN_5 11 -#define COLUMN_PIN_6 12 -#define COLUMN_PIN_7 13 -#define COLUMN_PIN_8 14 -#define COLUMN_PIN_9 15 -#define TONEPIN 23 - -// grid related -#define LED_COUNT 140 -#define COLCOUNT 10 -#define ROWCOUNT 14 - -#define HEX_DIRECTION_EAST 0 -#define HEX_DIRECTION_NE 1 -#define HEX_DIRECTION_NW 2 -#define HEX_DIRECTION_WEST 3 -#define HEX_DIRECTION_SW 4 -#define HEX_DIRECTION_SE 5 - -#define CMDBTN_0 0 -#define CMDBTN_1 20 -#define CMDBTN_2 40 -#define CMDBTN_3 60 -#define CMDBTN_4 80 -#define CMDBTN_5 100 -#define CMDBTN_6 120 -#define CMDCOUNT 7 - -// microtonal related -#define TUNINGCOUNT 13 - -#define TUNING_12EDO 0 -#define TUNING_17EDO 1 -#define TUNING_19EDO 2 -#define TUNING_22EDO 3 -#define TUNING_24EDO 4 -#define TUNING_31EDO 5 -#define TUNING_41EDO 6 -#define TUNING_53EDO 7 -#define TUNING_72EDO 8 -#define TUNING_BP 9 -#define TUNING_ALPHA 10 -#define TUNING_BETA 11 -#define TUNING_GAMMA 12 - -#define MAX_SCALE_DIVISIONS 72 -#define ALL_TUNINGS 255 - -// MIDI-related -#define CONCERT_A_HZ 440.0 -#define PITCH_BEND_SEMIS 2 -#define CMDB 192 -#define UNUSED_NOTE 255 -#define CC_MSG_COOLDOWN_MICROSECONDS 32768 - -// buzzer related -#define TONE_SL 3 -#define TONE_CH 1 -#define WAVEFORM_SQUARE 0 -#define WAVEFORM_SAW 1 -#define POLL_INTERVAL_IN_MICROSECONDS 32 -#define POLYPHONY_LIMIT 15 -#define ALARM_NUM 2 -#define ALARM_IRQ TIMER_IRQ_2 -#define BUZZ_OFF 0 -#define BUZZ_MONO 1 -#define BUZZ_ARPEGGIO 2 -#define BUZZ_POLY 3 - -// LED related - -// value / brightness ranges from 0..255 -// black = 0, full strength = 255 - -#define VALUE_BLACK 0 -#define VALUE_LOW 64 -#define VALUE_SHADE 128 -#define VALUE_NORMAL 192 -#define VALUE_FULL 255 - -// saturation ranges from 0..255 -// 0 = black and white -// 255 = full chroma - -#define SAT_BW 0 -#define SAT_TINT 32 -#define SAT_DULL 85 -#define SAT_MODERATE 170 -#define SAT_VIVID 255 - -// hue is an angle from 0.0 to 359.9 -// there is a transform function to map "perceptual" -// hues to RGB. the hue values below are perceptual. -#define HUE_NONE 0.0 -#define HUE_RED 0.0 -#define HUE_ORANGE 36.0 -#define HUE_YELLOW 72.0 -#define HUE_LIME 108.0 -#define HUE_GREEN 144.0 -#define HUE_CYAN 180.0 -#define HUE_BLUE 216.0 -#define HUE_INDIGO 252.0 -#define HUE_PURPLE 288.0 -#define HUE_MAGENTA 324.0 - -#define RAINBOW_MODE 0 -#define TIERED_COLOR_MODE 1 - -// animations -#define ANIMATE_NONE 0 -#define ANIMATE_STAR 1 -#define ANIMATE_SPLASH 2 -#define ANIMATE_ORBIT 3 -#define ANIMATE_OCTAVE 4 -#define ANIMATE_BY_NOTE 5 - -// menu-related -#define MENU_ITEM_HEIGHT 10 -#define MENU_PAGE_SCREEN_TOP_OFFSET 10 -#define MENU_VALUES_LEFT_OFFSET 78 - -// debug -#define DIAGNOSTIC_OFF 0 -#define DIAGNOSTIC_ON 1
\ No newline at end of file diff --git a/HexBoard.ino b/HexBoard.ino new file mode 100644 index 0000000..4718c49 --- /dev/null +++ b/HexBoard.ino @@ -0,0 +1,2719 @@ +// @readme + /* + HexBoard + Copyright 2022-2023 Jared DeCook and Zach DeCook + with help from Nicholas Fox (he's too modest, this was a complete rewrite) + Firmware v1.0.0 2024-05-16 + Licensed under the GNU GPL Version 3. + + Hardware information: + Generic RP2040 running at 133MHz with 16MB of flash + https://github.com/earlephilhower/arduino-pico + Additional board manager URL: + https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json + Tools > USB Stack > (Adafruit TinyUSB) + Sketch > Export Compiled Binary + + Compilation instructions: + Using arduino-cli... + # Download the board index + arduino-cli --additional-urls=https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json core update-index + # Install the core for rp2040 + arduino-cli --additional-urls=https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json core download rp2040:rp2040 + arduino-cli --additional-urls=https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json core install rp2040:rp2040 + # Install libraries + arduino-cli lib install "MIDI library" + arduino-cli lib install "Adafruit NeoPixel" + arduino-cli lib install "U8g2" # dependency for GEM + arduino-cli lib install "Adafruit GFX Library" # dependency for GEM + arduino-cli lib install "GEM" + sed -i 's@#include "config/enable-glcd.h"@//\0@g' ~/Arduino/libraries/GEM/src/config.h # remove dependency from GEM + # Run Make to build the firmware + make + --------------------------- + New to programming Arduino? + --------------------------- + Coding the Hexboard is, basically, done in C++. + + When the HexBoard is plugged in, it runs + void setup() and void setup1(), then + runs void loop() and void loop1() on an + infinite loop until the HexBoard powers down. + There are two cores running independently. + You can pretend that the compiler tosses + these two routines inside an int main() for + each processor. + + To #include libraries, the Arduino + compiler expects them to be installed from + a centralized repository. You can also bring + your own .h / .cpp code but it must be saved + in "/src/____/___.h" to be valid. + + We found this really annoying so to the + extent possible we have consolidated + this code into one single .ino sketch file. + However, the code is sectioned into something + like a library format for each feature + of the HexBoard, so that if the code becomes + too long to manage in a single file in the + future, it is easier to air-lift parts of + the code into a library at that point. + */ +// @init + #define HARDWARE_VERSION 1 // 1 = v1.1 board. 2 = v1.2 board. (may not be necessary as there are detectable differences) + #include <Arduino.h> // this is necessary to talk to the Hexboard! + #include <Wire.h> // this is necessary to deal with the pins and wires + #define SDAPIN 16 + #define SCLPIN 17 + #include <GEM_u8g2.h> // library of code to create menu objects on the B&W display + #include <numeric> // need that GCD function, son + #include <string> // standard C++ library string classes (use "std::string" to invoke it); these do not cause the memory corruption that Arduino::String does. + #include <queue> // standard C++ library construction to store open channels in microtonal mode (use "std::queue" to invoke it) +// @helpers + /* + C++ returns a negative value for + negative N % D. This function + guarantees the mod value is always + positive. + */ + int positiveMod(int n, int d) { + return (((n % d) + d) % d); + } + /* + There may already exist linear interpolation + functions in the standard library. This one is helpful + because it will do the weighting division for you. + It only works on byte values since it's intended + to blend color values together. A better C++ + coder may be able to allow automatic type casting here. + */ + byte byteLerp(byte xOne, byte xTwo, float yOne, float yTwo, float y) { + float weight = (y - yOne) / (yTwo - yOne); + int temp = xOne + ((xTwo - xOne) * weight); + if (temp < xOne) {temp = xOne;} + if (temp > xTwo) {temp = xTwo;} + return temp; + } + +// @defaults + /* + This section sets default values + for user-editable options + */ + int transposeSteps = 0; + byte scaleLock = 0; + byte perceptual = 1; + byte paletteBeginsAtKeyCenter = 1; + byte animationFPS = 32; // actually frames per 2^20 microseconds. close enough to 30fps + + byte wheelMode = 0; // standard vs. fine tune mode + byte modSticky = 0; + byte pbSticky = 0; + byte velSticky = 1; + int modWheelSpeed = 8; + int pbWheelSpeed = 1024; + int velWheelSpeed = 8; + + #define SYNTH_OFF 0 + #define SYNTH_MONO 1 + #define SYNTH_ARPEGGIO 2 + #define SYNTH_POLY 3 + byte playbackMode = SYNTH_OFF; + + #define WAVEFORM_SINE 0 + #define WAVEFORM_STRINGS 1 + #define WAVEFORM_CLARINET 2 + #define WAVEFORM_HYBRID 7 + #define WAVEFORM_SQUARE 8 + #define WAVEFORM_SAW 9 + #define WAVEFORM_TRIANGLE 10 + byte currWave = WAVEFORM_HYBRID; + + #define RAINBOW_MODE 0 + #define TIERED_COLOR_MODE 1 + #define ALTERNATE_COLOR_MODE 2 + byte colorMode = RAINBOW_MODE; + + #define ANIMATE_NONE 0 + #define ANIMATE_STAR 1 + #define ANIMATE_SPLASH 2 + #define ANIMATE_ORBIT 3 + #define ANIMATE_OCTAVE 4 + #define ANIMATE_BY_NOTE 5 + byte animationType = ANIMATE_NONE; + + #define BRIGHT_MAX 255 + #define BRIGHT_HIGH 210 + #define BRIGHT_MID 180 + #define BRIGHT_LOW 150 + #define BRIGHT_DIM 110 + byte globalBrightness = BRIGHT_MID; + +// @microtonal + /* + Most users will stick to playing in standard Western + tuning, but for those looking to play microtonally, + the Hexboard accommodates equal step tuning systems + of any arbitrary size. + */ + /* + Each tuning system needs to be + pre-defined, pre-counted, and enumerated as below. + Future editions of this sketch may enable free + definition and smart pointer references to tuning + presets without requiring an enumeration. + */ + #define TUNINGCOUNT 13 + #define TUNING_12EDO 0 + #define TUNING_17EDO 1 + #define TUNING_19EDO 2 + #define TUNING_22EDO 3 + #define TUNING_24EDO 4 + #define TUNING_31EDO 5 + #define TUNING_41EDO 6 + #define TUNING_53EDO 7 + #define TUNING_72EDO 8 + #define TUNING_BP 9 + #define TUNING_ALPHA 10 + #define TUNING_BETA 11 + #define TUNING_GAMMA 12 + /* + Note names and palette arrays are allocated in memory + at runtime. Their usable size is based on the number + of steps (in standard tuning, semitones) in a tuning + system before a new period is reached (in standard + tuning, the octave). This value provides a maximum + array size that handles almost all useful tunings + without wasting much space. + */ + #define MAX_SCALE_DIVISIONS 72 + /* + A dictionary of musical scales is defined in the code. + A scale is tied to one tuning system, with the exception + of "no scale" (i.e. every note is part of the scale). + "No scale" is tied to this value "ALL_TUNINGS" so it can + always be chosen in the menu. + */ + #define ALL_TUNINGS 255 + /* + MIDI notes are enumerated 0-127 (7 bits). + Values of 128-255 can be used to indicate + command instructions for non-note buttons. + These definitions support this function. + */ + #define CMDB 192 + #define UNUSED_NOTE 255 + /* + When sending smoothly-varying pitch bend + or modulation messages over MIDI, the + code uses a cool-down period of about + 1/30 of a second in between messages, enough + for changes to sound continuous without + overloading the MIDI message queue. + */ + #define CC_MSG_COOLDOWN_MICROSECONDS 32768 + /* + This class provides the seed values + needed to map buttons to note frequencies + and palette colors, and to populate + the menu with correct key names and + scale choices, for a given equal step + tuning system. + */ + class tuningDef { + public: + std::string name; // limit is 17 characters for GEM menu + byte cycleLength; // steps before period/cycle/octave repeats + float stepSize; // in cents, 100 = "normal" semitone. + SelectOptionInt keyChoices[MAX_SCALE_DIVISIONS]; + int spanCtoA() { + return keyChoices[0].val_int; + } + }; + /* + Note that for all practical musical purposes, + expressing step sizes to six significant figures is + sufficient to eliminate any detectable tuning artifacts + due to rounding. + + The note names are formatted in an array specifically to + match the format needed for the GEM Menu to accept directly + as a spinner selection item. The number next to the note name + is the number of steps from the anchor note A that key is. + + There are other ways the tuning could be calculated. + Some microtonal players choose an anchor note + other than A 440. Future versions will allow for + more flexibility in anchor selection, which will also + change the implementation of key options. + */ + tuningDef tuningOptions[] = { + { "12 EDO", 12, 100.000, + {{"C" ,-9},{"C#",-8},{"D" ,-7},{"Eb",-6},{"E" ,-5},{"F",-4} + ,{"F#",-3},{"G" ,-2},{"G#",-1},{"A" , 0},{"Bb", 1},{"B", 2} + }}, + { "17 EDO", 17, 70.5882, + {{"C",-13},{"Db",-12},{"C#",-11},{"D",-10},{"Eb",-9},{"D#",-8} + ,{"E", -7},{"F" , -6},{"Gb", -5},{"F#",-4},{"G", -3},{"Ab",-2} + ,{"G#",-1},{"A" , 0},{"Bb", 1},{"A#", 2},{"B", 3} + }}, + { "19 EDO", 19, 63.1579, + {{"C" ,-14},{"C#",-13},{"Db",-12},{"D",-11},{"D#",-10},{"Eb",-9},{"E",-8} + ,{"E#", -7},{"F" , -6},{"F#", -5},{"Gb",-4},{"G", -3},{"G#",-2} + ,{"Ab", -1},{"A" , 0},{"A#", 1},{"Bb", 2},{"B", 3},{"Cb", 4} + }}, + { "22 EDO", 22, 54.5455, + {{" C", -17},{"^C",-16},{"vC#",-15},{"vD",-14},{" D",-13},{"^D",-12} + ,{"^Eb",-11},{"vE",-10},{" E", -9},{" F", -8},{"^F", -7},{"vF#",-6} + ,{"vG", -5},{" G", -4},{"^G", -3},{"vG#",-2},{"vA", -1},{" A", 0} + ,{"^A", 1},{"^Bb", 2},{"vB", 3},{" B", 4} + }}, + { "24 EDO", 24, 50.0000, + {{"C", -18},{"C+",-17},{"C#",-16},{"Dd",-15},{"D",-14},{"D+",-13} + ,{"Eb",-12},{"Ed",-11},{"E", -10},{"E+", -9},{"F", -8},{"F+", -7} + ,{"F#", -6},{"Gd", -5},{"G", -4},{"G+", -3},{"G#",-2},{"Ad", -1} + ,{"A", 0},{"A+", 1},{"Bb", 2},{"Bd", 3},{"B", 4},{"Cd", 5} + }}, + { "31 EDO", 31, 38.7097, + {{"C",-23},{"C+",-22},{"C#",-21},{"Db",-20},{"Dd",-19} + ,{"D",-18},{"D+",-17},{"D#",-16},{"Eb",-15},{"Ed",-14} + ,{"E",-13},{"E+",-12} ,{"Fd",-11} + ,{"F",-10},{"F+", -9},{"F#", -8},{"Gb", -7},{"Gd", -6} + ,{"G", -5},{"G+", -4},{"G#", -3},{"Ab", -2},{"Ad", -1} + ,{"A", 0},{"A+", 1},{"A#", 2},{"Bb", 3},{"Bd", 4} + ,{"B", 5},{"B+", 6} ,{"Cd", 7} + }}, + { "41 EDO", 41, 29.2683, + {{" C",-31},{"^C",-30},{" C+",-29},{" Db",-28},{" C#",-27},{" Dd",-26},{"vD",-24} + ,{" D",-24},{"^D",-23},{" D+",-22},{" Eb",-21},{" D#",-20},{" Ed",-19},{"vE",-18} + ,{" E",-17},{"^E",-16} ,{"vF",-15} + ,{" F",-14},{"^F",-13},{" F+",-12},{" Gb",-11},{" F#",-10},{" Gd", -9},{"vG", -8} + ,{" G", -7},{"^G", -6},{" G+", -5},{" Ab", -4},{" G#", -3},{" Ad", -2},{"vA", -1} + ,{" A", 0},{"^A", 1},{" A+", 2},{" Bb", 3},{" A#", 4},{" Bd", 5},{"vB", 6} + ,{" B", 7},{"^B", 8} ,{"vC", 9} + }}, + { "53 EDO", 53, 22.6415, + {{" C", -40},{"^C", -39},{">C",-38},{"vDb",-37},{"Db",-36} + ,{" C#",-35},{"^C#",-34},{"<D",-33},{"vD", -32} + ,{" D", -31},{"^D", -30},{">D",-29},{"vEb",-28},{"Eb",-27} + ,{" D#",-26},{"^D#",-25},{"<E",-24},{"vE", -23} + ,{" E", -22},{"^E", -21},{">E",-20},{"vF", -19} + ,{" F", -18},{"^F", -17},{">F",-16},{"vGb",-15},{"Gb",-14} + ,{" F#",-13},{"^F#",-12},{"<G",-11},{"vG", -10} + ,{" G", -9},{"^G", -8},{">G", -7},{"vAb", -6},{"Ab", -5} + ,{" G#", -4},{"^G#", -3},{"<A", -2},{"vA", -1} + ,{" A", 0},{"^A", 1},{">A", 2},{"vBb", 3},{"Bb", 4} + ,{" A#", 5},{"^A#", 6},{"<B", 7},{"vB", 8} + ,{" B", 9},{"^B", 10},{"<C", 11},{"vC", 12} + }}, + { "72 EDO", 72, 16.6667, + {{" C", -54},{"^C", -53},{">C", -52},{" C+",-51},{"<C#",-50},{"vC#",-49} + ,{" C#",-48},{"^C#",-47},{">C#",-46},{" Dd",-45},{"<D" ,-44},{"vD" ,-43} + ,{" D", -42},{"^D", -41},{">D", -40},{" D+",-39},{"<Eb",-38},{"vEb",-37} + ,{" Eb",-36},{"^Eb",-35},{">Eb",-34},{" Ed",-33},{"<E" ,-32},{"vE" ,-31} + ,{" E", -30},{"^E", -29},{">E", -28},{" E+",-27},{"<F" ,-26},{"vF" ,-25} + ,{" F", -24},{"^F", -23},{">F", -22},{" F+",-21},{"<F#",-20},{"vF#",-19} + ,{" F#",-18},{"^F#",-17},{">F#",-16},{" Gd",-15},{"<G" ,-14},{"vG" ,-13} + ,{" G", -12},{"^G", -11},{">G", -10},{" G+", -9},{"<G#", -8},{"vG#", -7} + ,{" G#", -6},{"^G#", -5},{">G#", -4},{" Ad", -3},{"<A" , -2},{"vA" , -1} + ,{" A", 0},{"^A", 1},{">A", 2},{" A+", 3},{"<Bb", 4},{"vBb", 5} + ,{" Bb", 6},{"^Bb", 7},{">Bb", 8},{" Bd", 9},{"<B" , 10},{"vB" , 11} + ,{" B", 12},{"^B", 13},{">B", 14},{" Cd", 15},{"<C" , 16},{"vC" , 17} + }}, + { "Bohlen-Pierce", 13, 146.304, + {{"C",-10},{"Db",-9},{"D",-8},{"E",-7},{"F",-6},{"Gb",-5} + ,{"G",-4},{"H",-3},{"Jb",-2},{"J",-1},{"A",0},{"Bb",1},{"B",2} + }}, + { "Carlos Alpha", 9, 77.9650, + {{"I",0},{"I#",1},{"II-",2},{"II+",3},{"III",4} + ,{"III#",5},{"IV-",6},{"IV+",7},{"Ib",8} + }}, + { "Carlos Beta", 11, 63.8329, + {{"I",0},{"I#",1},{"IIb",2},{"II",3},{"II#",4},{"III",5} + ,{"III#",6},{"IVb",7},{"IV",8},{"IV#",9},{"Ib",10} + }}, + { "Carlos Gamma", 20, 35.0985, + {{" I", 0},{"^I", 1},{" IIb", 2},{"^IIb", 3},{" I#", 4},{"^I#", 5} + ,{" II", 6},{"^II", 7} + ,{" III",8},{"^III",9},{" IVb",10},{"^IVb",11},{" III#",12},{"^III#",13} + ,{" IV",14},{"^IV",15},{" Ib", 16},{"^Ib", 17},{" IV#", 18},{"^IV#", 19} + }}, + }; + +// @layout + /* + This section defines the different + preset note layout options. + */ + /* + This class provides the seed values + needed to implement a given isomorphic + note layout. From it, the map of buttons + to note frequencies can be calculated. + + A layout is tied to a specific tuning. + */ + class layoutDef { + public: + std::string name; // limit is 17 characters for GEM menu + bool isPortrait; // affects orientation of the GEM menu only. + byte hexMiddleC; // instead of "what note is button 1", "what button is the middle" + int8_t acrossSteps; // defined this way to be compatible with original v1.1 firmare + int8_t dnLeftSteps; // defined this way to be compatible with original v1.1 firmare + byte tuning; // index of the tuning that this layout is designed for + }; + /* + Isomorphic layouts are defined by + establishing where the center of the + layout is, and then the number of tuning + steps to go up or down for the hex button + across or down diagonally. + */ + layoutDef layoutOptions[] = { + { "Wicki-Hayden", 1, 64, 2, -7, TUNING_12EDO }, + { "Harmonic Table", 0, 75, -7, 3, TUNING_12EDO }, + { "Janko", 0, 65, -1, -1, TUNING_12EDO }, + { "Gerhard", 0, 65, -1, -3, TUNING_12EDO }, + { "Accordion C-sys.", 1, 75, 2, -3, TUNING_12EDO }, + { "Accordion B-sys.", 1, 64, 1, -3, TUNING_12EDO }, + + { "Full Gamut", 1, 65, 1, -9, TUNING_17EDO }, + { "Bosanquet-Wilson", 0, 65, -2, -1, TUNING_17EDO }, + { "Neutral Thirds A", 0, 65, -1, -2, TUNING_17EDO }, + { "Neutral Thirds B", 0, 65, 1, -3, TUNING_17EDO }, + + { "Full Gamut", 1, 65, 1, -9, TUNING_19EDO }, + { "Bosanquet-Wilson", 0, 65, -1, -2, TUNING_19EDO }, + { "Kleismic", 0, 65, -1, -4, TUNING_19EDO }, + + { "Full Gamut", 1, 65, 1, -8, TUNING_22EDO }, + { "Bosanquet-Wilson", 0, 65, -3, -1, TUNING_22EDO }, + { "Porcupine", 0, 65, 1, -4, TUNING_22EDO }, + + { "Full Gamut", 1, 65, 1, -9, TUNING_24EDO }, + { "Bosanquet-Wilson", 0, 65, -1, -3, TUNING_24EDO }, + { "Inverted", 0, 65, 1, -4, TUNING_24EDO }, + + { "Full Gamut", 1, 65, 1, -7, TUNING_31EDO }, + { "Bosanquet-Wilson", 0, 65, -2, -3, TUNING_31EDO }, + { "Double Bosanquet", 0, 65, -1, -4, TUNING_31EDO }, + { "Anti-Double Bos.", 0, 65, 1, -5, TUNING_31EDO }, + + { "Full Gamut", 0, 65, 1, -8, TUNING_41EDO }, // forty-one #3 + { "Bosanquet-Wilson", 0, 65, -4, -3, TUNING_41EDO }, // forty-one #1 + { "Gerhard", 0, 65, 3, -10, TUNING_41EDO }, // forty-one #2 + { "Baldy", 0, 65, -1, -6, TUNING_41EDO }, + { "Rodan", 1, 65, -1, -7, TUNING_41EDO }, + + { "Wicki-Hayden", 1, 64, 9, -31, TUNING_53EDO }, + { "Bosanquet-Wilson", 0, 65, -5, -4, TUNING_53EDO }, + { "Kleismic A", 0, 65, -8, -3, TUNING_53EDO }, + { "Kleismic B", 0, 65, -5, -3, TUNING_53EDO }, + { "Harmonic Table", 0, 75, -31, 14, TUNING_53EDO }, + { "Buzzard", 0, 65, -9, -1, TUNING_53EDO }, + + { "Full Gamut", 1, 65, 1, -9, TUNING_72EDO }, + { "Expanded Janko", 0, 65, -1, -6, TUNING_72EDO }, + + { "Full Gamut", 1, 65, 1, -9, TUNING_BP }, + { "Standard", 0, 65, -2, -1, TUNING_BP }, + + { "Full Gamut", 1, 65, 1, -9, TUNING_ALPHA }, + { "Compressed", 0, 65, -2, -1, TUNING_ALPHA }, + + { "Full Gamut", 1, 65, 1, -9, TUNING_BETA }, + { "Compressed", 0, 65, -2, -1, TUNING_BETA }, + + { "Full Gamut", 1, 65, 1, -9, TUNING_GAMMA }, + { "Compressed", 0, 65, -2, -1, TUNING_GAMMA } + }; + const byte layoutCount = sizeof(layoutOptions) / sizeof(layoutDef); +// @scales + /* + This class defines a scale pattern + for a given tuning. It is basically + an array with the number of steps in + between each degree of the scale. For + example, the major scale in 12EDO + is 2, 2, 1, 2, 2, 2, 1. + + A scale is tied to a specific tuning. + */ + class scaleDef { + public: + std::string name; + byte tuning; + byte pattern[MAX_SCALE_DIVISIONS]; + }; + scaleDef scaleOptions[] = { + { "None", ALL_TUNINGS, { 0 } }, + // 12 EDO + { "Major", TUNING_12EDO, { 2,2,1,2,2,2,1 } }, + { "Minor, natural", TUNING_12EDO, { 2,1,2,2,1,2,2 } }, + { "Minor, melodic", TUNING_12EDO, { 2,1,2,2,2,2,1 } }, + { "Minor, harmonic", TUNING_12EDO, { 2,1,2,2,1,3,1 } }, + { "Pentatonic, major", TUNING_12EDO, { 2,2,3,2,3 } }, + { "Pentatonic, minor", TUNING_12EDO, { 3,2,2,3,2 } }, + { "Blues", TUNING_12EDO, { 3,1,1,1,1,3,2 } }, + { "Double Harmonic", TUNING_12EDO, { 1,3,1,2,1,3,1 } }, + { "Phrygian", TUNING_12EDO, { 1,2,2,2,1,2,2 } }, + { "Phrygian Dominant", TUNING_12EDO, { 1,3,1,2,1,2,2 } }, + { "Dorian", TUNING_12EDO, { 2,1,2,2,2,1,2 } }, + { "Lydian", TUNING_12EDO, { 2,2,2,1,2,2,1 } }, + { "Lydian Dominant", TUNING_12EDO, { 2,2,2,1,2,1,2 } }, + { "Mixolydian", TUNING_12EDO, { 2,2,1,2,2,1,2 } }, + { "Locrian", TUNING_12EDO, { 1,2,2,1,2,2,2 } }, + { "Whole tone", TUNING_12EDO, { 2,2,2,2,2,2 } }, + { "Octatonic", TUNING_12EDO, { 2,1,2,1,2,1,2,1 } }, + // 17 EDO; for more: https://en.xen.wiki/w/17edo#Scales + { "Diatonic", TUNING_17EDO, { 3,3,1,3,3,3,1 } }, + { "Pentatonic", TUNING_17EDO, { 3,3,4,3,4 } }, + { "Harmonic", TUNING_17EDO, { 3,2,3,2,2,2,3 } }, + { "Husayni maqam", TUNING_17EDO, { 2,2,3,3,2,1,1,3 } }, + { "Blues", TUNING_17EDO, { 4,3,1,1,1,4,3 } }, + { "Hydra", TUNING_17EDO, { 3,3,1,1,2,3,2,1,1 } }, + // 19 EDO; for more: https://en.xen.wiki/w/19edo#Scales + { "Diatonic", TUNING_19EDO, { 3,3,2,3,3,3,2 } }, + { "Pentatonic", TUNING_19EDO, { 3,3,5,3,5 } }, + { "Semaphore", TUNING_19EDO, { 3,1,3,1,3,3,1,3,1 } }, + { "Negri", TUNING_19EDO, { 2,2,2,2,2,1,2,2,2,2 } }, + { "Sensi", TUNING_19EDO, { 2,2,1,2,2,2,1,2,2,2,1 } }, + { "Kleismic", TUNING_19EDO, { 1,3,1,1,3,1,1,3,1,3,1 } }, + { "Magic", TUNING_19EDO, { 3,1,1,1,3,1,1,1,3,1,1,1,1 } }, + { "Kind of blues", TUNING_19EDO, { 4,4,1,2,4,4 } }, + // 22 EDO; for more: https://en.xen.wiki/w/22edo_modes + { "Diatonic", TUNING_22EDO, { 4,4,1,4,4,4,1 } }, + { "Pentatonic", TUNING_22EDO, { 4,4,5,4,5 } }, + { "Orwell", TUNING_22EDO, { 3,2,3,2,3,2,3,2,2 } }, + { "Porcupine", TUNING_22EDO, { 4,3,3,3,3,3,3 } }, + { "Pajara", TUNING_22EDO, { 2,2,3,2,2,2,3,2,2,2 } }, + // 24 EDO; for more: https://en.xen.wiki/w/24edo_scales + { "Diatonic 12", TUNING_24EDO, { 4,4,2,4,4,4,2 } }, + { "Diatonic Soft", TUNING_24EDO, { 3,5,2,3,5,4,2 } }, + { "Diatonic Neutral", TUNING_24EDO, { 4,3,3,4,3,4,3 } }, + { "Pentatonic (12)", TUNING_24EDO, { 4,4,6,4,6 } }, + { "Pentatonic (Haba)", TUNING_24EDO, { 5,5,5,5,4 } }, + { "Invert Pentatonic", TUNING_24EDO, { 6,3,6,6,3 } }, + { "Rast maqam", TUNING_24EDO, { 4,3,3,4,4,2,1,3 } }, + { "Bayati maqam", TUNING_24EDO, { 3,3,4,4,2,1,3,4 } }, + { "Hijaz maqam", TUNING_24EDO, { 2,6,2,4,2,1,3,4 } }, + { "8-EDO", TUNING_24EDO, { 3,3,3,3,3,3,3,3 } }, + { "Wyschnegradsky", TUNING_24EDO, { 2,2,2,2,2,1,2,2,2,2,2,2,1 } }, + // 31 EDO; for more: https://en.xen.wiki/w/31edo#Scales + { "Diatonic", TUNING_31EDO, { 5,5,3,5,5,5,3 } }, + { "Pentatonic", TUNING_31EDO, { 5,5,8,5,8 } }, + { "Harmonic", TUNING_31EDO, { 5,5,4,4,4,3,3,3 } }, + { "Mavila", TUNING_31EDO, { 5,3,3,3,5,3,3,3,3 } }, + { "Quartal", TUNING_31EDO, { 2,2,7,2,2,7,2,7 } }, + { "Orwell", TUNING_31EDO, { 4,3,4,3,4,3,4,3,3 } }, + { "Neutral", TUNING_31EDO, { 4,4,4,4,4,4,4,3 } }, + { "Miracle", TUNING_31EDO, { 4,3,3,3,3,3,3,3,3,3 } }, + // 41 EDO; for more: https://en.xen.wiki/w/41edo#Scales_and_modes + { "Diatonic", TUNING_41EDO, { 7,7,3,7,7,7,3 } }, + { "Pentatonic", TUNING_41EDO, { 7,7,10,7,10 } }, + { "Pure major", TUNING_41EDO, { 7,6,4,7,6,7,4 } }, + { "5-limit chromatic", TUNING_41EDO, { 4,3,4,2,4,3,4,4,2,4,3,4 } }, + { "7-limit chromatic", TUNING_41EDO, { 3,4,2,4,4,3,4,2,4,3,3,4 } }, + { "Harmonic", TUNING_41EDO, { 5,4,4,4,4,3,3,3,3,3,2,3 } }, + { "Middle East-ish", TUNING_41EDO, { 7,5,7,5,5,7,5 } }, + { "Thai", TUNING_41EDO, { 6,6,6,6,6,6,5 } }, + { "Slendro", TUNING_41EDO, { 8,8,8,8,9 } }, + { "Pelog / Mavila", TUNING_41EDO, { 8,5,5,8,5,5,5 } }, + // 53 EDO + { "Diatonic", TUNING_53EDO, { 9,9,4,9,9,9,4 } }, + { "Pentatonic", TUNING_53EDO, { 9,9,13,9,13 } }, + { "Rast makam", TUNING_53EDO, { 9,8,5,9,9,4,4,5 } }, + { "Usshak makam", TUNING_53EDO, { 7,6,9,9,4,4,5,9 } }, + { "Hicaz makam", TUNING_53EDO, { 5,12,5,9,4,9,9 } }, + { "Orwell", TUNING_53EDO, { 7,5,7,5,7,5,7,5,5 } }, + { "Sephiroth", TUNING_53EDO, { 6,5,5,6,5,5,6,5,5,5 } }, + { "Smitonic", TUNING_53EDO, { 11,11,3,11,3,11,3 } }, + { "Slendric", TUNING_53EDO, { 7,3,7,3,7,3,7,3,7,3,3 } }, + { "Semiquartal", TUNING_53EDO, { 9,2,9,2,9,2,9,2,9 } }, + // 72 EDO + { "Diatonic", TUNING_72EDO, { 12,12,6,12,12,12,6 } }, + { "Pentatonic", TUNING_72EDO, { 12,12,18,12,18 } }, + { "Ben Johnston", TUNING_72EDO, { 6,6,6,5,5,5,9,8,4,4,7,7 } }, + { "18-EDO", TUNING_72EDO, { 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4 } }, + { "Miracle", TUNING_72EDO, { 5,2,5,2,5,2,2,5,2,5,2,5,2,5,2,5,2,5,2,5,2 } }, + { "Marvolo", TUNING_72EDO, { 5,5,5,5,5,5,5,2,5,5,5,5,5,5 } }, + { "Catakleismic", TUNING_72EDO, { 4,7,4,4,4,7,4,4,4,7,4,4,4,7,4 } }, + { "Palace", TUNING_72EDO, { 10,9,11,12,10,9,11 } }, + // BP + { "Lambda", TUNING_BP, { 2,1,2,1,2,1,2,1,1 } }, + // Alpha + { "Super Meta Lydian", TUNING_ALPHA, { 3,2,2,2 } }, + // Beta + { "Super Meta Lydian", TUNING_BETA, { 3,3,3,2 } }, + // Gamma + { "Super Meta Lydian", TUNING_GAMMA, { 6,5,5,4 } } + }; + const byte scaleCount = sizeof(scaleOptions) / sizeof(scaleDef); + +// @palettes + /* + This section defines the code needed + to determine colors for each hex. + */ + /* + LED colors are defined in the code + on a perceptual basis. Instead of + calculating RGB codes, the program + uses an artist's color wheel approach. + + For value / brightness, two sets of + named constants are defined. The BRIGHT_ + series (see the defaults section above) + corresponds to the overall + level of lights from the HexBoard, from + dim to maximum. The VALUE_ series + is used to differentiate light and dark + colors in a palette. The BRIGHT and VALUE + are multiplied together (and normalized) + to get the output brightness. + */ + #define VALUE_BLACK 0 + #define VALUE_LOW 127 + #define VALUE_SHADE 164 + #define VALUE_NORMAL 180 + #define VALUE_FULL 255 + /* + Saturation is zero for black and white, and 255 + for fully chromatic color. Value is the + brightness level of the LED, from 0 = off + to 255 = max. + */ + #define SAT_BW 0 + #define SAT_TINT 32 + #define SAT_DULL 85 + #define SAT_MODERATE 120 + #define SAT_VIVID 255 + /* + Hues are angles from 0 to 360, starting + at red and towards yellow->green->blue + when the hue angle increases. + */ + #define HUE_NONE 0.0 + #define HUE_RED 0.0 + #define HUE_ORANGE 36.0 + #define HUE_YELLOW 72.0 + #define HUE_LIME 108.0 + #define HUE_GREEN 144.0 + #define HUE_CYAN 180.0 + #define HUE_BLUE 216.0 + #define HUE_INDIGO 252.0 + #define HUE_PURPLE 288.0 + #define HUE_MAGENTA 324.0 + /* + This class is a basic hue, saturation, + and value triplet, with some limited + transformation functions. Rather than + load a full color space library, this + program uses non-class procedures to + perform conversions to and from LED- + friendly color codes. + */ + class colorDef { + public: + float hue; + byte sat; + byte val; + colorDef tint() { + colorDef temp; + temp.hue = this->hue; + temp.sat = ((this->sat > SAT_MODERATE) ? SAT_MODERATE : this->sat); + temp.val = VALUE_FULL; + return temp; + } + colorDef shade() { + colorDef temp; + temp.hue = this->hue; + temp.sat = ((this->sat > SAT_DULL) ? SAT_DULL : this->sat); + temp.val = VALUE_LOW; + return temp; + } + }; + /* + This class defines a palette, which is + a map of musical scale degrees to + colors. A palette is tied to a specific + tuning but not to a specific layout. + */ + class paletteDef { + public: + colorDef swatch[MAX_SCALE_DIVISIONS]; // the different colors used in this palette + byte colorNum[MAX_SCALE_DIVISIONS]; // map key (c,d...) to swatches + colorDef getColor(byte givenStepFromC) { + return swatch[colorNum[givenStepFromC] - 1]; + } + float getHue(byte givenStepFromC) { + return getColor(givenStepFromC).hue; + } + byte getSat(byte givenStepFromC) { + return getColor(givenStepFromC).sat; + } + byte getVal(byte givenStepFromC) { + return getColor(givenStepFromC).val; + } + }; + /* + Palettes are defined by creating + a set of colors, and then making + an array of numbers that map the + intervals of that tuning to the + chosen colors. It's like paint + by numbers! Note that the indexes + start with 1, because the arrays are + padded with 0 for entries after + those intialized. + */ + paletteDef palette[] = { + // 12 EDO + {{{HUE_NONE, SAT_BW, VALUE_NORMAL} + , {HUE_BLUE, SAT_DULL, VALUE_SHADE } + , {HUE_CYAN, SAT_DULL, VALUE_NORMAL} + , {HUE_INDIGO, SAT_VIVID, VALUE_NORMAL} + },{1,2,1,2,1,3,4,3,4,3,4,3}}, + // 17 EDO + {{{HUE_NONE, SAT_BW, VALUE_NORMAL} + , {HUE_INDIGO, SAT_VIVID, VALUE_NORMAL} + , {HUE_RED, SAT_VIVID, VALUE_NORMAL} + },{1,2,3,1,2,3,1,1,2,3,1,2,3,1,2,3,1}}, + // 19 EDO + {{{HUE_NONE, SAT_BW, VALUE_NORMAL} // n + , {HUE_YELLOW, SAT_VIVID, VALUE_NORMAL} // # + , {HUE_BLUE, SAT_VIVID, VALUE_NORMAL} // b + , {HUE_MAGENTA, SAT_VIVID, VALUE_NORMAL} // enh + },{1,2,3,1,2,3,1,4,1,2,3,1,2,3,1,2,3,1,4}}, + // 22 EDO + {{{HUE_NONE, SAT_BW, VALUE_NORMAL} // n + , {HUE_BLUE, SAT_VIVID, VALUE_NORMAL} // ^ + , {HUE_MAGENTA, SAT_VIVID, VALUE_NORMAL} // mid + , {HUE_YELLOW, SAT_VIVID, VALUE_NORMAL} // v + },{1,2,3,4,1,2,3,4,1,1,2,3,4,1,2,3,4,1,2,3,4,1}}, + // 24 EDO + {{{HUE_NONE, SAT_BW, VALUE_NORMAL} // n + , {HUE_LIME, SAT_DULL, VALUE_SHADE } // + + , {HUE_CYAN, SAT_VIVID, VALUE_NORMAL} // #/b + , {HUE_INDIGO, SAT_DULL, VALUE_SHADE } // d + , {HUE_CYAN, SAT_DULL, VALUE_SHADE } // enh + },{1,2,3,4,1,2,3,4,1,5,1,2,3,4,1,2,3,4,1,2,3,4,1,5}}, + // 31 EDO + {{{HUE_NONE, SAT_BW, VALUE_NORMAL} // n + , {HUE_RED, SAT_DULL, VALUE_NORMAL} // + + , {HUE_YELLOW, SAT_DULL, VALUE_SHADE } // # + , {HUE_CYAN, SAT_DULL, VALUE_SHADE } // b + , {HUE_INDIGO, SAT_DULL, VALUE_NORMAL} // d + , {HUE_RED, SAT_DULL, VALUE_SHADE } // enh E+ Fb + , {HUE_INDIGO, SAT_DULL, VALUE_SHADE } // enh E# Fd + },{1,2,3,4,5,1,2,3,4,5,1,6,7,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,6,7}}, + // 41 EDO + {{{HUE_NONE, SAT_BW, VALUE_NORMAL} // n + , {HUE_RED, SAT_DULL, VALUE_NORMAL} // ^ + , {HUE_BLUE, SAT_VIVID, VALUE_NORMAL} // + + , {HUE_CYAN, SAT_DULL, VALUE_SHADE } // b + , {HUE_GREEN, SAT_DULL, VALUE_SHADE } // # + , {HUE_MAGENTA, SAT_DULL, VALUE_NORMAL} // d + , {HUE_YELLOW, SAT_VIVID, VALUE_NORMAL} // v + },{1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,1,2,3,4,5,6,7, + 1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,6,7}}, + // 53 EDO + {{{HUE_NONE, SAT_BW, VALUE_NORMAL} // n + , {HUE_ORANGE, SAT_VIVID, VALUE_NORMAL} // ^ + , {HUE_MAGENTA, SAT_DULL, VALUE_NORMAL} // L + , {HUE_INDIGO, SAT_VIVID, VALUE_NORMAL} // bv + , {HUE_GREEN, SAT_VIVID, VALUE_SHADE } // b + , {HUE_YELLOW, SAT_VIVID, VALUE_SHADE } // # + , {HUE_RED, SAT_VIVID, VALUE_NORMAL} // #^ + , {HUE_PURPLE, SAT_DULL, VALUE_NORMAL} // 7 + , {HUE_CYAN, SAT_VIVID, VALUE_SHADE } // v + },{1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7,8,9,1,2,3,9,1,2,3,4,5,6,7,8,9, + 1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7,8,9,1,2,3,9}}, + // 72 EDO + {{{HUE_NONE, SAT_BW, VALUE_NORMAL} // n + , {HUE_GREEN, SAT_DULL, VALUE_SHADE } // ^ + , {HUE_RED, SAT_DULL, VALUE_SHADE } // L + , {HUE_PURPLE, SAT_DULL, VALUE_SHADE } // +/d + , {HUE_BLUE, SAT_DULL, VALUE_SHADE } // 7 + , {HUE_YELLOW, SAT_DULL, VALUE_SHADE } // v + , {HUE_INDIGO, SAT_VIVID, VALUE_SHADE } // #/b + },{1,2,3,4,5,6,7,2,3,4,5,6,1,2,3,4,5,6,7,2,3,4,5,6,1,2,3,4,5,6,1,2,3,4,5,6, + 7,2,3,4,5,6,1,2,3,4,5,6,7,2,3,4,5,6,1,2,3,4,5,6,7,2,3,4,5,6,1,2,3,4,5,6}}, + // BOHLEN PIERCE + {{{HUE_NONE, SAT_BW, VALUE_NORMAL} + , {HUE_INDIGO, SAT_VIVID, VALUE_NORMAL} + , {HUE_RED, SAT_VIVID, VALUE_NORMAL} + },{1,2,3,1,2,3,1,1,2,3,1,2,3}}, + // ALPHA + {{{HUE_NONE, SAT_BW, VALUE_NORMAL} // n + , {HUE_YELLOW, SAT_VIVID, VALUE_NORMAL} // # + , {HUE_INDIGO, SAT_VIVID, VALUE_NORMAL} // d + , {HUE_LIME, SAT_VIVID, VALUE_NORMAL} // + + , {HUE_RED, SAT_VIVID, VALUE_NORMAL} // enharmonic + , {HUE_CYAN, SAT_VIVID, VALUE_NORMAL} // b + },{1,2,3,4,1,2,3,5,6}}, + // BETA + {{{HUE_NONE, SAT_BW, VALUE_NORMAL} // n + , {HUE_INDIGO, SAT_VIVID, VALUE_NORMAL} // # + , {HUE_RED, SAT_VIVID, VALUE_NORMAL} // b + , {HUE_MAGENTA, SAT_DULL, VALUE_NORMAL} // enharmonic + },{1,2,3,1,4,1,2,3,1,2,3}}, + // GAMMA + {{{HUE_NONE, SAT_BW, VALUE_NORMAL} // n + , {HUE_RED, SAT_VIVID, VALUE_NORMAL} // b + , {HUE_BLUE, SAT_VIVID, VALUE_NORMAL} // # + , {HUE_YELLOW, SAT_VIVID, VALUE_NORMAL} // n^ + , {HUE_PURPLE, SAT_VIVID, VALUE_NORMAL} // b^ + , {HUE_GREEN, SAT_VIVID, VALUE_NORMAL} // #^ + }, {1,4,2,5,3,6,1,4,1,4,2,5,3,6,1,4,2,5,3,6}}, + }; + +// @presets + /* + This section of the code defines + a "preset" as a collection of + parameters that control how the + hexboard is operating and playing. + + In the long run this will serve as + a foundation for saving and loading + preferences / settings through the + file system. + */ + class presetDef { + public: + std::string presetName; + int tuningIndex; // instead of using pointers, i chose to store index value of each option, to be saved to a .pref or .ini or something + int layoutIndex; + int scaleIndex; + int keyStepsFromA; // what key the scale is in, where zero equals A. + int transpose; + // define simple recall functions + tuningDef tuning() { + return tuningOptions[tuningIndex]; + } + layoutDef layout() { + return layoutOptions[layoutIndex]; + } + scaleDef scale() { + return scaleOptions[scaleIndex]; + } + int layoutsBegin() { + if (tuningIndex == TUNING_12EDO) { + return 0; + } else { + int temp = 0; + while (layoutOptions[temp].tuning < tuningIndex) { + temp++; + } + return temp; + } + } + int keyStepsFromC() { + return tuning().spanCtoA() - keyStepsFromA; + } + int pitchRelToA4(int givenStepsFromC) { + return givenStepsFromC + tuning().spanCtoA() + transpose; + } + int keyDegree(int givenStepsFromC) { + return positiveMod(givenStepsFromC + keyStepsFromC(), tuning().cycleLength); + } + }; + + presetDef current = { + "Default", // name + TUNING_12EDO, // tuning + 0, // default to the first layout, wicki hayden + 0, // default to using no scale (chromatic) + -9, // default to the key of C, which in 12EDO is -9 steps from A. + 0 // default to no transposition + }; + +// @diagnostics + /* + This section of the code handles + optional sending of log messages + to the Serial port + */ + #define DIAGNOSTICS_ON false + void sendToLog(std::string msg) { + if (DIAGNOSTICS_ON) { + Serial.println(msg.c_str()); + } + } + +// @timing + /* + This section of the code handles basic + timekeeping stuff + */ + #include "hardware/timer.h" // library of code to access the processor's clock functions + uint64_t runTime = 0; // Program loop consistent variable for time in microseconds since power on + uint64_t lapTime = 0; // Used to keep track of how long each loop takes. Useful for rate-limiting. + uint64_t loopTime = 0; // Used to check speed of the loop + uint64_t readClock() { + uint64_t temp = timer_hw->timerawh; + return (temp << 32) | timer_hw->timerawl; + } + void timeTracker() { + lapTime = runTime - loopTime; + loopTime = runTime; // Update previousTime variable to give us a reference point for next loop + runTime = readClock(); // Store the current time in a uniform variable for this program loop + } + +// @fileSystem + /* + This section of the code handles the + file system. There isn't much being + done with it yet, per se. + If so, this section might be relocated + */ + #include "LittleFS.h" // code to use flash drive space as a file system -- not implemented yet, as of May 2024 + void setupFileSystem() { + Serial.begin(115200); // Set serial to make uploads work without bootsel button + LittleFSConfig cfg; // Configure file system defaults + cfg.setAutoFormat(true); // Formats file system if it cannot be mounted. + LittleFS.setConfig(cfg); + LittleFS.begin(); // Mounts file system. + if (!LittleFS.begin()) { + sendToLog("An Error has occurred while mounting LittleFS"); + } else { + sendToLog("LittleFS mounted OK"); + } + } + +// @gridSystem + /* + This section of the code handles the hex grid + Hexagonal coordinates + https://www.redblobgames.com/grids/hexagons/ + http://ondras.github.io/rot.js/manual/#hex/indexing + The HexBoard contains a grid of 140 buttons with + hexagonal keycaps. The processor has 10 pins connected + to a multiplexing unit, which hotswaps between the 14 rows + of ten buttons to allow all 140 inputs to be read in one + program read cycle. + */ + #define MPLEX_1_PIN 4 + #define MPLEX_2_PIN 5 + #define MPLEX_4_PIN 2 + #define MPLEX_8_PIN 3 + #define COLUMN_PIN_0 6 + #define COLUMN_PIN_1 7 + #define COLUMN_PIN_2 8 + #define COLUMN_PIN_3 9 + #define COLUMN_PIN_4 10 + #define COLUMN_PIN_5 11 + #define COLUMN_PIN_6 12 + #define COLUMN_PIN_7 13 + #define COLUMN_PIN_8 14 + #define COLUMN_PIN_9 15 + /* + There are 140 LED pixels on the Hexboard. + LED instructions all go through the LED_PIN. + It so happens that each LED pixel corresponds + to one and only one hex button, so both a LED + and its button can have the same index from 0-139. + Since these parameters are pre-defined by the + hardware build, the dimensions of the grid + are therefore constants. + */ + #define LED_COUNT 140 + #define COLCOUNT 10 + #define ROWCOUNT 14 + /* + Of the 140 buttons, 7 are offset to the bottom left + quadrant of the Hexboard and are reserved as command + buttons. Their LED reference is pre-defined here. + If you want those seven buttons remapped to play + notes, you may wish to change or remove these + variables and alter the value of CMDCOUNT to agree + with how many buttons you reserve for non-note use. + */ + #define CMDBTN_0 0 + #define CMDBTN_1 20 + #define CMDBTN_2 40 + #define CMDBTN_3 60 + #define CMDBTN_4 80 + #define CMDBTN_5 100 + #define CMDBTN_6 120 + #define CMDCOUNT 7 + /* + This class defines the hexagon button + as an object. It stores all real-time + properties of the button -- its coordinates, + its current pressed state, the color + codes to display based on what action is + taken, what note and frequency is assigned, + whether the button is a command or not, + whether the note is in the selected scale, + whether the button is flagged to be animated, + and whether the note is currently + sounding on MIDI / the synth. + + Needless to say, this is an important class. + */ + class buttonDef { + public: + byte btnState = 0; // binary 00 = off, 01 = just pressed, 10 = just released, 11 = held + void interpBtnPress(bool isPress) { + btnState = (((btnState << 1) + isPress) & 3); + } + int8_t coordRow = 0; // hex coordinates + int8_t coordCol = 0; // hex coordinates + uint64_t timePressed = 0; // timecode of last press + uint32_t LEDcodeAnim = 0; // calculate it once and store value, to make LED playback snappier + uint32_t LEDcodePlay = 0; // calculate it once and store value, to make LED playback snappier + uint32_t LEDcodeRest = 0; // calculate it once and store value, to make LED playback snappier + uint32_t LEDcodeOff = 0; // calculate it once and store value, to make LED playback snappier + uint32_t LEDcodeDim = 0; // calculate it once and store value, to make LED playback snappier + bool animate = 0; // hex is flagged as part of the animation in this frame, helps make animations smoother + int16_t stepsFromC = 0; // number of steps from C4 (semitones in 12EDO; microtones if >12EDO) + bool isCmd = 0; // 0 if it's a MIDI note; 1 if it's a MIDI control cmd + bool inScale = 0; // 0 if it's not in the selected scale; 1 if it is + byte note = UNUSED_NOTE; // MIDI note or control parameter corresponding to this hex + int16_t bend = 0; // in microtonal mode, the pitch bend for this note needed to be tuned correctly + byte MIDIch = 0; // what MIDI channel this note is playing on + byte synthCh = 0; // what synth polyphony ch this is playing on + float frequency = 0.0; // what frequency to ring on the synther + }; + /* + This class is like a virtual wheel. + It takes references / pointers to + the state of three command buttons, + translates presses of those buttons + into wheel turns, and converts + these movements into corresponding + values within a range. + + This lets us generalize the + behavior of a virtual pitch bend + wheel or mod wheel using the same + code, only needing to modify the + range of output and the connected + buttons to operate it. + */ + class wheelDef { + public: + byte* alternateMode; // two ways to control + byte* isSticky; // TRUE if you leave value unchanged when no buttons pressed + byte* topBtn; // pointer to the key Status of the button you use as this button + byte* midBtn; + byte* botBtn; + int16_t minValue; + int16_t maxValue; + int* stepValue; // this can be changed via GEM menu + int16_t defValue; // snapback value + int16_t curValue; + int16_t targetValue; + uint64_t timeLastChanged; + void setTargetValue() { + if (*alternateMode) { + if (*midBtn >> 1) { // middle button toggles target (0) vs. step (1) mode + int16_t temp = curValue; + if (*topBtn == 1) {temp += *stepValue;} // tap button + if (*botBtn == 1) {temp -= *stepValue;} // tap button + if (temp > maxValue) {temp = maxValue;} + else if (temp <= minValue) {temp = minValue;} + targetValue = temp; + } else { + switch (((*topBtn >> 1) << 1) + (*botBtn >> 1)) { + case 0b10: targetValue = maxValue; break; + case 0b11: targetValue = defValue; break; + case 0b01: targetValue = minValue; break; + default: targetValue = curValue; break; + } + } + } else { + switch (((*topBtn >> 1) << 2) + ((*midBtn >> 1) << 1) + (*botBtn >> 1)) { + case 0b100: targetValue = maxValue; break; + case 0b110: targetValue = (3 * maxValue + minValue) / 4; break; + case 0b010: + case 0b111: + case 0b101: targetValue = (maxValue + minValue) / 2; break; + case 0b011: targetValue = (maxValue + 3 * minValue) / 4; break; + case 0b001: targetValue = minValue; break; + case 0b000: targetValue = (*isSticky ? curValue : defValue); break; + default: break; + } + } + } + bool updateValue(uint64_t givenTime) { + int16_t temp = targetValue - curValue; + if (temp != 0) { + if ((givenTime - timeLastChanged) >= CC_MSG_COOLDOWN_MICROSECONDS ) { + timeLastChanged = givenTime; + if (abs(temp) < *stepValue) { + curValue = targetValue; + } else { + curValue = curValue + (*stepValue * (temp / abs(temp))); + } + return 1; + } else { + return 0; + } + } else { + return 0; + } + } + }; + const byte mPin[] = { + MPLEX_1_PIN, MPLEX_2_PIN, MPLEX_4_PIN, MPLEX_8_PIN + }; + const byte cPin[] = { + COLUMN_PIN_0, COLUMN_PIN_1, COLUMN_PIN_2, COLUMN_PIN_3, + COLUMN_PIN_4, COLUMN_PIN_5, COLUMN_PIN_6, + COLUMN_PIN_7, COLUMN_PIN_8, COLUMN_PIN_9 + }; + const byte assignCmd[] = { + CMDBTN_0, CMDBTN_1, CMDBTN_2, CMDBTN_3, + CMDBTN_4, CMDBTN_5, CMDBTN_6 + }; + + /* + define h, which is a collection of all the + buttons from 0 to 139. h[i] refers to the + button with the LED address = i. + */ + buttonDef h[LED_COUNT]; + + wheelDef modWheel = { &wheelMode, &modSticky, + &h[assignCmd[4]].btnState, &h[assignCmd[5]].btnState, &h[assignCmd[6]].btnState, + 0, 127, &modWheelSpeed, 0, 0, 0, 0 + }; + wheelDef pbWheel = { &wheelMode, &pbSticky, + &h[assignCmd[4]].btnState, &h[assignCmd[5]].btnState, &h[assignCmd[6]].btnState, + -8192, 8191, &pbWheelSpeed, 0, 0, 0, 0 + }; + wheelDef velWheel = { &wheelMode, &velSticky, + &h[assignCmd[0]].btnState, &h[assignCmd[1]].btnState, &h[assignCmd[2]].btnState, + 0, 127, &velWheelSpeed, 96, 96, 96, 0 + }; + + bool toggleWheel = 0; // 0 for mod, 1 for pb + + void setupPins() { + for (byte p = 0; p < sizeof(cPin); p++) { // For each column pin... + pinMode(cPin[p], INPUT_PULLUP); // set the pinMode to INPUT_PULLUP (+3.3V / HIGH). + } + for (byte p = 0; p < sizeof(mPin); p++) { // For each column pin... + pinMode(mPin[p], OUTPUT); // Setting the row multiplexer pins to output. + } + sendToLog("Pins mounted"); + } + + void setupGrid() { + for (byte i = 0; i < LED_COUNT; i++) { + h[i].coordRow = (i / 10); + h[i].coordCol = (2 * (i % 10)) + (h[i].coordRow & 1); + h[i].isCmd = 0; + h[i].note = UNUSED_NOTE; + h[i].btnState = 0; + } + for (byte c = 0; c < CMDCOUNT; c++) { + h[assignCmd[c]].isCmd = 1; + h[assignCmd[c]].note = CMDB + c; + } + } + +// @LED + /* + This section of the code handles sending + color data to the LED pixels underneath + the hex buttons. + */ + #include <Adafruit_NeoPixel.h> // library of code to interact with the LED array + #define LED_PIN 22 + + Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800); + int32_t rainbowDegreeTime = 65'536; // microseconds to go through 1/360 of rainbow + /* + This is actually a hacked together approximation + of the color space OKLAB. A true conversion would + take the hue, saturation, and value bits and + turn them into linear RGB to feed directly into + the LED class. This conversion is... not very OK... + but does the job for now. A proper implementation + of OKLAB is in the works. + + For transforming hues, the okLAB hue degree (0-360) is + mapped to the RGB hue degree from 0 to 65535, using + simple linear interpolation I created by hand comparing + my HexBoard outputs to a Munsell color chip book. + */ + int16_t transformHue(float h) { + float D = fmod(h,360); + if (!perceptual) { + return 65536 * D / 360; + } else { + // red yellow green cyan blue + int hueIn[] = { 0, 9, 18, 102, 117, 135, 142, 155, 203, 240, 252, 261, 306, 333, 360}; + // #ff0000 #ffff00 #00ff00 #00ffff #0000ff #ff00ff + int hueOut[] = { 0, 3640, 5861,10922,12743,16384,21845,27306,32768,38229,43690,49152,54613,58254,65535}; + byte B = 0; + while (D - hueIn[B] > 0) { + B++; + } + float T = (D - hueIn[B - 1]) / (float)(hueIn[B] - hueIn[B - 1]); + return (hueOut[B - 1] * (1 - T)) + (hueOut[B] * T); + } + } + /* + Saturation and Brightness are taken as is (already in a 0-255 range). + The global brightness / 255 attenuates the resulting color for the + user's brightness selection. Then the resulting RGB (HSV) color is + "un-gamma'd" to be converted to the LED strip color. + */ + uint32_t getLEDcode(colorDef c) { + return strip.gamma32(strip.ColorHSV(transformHue(c.hue),c.sat,c.val * globalBrightness / 255)); + } + /* + This function cycles through each button, and based on what color + palette is active, it calculates the LED color code in the palette, + plus its variations for being animated, played, or out-of-scale, and + stores it for recall during playback and animation. The color + codes remain in the object until this routine is called again. + */ + void setLEDcolorCodes() { + for (byte i = 0; i < LED_COUNT; i++) { + if (!(h[i].isCmd)) { + colorDef setColor; + byte paletteIndex = positiveMod(h[i].stepsFromC,current.tuning().cycleLength); + if (paletteBeginsAtKeyCenter) { + paletteIndex = current.keyDegree(paletteIndex); + } + switch (colorMode) { + case TIERED_COLOR_MODE: // This mode sets the color based on the palettes defined above. + setColor = palette[current.tuningIndex].getColor(paletteIndex); + break; + case RAINBOW_MODE: // This mode assigns the root note as red, and the rest as saturated spectrum colors across the rainbow. + setColor = + { 360 * ((float)paletteIndex / (float)current.tuning().cycleLength) + , SAT_VIVID + , VALUE_NORMAL + }; + break; + case ALTERNATE_COLOR_MODE: + // This mode assigns each note a color based on the interval it forms with the root note. + // This is an adaptation of an algorithm developed by Nicholas Fox and Kite Giedraitis. + float cents = current.tuning().stepSize * paletteIndex; + bool perf = 0; + float center = 0.0; + if (cents < 50) {perf = 1; center = 0.0;} + else if ((cents >= 50) && (cents < 250)) { center = 147.1;} + else if ((cents >= 250) && (cents < 450)) { center = 351.0;} + else if ((cents >= 450) && (cents < 600)) {perf = 1; center = 498.0;} + else if ((cents >= 600) && (cents <= 750)) {perf = 1; center = 702.0;} + else if ((cents > 750) && (cents <= 950)) { center = 849.0;} + else if ((cents > 950) && (cents <=1150)) { center = 1053.0;} + else if ((cents > 1150) && (cents < 1250)) {perf = 1; center = 1200.0;} + else if ((cents >=1250) && (cents < 1450)) { center = 1347.1;} + else if ((cents >=1450) && (cents < 1650)) { center = 1551.0;} + else if ((cents >=1650) && (cents < 1850)) {perf = 1; center = 1698.0;} + else if ((cents >=1800) && (cents <=1950)) {perf = 1; center = 1902.0;} + float offCenter = cents - center; + int16_t altHue = positiveMod((int)(150 + (perf * ((offCenter > 0) ? -72 : 72)) - round(1.44 * offCenter)), 360); + float deSaturate = perf * (abs(offCenter) < 20) * (1 - (0.02 * abs(offCenter))); + setColor = { + (float)altHue, + (byte)(255 - round(255 * deSaturate)), + (byte)(cents ? VALUE_SHADE : VALUE_NORMAL) }; + break; + } + h[i].LEDcodeRest = getLEDcode(setColor); + h[i].LEDcodePlay = getLEDcode(setColor.tint()); + h[i].LEDcodeDim = getLEDcode(setColor.shade()); + setColor = {HUE_NONE,SAT_BW,VALUE_BLACK}; + h[i].LEDcodeOff = getLEDcode(setColor); // turn off entirely + h[i].LEDcodeAnim = h[i].LEDcodePlay; + } + } + sendToLog("LED codes re-calculated."); + } + + void resetVelocityLEDs() { + colorDef tempColor = + { (runTime % (rainbowDegreeTime * 360)) / (float)rainbowDegreeTime + , SAT_MODERATE + , byteLerp(0,255,85,127,velWheel.curValue) + }; + strip.setPixelColor(assignCmd[0], getLEDcode(tempColor)); + + tempColor.val = byteLerp(0,255,42,85,velWheel.curValue); + strip.setPixelColor(assignCmd[1], getLEDcode(tempColor)); + + tempColor.val = byteLerp(0,255,0,42,velWheel.curValue); + strip.setPixelColor(assignCmd[2], getLEDcode(tempColor)); + } + void resetWheelLEDs() { + // middle button + byte tempSat = SAT_BW; + colorDef tempColor = {HUE_NONE, tempSat, (byte)(toggleWheel ? VALUE_SHADE : VALUE_LOW)}; + strip.setPixelColor(assignCmd[3], getLEDcode(tempColor)); + if (toggleWheel) { + // pb red / green + tempSat = byteLerp(SAT_BW,SAT_VIVID,0,8192,abs(pbWheel.curValue)); + tempColor = {(float)((pbWheel.curValue > 0) ? HUE_RED : HUE_CYAN), tempSat, VALUE_FULL}; + strip.setPixelColor(assignCmd[5], getLEDcode(tempColor)); + + tempColor.val = tempSat * (pbWheel.curValue > 0); + strip.setPixelColor(assignCmd[4], getLEDcode(tempColor)); + + tempColor.val = tempSat * (pbWheel.curValue < 0); + strip.setPixelColor(assignCmd[6], getLEDcode(tempColor)); + } else { + // mod blue / yellow + tempSat = byteLerp(SAT_BW,SAT_VIVID,0,64,abs(modWheel.curValue - 63)); + tempColor = { + (float)((modWheel.curValue > 63) ? HUE_YELLOW : HUE_INDIGO), + tempSat, + (byte)(127 + (tempSat / 2)) + }; + strip.setPixelColor(assignCmd[6], getLEDcode(tempColor)); + + if (modWheel.curValue <= 63) { + tempColor.val = 127 - (tempSat / 2); + } + strip.setPixelColor(assignCmd[5], getLEDcode(tempColor)); + + tempColor.val = tempSat * (modWheel.curValue > 63); + strip.setPixelColor(assignCmd[4], getLEDcode(tempColor)); + } + } + uint32_t applyNotePixelColor(byte x) { + if (h[x].animate) { return h[x].LEDcodeAnim; + } else if (h[x].MIDIch) { return h[x].LEDcodePlay; + } else if (h[x].inScale) { return h[x].LEDcodeRest; + } else if (scaleLock) { return h[x].LEDcodeOff; + } else { return h[x].LEDcodeDim; + } + } + void setupLEDs() { + strip.begin(); // INITIALIZE NeoPixel strip object + strip.show(); // Turn OFF all pixels ASAP + sendToLog("LEDs started..."); + setLEDcolorCodes(); + } + void lightUpLEDs() { + for (byte i = 0; i < LED_COUNT; i++) { + if (!(h[i].isCmd)) { + strip.setPixelColor(i,applyNotePixelColor(i)); + } + } + resetVelocityLEDs(); + resetWheelLEDs(); + strip.show(); + } + +// @MIDI + /* + This section of the code handles all + things related to MIDI messages. + */ + #include <Adafruit_TinyUSB.h> // library of code to get the USB port working + #include <MIDI.h> // library of code to send and receive MIDI messages + /* + These values support correct MIDI output. + Note frequencies are converted to MIDI note + and pitch bend messages assuming note 69 + equals concert A4, as defined below. + */ + #define CONCERT_A_HZ 440.0 + /* + Pitch bend messages are calibrated + to a pitch bend range where + -8192 to 8191 = -200 to +200 cents, + or two semitones. + */ + #define PITCH_BEND_SEMIS 2 + /* + Create a new instance of the Arduino MIDI Library, + and attach usb_midi as the transport. + */ + Adafruit_USBD_MIDI usb_midi; + MIDI_CREATE_INSTANCE(Adafruit_USBD_MIDI, usb_midi, MIDI); + std::queue<byte> MPEchQueue; + byte MPEpitchBendsNeeded; + + float freqToMIDI(float Hz) { // formula to convert from Hz to MIDI note + return 69.0 + 12.0 * log2f(Hz / 440.0); + } + float MIDItoFreq(float MIDI) { // formula to convert from MIDI note to Hz + return 440.0 * exp2((MIDI - 69.0) / 12.0); + } + float stepsToMIDI(int16_t stepsFromA) { // return the MIDI pitch associated + return freqToMIDI(CONCERT_A_HZ) + ((float)stepsFromA * (float)current.tuning().stepSize / 100.0); + } + + void setPitchBendRange(byte Ch, byte semitones) { + MIDI.beginRpn(0, Ch); + MIDI.sendRpnValue(semitones << 7, Ch); + MIDI.endRpn(Ch); + sendToLog( + "set pitch bend range on ch " + + std::to_string(Ch) + " to be " + + std::to_string(semitones) + " semitones" + ); + } + + void setMPEzone(byte masterCh, byte sizeOfZone) { + MIDI.beginRpn(6, masterCh); + MIDI.sendRpnValue(sizeOfZone << 7, masterCh); + MIDI.endRpn(masterCh); + sendToLog( + "tried sending MIDI msg to set MPE zone, master ch " + + std::to_string(masterCh) + ", zone of this size: " + std::to_string(sizeOfZone) + ); + } + + void resetTuningMIDI() { + /* + currently the only way that microtonal + MIDI works is via MPE (MIDI polyphonic expression). + This assigns re-tuned notes to an independent channel + so they can be pitched separately. + + if operating in a standard 12-EDO tuning, or in a + tuning with steps that are all exact multiples of + 100 cents, then MPE is not necessary. + */ + if (current.tuning().stepSize == 100.0) { + MPEpitchBendsNeeded = 1; + /* this was an attempt to allow unlimited polyphony for certain EDOs. doesn't work in Logic Pro. + } else if (round(current.tuning().cycleLength * current.tuning().stepSize) == 1200) { + MPEpitchBendsNeeded = current.tuning().cycleLength / std::gcd(12, current.tuning().cycleLength); + */ + } else { + MPEpitchBendsNeeded = 255; + } + if (MPEpitchBendsNeeded > 15) { + setMPEzone(1, 15); // MPE zone 1 = ch 2 thru 16 + while (!MPEchQueue.empty()) { // empty the channel queue + MPEchQueue.pop(); + } + for (byte i = 2; i <= 16; i++) { + MPEchQueue.push(i); // fill the channel queue + sendToLog("pushed ch " + std::to_string(i) + " to the open channel queue"); + } + } else { + setMPEzone(1, 0); + } + // force pitch bend back to the expected range of 2 semitones. + for (byte i = 1; i <= 16; i++) { + MIDI.sendControlChange(123, 0, i); + setPitchBendRange(i, PITCH_BEND_SEMIS); + } + } + + void sendMIDImodulationToCh1() { + MIDI.sendControlChange(1, modWheel.curValue, 1); + sendToLog("sent mod value " + std::to_string(modWheel.curValue) + " to ch 1"); + } + + void sendMIDIpitchBendToCh1() { + MIDI.sendPitchBend(pbWheel.curValue, 1); + sendToLog("sent pb wheel value " + std::to_string(pbWheel.curValue) + " to ch 1"); + } + + void tryMIDInoteOn(byte x) { + // this gets called on any non-command hex + // that is not scale-locked. + if (!(h[x].MIDIch)) { + if (MPEpitchBendsNeeded == 1) { + h[x].MIDIch = 1; + } else if (MPEpitchBendsNeeded <= 15) { + h[x].MIDIch = 2 + positiveMod(h[x].stepsFromC, MPEpitchBendsNeeded); + } else { + if (MPEchQueue.empty()) { // if there aren't any open channels + sendToLog("MPE queue was empty so did not play a midi note"); + } else { + h[x].MIDIch = MPEchQueue.front(); // value in MIDI terms (1-16) + MPEchQueue.pop(); + sendToLog("popped " + std::to_string(h[x].MIDIch) + " off the MPE queue"); + } + } + if (h[x].MIDIch) { + MIDI.sendNoteOn(h[x].note, velWheel.curValue, h[x].MIDIch); // ch 1-16 + MIDI.sendPitchBend(h[x].bend, h[x].MIDIch); // ch 1-16 + sendToLog( + "sent MIDI noteOn: " + std::to_string(h[x].note) + + " pb " + std::to_string(h[x].bend) + + " vel " + std::to_string(velWheel.curValue) + + " ch " + std::to_string(h[x].MIDIch) + ); + } + } + } + + void tryMIDInoteOff(byte x) { + // this gets called on any non-command hex + // that is not scale-locked. + if (h[x].MIDIch) { // but just in case, check + MIDI.sendNoteOff(h[x].note, velWheel.curValue, h[x].MIDIch); + sendToLog( + "sent note off: " + std::to_string(h[x].note) + + " pb " + std::to_string(h[x].bend) + + " vel " + std::to_string(velWheel.curValue) + + " ch " + std::to_string(h[x].MIDIch) + ); + if (MPEpitchBendsNeeded > 15) { + MPEchQueue.push(h[x].MIDIch); + sendToLog("pushed " + std::to_string(h[x].MIDIch) + " on the MPE queue"); + } + h[x].MIDIch = 0; + } + } + + void setupMIDI() { + usb_midi.setStringDescriptor("HexBoard MIDI"); // Initialize MIDI, and listen to all MIDI channels + MIDI.begin(MIDI_CHANNEL_OMNI); // This will also call usb_midi's begin() + resetTuningMIDI(); + sendToLog("setupMIDI okay"); + } + +// @synth + /* + This section of the code handles audio + output via the piezo buzzer and/or the + headphone jack (on hardware v1.2 only) + */ + #include "hardware/pwm.h" // library of code to access the processor's built in pulse wave modulation features + #include "hardware/irq.h" // library of code to let you interrupt code execution to run something of higher priority + /* + It is more convenient to pre-define the correct + pulse wave modulation slice and channel associated + with the PIEZO_PIN on this processor (see RP2040 + manual) than to have it looked up each time. + */ + #define PIEZO_PIN 23 + #define PIEZO_SLICE 3 + #define PIEZO_CHNL 1 + #define AUDIO_PIN 25 + #define AUDIO_SLICE 4 + #define AUDIO_CHNL 1 + /* + These definitions provide 8-bit samples to emulate. + You can add your own as desired; it must + be an array of 256 values, each from 0 to 255. + Ideally the waveform is normalized so that the + peaks are at 0 to 255, with 127 representing + no wave movement. + */ + byte sine[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 3, + 4, 5, 6, 7, 8, 9, 10, 12, 13, 15, 16, 18, 19, 21, 23, 25, + 27, 29, 31, 33, 35, 37, 39, 42, 44, 46, 49, 51, 54, 56, 59, 62, + 64, 67, 70, 73, 76, 79, 81, 84, 87, 90, 93, 96, 99, 103, 106, 109, + 112, 115, 118, 121, 124, 127, 131, 134, 137, 140, 143, 146, 149, 152, 156, 159, + 162, 165, 168, 171, 174, 176, 179, 182, 185, 188, 191, 193, 196, 199, 201, 204, + 206, 209, 211, 213, 216, 218, 220, 222, 224, 226, 228, 230, 232, 234, 236, 237, + 239, 240, 242, 243, 245, 246, 247, 248, 249, 250, 251, 252, 252, 253, 254, 254, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 254, 253, 252, 252, + 251, 250, 249, 248, 247, 246, 245, 243, 242, 240, 239, 237, 236, 234, 232, 230, + 228, 226, 224, 222, 220, 218, 216, 213, 211, 209, 206, 204, 201, 199, 196, 193, + 191, 188, 185, 182, 179, 176, 174, 171, 168, 165, 162, 159, 156, 152, 149, 146, + 143, 140, 137, 134, 131, 127, 124, 121, 118, 115, 112, 109, 106, 103, 99, 96, + 93, 90, 87, 84, 81, 79, 76, 73, 70, 67, 64, 62, 59, 56, 54, 51, + 49, 46, 44, 42, 39, 37, 35, 33, 31, 29, 27, 25, 23, 21, 19, 18, + 16, 15, 13, 12, 10, 9, 8, 7, 6, 5, 4, 3, 3, 2, 1, 1 + }; + byte strings[] = { + 0, 0, 0, 1, 3, 6, 10, 14, 20, 26, 33, 41, 50, 59, 68, 77, + 87, 97, 106, 115, 124, 132, 140, 146, 152, 157, 161, 164, 166, 167, 167, 167, + 165, 163, 160, 157, 153, 149, 144, 140, 135, 130, 126, 122, 118, 114, 111, 109, + 106, 104, 103, 101, 101, 100, 100, 100, 100, 101, 101, 102, 103, 103, 104, 105, + 106, 107, 108, 109, 110, 111, 113, 114, 115, 116, 117, 119, 120, 121, 123, 124, + 126, 127, 129, 131, 132, 134, 135, 136, 138, 139, 140, 141, 142, 144, 145, 146, + 147, 148, 149, 150, 151, 152, 152, 153, 154, 154, 155, 155, 155, 155, 154, 154, + 152, 151, 149, 146, 144, 140, 137, 133, 129, 125, 120, 115, 111, 106, 102, 98, + 95, 92, 90, 88, 88, 88, 89, 91, 94, 98, 103, 109, 115, 123, 131, 140, + 149, 158, 168, 178, 187, 196, 205, 214, 222, 229, 235, 241, 245, 249, 252, 254, + 255, 255, 255, 254, 253, 250, 248, 245, 242, 239, 236, 233, 230, 227, 224, 222, + 220, 218, 216, 215, 214, 213, 212, 211, 210, 210, 209, 208, 207, 206, 205, 203, + 201, 199, 197, 194, 191, 188, 184, 180, 175, 171, 166, 161, 156, 150, 145, 139, + 133, 127, 122, 116, 110, 105, 99, 94, 89, 84, 80, 75, 71, 67, 64, 61, + 58, 56, 54, 52, 50, 49, 48, 47, 46, 45, 45, 44, 43, 42, 41, 40, + 39, 37, 35, 33, 31, 28, 25, 22, 19, 16, 13, 10, 7, 5, 2, 1 + }; + byte clarinet[] = { + 0, 0, 2, 7, 14, 21, 30, 38, 47, 54, 61, 66, 70, 72, 73, 74, + 73, 73, 72, 71, 70, 71, 72, 74, 76, 80, 84, 88, 93, 97, 101, 105, + 109, 111, 113, 114, 114, 114, 113, 112, 111, 110, 109, 109, 109, 110, 112, 114, + 116, 118, 121, 123, 126, 127, 128, 129, 128, 127, 126, 123, 121, 118, 116, 114, + 112, 110, 109, 109, 109, 110, 111, 112, 113, 114, 114, 114, 113, 111, 109, 105, + 101, 97, 93, 88, 84, 80, 76, 74, 72, 71, 70, 71, 72, 73, 73, 74, + 73, 72, 70, 66, 61, 54, 47, 38, 30, 21, 14, 7, 2, 0, 0, 2, + 9, 18, 31, 46, 64, 84, 105, 127, 150, 171, 191, 209, 224, 237, 246, 252, + 255, 255, 253, 248, 241, 234, 225, 217, 208, 201, 194, 189, 185, 183, 182, 181, + 182, 182, 183, 184, 185, 184, 183, 181, 179, 175, 171, 167, 162, 158, 154, 150, + 146, 144, 142, 141, 141, 141, 142, 143, 144, 145, 146, 146, 146, 145, 143, 141, + 139, 136, 134, 132, 129, 128, 127, 126, 127, 128, 129, 132, 134, 136, 139, 141, + 143, 145, 146, 146, 146, 145, 144, 143, 142, 141, 141, 141, 142, 144, 146, 150, + 154, 158, 162, 167, 171, 175, 179, 181, 183, 184, 185, 184, 183, 182, 182, 181, + 182, 183, 185, 189, 194, 201, 208, 217, 225, 234, 241, 248, 253, 255, 255, 252, + 246, 237, 224, 209, 191, 171, 150, 127, 105, 84, 64, 46, 31, 18, 9, 2, + }; + /* + The hybrid synth sound blends between + square, saw, and triangle waveforms + at different frequencies. Said frequencies + are controlled via constants here. + */ + #define TRANSITION_SQUARE 220.0 + #define TRANSITION_SAW_LOW 440.0 + #define TRANSITION_SAW_HIGH 880.0 + #define TRANSITION_TRIANGLE 1760.0 + /* + The poll interval represents how often a + new sample value is emulated on the PWM + hardware. It is the inverse of the digital + audio sample rate. 24 microseconds has been + determined to be the sweet spot, and corresponds + to approximately 41 kHz, which is close to + CD-quality (44.1 kHz). A shorter poll interval + may produce more pleasant tones, but if the + poll is too short then the code will not have + enough time to calculate the new sample and + the resulting audio becomes unstable and + inaccurate. + */ + #define POLL_INTERVAL_IN_MICROSECONDS 24 + /* + Eight voice polyphony can be simulated. + Any more voices and the + resolution is too low to distinguish; + also, the code becomes too slow to keep + up with the poll interval. This value + can be safely reduced below eight if + there are issues. + + Note this is NOT the same as the MIDI + polyphony limit, which is 15 (based + on using channel 2 through 16 for + polyphonic expression mode). + */ + #define POLYPHONY_LIMIT 8 + /* + This defines which hardware alarm + and interrupt address are used + to time the call of the poll() function. + */ + #define ALARM_NUM 2 + #define ALARM_IRQ TIMER_IRQ_2 + /* + A basic EQ level can be stored to perform + simple loudness adjustments at certain + frequencies where human hearing is sensitive. + + By default it's off but you can change this + flag to "true" to enable it. This may also + be moved to a Testing menu option. + */ + #define EQUAL_LOUDNESS_ADJUST true + /* + This class defines a virtual oscillator. + It stores an oscillation frequency in + the form of an increment value, which is + how much a counter would have to be increased + every time the poll() interval is reached, + such that a counter overflows from 0 to 65,535 + back to zero at some frequency per second. + + The value of the counter is useful for reading + a waveform sample, so that an analog signal + can be emulated by reading the sample at each + poll() based on how far the counter has moved + towards 65,536. + */ + class oscillator { + public: + uint16_t increment = 0; + uint16_t counter = 0; + byte a = 127; + byte b = 128; + byte c = 255; + uint16_t ab = 0; + uint16_t cd = 0; + byte eq = 0; + }; + oscillator synth[POLYPHONY_LIMIT]; // maximum polyphony + std::queue<byte> synthChQueue; + const byte attenuation[] = {64,24,17,14,12,11,10,9,8}; // full volume in mono mode; equalized volume in poly. + + byte arpeggiatingNow = UNUSED_NOTE; // if this is 255, set to off (0% duty cycle) + uint64_t arpeggiateTime = 0; // Used to keep track of when this note started playing in ARPEG mode + uint64_t arpeggiateLength = 65'536; // in microseconds. approx a 1/32 note at 114 BPM + + // RUN ON CORE 2 + void poll() { + hw_clear_bits(&timer_hw->intr, 1u << ALARM_NUM); + timer_hw->alarm[ALARM_NUM] = readClock() + POLL_INTERVAL_IN_MICROSECONDS; + uint32_t mix = 0; + byte voices = POLYPHONY_LIMIT; + uint16_t p; + byte t; + byte level = 0; + for (byte i = 0; i < POLYPHONY_LIMIT; i++) { + if (synth[i].increment) { + synth[i].counter += synth[i].increment; // should loop from 65536 -> 0 + p = synth[i].counter; + t = p >> 8; + switch (currWave) { + case WAVEFORM_SAW: break; + case WAVEFORM_TRIANGLE: p = 2 * ((p >> 15) ? p : (65535 - p)); break; + case WAVEFORM_SQUARE: p = 0 - (p > (32768 - modWheel.curValue * 7 * 16)); break; + case WAVEFORM_HYBRID: if (t <= synth[i].a) { + p = 0; + } else if (t < synth[i].b) { + p = (t - synth[i].a) * synth[i].ab; + } else if (t <= synth[i].c) { + p = 65535; + } else { + p = (256 - t) * synth[i].cd; + }; break; + case WAVEFORM_SINE: p = sine[t] << 8; break; + case WAVEFORM_STRINGS: p = strings[t] << 8; break; + case WAVEFORM_CLARINET: p = clarinet[t] << 8; break; + default: break; + } + mix += (p * synth[i].eq); // P[16bit] * EQ[3bit] =[19bit] + } else { + --voices; + } + } + mix *= attenuation[(playbackMode == SYNTH_POLY) * voices]; // [19bit]*atten[6bit] = [25bit] + mix *= velWheel.curValue; // [25bit]*vel[7bit]=[32bit], poly+ + level = mix >> 24; // [32bit] - [8bit] = [24bit] + pwm_set_chan_level(PIEZO_SLICE, PIEZO_CHNL, level); + } + // RUN ON CORE 1 + byte isoTwoTwentySix(float f) { + /* + a very crude implementation of ISO 226 + equal loudness curves + Hz dB Amp ~ sqrt(10^(dB/10)) + 200 0 8 + 800 -3 6 + 1500 0 8 + 3250 -6 4 + 5000 0 8 + */ + if ((f < 8.0) || (f > 12500.0)) { // really crude low- and high-pass + return 0; + } else { + if (EQUAL_LOUDNESS_ADJUST) { + if ((f <= 200.0) || (f >= 5000.0)) { + return 8; + } else { + if (f < 1500.0) { + return 6 + 2 * (float)(abs(f-800) / 700); + } else { + return 4 + 4 * (float)(abs(f-3250) / 1750); + } + } + } else { + return 8; + } + } + } + void setSynthFreq(float frequency, byte channel) { + byte c = channel - 1; + float f = frequency * exp2(pbWheel.curValue * PITCH_BEND_SEMIS / 98304.0); + synth[c].counter = 0; + synth[c].increment = round(f * POLL_INTERVAL_IN_MICROSECONDS * 0.065536); // cycle 0-65535 at resultant frequency + synth[c].eq = isoTwoTwentySix(f); + if (currWave == WAVEFORM_HYBRID) { + if (f < TRANSITION_SQUARE) { + synth[c].b = 128; + } else if (f < TRANSITION_SAW_LOW) { + synth[c].b = (byte)(128 + 127 * (f - TRANSITION_SQUARE) / (TRANSITION_SAW_LOW - TRANSITION_SQUARE)); + } else if (f < TRANSITION_SAW_HIGH) { + synth[c].b = 255; + } else if (f < TRANSITION_TRIANGLE) { + synth[c].b = (byte)(127 + 128 * (TRANSITION_TRIANGLE - f) / (TRANSITION_TRIANGLE - TRANSITION_SAW_HIGH)); + } else { + synth[c].b = 127; + } + if (f < TRANSITION_SAW_LOW) { + synth[c].a = 255 - synth[c].b; + synth[c].c = 255; + } else { + synth[c].a = 0; + synth[c].c = synth[c].b; + } + if (synth[c].a > 126) { + synth[c].ab = 65535; + } else { + synth[c].ab = 65535 / (synth[c].b - synth[c].a - 1); + } + synth[c].cd = 65535 / (256 - synth[c].c); + } + } + + // USE THIS IN MONO OR ARPEG MODE ONLY + + byte findNextHeldNote() { + byte n = UNUSED_NOTE; + for (byte i = 1; i <= LED_COUNT; i++) { + byte j = positiveMod(arpeggiatingNow + i, LED_COUNT); + if ((h[j].MIDIch) && (!h[j].isCmd)) { + n = j; + break; + } + } + return n; + } + void replaceMonoSynthWith(byte x) { + h[arpeggiatingNow].synthCh = 0; + arpeggiatingNow = x; + if (arpeggiatingNow != UNUSED_NOTE) { + h[arpeggiatingNow].synthCh = 1; + setSynthFreq(h[arpeggiatingNow].frequency, 1); + } else { + setSynthFreq(0, 1); + } + } + + void resetSynthFreqs() { + while (!synthChQueue.empty()) { + synthChQueue.pop(); + } + for (byte i = 0; i < POLYPHONY_LIMIT; i++) { + synth[i].increment = 0; + synth[i].counter = 0; + } + for (byte i = 0; i < LED_COUNT; i++) { + h[i].synthCh = 0; + } + if (playbackMode == SYNTH_POLY) { + for (byte i = 0; i < POLYPHONY_LIMIT; i++) { + synthChQueue.push(i + 1); + } + } + } + + void updateSynthWithNewFreqs() { + MIDI.sendPitchBend(pbWheel.curValue, 1); + for (byte i = 0; i < LED_COUNT; i++) { + if (!(h[i].isCmd)) { + if (h[i].synthCh) { + setSynthFreq(h[i].frequency,h[i].synthCh); // pass all notes thru synth again if the pitch bend changes + } + } + } + } + + void trySynthNoteOn(byte x) { + if (playbackMode != SYNTH_OFF) { + if (playbackMode == SYNTH_POLY) { + // operate independently of MIDI + if (synthChQueue.empty()) { + sendToLog("synth channels all firing, so did not add one"); + } else { + h[x].synthCh = synthChQueue.front(); + synthChQueue.pop(); + sendToLog("popped " + std::to_string(h[x].synthCh) + " off the synth queue"); + setSynthFreq(h[x].frequency, h[x].synthCh); + } + } else { + // operate in lockstep with MIDI + if (h[x].MIDIch) { + replaceMonoSynthWith(x); + } + } + } + } + + void trySynthNoteOff(byte x) { + if (playbackMode && (playbackMode != SYNTH_POLY)) { + replaceMonoSynthWith(findNextHeldNote()); + } + if (playbackMode == SYNTH_POLY) { + if (h[x].synthCh) { + setSynthFreq(0, h[x].synthCh); + synthChQueue.push(h[x].synthCh); + h[x].synthCh = 0; + } + } + } + + void setupSynth() { + gpio_set_function(PIEZO_PIN, GPIO_FUNC_PWM); // set that pin as PWM + pwm_set_phase_correct(PIEZO_SLICE, true); // phase correct sounds better + pwm_set_wrap(PIEZO_SLICE, 254); // 0 - 254 allows 0 - 255 level + pwm_set_clkdiv(PIEZO_SLICE, 1.0f); // run at full clock speed + pwm_set_chan_level(PIEZO_SLICE, PIEZO_CHNL, 0); // initialize at zero to prevent whining sound + pwm_set_enabled(PIEZO_SLICE, true); // ENGAGE! + hw_set_bits(&timer_hw->inte, 1u << ALARM_NUM); // initialize the timer + irq_set_exclusive_handler(ALARM_IRQ, poll); // function to run every interrupt + irq_set_enabled(ALARM_IRQ, true); // ENGAGE! + timer_hw->alarm[ALARM_NUM] = readClock() + POLL_INTERVAL_IN_MICROSECONDS; + resetSynthFreqs(); + sendToLog("synth is ready."); + } + + void arpeggiate() { + if (playbackMode == SYNTH_ARPEGGIO) { + if (runTime - arpeggiateTime > arpeggiateLength) { + arpeggiateTime = runTime; + replaceMonoSynthWith(findNextHeldNote()); + } + } + } + +// @animate + /* + This section of the code handles + LED animation responsive to key + presses + */ + /* + The coordinate system used to locate hex buttons + a certain distance and direction away relies on + a preset array of coordinate offsets corresponding + to each of the six linear directions on the hex grid. + These cardinal directions are enumerated to make + the code more legible for humans. + */ + #define HEX_DIRECTION_EAST 0 + #define HEX_DIRECTION_NE 1 + #define HEX_DIRECTION_NW 2 + #define HEX_DIRECTION_WEST 3 + #define HEX_DIRECTION_SW 4 + #define HEX_DIRECTION_SE 5 + // animation variables E NE NW W SW SE + int8_t vertical[] = { 0,-1,-1, 0, 1, 1}; + int8_t horizontal[] = { 2, 1,-1,-2,-1, 1}; + + uint64_t animFrame(byte x) { + if (h[x].timePressed) { // 2^20 microseconds is close enough to 1 second + return 1 + (((runTime - h[x].timePressed) * animationFPS) >> 20); + } else { + return 0; + } + } + void flagToAnimate(int8_t r, int8_t c) { + if (! + ( ( r < 0 ) || ( r >= ROWCOUNT ) + || ( c < 0 ) || ( c >= (2 * COLCOUNT) ) + || ( ( c + r ) & 1 ) + ) + ) { + h[(10 * r) + (c / 2)].animate = 1; + } + } + void animateMirror() { + for (byte i = 0; i < LED_COUNT; i++) { // check every hex + if ((!(h[i].isCmd)) && (h[i].MIDIch)) { // that is a held note + for (byte j = 0; j < LED_COUNT; j++) { // compare to every hex + if ((!(h[j].isCmd)) && (!(h[j].MIDIch))) { // that is a note not being played + int16_t temp = h[i].stepsFromC - h[j].stepsFromC; // look at difference between notes + if (animationType == ANIMATE_OCTAVE) { // set octave diff to zero if need be + temp = positiveMod(temp, current.tuning().cycleLength); + } + if (temp == 0) { // highlight if diff is zero + h[j].animate = 1; + } + } + } + } + } + } + + void animateOrbit() { + for (byte i = 0; i < LED_COUNT; i++) { // check every hex + if ((!(h[i].isCmd)) && (h[i].MIDIch) && ((h[i].inScale) || (!scaleLock))) { // that is a held note + byte tempDir = (animFrame(i) % 6); + flagToAnimate(h[i].coordRow + vertical[tempDir], h[i].coordCol + horizontal[tempDir]); // different neighbor each frame + } + } + } + + void animateRadial() { + for (byte i = 0; i < LED_COUNT; i++) { // check every hex + if (!(h[i].isCmd) && (h[i].inScale || !scaleLock)) { // that is a note + uint64_t radius = animFrame(i); + if ((radius > 0) && (radius < 16)) { // played in the last 16 frames + byte steps = ((animationType == ANIMATE_SPLASH) ? radius : 1); // star = 1 step to next corner; ring = 1 step per hex + int8_t turtleRow = h[i].coordRow + (radius * vertical[HEX_DIRECTION_SW]); + int8_t turtleCol = h[i].coordCol + (radius * horizontal[HEX_DIRECTION_SW]); + for (byte dir = HEX_DIRECTION_EAST; dir < 6; dir++) { // walk along the ring in each of the 6 hex directions + for (byte i = 0; i < steps; i++) { // # of steps to the next corner + flagToAnimate(turtleRow,turtleCol); // flag for animation + turtleRow += (vertical[dir] * (radius / steps)); + turtleCol += (horizontal[dir] * (radius / steps)); + } + } + } + } + } + } + void animateLEDs() { + for (byte i = 0; i < LED_COUNT; i++) { + h[i].animate = 0; + } + if (animationType) { + switch (animationType) { + case ANIMATE_STAR: case ANIMATE_SPLASH: + animateRadial(); + break; + case ANIMATE_ORBIT: + animateOrbit(); + break; + case ANIMATE_OCTAVE: case ANIMATE_BY_NOTE: + animateMirror(); + break; + default: + break; + } + } + } + +// @assignment + /* + This section of the code contains broad + procedures for assigning musical notes + and related values to each button + of the hex grid. + */ + // run this if the layout, key, or transposition changes, but not if color or scale changes + void assignPitches() { + sendToLog("assignPitch was called:"); + for (byte i = 0; i < LED_COUNT; i++) { + if (!(h[i].isCmd)) { + // steps is the distance from C + // the stepsToMIDI function needs distance from A4 + // it also needs to reflect any transposition, but + // NOT the key of the scale. + float N = stepsToMIDI(current.pitchRelToA4(h[i].stepsFromC)); + if (N < 0 || N >= 128) { + h[i].note = UNUSED_NOTE; + h[i].bend = 0; + h[i].frequency = 0.0; + } else { + h[i].note = ((N >= 127) ? 127 : round(N)); + h[i].bend = (ldexp(N - h[i].note, 13) / PITCH_BEND_SEMIS); + h[i].frequency = MIDItoFreq(N); + } + sendToLog( + "hex #" + std::to_string(i) + ", " + + "steps=" + std::to_string(h[i].stepsFromC) + ", " + + "isCmd? " + std::to_string(h[i].isCmd) + ", " + + "note=" + std::to_string(h[i].note) + ", " + + "bend=" + std::to_string(h[i].bend) + ", " + + "freq=" + std::to_string(h[i].frequency) + ", " + + "inScale? " + std::to_string(h[i].inScale) + "." + ); + } + } + sendToLog("assignPitches complete."); + } + void applyScale() { + sendToLog("applyScale was called:"); + for (byte i = 0; i < LED_COUNT; i++) { + if (!(h[i].isCmd)) { + if (current.scale().tuning == ALL_TUNINGS) { + h[i].inScale = 1; + } else { + byte degree = current.keyDegree(h[i].stepsFromC); + if (degree == 0) { + h[i].inScale = 1; // the root is always in the scale + } else { + byte tempSum = 0; + byte iterator = 0; + while (degree > tempSum) { + tempSum += current.scale().pattern[iterator]; + iterator++; + } // add the steps in the scale, and you're in scale + h[i].inScale = (tempSum == degree); // if the note lands on one of those sums + } + } + sendToLog( + "hex #" + std::to_string(i) + ", " + + "steps=" + std::to_string(h[i].stepsFromC) + ", " + + "isCmd? " + std::to_string(h[i].isCmd) + ", " + + "note=" + std::to_string(h[i].note) + ", " + + "inScale? " + std::to_string(h[i].inScale) + "." + ); + } + } + setLEDcolorCodes(); + sendToLog("applyScale complete."); + } + void applyLayout() { // call this function when the layout changes + sendToLog("buildLayout was called:"); + for (byte i = 0; i < LED_COUNT; i++) { + if (!(h[i].isCmd)) { + int8_t distCol = h[i].coordCol - h[current.layout().hexMiddleC].coordCol; + int8_t distRow = h[i].coordRow - h[current.layout().hexMiddleC].coordRow; + h[i].stepsFromC = ( + (distCol * current.layout().acrossSteps) + + (distRow * ( + current.layout().acrossSteps + + (2 * current.layout().dnLeftSteps) + )) + ) / 2; + sendToLog( + "hex #" + std::to_string(i) + ", " + + "steps from C4=" + std::to_string(h[i].stepsFromC) + "." + ); + } + } + applyScale(); // when layout changes, have to re-apply scale and re-apply LEDs + assignPitches(); // same with pitches + sendToLog("buildLayout complete."); + } + void cmdOn(byte x) { // volume and mod wheel read all current buttons + switch (h[x].note) { + case CMDB + 3: + toggleWheel = !toggleWheel; + break; + default: + // the rest should all be taken care of within the wheelDef structure + break; + } + } + void cmdOff(byte x) { // pitch bend wheel only if buttons held. + switch (h[x].note) { + default: + break; // nothing; should all be taken care of within the wheelDef structure + } + } + +// @menu + /* + This section of the code handles the + dot matrix screen and, most importantly, + the menu system display and controls. + + The following library is used: documentation + is also available here. + https://github.com/Spirik/GEM + */ + #define GEM_DISABLE_GLCD // this line is needed to get the B&W display to work + /* + The GEM menu library accepts initialization + values to set the width of various components + of the menu display, as below. + */ + #define MENU_ITEM_HEIGHT 10 + #define MENU_PAGE_SCREEN_TOP_OFFSET 10 + #define MENU_VALUES_LEFT_OFFSET 78 + #define CONTRAST_AWAKE 63 + #define CONTRAST_SCREENSAVER 1 + // Create an instance of the U8g2 graphics library. + U8G2_SH1107_SEEED_128X128_F_HW_I2C u8g2(U8G2_R2, /* reset=*/ U8X8_PIN_NONE); + // Create menu object of class GEM_u8g2. Supply its constructor with reference to u8g2 object we created earlier + GEM_u8g2 menu( + u8g2, GEM_POINTER_ROW, GEM_ITEMS_COUNT_AUTO, + MENU_ITEM_HEIGHT, MENU_PAGE_SCREEN_TOP_OFFSET, MENU_VALUES_LEFT_OFFSET + ); + bool screenSaverOn = 0; + uint64_t screenTime = 0; // GFX timer to count if screensaver should go on + const uint64_t screenSaverTimeout = (1u << 23); // 2^23 microseconds ~ 8 seconds + /* + Create menu page object of class GEMPage. + Menu page holds menu items (GEMItem) and represents menu level. + Menu can have multiple menu pages (linked to each other) with multiple menu items each. + + GEMPage constructor creates each page with the associated label. + GEMItem constructor can create many different sorts of menu items. + The items here are navigation links. + The first parameter is the item label. + The second parameter is the destination page when that item is selected. + */ + GEMPage menuPageMain("HexBoard MIDI Controller"); + GEMPage menuPageTuning("Tuning"); + GEMItem menuTuningBack("<< Back", menuPageMain); + GEMItem menuGotoTuning("Tuning", menuPageTuning); + GEMPage menuPageLayout("Layout"); + GEMItem menuGotoLayout("Layout", menuPageLayout); + GEMItem menuLayoutBack("<< Back", menuPageMain); + GEMPage menuPageScales("Scales"); + GEMItem menuGotoScales("Scales", menuPageScales); + GEMItem menuScalesBack("<< Back", menuPageMain); + GEMPage menuPageColors("Color options"); + GEMItem menuGotoColors("Color options", menuPageColors); + GEMItem menuColorsBack("<< Back", menuPageMain); + GEMPage menuPageSynth("Synth options"); + GEMItem menuGotoSynth("Synth options", menuPageSynth); + GEMItem menuSynthBack("<< Back", menuPageMain); + GEMPage menuPageControl("Control wheel"); + GEMItem menuGotoControl("Control wheel", menuPageControl); + GEMItem menuControlBack("<< Back", menuPageMain); + GEMPage menuPageTesting("Advanced"); + GEMItem menuGotoTesting("Advanced", menuPageTesting); + GEMItem menuTestingBack("<< Back", menuPageMain); + GEMPage menuPageReboot("Ready to flash firmware!"); + /* + We haven't written the code for some procedures, + but the menu item needs to know the address + of procedures it has to run when it's selected. + So we forward-declare a placeholder for the + procedure like this, so that the menu item + can be built, and then later we will define + this procedure in full. + */ + void changeTranspose(); + void rebootToBootloader(); + /* + This GEMItem is meant to just be a read-only text label. + To be honest I don't know how to get just a plain text line to show here other than this! + */ + void fakeButton() {} + GEMItem menuItemVersion("v1.0.0", fakeButton); + /* + This GEMItem runs a given procedure when you select it. + We must declare or define that procedure first. + */ + GEMItem menuItemUSBBootloader("Update Firmware", rebootToBootloader); + /* + Tunings, layouts, scales, and keys are defined + earlier in this code. We should not have to + manually type in menu objects for those + pre-loaded values. Instead, we will use routines to + construct menu items automatically. + + These lines are forward declarations for + the menu objects we will make later. + This allocates space in memory with + enough size to procedurally fill + the objects based on the contents of + the pre-loaded tuning/layout/etc. definitions + we defined above. + */ + GEMItem* menuItemTuning[TUNINGCOUNT]; + GEMItem* menuItemLayout[layoutCount]; + GEMItem* menuItemScales[scaleCount]; + GEMSelect* selectKey[TUNINGCOUNT]; + GEMItem* menuItemKeys[TUNINGCOUNT]; + /* + We are now creating some GEMItems that let you + 1) select a value from a list of options, + 2) update a given variable based on what was chosen, + 3) if necessary, run a procedure as well once the value's chosen. + + The list of options is in the form of a 2-d array. + There are A arrays, one for each option. + Each is 2 entries long. First entry is the label + for that choice, second entry is the value associated. + + These arrays go into a typedef that depends on the type of the variable + being selected (i.e. Byte for small positive integers; Int for + sign-dependent and large integers). + + Then that typeDef goes into a GEMSelect object, with parameters + equal to the number of entries in the array, and the storage size of one element + in the array. The GEMSelect object is basically just a pointer to the + array of choices. The GEMItem then takes the GEMSelect pointer as a parameter. + + The fact that GEM expects pointers and references makes it tricky + to work with if you are new to C++. + */ + SelectOptionByte optionByteYesOrNo[] = { { "No", 0 }, { "Yes" , 1 } }; + GEMSelect selectYesOrNo( sizeof(optionByteYesOrNo) / sizeof(SelectOptionByte), optionByteYesOrNo); + GEMItem menuItemScaleLock( "Scale lock?", scaleLock, selectYesOrNo); + GEMItem menuItemPercep( "Fix color:", perceptual, selectYesOrNo, setLEDcolorCodes); + GEMItem menuItemShiftColor( "ColorByKey", paletteBeginsAtKeyCenter, selectYesOrNo, setLEDcolorCodes); + GEMItem menuItemWheelAlt( "Alt wheel?", wheelMode, selectYesOrNo); + + SelectOptionByte optionByteWheelType[] = { { "Springy", 0 }, { "Sticky", 1} }; + GEMSelect selectWheelType( sizeof(optionByteWheelType) / sizeof(SelectOptionByte), optionByteWheelType); + GEMItem menuItemPBBehave( "Pitch bend", pbSticky, selectWheelType); + GEMItem menuItemModBehave( "Mod wheel", modSticky, selectWheelType); + + SelectOptionByte optionBytePlayback[] = { { "Off", SYNTH_OFF }, { "Mono", SYNTH_MONO }, { "Arp'gio", SYNTH_ARPEGGIO }, { "Poly", SYNTH_POLY } }; + GEMSelect selectPlayback(sizeof(optionBytePlayback) / sizeof(SelectOptionByte), optionBytePlayback); + GEMItem menuItemPlayback( "Synth mode:", playbackMode, selectPlayback, resetSynthFreqs); + + // doing this long-hand because the STRUCT has problems accepting string conversions of numbers for some reason + SelectOptionInt optionIntTransposeSteps[] = { + {"-127",-127},{"-126",-126},{"-125",-125},{"-124",-124},{"-123",-123},{"-122",-122},{"-121",-121},{"-120",-120},{"-119",-119},{"-118",-118},{"-117",-117},{"-116",-116},{"-115",-115},{"-114",-114},{"-113",-113}, + {"-112",-112},{"-111",-111},{"-110",-110},{"-109",-109},{"-108",-108},{"-107",-107},{"-106",-106},{"-105",-105},{"-104",-104},{"-103",-103},{"-102",-102},{"-101",-101},{"-100",-100},{"- 99",- 99},{"- 98",- 98}, + {"- 97",- 97},{"- 96",- 96},{"- 95",- 95},{"- 94",- 94},{"- 93",- 93},{"- 92",- 92},{"- 91",- 91},{"- 90",- 90},{"- 89",- 89},{"- 88",- 88},{"- 87",- 87},{"- 86",- 86},{"- 85",- 85},{"- 84",- 84},{"- 83",- 83}, + {"- 82",- 82},{"- 81",- 81},{"- 80",- 80},{"- 79",- 79},{"- 78",- 78},{"- 77",- 77},{"- 76",- 76},{"- 75",- 75},{"- 74",- 74},{"- 73",- 73},{"- 72",- 72},{"- 71",- 71},{"- 70",- 70},{"- 69",- 69},{"- 68",- 68}, + {"- 67",- 67},{"- 66",- 66},{"- 65",- 65},{"- 64",- 64},{"- 63",- 63},{"- 62",- 62},{"- 61",- 61},{"- 60",- 60},{"- 59",- 59},{"- 58",- 58},{"- 57",- 57},{"- 56",- 56},{"- 55",- 55},{"- 54",- 54},{"- 53",- 53}, + {"- 52",- 52},{"- 51",- 51},{"- 50",- 50},{"- 49",- 49},{"- 48",- 48},{"- 47",- 47},{"- 46",- 46},{"- 45",- 45},{"- 44",- 44},{"- 43",- 43},{"- 42",- 42},{"- 41",- 41},{"- 40",- 40},{"- 39",- 39},{"- 38",- 38}, + {"- 37",- 37},{"- 36",- 36},{"- 35",- 35},{"- 34",- 34},{"- 33",- 33},{"- 32",- 32},{"- 31",- 31},{"- 30",- 30},{"- 29",- 29},{"- 28",- 28},{"- 27",- 27},{"- 26",- 26},{"- 25",- 25},{"- 24",- 24},{"- 23",- 23}, + {"- 22",- 22},{"- 21",- 21},{"- 20",- 20},{"- 19",- 19},{"- 18",- 18},{"- 17",- 17},{"- 16",- 16},{"- 15",- 15},{"- 14",- 14},{"- 13",- 13},{"- 12",- 12},{"- 11",- 11},{"- 10",- 10},{"- 9",- 9},{"- 8",- 8}, + {"- 7",- 7},{"- 6",- 6},{"- 5",- 5},{"- 4",- 4},{"- 3",- 3},{"- 2",- 2},{"- 1",- 1},{"+/-0", 0},{"+ 1", 1},{"+ 2", 2},{"+ 3", 3},{"+ 4", 4},{"+ 5", 5},{"+ 6", 6},{"+ 7", 7}, + {"+ 8", 8},{"+ 9", 9},{"+ 10", 10},{"+ 11", 11},{"+ 12", 12},{"+ 13", 13},{"+ 14", 14},{"+ 15", 15},{"+ 16", 16},{"+ 17", 17},{"+ 18", 18},{"+ 19", 19},{"+ 20", 20},{"+ 21", 21},{"+ 22", 22}, + {"+ 23", 23},{"+ 24", 24},{"+ 25", 25},{"+ 26", 26},{"+ 27", 27},{"+ 28", 28},{"+ 29", 29},{"+ 30", 30},{"+ 31", 31},{"+ 32", 32},{"+ 33", 33},{"+ 34", 34},{"+ 35", 35},{"+ 36", 36},{"+ 37", 37}, + {"+ 38", 38},{"+ 39", 39},{"+ 40", 40},{"+ 41", 41},{"+ 42", 42},{"+ 43", 43},{"+ 44", 44},{"+ 45", 45},{"+ 46", 46},{"+ 47", 47},{"+ 48", 48},{"+ 49", 49},{"+ 50", 50},{"+ 51", 51},{"+ 52", 52}, + {"+ 53", 53},{"+ 54", 54},{"+ 55", 55},{"+ 56", 56},{"+ 57", 57},{"+ 58", 58},{"+ 59", 59},{"+ 60", 60},{"+ 61", 61},{"+ 62", 62},{"+ 63", 63},{"+ 64", 64},{"+ 65", 65},{"+ 66", 66},{"+ 67", 67}, + {"+ 68", 68},{"+ 69", 69},{"+ 70", 70},{"+ 71", 71},{"+ 72", 72},{"+ 73", 73},{"+ 74", 74},{"+ 75", 75},{"+ 76", 76},{"+ 77", 77},{"+ 78", 78},{"+ 79", 79},{"+ 80", 80},{"+ 81", 81},{"+ 82", 82}, + {"+ 83", 83},{"+ 84", 84},{"+ 85", 85},{"+ 86", 86},{"+ 87", 87},{"+ 88", 88},{"+ 89", 89},{"+ 90", 90},{"+ 91", 91},{"+ 92", 92},{"+ 93", 93},{"+ 94", 94},{"+ 95", 95},{"+ 96", 96},{"+ 97", 97}, + {"+ 98", 98},{"+ 99", 99},{"+100", 100},{"+101", 101},{"+102", 102},{"+103", 103},{"+104", 104},{"+105", 105},{"+106", 106},{"+107", 107},{"+108", 108},{"+109", 109},{"+110", 110},{"+111", 111},{"+112", 112}, + {"+113", 113},{"+114", 114},{"+115", 115},{"+116", 116},{"+117", 117},{"+118", 118},{"+119", 119},{"+120", 120},{"+121", 121},{"+122", 122},{"+123", 123},{"+124", 124},{"+125", 125},{"+126", 126},{"+127", 127} + }; + GEMSelect selectTransposeSteps( 255, optionIntTransposeSteps); + GEMItem menuItemTransposeSteps( "Transpose:", transposeSteps, selectTransposeSteps, changeTranspose); + + SelectOptionByte optionByteColor[] = { { "Rainbow", RAINBOW_MODE }, { "Tiered" , TIERED_COLOR_MODE }, {"Alt", ALTERNATE_COLOR_MODE} }; + GEMSelect selectColor( sizeof(optionByteColor) / sizeof(SelectOptionByte), optionByteColor); + GEMItem menuItemColor( "Color mode:", colorMode, selectColor, setLEDcolorCodes); + + SelectOptionByte optionByteAnimate[] = { { "None" , ANIMATE_NONE }, { "Octave", ANIMATE_OCTAVE }, + { "By Note", ANIMATE_BY_NOTE }, { "Star", ANIMATE_STAR }, { "Splash" , ANIMATE_SPLASH }, { "Orbit", ANIMATE_ORBIT } }; + GEMSelect selectAnimate( sizeof(optionByteAnimate) / sizeof(SelectOptionByte), optionByteAnimate); + GEMItem menuItemAnimate( "Animation:", animationType, selectAnimate); + + SelectOptionByte optionByteBright[] = { { "Dim", BRIGHT_DIM}, {"Low", BRIGHT_LOW}, {"Normal", BRIGHT_MID}, {"High", BRIGHT_HIGH}, {"THE SUN", BRIGHT_MAX } }; + GEMSelect selectBright( sizeof(optionByteBright) / sizeof(SelectOptionByte), optionByteBright); + GEMItem menuItemBright( "Brightness", globalBrightness, selectBright, setLEDcolorCodes); + + SelectOptionByte optionByteWaveform[] = { { "Hybrid", WAVEFORM_HYBRID }, { "Square", WAVEFORM_SQUARE }, { "Saw", WAVEFORM_SAW }, + {"Triangl", WAVEFORM_TRIANGLE}, {"Sine", WAVEFORM_SINE}, {"Strings", WAVEFORM_STRINGS}, {"Clrinet", WAVEFORM_CLARINET} }; + GEMSelect selectWaveform(sizeof(optionByteWaveform) / sizeof(SelectOptionByte), optionByteWaveform); + GEMItem menuItemWaveform( "Waveform:", currWave, selectWaveform, resetSynthFreqs); + + SelectOptionInt optionIntModWheel[] = { { "too slo", 1 }, { "Turtle", 2 }, { "Slow", 4 }, + { "Medium", 8 }, { "Fast", 16 }, { "Cheetah", 32 }, { "Instant", 127 } }; + GEMSelect selectModSpeed(sizeof(optionIntModWheel) / sizeof(SelectOptionInt), optionIntModWheel); + GEMItem menuItemModSpeed( "Mod wheel:", modWheelSpeed, selectModSpeed); + GEMItem menuItemVelSpeed( "Vel wheel:", velWheelSpeed, selectModSpeed); + + SelectOptionInt optionIntPBWheel[] = { { "too slo", 128 }, { "Turtle", 256 }, { "Slow", 512 }, + { "Medium", 1024 }, { "Fast", 2048 }, { "Cheetah", 4096 }, { "Instant", 16384 } }; + GEMSelect selectPBSpeed(sizeof(optionIntPBWheel) / sizeof(SelectOptionInt), optionIntPBWheel); + GEMItem menuItemPBSpeed( "PB wheel:", pbWheelSpeed, selectPBSpeed); + + // Call this procedure to return to the main menu + void menuHome() { + menu.setMenuPageCurrent(menuPageMain); + menu.drawMenu(); + } + + void rebootToBootloader() { + menu.setMenuPageCurrent(menuPageReboot); + menu.drawMenu(); + strip.clear(); + strip.show(); + rp2040.rebootToBootloader(); + } + /* + This procedure sets each layout menu item to be either + visible if that layout is available in the current tuning, + or hidden if not. + + It should run once after the layout menu items are + generated, and then once any time the tuning changes. + */ + void showOnlyValidLayoutChoices() { + for (byte L = 0; L < layoutCount; L++) { + menuItemLayout[L]->hide((layoutOptions[L].tuning != current.tuningIndex)); + } + sendToLog("menu: Layout choices were updated."); + } + /* + This procedure sets each scale menu item to be either + visible if that scale is available in the current tuning, + or hidden if not. + + It should run once after the scale menu items are + generated, and then once any time the tuning changes. + */ + void showOnlyValidScaleChoices() { + for (int S = 0; S < scaleCount; S++) { + menuItemScales[S]->hide((scaleOptions[S].tuning != current.tuningIndex) && (scaleOptions[S].tuning != ALL_TUNINGS)); + } + sendToLog("menu: Scale choices were updated."); + } + /* + This procedure sets each key spinner menu item to be either + visible if the key names correspond to the current tuning, + or hidden if not. + + It should run once after the key selectors are + generated, and then once any time the tuning changes. + */ + void showOnlyValidKeyChoices() { + for (int T = 0; T < TUNINGCOUNT; T++) { + menuItemKeys[T]->hide((T != current.tuningIndex)); + } + sendToLog("menu: Key choices were updated."); + } + + void updateLayoutAndRotate() { + applyLayout(); + u8g2.setDisplayRotation(current.layout().isPortrait ? U8G2_R2 : U8G2_R1); // and landscape / portrait rotation + } + /* + This procedure is run when a layout is selected via the menu. + It sets the current layout to the selected value. + If it's different from the previous one, then + re-apply the layout to the grid. In any case, go to the + main menu when done. + */ + void changeLayout(GEMCallbackData callbackData) { + byte selection = callbackData.valByte; + if (selection != current.layoutIndex) { + current.layoutIndex = selection; + updateLayoutAndRotate(); + } + menuHome(); + } + /* + This procedure is run when a scale is selected via the menu. + It sets the current scale to the selected value. + If it's different from the previous one, then + re-apply the scale to the grid. In any case, go to the + main menu when done. + */ + void changeScale(GEMCallbackData callbackData) { // when you change the scale via the menu + int selection = callbackData.valInt; + if (selection != current.scaleIndex) { + current.scaleIndex = selection; + applyScale(); + } + menuHome(); + } + /* + This procedure is run when the key is changed via the menu. + A key change results in a shift in the location of the + scale notes relative to the grid. + In this program, the only thing that occurs is that + the scale is reapplied to the grid. + The menu does not go home because the intent is to stay + on the scale/key screen. + */ + void changeKey() { // when you change the key via the menu + applyScale(); + } + /* + This procedure was declared already and is being defined now. + It's run when the transposition is changed via the menu. + It sets the current transposition to the selected value. + The effect of transposition is to change the sounded + notes but not the layout or display. + The procedure to re-assign pitches is therefore called. + The menu doesn't change because the transpose is a spinner select. + */ + void changeTranspose() { // when you change the transpose via the menu + current.transpose = transposeSteps; + assignPitches(); + updateSynthWithNewFreqs(); + } + /* + This procedure is run when the tuning is changed via the menu. + It affects almost everything in the program, so + quite a few items are reset, refreshed, and redone + when the tuning changes. + */ + void changeTuning(GEMCallbackData callbackData) { + byte selection = callbackData.valByte; + if (selection != current.tuningIndex) { + current.tuningIndex = selection; + current.layoutIndex = current.layoutsBegin(); // reset layout to first in list + current.scaleIndex = 0; // reset scale to "no scale" + current.keyStepsFromA = current.tuning().spanCtoA(); // reset key to C + showOnlyValidLayoutChoices(); // change list of choices in GEM Menu + showOnlyValidScaleChoices(); // change list of choices in GEM Menu + showOnlyValidKeyChoices(); // change list of choices in GEM Menu + updateLayoutAndRotate(); // apply changes above + resetTuningMIDI(); // clear out MIDI queue + resetSynthFreqs(); + } + menuHome(); + } + /* + The procedure below builds menu items for tuning, + layout, scales, and keys based on what's preloaded. + We already declared arrays of menu item objects earlier. + Now we cycle through those arrays, and create GEMItem objects for + each index. What's nice about doing this in an array is, + we do not have to assign a variable name to each object; we just + refer to it by its index in the array. + + The constructor "new GEMItem" is populated with the different + variables in the preset objects we defined earlier. + Then the menu item is added to the associated page. + The item must be entered with the asterisk operator + because an array index technically returns an address in memory + pointing to the object; the addMenuItem procedure wants + the contents of that item, which is what the * beforehand does. + */ + void createTuningMenuItems() { + for (byte T = 0; T < TUNINGCOUNT; T++) { + menuItemTuning[T] = new GEMItem(tuningOptions[T].name.c_str(), changeTuning, T); + menuPageTuning.addMenuItem(*menuItemTuning[T]); + } + } + void createLayoutMenuItems() { + for (byte L = 0; L < layoutCount; L++) { // create pointers to all layouts + menuItemLayout[L] = new GEMItem(layoutOptions[L].name.c_str(), changeLayout, L); + menuPageLayout.addMenuItem(*menuItemLayout[L]); + } + showOnlyValidLayoutChoices(); + } + void createKeyMenuItems() { + for (byte T = 0; T < TUNINGCOUNT; T++) { + selectKey[T] = new GEMSelect(tuningOptions[T].cycleLength, tuningOptions[T].keyChoices); + menuItemKeys[T] = new GEMItem("Key:", current.keyStepsFromA, *selectKey[T], changeKey); + menuPageScales.addMenuItem(*menuItemKeys[T]); + } + showOnlyValidKeyChoices(); + } + void createScaleMenuItems() { + for (int S = 0; S < scaleCount; S++) { // create pointers to all scale items, filter them as you go + menuItemScales[S] = new GEMItem(scaleOptions[S].name.c_str(), changeScale, S); + menuPageScales.addMenuItem(*menuItemScales[S]); + } + showOnlyValidScaleChoices(); + } + + void setupMenu() { + menu.setSplashDelay(0); + menu.init(); + /* + addMenuItem procedure adds that GEM object to the given page. + The menu items appear in the order they are added, + so to change the order in the menu change the order in the code. + */ + menuPageMain.addMenuItem(menuGotoTuning); + createTuningMenuItems(); + menuPageTuning.addMenuItem(menuTuningBack); + menuPageMain.addMenuItem(menuGotoLayout); + createLayoutMenuItems(); + menuPageLayout.addMenuItem(menuLayoutBack); + menuPageMain.addMenuItem(menuGotoScales); + createKeyMenuItems(); + menuPageScales.addMenuItem(menuItemScaleLock); + createScaleMenuItems(); + menuPageScales.addMenuItem(menuScalesBack); + menuPageMain.addMenuItem(menuGotoControl); + menuPageControl.addMenuItem(menuItemPBSpeed); + menuPageControl.addMenuItem(menuItemModSpeed); + menuPageControl.addMenuItem(menuItemVelSpeed); + menuPageControl.addMenuItem(menuControlBack); + menuPageMain.addMenuItem(menuGotoColors); + menuPageColors.addMenuItem(menuItemColor); + menuPageColors.addMenuItem(menuItemBright); + menuPageColors.addMenuItem(menuItemAnimate); + menuPageColors.addMenuItem(menuColorsBack); + menuPageMain.addMenuItem(menuGotoSynth); + menuPageSynth.addMenuItem(menuItemPlayback); + menuPageSynth.addMenuItem(menuItemWaveform); + menuPageSynth.addMenuItem(menuSynthBack); + menuPageMain.addMenuItem(menuItemTransposeSteps); + menuPageMain.addMenuItem(menuGotoTesting); + menuPageTesting.addMenuItem(menuItemVersion); + menuPageTesting.addMenuItem(menuItemPercep); + menuPageTesting.addMenuItem(menuItemShiftColor); + menuPageTesting.addMenuItem(menuItemWheelAlt); + menuPageTesting.addMenuItem(menuItemPBBehave); + menuPageTesting.addMenuItem(menuItemModBehave); + menuPageTesting.addMenuItem(menuItemUSBBootloader); + menuPageTesting.addMenuItem(menuTestingBack); + menuHome(); + } + void setupGFX() { + u8g2.begin(); // Menu and graphics setup + u8g2.setBusClock(1000000); // Speed up display + u8g2.setContrast(CONTRAST_AWAKE); // Set contrast + sendToLog("U8G2 graphics initialized."); + } + void screenSaver() { + if (screenTime <= screenSaverTimeout) { + screenTime = screenTime + lapTime; + if (screenSaverOn) { + screenSaverOn = 0; + u8g2.setContrast(CONTRAST_AWAKE); + } + } else { + if (!screenSaverOn) { + screenSaverOn = 1; + u8g2.setContrast(CONTRAST_SCREENSAVER); + } + } + } + +// @interface + /* + This section of the code handles reading + the rotary knob and physical hex buttons. + + Documentation: + Rotary knob code: + https://github.com/buxtronix/arduino/tree/master/libraries/Rotary + + when the mechanical rotary knob is turned, + the two pins go through a set sequence of + states during one physical "click", as follows: + Direction Binary state of pin A\B + Counterclockwise = 1\1, 0\1, 0\0, 1\0, 1\1 + Clockwise = 1\1, 1\0, 0\0, 0\1, 1\1 + + The neutral state of the knob is 1\1; a turn + is complete when 1\1 is reached again after + passing through all the valid states above, + at which point action should be taken depending + on the direction of the turn. + + The variable rotaryState stores all of this + data and refreshes it each loop of the 2nd processor. + Value Meaning + 0, 4 Knob is in neutral state + 1, 2, 3 CCW turn state 1, 2, 3 + 5, 6, 7 CW turn state 1, 2, 3 + 8, 16 Completed turn CCW, CW + */ + #define ROT_PIN_A 20 + #define ROT_PIN_B 21 + #define ROT_PIN_C 24 + byte rotaryState = 0; + const byte rotaryStateTable[8][4] = { + {0,5,1,0},{2,0,1,0},{2,3,1,0},{2,3,0,8}, + {0,5,1,0},{6,5,0,0},{6,5,7,0},{6,0,7,16} + }; + byte storeRotaryTurn = 0; + bool rotaryClicked = HIGH; + + void readHexes() { + for (byte r = 0; r < ROWCOUNT; r++) { // Iterate through each of the row pins on the multiplexing chip. + for (byte d = 0; d < 4; d++) { + digitalWrite(mPin[d], (r >> d) & 1); + } + for (byte c = 0; c < COLCOUNT; c++) { // Now iterate through each of the column pins that are connected to the current row pin. + byte p = cPin[c]; // Hold the currently selected column pin in a variable. + pinMode(p, INPUT_PULLUP); // Set that row pin to INPUT_PULLUP mode (+3.3V / HIGH). + byte i = c + (r * COLCOUNT); + delayMicroseconds(6); // delay while column pin mode + bool didYouPressHex = (digitalRead(p) == LOW); // hex is pressed if it returns LOW. else not pressed + h[i].interpBtnPress(didYouPressHex); + if (h[i].btnState == 1) { + h[i].timePressed = runTime; // log the time + } + pinMode(p, INPUT); // Set the selected column pin back to INPUT mode (0V / LOW). + } + } + for (byte i = 0; i < LED_COUNT; i++) { // For all buttons in the deck + switch (h[i].btnState) { + case 1: // just pressed + if (h[i].isCmd) { + cmdOn(i); + } else if (h[i].inScale || (!scaleLock)) { + tryMIDInoteOn(i); + trySynthNoteOn(i); + } + break; + case 2: // just released + if (h[i].isCmd) { + cmdOff(i); + } else if (h[i].inScale || (!scaleLock)) { + tryMIDInoteOff(i); + trySynthNoteOff(i); + } + break; + case 3: // held + break; + default: // inactive + break; + } + } + } + void updateWheels() { + velWheel.setTargetValue(); + bool upd = velWheel.updateValue(runTime); + if (upd) { + sendToLog("vel became " + std::to_string(velWheel.curValue)); + } + if (toggleWheel) { + pbWheel.setTargetValue(); + upd = pbWheel.updateValue(runTime); + if (upd) { + sendMIDIpitchBendToCh1(); + updateSynthWithNewFreqs(); + } + } else { + modWheel.setTargetValue(); + upd = modWheel.updateValue(runTime); + if (upd) { + sendMIDImodulationToCh1(); + } + } + } + void setupRotary() { + pinMode(ROT_PIN_A, INPUT_PULLUP); + pinMode(ROT_PIN_B, INPUT_PULLUP); + pinMode(ROT_PIN_C, INPUT_PULLUP); + } + void readKnob() { + rotaryState = rotaryStateTable[rotaryState & 7][ + (digitalRead(ROT_PIN_B) << 1) | digitalRead(ROT_PIN_A) + ]; + if (rotaryState & 24) { + storeRotaryTurn = rotaryState; + } + } + void dealWithRotary() { + if (menu.readyForKey()) { + bool temp = digitalRead(ROT_PIN_C); + if (temp > rotaryClicked) { + menu.registerKeyPress(GEM_KEY_OK); + screenTime = 0; + } + rotaryClicked = temp; + if (storeRotaryTurn != 0) { + menu.registerKeyPress((storeRotaryTurn == 8) ? GEM_KEY_UP : GEM_KEY_DOWN); + storeRotaryTurn = 0; + screenTime = 0; + } + } + } + +// @mainLoop + /* + An Arduino program runs + the setup() function once, then + runs the loop() function on repeat + until the machine is powered off. + + The RP2040 has two identical cores. + Anything called from setup() and loop() + runs on the first core. + Anything called from setup1() and loop1() + runs on the second core. + + On the HexBoard, the second core is + dedicated to two timing-critical tasks: + running the synth emulator, and tracking + the rotary knob inputs. + Everything else runs on the first core. + */ + void setup() { + #if (defined(ARDUINO_ARCH_MBED) && defined(ARDUINO_ARCH_RP2040)) + TinyUSB_Device_Init(0); // Manual begin() is required on core without built-in support for TinyUSB such as mbed rp2040 + #endif + setupMIDI(); + setupFileSystem(); + Wire.setSDA(SDAPIN); + Wire.setSCL(SCLPIN); + setupPins(); + setupGrid(); + applyLayout(); + setupLEDs(); + setupGFX(); + setupRotary(); + setupMenu(); + for (byte i = 0; i < 5 && !TinyUSBDevice.mounted(); i++) { + delay(1); // wait until device mounted, maybe + } + } + void loop() { // run on first core + timeTracker(); // Time tracking functions + screenSaver(); // Reduces wear-and-tear on OLED panel + readHexes(); // Read and store the digital button states of the scanning matrix + arpeggiate(); // arpeggiate if synth mode allows it + updateWheels(); // deal with the pitch/mod wheel + animateLEDs(); // deal with animations + lightUpLEDs(); // refresh LEDs + dealWithRotary(); // deal with menu + } + void setup1() { // set up on second core + setupSynth(); + } + void loop1() { // run on second core + readKnob(); + } diff --git a/Hexperiment.ino b/Hexperiment.ino deleted file mode 100644 index 0d361a6..0000000 --- a/Hexperiment.ino +++ /dev/null @@ -1,2033 +0,0 @@ -// ====== Hexperiment v1.2 - // Copyright 2022-2023 Jared DeCook and Zach DeCook - // with help from Nicholas Fox - // Hardware Information: - // https://github.com/earlephilhower/arduino-pico - // Generic RP2040 running at 133MHz with 16MB of flash - // Licensed under the GNU GPL Version 3. - // (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 - // - // Patches needed for U8G2, Rotary.h - // - // Wishlist: - // * multiple palettes per tuning - // * customize wheel placement and order - // * - // ============================================================================== - - #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> - #include "hardware/pwm.h" - #include "hardware/timer.h" - #include "hardware/irq.h" - #include <queue> // std::queue construct to store open channels in microtonal mode - #include <string> - - // hardware pins - #define SDAPIN 16 - #define SCLPIN 17 - #define LED_PIN 22 - #define ROT_PIN_A 20 - #define ROT_PIN_B 21 - #define ROT_PIN_C 24 - #define MPLEX_1_PIN 4 - #define MPLEX_2_PIN 5 - #define MPLEX_4_PIN 2 - #define MPLEX_8_PIN 3 - #define COLUMN_PIN_0 6 - #define COLUMN_PIN_1 7 - #define COLUMN_PIN_2 8 - #define COLUMN_PIN_3 9 - #define COLUMN_PIN_4 10 - #define COLUMN_PIN_5 11 - #define COLUMN_PIN_6 12 - #define COLUMN_PIN_7 13 - #define COLUMN_PIN_8 14 - #define COLUMN_PIN_9 15 - #define TONEPIN 23 - - // grid related - #define LED_COUNT 140 - #define COLCOUNT 10 - #define ROWCOUNT 14 - - #define HEX_DIRECTION_EAST 0 - #define HEX_DIRECTION_NE 1 - #define HEX_DIRECTION_NW 2 - #define HEX_DIRECTION_WEST 3 - #define HEX_DIRECTION_SW 4 - #define HEX_DIRECTION_SE 5 - - #define CMDBTN_0 0 - #define CMDBTN_1 20 - #define CMDBTN_2 40 - #define CMDBTN_3 60 - #define CMDBTN_4 80 - #define CMDBTN_5 100 - #define CMDBTN_6 120 - #define CMDCOUNT 7 - - // microtonal related - #define TUNINGCOUNT 13 - - #define TUNING_12EDO 0 - #define TUNING_17EDO 1 - #define TUNING_19EDO 2 - #define TUNING_22EDO 3 - #define TUNING_24EDO 4 - #define TUNING_31EDO 5 - #define TUNING_41EDO 6 - #define TUNING_53EDO 7 - #define TUNING_72EDO 8 - #define TUNING_BP 9 - #define TUNING_ALPHA 10 - #define TUNING_BETA 11 - #define TUNING_GAMMA 12 - - #define MAX_SCALE_DIVISIONS 72 - #define ALL_TUNINGS 255 - - // MIDI-related - #define CONCERT_A_HZ 440.0 - #define PITCH_BEND_SEMIS 2 - #define CMDB 192 - #define UNUSED_NOTE 255 - #define CC_MSG_COOLDOWN_MICROSECONDS 32768 - - // buzzer related - #define TONE_SL 3 - #define TONE_CH 1 - #define WAVEFORM_SINE 0 - #define WAVEFORM_STRINGS 1 - #define WAVEFORM_CLARINET 2 - #define WAVEFORM_SQUARE 8 - #define WAVEFORM_SAW 9 - #define WAVEFORM_TRIANGLE 10 - #define POLL_INTERVAL_IN_MICROSECONDS 24 - // buzzer polyphony limit should be 8. - // MIDI should be independent and use MPE logic. - #define POLYPHONY_LIMIT 8 - #define ALARM_NUM 2 - #define ALARM_IRQ TIMER_IRQ_2 - #define BUZZ_OFF 0 - #define BUZZ_MONO 1 - #define BUZZ_ARPEGGIO 2 - #define BUZZ_POLY 3 - - // LED related - - // value / brightness ranges from 0..255 - // black = 0, full strength = 255 - #define BRIGHT_MAX 255 - #define BRIGHT_HIGH 192 - #define BRIGHT_MID 168 - #define BRIGHT_LOW 144 - #define BRIGHT_DIM 108 - - - #define VALUE_BLACK 0 - #define VALUE_LOW 127 - #define VALUE_SHADE 170 - #define VALUE_NORMAL 192 - #define VALUE_FULL 255 - - // saturation ranges from 0..255 - // 0 = black and white - // 255 = full chroma - - #define SAT_BW 0 - #define SAT_TINT 32 - #define SAT_DULL 85 - #define SAT_MODERATE 170 - #define SAT_VIVID 255 - - // hue is an angle from 0.0 to 359.9 - // there is a transform function to map "perceptual" - // hues to RGB. the hue values below are perceptual. - #define HUE_NONE 0.0 - #define HUE_RED 0.0 - #define HUE_ORANGE 36.0 - #define HUE_YELLOW 72.0 - #define HUE_LIME 108.0 - #define HUE_GREEN 144.0 - #define HUE_CYAN 180.0 - #define HUE_BLUE 216.0 - #define HUE_INDIGO 252.0 - #define HUE_PURPLE 288.0 - #define HUE_MAGENTA 324.0 - - #define RAINBOW_MODE 0 - #define TIERED_COLOR_MODE 1 - #define ALTERNATE_COLOR_MODE 2 - - // animations - #define ANIMATE_NONE 0 - #define ANIMATE_STAR 1 - #define ANIMATE_SPLASH 2 - #define ANIMATE_ORBIT 3 - #define ANIMATE_OCTAVE 4 - #define ANIMATE_BY_NOTE 5 - - // menu-related - #define MENU_ITEM_HEIGHT 10 - #define MENU_PAGE_SCREEN_TOP_OFFSET 10 - #define MENU_VALUES_LEFT_OFFSET 78 - - // debug - #define DIAGNOSTIC_OFF 0 - #define DIAGNOSTIC_ON 1 - - // class definitions are in a header so that - // they get read before compiling the main program. - - class tuningDef { - public: - std::string name; // limit is 17 characters for GEM menu - byte cycleLength; // steps before period/cycle/octave repeats - float stepSize; // in cents, 100 = "normal" semitone. - SelectOptionInt keyChoices[MAX_SCALE_DIVISIONS]; - int spanCtoA() { - return keyChoices[0].val_int; - } - }; - - class layoutDef { - public: - std::string name; // limit is 17 characters for GEM menu - bool isPortrait; // affects orientation of the GEM menu only. - byte hexMiddleC; // instead of "what note is button 1", "what button is the middle" - int8_t acrossSteps; // defined this way to be compatible with original v1.1 firmare - int8_t dnLeftSteps; // defined this way to be compatible with original v1.1 firmare - byte tuning; // index of the tuning that this layout is designed for - }; - - class colorDef { - public: - float hue; - byte sat; - byte val; - colorDef tint() { - colorDef temp; - temp.hue = this->hue; - temp.sat = ((this->sat > SAT_TINT) ? SAT_TINT : this->sat); - temp.val = VALUE_FULL; - return temp; - } - colorDef shade() { - colorDef temp; - temp.hue = this->hue; - temp.sat = ((this->sat > SAT_TINT) ? SAT_TINT : this->sat); - temp.val = VALUE_LOW; - return temp; - } - }; - - class paletteDef { - public: - colorDef swatch[MAX_SCALE_DIVISIONS]; // the different colors used in this palette - byte colorNum[MAX_SCALE_DIVISIONS]; // map key (c,d...) to swatches - colorDef getColor(byte givenStepFromC) { - return swatch[colorNum[givenStepFromC] - 1]; - } - float getHue(byte givenStepFromC) { - return getColor(givenStepFromC).hue; - } - byte getSat(byte givenStepFromC) { - return getColor(givenStepFromC).sat; - } - byte getVal(byte givenStepFromC) { - return getColor(givenStepFromC).val; - } - }; - - class buttonDef { - public: - byte btnState = 0; // binary 00 = off, 01 = just pressed, 10 = just released, 11 = held - void interpBtnPress(bool isPress) { - btnState = (((btnState << 1) + isPress) & 3); - } - int8_t coordRow = 0; // hex coordinates - int8_t coordCol = 0; // hex coordinates - uint32_t timePressed = 0; // timecode of last press - uint32_t LEDcodeAnim = 0; // calculate it once and store value, to make LED playback snappier - uint32_t LEDcodePlay = 0; // calculate it once and store value, to make LED playback snappier - uint32_t LEDcodeRest = 0; // calculate it once and store value, to make LED playback snappier - uint32_t LEDcodeOff = 0; // calculate it once and store value, to make LED playback snappier - uint32_t LEDcodeDim = 0; // calculate it once and store value, to make LED playback snappier - bool animate = 0; // hex is flagged as part of the animation in this frame, helps make animations smoother - int16_t stepsFromC = 0; // number of steps from C4 (semitones in 12EDO; microtones if >12EDO) - bool isCmd = 0; // 0 if it's a MIDI note; 1 if it's a MIDI control cmd - bool inScale = 0; // 0 if it's not in the selected scale; 1 if it is - byte note = UNUSED_NOTE; // MIDI note or control parameter corresponding to this hex - int16_t bend = 0; // in microtonal mode, the pitch bend for this note needed to be tuned correctly - byte MIDIch = 0; // what MIDI channel this note is playing on - byte buzzCh = 0; // what buzzer ch this is playing on - float frequency = 0.0; // what frequency to ring on the buzzer - }; - - class wheelDef { - public: - bool alternateMode; // two ways to control - bool isSticky; // TRUE if you leave value unchanged when no buttons pressed - byte* topBtn; // pointer to the key Status of the button you use as this button - byte* midBtn; - byte* botBtn; - int16_t minValue; - int16_t maxValue; - int* stepValue; // this can be changed via GEM menu - int16_t defValue; // snapback value - int16_t curValue; - int16_t targetValue; - uint32_t timeLastChanged; - void setTargetValue() { - if (alternateMode) { - if (*midBtn >> 1) { // middle button toggles target (0) vs. step (1) mode - int16_t temp = curValue; - if (*topBtn == 1) {temp += *stepValue;} // tap button - if (*botBtn == 1) {temp -= *stepValue;} // tap button - if (temp > maxValue) {temp = maxValue;} - else if (temp <= minValue) {temp = minValue;} - targetValue = temp; - } else { - switch (((*topBtn >> 1) << 1) + (*botBtn >> 1)) { - case 0b10: targetValue = maxValue; break; - case 0b11: targetValue = defValue; break; - case 0b01: targetValue = minValue; break; - default: targetValue = curValue; break; - } - } - } else { - switch (((*topBtn >> 1) << 2) + ((*midBtn >> 1) << 1) + (*botBtn >> 1)) { - case 0b100: targetValue = maxValue; break; - case 0b110: targetValue = (3 * maxValue + minValue) / 4; break; - case 0b010: - case 0b111: - case 0b101: targetValue = (maxValue + minValue) / 2; break; - case 0b011: targetValue = (maxValue + 3 * minValue) / 4; break; - case 0b001: targetValue = minValue; break; - case 0b000: targetValue = (isSticky ? curValue : defValue); break; - default: break; - } - } - } - bool updateValue(uint32_t givenTime) { - int16_t temp = targetValue - curValue; - if (temp != 0) { - if ((givenTime - timeLastChanged) >= CC_MSG_COOLDOWN_MICROSECONDS ) { - timeLastChanged = givenTime; - if (abs(temp) < *stepValue) { - curValue = targetValue; - } else { - curValue = curValue + (*stepValue * (temp / abs(temp))); - } - return 1; - } else { - return 0; - } - } else { - return 0; - } - } - }; - - class scaleDef { - public: - std::string name; - byte tuning; - byte pattern[MAX_SCALE_DIVISIONS]; - }; - - // this class should only be touched by the 2nd core - class oscillator { - public: - uint16_t increment = 0; - uint16_t counter = 0; - byte eq = 0; - }; - - // 1/8192 of a whole tone pitch bend accuracy ~ 0.025 cents. - // over 128 possible notes, error shd be less than 0.0002 cents to avoid drift. - // expressing cents to 6 sig figs should be sufficient. - // notation -- comma delimited string. - // first entry should be the label for A=440. - // last entry should be C, i.e. the "home key". - // the rest of the scale C thru G will be spelled using the same pattern. - // the number of commas is used to count where A and C are located in step space. - tuningDef tuningOptions[] = { - { "12 EDO", 12, 100.000, - {{"C" ,-9},{"C#",-8},{"D" ,-7},{"Eb",-6},{"E" ,-5},{"F",-4} - ,{"F#",-3},{"G" ,-2},{"G#",-1},{"A" , 0},{"Bb", 1},{"B", 2} - }}, - { "17 EDO", 17, 70.5882, - {{"C",-13},{"Db",-12},{"C#",-11},{"D",-10},{"Eb",-9},{"D#",-8} - ,{"E", -7},{"F" , -6},{"Gb", -5},{"F#",-4},{"G", -3},{"Ab",-2} - ,{"G#",-1},{"A" , 0},{"Bb", 1},{"A#", 2},{"B", 3} - }}, - { "19 EDO", 19, 63.1579, - {{"C" ,-14},{"C#",-13},{"Db",-12},{"D",-11},{"D#",-10},{"Eb",-9},{"E",-8} - ,{"E#", -7},{"F" , -6},{"F#", -5},{"Gb",-4},{"G", -3},{"G#",-2} - ,{"Ab", -1},{"A" , 0},{"A#", 1},{"Bb", 2},{"B", 3},{"Cb", 4} - }}, - { "22 EDO", 22, 54.5455, - {{" C", -17},{"^C",-16},{"vC#",-15},{"vD",-14},{" D",-13},{"^D",-12} - ,{"^Eb",-11},{"vE",-10},{" E", -9},{" F", -8},{"^F", -7},{"vF#",-6} - ,{"vG", -5},{" G", -4},{"^G", -3},{"vG#",-2},{"vA", -1},{" A", 0} - ,{"^A", 1},{"^Bb", 2},{"vB", 3},{" B", 4} - }}, - { "24 EDO", 24, 50.0000, - {{"C", -18},{"C+",-17},{"C#",-16},{"Dd",-15},{"D",-14},{"D+",-13} - ,{"Eb",-12},{"Ed",-11},{"E", -10},{"E+", -9},{"F", -8},{"F+", -7} - ,{"F#", -6},{"Gd", -5},{"G", -4},{"G+", -3},{"G#",-2},{"Ad", -1} - ,{"A", 0},{"A+", 1},{"Bb", 2},{"Bd", 3},{"B", 4},{"Cd", 5} - }}, - { "31 EDO", 31, 38.7097, - {{"C",-23},{"C+",-22},{"C#",-21},{"Db",-20},{"Dd",-19} - ,{"D",-18},{"D+",-17},{"D#",-16},{"Eb",-15},{"Ed",-14} - ,{"E",-13},{"E+",-12} ,{"Fd",-11} - ,{"F",-10},{"F+", -9},{"F#", -8},{"Gb", -7},{"Gd", -6} - ,{"G", -5},{"G+", -4},{"G#", -3},{"Ab", -2},{"Ad", -1} - ,{"A", 0},{"A+", 1},{"A#", 2},{"Bb", 3},{"Bd", 4} - ,{"B", 5},{"B+", 6} ,{"Cd", 7} - }}, - { "41 EDO", 41, 29.2683, - {{" C",-31},{"^C",-30},{" C+",-29},{" Db",-28},{" C#",-27},{" Dd",-26},{"vD",-24} - ,{" D",-24},{"^D",-23},{" D+",-22},{" Eb",-21},{" D#",-20},{" Ed",-19},{"vE",-18} - ,{" E",-17},{"^E",-16} ,{"vF",-15} - ,{" F",-14},{"^F",-13},{" F+",-12},{" Gb",-11},{" F#",-10},{" Gd", -9},{"vG", -8} - ,{" G", -7},{"^G", -6},{" G+", -5},{" Ab", -4},{" G#", -3},{" Ad", -2},{"vA", -1} - ,{" A", 0},{"^A", 1},{" A+", 2},{" Bb", 3},{" A#", 4},{" Bd", 5},{"vB", 6} - ,{" B", 7},{"^B", 8} ,{"vC", 9} - }}, - { "53 EDO", 53, 22.6415, - {{" C", -40},{"^C", -39},{">C",-38},{"vDb",-37},{"Db",-36} - ,{" C#",-35},{"^C#",-34},{"<D",-33},{"vD", -32} - ,{" D", -31},{"^D", -30},{">D",-29},{"vEb",-28},{"Eb",-27} - ,{" D#",-26},{"^D#",-25},{"<E",-24},{"vE", -23} - ,{" E", -22},{"^E", -21},{">E",-20},{"vF", -19} - ,{" F", -18},{"^F", -17},{">F",-16},{"vGb",-15},{"Gb",-14} - ,{" F#",-13},{"^F#",-12},{"<G",-11},{"vG", -10} - ,{" G", -9},{"^G", -8},{">G", -7},{"vAb", -6},{"Ab", -5} - ,{" G#", -4},{"^G#", -3},{"<A", -2},{"vA", -1} - ,{" A", 0},{"^A", 1},{">A", 2},{"vBb", 3},{"Bb", 4} - ,{" A#", 5},{"^A#", 6},{"<B", 7},{"vB", 8} - ,{" B", 9},{"^B", 10},{"<C", 11},{"vC", 12} - }}, - { "72 EDO", 72, 16.6667, - {{" C", -54},{"^C", -53},{">C", -52},{" C+",-51},{"<C#",-50},{"vC#",-49} - ,{" C#",-48},{"^C#",-47},{">C#",-46},{" Dd",-45},{"<D" ,-44},{"vD" ,-43} - ,{" D", -42},{"^D", -41},{">D", -40},{" D+",-39},{"<Eb",-38},{"vEb",-37} - ,{" Eb",-36},{"^Eb",-35},{">Eb",-34},{" Ed",-33},{"<E" ,-32},{"vE" ,-31} - ,{" E", -30},{"^E", -29},{">E", -28},{" E+",-27},{"<F" ,-26},{"vF" ,-25} - ,{" F", -24},{"^F", -23},{">F", -22},{" F+",-21},{"<F#",-20},{"vF#",-19} - ,{" F#",-18},{"^F#",-17},{">F#",-16},{" Gd",-15},{"<G" ,-14},{"vG" ,-13} - ,{" G", -12},{"^G", -11},{">G", -10},{" G+", -9},{"<G#", -8},{"vG#", -7} - ,{" G#", -6},{"^G#", -5},{">G#", -4},{" Ad", -3},{"<A" , -2},{"vA" , -1} - ,{" A", 0},{"^A", 1},{">A", 2},{" A+", 3},{"<Bb", 4},{"vBb", 5} - ,{" Bb", 6},{"^Bb", 7},{">Bb", 8},{" Bd", 9},{"<B" , 10},{"vB" , 11} - ,{" B", 12},{"^B", 13},{">B", 14},{" Cd", 15},{"<C" , 16},{"vC" , 17} - }}, - { "Bohlen-Pierce", 13, 146.304, - {{"C",-10},{"Db",-9},{"D",-8},{"E",-7},{"F",-6},{"Gb",-5} - ,{"G",-4},{"H",-3},{"Jb",-2},{"J",-1},{"A",0},{"Bb",1},{"B",2} - }}, - { "Carlos Alpha", 9, 77.9650, - {{"I",0},{"I#",1},{"II-",2},{"II+",3},{"III",4} - ,{"III#",5},{"IV-",6},{"IV+",7},{"Ib",8} - }}, - { "Carlos Beta", 11, 63.8329, - {{"I",0},{"I#",1},{"IIb",2},{"II",3},{"II#",4},{"III",5} - ,{"III#",6},{"IVb",7},{"IV",8},{"IV#",9},{"Ib",10} - }}, - { "Carlos Gamma", 20, 35.0985, - {{" I", 0},{"^I", 1},{" IIb", 2},{"^IIb", 3},{" I#", 4},{"^I#", 5} - ,{" II", 6},{"^II", 7} - ,{" III",8},{"^III",9},{" IVb",10},{"^IVb",11},{" III#",12},{"^III#",13} - ,{" IV",14},{"^IV",15},{" Ib", 16},{"^Ib", 17},{" IV#", 18},{"^IV#", 19} - }}, - }; - - paletteDef palette[] = { - // 12 EDO - {{ {HUE_NONE, SAT_BW, VALUE_NORMAL } - , {HUE_BLUE, SAT_DULL, VALUE_SHADE } - , {HUE_CYAN, SAT_DULL, VALUE_NORMAL } - , {HUE_INDIGO, SAT_VIVID, VALUE_NORMAL } - }, {1,2,1,2,1,3,4,3,4,3,4,3}}, - // 17 EDO - {{ {HUE_NONE, SAT_BW, VALUE_NORMAL } - , {HUE_INDIGO, SAT_VIVID, VALUE_NORMAL } - , {HUE_RED, SAT_VIVID, VALUE_NORMAL } - }, {1,2,3,1,2,3,1,1,2,3,1,2,3,1,2,3,1}}, - // 19 EDO - {{ {HUE_NONE, SAT_BW, VALUE_NORMAL } // n - , {HUE_YELLOW, SAT_VIVID, VALUE_NORMAL } // # - , {HUE_BLUE, SAT_VIVID, VALUE_NORMAL } // b - , {HUE_MAGENTA, SAT_VIVID, VALUE_NORMAL } // enh - }, {1,2,3,1,2,3,1,4,1,2,3,1,2,3,1,2,3,1,4}}, - // 22 EDO - {{ {HUE_NONE, SAT_BW, VALUE_NORMAL } // n - , {HUE_BLUE, SAT_VIVID, VALUE_NORMAL } // ^ - , {HUE_MAGENTA, SAT_VIVID, VALUE_NORMAL } // mid - , {HUE_YELLOW, SAT_VIVID, VALUE_NORMAL } // v - }, {1,2,3,4,1,2,3,4,1,1,2,3,4,1,2,3,4,1,2,3,4,1}}, - // 24 EDO - {{ {HUE_NONE, SAT_BW, VALUE_NORMAL } // n - , {HUE_LIME, SAT_DULL, VALUE_SHADE } // + - , {HUE_CYAN, SAT_VIVID, VALUE_NORMAL } // #/b - , {HUE_INDIGO, SAT_DULL, VALUE_SHADE } // d - , {HUE_CYAN, SAT_DULL, VALUE_SHADE } // enh - }, {1,2,3,4,1,2,3,4,1,5,1,2,3,4,1,2,3,4,1,2,3,4,1,5}}, - // 31 EDO - {{ {HUE_NONE, SAT_BW, VALUE_NORMAL } // n - , {HUE_RED, SAT_DULL, VALUE_NORMAL } // + - , {HUE_YELLOW, SAT_DULL, VALUE_SHADE } // # - , {HUE_CYAN, SAT_DULL, VALUE_SHADE } // b - , {HUE_INDIGO, SAT_DULL, VALUE_NORMAL } // d - , {HUE_RED, SAT_DULL, VALUE_SHADE } // enh E+ Fb - , {HUE_INDIGO, SAT_DULL, VALUE_SHADE } // enh E# Fd - }, {1,2,3,4,5,1,2,3,4,5,1,6,7,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,6,7}}, - // 41 EDO - {{ {HUE_NONE, SAT_BW, VALUE_NORMAL } // n - , {HUE_RED, SAT_DULL, VALUE_NORMAL } // ^ - , {HUE_BLUE, SAT_VIVID, VALUE_NORMAL } // + - , {HUE_CYAN, SAT_DULL, VALUE_SHADE } // b - , {HUE_GREEN, SAT_DULL, VALUE_SHADE } // # - , {HUE_MAGENTA, SAT_DULL, VALUE_NORMAL } // d - , {HUE_YELLOW, SAT_VIVID, VALUE_NORMAL } // v - }, {1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2,3,1,2,3,4,5,6,7, - 1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,6,7}}, - // 53 EDO - {{ {HUE_NONE, SAT_BW, VALUE_NORMAL } // n - , {HUE_ORANGE, SAT_VIVID, VALUE_NORMAL } // ^ - , {HUE_MAGENTA, SAT_DULL, VALUE_NORMAL } // L - , {HUE_INDIGO, SAT_VIVID, VALUE_NORMAL } // bv - , {HUE_GREEN, SAT_VIVID, VALUE_SHADE } // b - , {HUE_YELLOW, SAT_VIVID, VALUE_SHADE } // # - , {HUE_RED, SAT_VIVID, VALUE_NORMAL } // #^ - , {HUE_PURPLE, SAT_DULL, VALUE_NORMAL } // 7 - , {HUE_CYAN, SAT_VIVID, VALUE_SHADE } // v - }, {1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7,8,9,1,2,3,9,1,2,3,4,5,6,7,8,9, - 1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7,8,9,1,2,3,9}}, - // 72 EDO - {{ {HUE_NONE, SAT_BW, VALUE_NORMAL } // n - , {HUE_GREEN, SAT_DULL, VALUE_SHADE } // ^ - , {HUE_RED, SAT_DULL, VALUE_SHADE } // L - , {HUE_PURPLE, SAT_DULL, VALUE_SHADE } // +/d - , {HUE_BLUE, SAT_DULL, VALUE_SHADE } // 7 - , {HUE_YELLOW, SAT_DULL, VALUE_SHADE } // v - , {HUE_INDIGO, SAT_VIVID, VALUE_SHADE } // #/b - }, {1,2,3,4,5,6,7,2,3,4,5,6,1,2,3,4,5,6,7,2,3,4,5,6,1,2,3,4,5,6,1,2,3,4,5,6, - 7,2,3,4,5,6,1,2,3,4,5,6,7,2,3,4,5,6,1,2,3,4,5,6,7,2,3,4,5,6,1,2,3,4,5,6}}, - // BOHLEN PIERCE - {{ {HUE_NONE, SAT_BW, VALUE_NORMAL } - , {HUE_INDIGO, SAT_VIVID, VALUE_NORMAL } - , {HUE_RED, SAT_VIVID, VALUE_NORMAL } - }, {1,2,3,1,2,3,1,1,2,3,1,2,3}}, - // ALPHA - {{ {HUE_NONE, SAT_BW, VALUE_NORMAL } // n - , {HUE_YELLOW, SAT_VIVID, VALUE_NORMAL } // # - , {HUE_INDIGO, SAT_VIVID, VALUE_NORMAL } // d - , {HUE_LIME, SAT_VIVID, VALUE_NORMAL } // + - , {HUE_RED, SAT_VIVID, VALUE_NORMAL } // enharmonic - , {HUE_CYAN, SAT_VIVID, VALUE_NORMAL } // b - }, {1,2,3,4,1,2,3,5,6}}, - // BETA - {{ {HUE_NONE, SAT_BW, VALUE_NORMAL } // n - , {HUE_INDIGO, SAT_VIVID, VALUE_NORMAL } // # - , {HUE_RED, SAT_VIVID, VALUE_NORMAL } // b - , {HUE_MAGENTA, SAT_DULL, VALUE_NORMAL } // enharmonic - }, {1,2,3,1,4,1,2,3,1,2,3}}, - // GAMMA - {{ {HUE_NONE, SAT_BW, VALUE_NORMAL } // n - , {HUE_RED, SAT_VIVID, VALUE_NORMAL } // b - , {HUE_BLUE, SAT_VIVID, VALUE_NORMAL } // # - , {HUE_YELLOW, SAT_VIVID, VALUE_NORMAL } // n^ - , {HUE_PURPLE, SAT_VIVID, VALUE_NORMAL } // b^ - , {HUE_GREEN, SAT_VIVID, VALUE_NORMAL } // #^ - }, {1,4,2,5,3,6,1,4,1,4,2,5,3,6,1,4,2,5,3,6}}, - }; - - layoutDef layoutOptions[] = { - { "Wicki-Hayden", 1, 64, 2, -7, TUNING_12EDO }, - { "Harmonic Table", 0, 75, -7, 3, TUNING_12EDO }, - { "Janko", 0, 65, -1, -1, TUNING_12EDO }, - { "Gerhard", 0, 65, -1, -3, TUNING_12EDO }, - { "Accordion C-sys.", 1, 75, 2, -3, TUNING_12EDO }, - { "Accordion B-sys.", 1, 64, 1, -3, TUNING_12EDO }, - - { "Full Gamut", 1, 65, 1, -9, TUNING_17EDO }, - { "Bosanquet-Wilson", 0, 65, -2, -1, TUNING_17EDO }, - { "Neutral Thirds A", 0, 65, -1, -2, TUNING_17EDO }, - { "Neutral Thirds B", 0, 65, 1, -3, TUNING_17EDO }, - - { "Full Gamut", 1, 65, 1, -9, TUNING_19EDO }, - { "Bosanquet-Wilson", 0, 65, -1, -2, TUNING_19EDO }, - { "Kleismic", 0, 65, -1, -4, TUNING_19EDO }, - - { "Full Gamut", 1, 65, 1, -8, TUNING_22EDO }, - { "Bosanquet-Wilson", 0, 65, -3, -1, TUNING_22EDO }, - { "Porcupine", 0, 65, 1, -4, TUNING_22EDO }, - - { "Full Gamut", 1, 65, 1, -9, TUNING_24EDO }, - { "Bosanquet-Wilson", 0, 65, -1, -3, TUNING_24EDO }, - { "Inverted", 0, 65, 1, -4, TUNING_24EDO }, - - { "Full Gamut", 1, 65, 1, -7, TUNING_31EDO }, - { "Bosanquet-Wilson", 0, 65, -2, -3, TUNING_31EDO }, - { "Double Bosanquet", 0, 65, -1, -4, TUNING_31EDO }, - { "Anti-Double Bos.", 0, 65, 1, -5, TUNING_31EDO }, - - { "Full Gamut", 0, 65, 1, -8, TUNING_41EDO }, // forty-one #3 - { "Bosanquet-Wilson", 0, 65, -4, -3, TUNING_41EDO }, // forty-one #1 - { "Gerhard", 0, 65, 3, -10, TUNING_41EDO }, // forty-one #2 - { "Baldy", 0, 65, -1, -6, TUNING_41EDO }, - { "Rodan", 1, 65, -1, -7, TUNING_41EDO }, - - { "Wicki-Hayden", 1, 64, 9, -31, TUNING_53EDO }, - { "Bosanquet-Wilson", 0, 65, -5, -4, TUNING_53EDO }, - { "Kleismic A", 0, 65, -8, -3, TUNING_53EDO }, - { "Kleismic B", 0, 65, -5, -3, TUNING_53EDO }, - { "Harmonic Table", 0, 75, -31, 14, TUNING_53EDO }, - { "Buzzard", 0, 65, -9, -1, TUNING_53EDO }, - - { "Full Gamut", 1, 65, 1, -9, TUNING_72EDO }, - { "Expanded Janko", 0, 65, -1, -6, TUNING_72EDO }, - - { "Full Gamut", 1, 65, 1, -9, TUNING_BP }, - { "Standard", 0, 65, -2, -1, TUNING_BP }, - - { "Full Gamut", 1, 65, 1, -9, TUNING_ALPHA }, - { "Compressed", 0, 65, -2, -1, TUNING_ALPHA }, - - { "Full Gamut", 1, 65, 1, -9, TUNING_BETA }, - { "Compressed", 0, 65, -2, -1, TUNING_BETA }, - - { "Full Gamut", 1, 65, 1, -9, TUNING_GAMMA }, - { "Compressed", 0, 65, -2, -1, TUNING_GAMMA } - }; - - scaleDef scaleOptions[] = { - { "None", ALL_TUNINGS, { 0 } }, - // 12 EDO - { "Major", TUNING_12EDO, { 2,2,1,2,2,2,1 } }, - { "Minor, natural", TUNING_12EDO, { 2,1,2,2,1,2,2 } }, - { "Minor, melodic", TUNING_12EDO, { 2,1,2,2,2,2,1 } }, - { "Minor, harmonic", TUNING_12EDO, { 2,1,2,2,1,3,1 } }, - { "Pentatonic, major", TUNING_12EDO, { 2,2,3,2,3 } }, - { "Pentatonic, minor", TUNING_12EDO, { 3,2,2,3,2 } }, - { "Blues", TUNING_12EDO, { 3,1,1,1,1,3,2 } }, - { "Double Harmonic", TUNING_12EDO, { 1,3,1,2,1,3,1 } }, - { "Phrygian", TUNING_12EDO, { 1,2,2,2,1,2,2 } }, - { "Phrygian Dominant", TUNING_12EDO, { 1,3,1,2,1,2,2 } }, - { "Dorian", TUNING_12EDO, { 2,1,2,2,2,1,2 } }, - { "Lydian", TUNING_12EDO, { 2,2,2,1,2,2,1 } }, - { "Lydian Dominant", TUNING_12EDO, { 2,2,2,1,2,1,2 } }, - { "Mixolydian", TUNING_12EDO, { 2,2,1,2,2,1,2 } }, - { "Locrian", TUNING_12EDO, { 1,2,2,1,2,2,2 } }, - { "Whole tone", TUNING_12EDO, { 2,2,2,2,2,2 } }, - { "Octatonic", TUNING_12EDO, { 2,1,2,1,2,1,2,1 } }, - // 17 EDO; for more: https://en.xen.wiki/w/17edo#Scales - { "Diatonic", TUNING_17EDO, { 3,3,1,3,3,3,1 } }, - { "Pentatonic", TUNING_17EDO, { 3,3,4,3,4 } }, - { "Harmonic", TUNING_17EDO, { 3,2,3,2,2,2,3 } }, - { "Husayni maqam", TUNING_17EDO, { 2,2,3,3,2,1,1,3 } }, - { "Blues", TUNING_17EDO, { 4,3,1,1,1,4,3 } }, - { "Hydra", TUNING_17EDO, { 3,3,1,1,2,3,2,1,1 } }, - // 19 EDO; for more: https://en.xen.wiki/w/19edo#Scales - { "Diatonic", TUNING_19EDO, { 3,3,2,3,3,3,2 } }, - { "Pentatonic", TUNING_19EDO, { 3,3,5,3,5 } }, - { "Semaphore", TUNING_19EDO, { 3,1,3,1,3,3,1,3,1 } }, - { "Negri", TUNING_19EDO, { 2,2,2,2,2,1,2,2,2,2 } }, - { "Sensi", TUNING_19EDO, { 2,2,1,2,2,2,1,2,2,2,1 } }, - { "Kleismic", TUNING_19EDO, { 1,3,1,1,3,1,1,3,1,3,1 } }, - { "Magic", TUNING_19EDO, { 3,1,1,1,3,1,1,1,3,1,1,1,1 } }, - { "Kind of blues", TUNING_19EDO, { 4,4,1,2,4,4 } }, - // 22 EDO; for more: https://en.xen.wiki/w/22edo_modes - { "Diatonic", TUNING_22EDO, { 4,4,1,4,4,4,1 } }, - { "Pentatonic", TUNING_22EDO, { 4,4,5,4,5 } }, - { "Orwell", TUNING_22EDO, { 3,2,3,2,3,2,3,2,2 } }, - { "Porcupine", TUNING_22EDO, { 4,3,3,3,3,3,3 } }, - { "Pajara", TUNING_22EDO, { 2,2,3,2,2,2,3,2,2,2 } }, - // 24 EDO; for more: https://en.xen.wiki/w/24edo_scales - { "Diatonic 12", TUNING_24EDO, { 4,4,2,4,4,4,2 } }, - { "Diatonic Soft", TUNING_24EDO, { 3,5,2,3,5,4,2 } }, - { "Diatonic Neutral", TUNING_24EDO, { 4,3,3,4,3,4,3 } }, - { "Pentatonic (12)", TUNING_24EDO, { 4,4,6,4,6 } }, - { "Pentatonic (Haba)", TUNING_24EDO, { 5,5,5,5,4 } }, - { "Invert Pentatonic", TUNING_24EDO, { 6,3,6,6,3 } }, - { "Rast maqam", TUNING_24EDO, { 4,3,3,4,4,2,1,3 } }, - { "Bayati maqam", TUNING_24EDO, { 3,3,4,4,2,1,3,4 } }, - { "Hijaz maqam", TUNING_24EDO, { 2,6,2,4,2,1,3,4 } }, - { "8-EDO", TUNING_24EDO, { 3,3,3,3,3,3,3,3 } }, - { "Wyschnegradsky", TUNING_24EDO, { 2,2,2,2,2,1,2,2,2,2,2,2,1 } }, - // 31 EDO; for more: https://en.xen.wiki/w/31edo#Scales - { "Diatonic", TUNING_31EDO, { 5,5,3,5,5,5,3 } }, - { "Pentatonic", TUNING_31EDO, { 5,5,8,5,8 } }, - { "Harmonic", TUNING_31EDO, { 5,5,4,4,4,3,3,3 } }, - { "Mavila", TUNING_31EDO, { 5,3,3,3,5,3,3,3,3 } }, - { "Quartal", TUNING_31EDO, { 2,2,7,2,2,7,2,7 } }, - { "Orwell", TUNING_31EDO, { 4,3,4,3,4,3,4,3,3 } }, - { "Neutral", TUNING_31EDO, { 4,4,4,4,4,4,4,3 } }, - { "Miracle", TUNING_31EDO, { 4,3,3,3,3,3,3,3,3,3 } }, - // 41 EDO; for more: https://en.xen.wiki/w/41edo#Scales_and_modes - { "Diatonic", TUNING_41EDO, { 7,7,3,7,7,7,3 } }, - { "Pentatonic", TUNING_41EDO, { 7,7,10,7,10 } }, - { "Pure major", TUNING_41EDO, { 7,6,4,7,6,7,4 } }, - { "5-limit chromatic", TUNING_41EDO, { 4,3,4,2,4,3,4,4,2,4,3,4 } }, - { "7-limit chromatic", TUNING_41EDO, { 3,4,2,4,4,3,4,2,4,3,3,4 } }, - { "Harmonic", TUNING_41EDO, { 5,4,4,4,4,3,3,3,3,3,2,3 } }, - { "Middle East-ish", TUNING_41EDO, { 7,5,7,5,5,7,5 } }, - { "Thai", TUNING_41EDO, { 6,6,6,6,6,6,5 } }, - { "Slendro", TUNING_41EDO, { 8,8,8,8,9 } }, - { "Pelog / Mavila", TUNING_41EDO, { 8,5,5,8,5,5,5 } }, - // 53 EDO - { "Diatonic", TUNING_53EDO, { 9,9,4,9,9,9,4 } }, - { "Pentatonic", TUNING_53EDO, { 9,9,13,9,13 } }, - { "Rast makam", TUNING_53EDO, { 9,8,5,9,9,4,4,5 } }, - { "Usshak makam", TUNING_53EDO, { 7,6,9,9,4,4,5,9 } }, - { "Hicaz makam", TUNING_53EDO, { 5,12,5,9,4,9,9 } }, - { "Orwell", TUNING_53EDO, { 7,5,7,5,7,5,7,5,5 } }, - { "Sephiroth", TUNING_53EDO, { 6,5,5,6,5,5,6,5,5,5 } }, - { "Smitonic", TUNING_53EDO, { 11,11,3,11,3,11,3 } }, - { "Slendric", TUNING_53EDO, { 7,3,7,3,7,3,7,3,7,3,3 } }, - { "Semiquartal", TUNING_53EDO, { 9,2,9,2,9,2,9,2,9 } }, - // 72 EDO - { "Diatonic", TUNING_72EDO, { 12,12,6,12,12,12,6 } }, - { "Pentatonic", TUNING_72EDO, { 12,12,18,12,18 } }, - { "Ben Johnston", TUNING_72EDO, { 6,6,6,5,5,5,9,8,4,4,7,7 } }, - { "18-EDO", TUNING_72EDO, { 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4 } }, - { "Miracle", TUNING_72EDO, { 5,2,5,2,5,2,2,5,2,5,2,5,2,5,2,5,2,5,2,5,2 } }, - { "Marvolo", TUNING_72EDO, { 5,5,5,5,5,5,5,2,5,5,5,5,5,5 } }, - { "Catakleismic", TUNING_72EDO, { 4,7,4,4,4,7,4,4,4,7,4,4,4,7,4 } }, - { "Palace", TUNING_72EDO, { 10,9,11,12,10,9,11 } }, - // BP - { "Lambda", TUNING_BP, { 2,1,2,1,2,1,2,1,1 } }, - // Alpha - { "Super Meta Lydian", TUNING_ALPHA, { 3,2,2,2 } }, - // Beta - { "Super Meta Lydian", TUNING_BETA, { 3,3,3,2 } }, - // Gamma - { "Super Meta Lydian", TUNING_GAMMA, { 6,5,5,4 } } - }; - - byte sine[] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 3, - 4, 5, 6, 7, 8, 9, 10, 12, 13, 15, 16, 18, 19, 21, 23, 25, - 27, 29, 31, 33, 35, 37, 39, 42, 44, 46, 49, 51, 54, 56, 59, 62, - 64, 67, 70, 73, 76, 79, 81, 84, 87, 90, 93, 96, 99, 103, 106, 109, - 112, 115, 118, 121, 124, 127, 131, 134, 137, 140, 143, 146, 149, 152, 156, 159, - 162, 165, 168, 171, 174, 176, 179, 182, 185, 188, 191, 193, 196, 199, 201, 204, - 206, 209, 211, 213, 216, 218, 220, 222, 224, 226, 228, 230, 232, 234, 236, 237, - 239, 240, 242, 243, 245, 246, 247, 248, 249, 250, 251, 252, 252, 253, 254, 254, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 254, 253, 252, 252, - 251, 250, 249, 248, 247, 246, 245, 243, 242, 240, 239, 237, 236, 234, 232, 230, - 228, 226, 224, 222, 220, 218, 216, 213, 211, 209, 206, 204, 201, 199, 196, 193, - 191, 188, 185, 182, 179, 176, 174, 171, 168, 165, 162, 159, 156, 152, 149, 146, - 143, 140, 137, 134, 131, 127, 124, 121, 118, 115, 112, 109, 106, 103, 99, 96, - 93, 90, 87, 84, 81, 79, 76, 73, 70, 67, 64, 62, 59, 56, 54, 51, - 49, 46, 44, 42, 39, 37, 35, 33, 31, 29, 27, 25, 23, 21, 19, 18, - 16, 15, 13, 12, 10, 9, 8, 7, 6, 5, 4, 3, 3, 2, 1, 1 - }; - - byte strings[] = { - 0, 0, 0, 1, 3, 6, 10, 14, 20, 26, 33, 41, 50, 59, 68, 77, - 87, 97, 106, 115, 124, 132, 140, 146, 152, 157, 161, 164, 166, 167, 167, 167, - 165, 163, 160, 157, 153, 149, 144, 140, 135, 130, 126, 122, 118, 114, 111, 109, - 106, 104, 103, 101, 101, 100, 100, 100, 100, 101, 101, 102, 103, 103, 104, 105, - 106, 107, 108, 109, 110, 111, 113, 114, 115, 116, 117, 119, 120, 121, 123, 124, - 126, 127, 129, 131, 132, 134, 135, 136, 138, 139, 140, 141, 142, 144, 145, 146, - 147, 148, 149, 150, 151, 152, 152, 153, 154, 154, 155, 155, 155, 155, 154, 154, - 152, 151, 149, 146, 144, 140, 137, 133, 129, 125, 120, 115, 111, 106, 102, 98, - 95, 92, 90, 88, 88, 88, 89, 91, 94, 98, 103, 109, 115, 123, 131, 140, - 149, 158, 168, 178, 187, 196, 205, 214, 222, 229, 235, 241, 245, 249, 252, 254, - 255, 255, 255, 254, 253, 250, 248, 245, 242, 239, 236, 233, 230, 227, 224, 222, - 220, 218, 216, 215, 214, 213, 212, 211, 210, 210, 209, 208, 207, 206, 205, 203, - 201, 199, 197, 194, 191, 188, 184, 180, 175, 171, 166, 161, 156, 150, 145, 139, - 133, 127, 122, 116, 110, 105, 99, 94, 89, 84, 80, 75, 71, 67, 64, 61, - 58, 56, 54, 52, 50, 49, 48, 47, 46, 45, 45, 44, 43, 42, 41, 40, - 39, 37, 35, 33, 31, 28, 25, 22, 19, 16, 13, 10, 7, 5, 2, 1 - }; - - byte clarinet[] = { - 0, 0, 2, 7, 14, 21, 30, 38, 47, 54, 61, 66, 70, 72, 73, 74, - 73, 73, 72, 71, 70, 71, 72, 74, 76, 80, 84, 88, 93, 97, 101, 105, - 109, 111, 113, 114, 114, 114, 113, 112, 111, 110, 109, 109, 109, 110, 112, 114, - 116, 118, 121, 123, 126, 127, 128, 129, 128, 127, 126, 123, 121, 118, 116, 114, - 112, 110, 109, 109, 109, 110, 111, 112, 113, 114, 114, 114, 113, 111, 109, 105, - 101, 97, 93, 88, 84, 80, 76, 74, 72, 71, 70, 71, 72, 73, 73, 74, - 73, 72, 70, 66, 61, 54, 47, 38, 30, 21, 14, 7, 2, 0, 0, 2, - 9, 18, 31, 46, 64, 84, 105, 127, 150, 171, 191, 209, 224, 237, 246, 252, - 255, 255, 253, 248, 241, 234, 225, 217, 208, 201, 194, 189, 185, 183, 182, 181, - 182, 182, 183, 184, 185, 184, 183, 181, 179, 175, 171, 167, 162, 158, 154, 150, - 146, 144, 142, 141, 141, 141, 142, 143, 144, 145, 146, 146, 146, 145, 143, 141, - 139, 136, 134, 132, 129, 128, 127, 126, 127, 128, 129, 132, 134, 136, 139, 141, - 143, 145, 146, 146, 146, 145, 144, 143, 142, 141, 141, 141, 142, 144, 146, 150, - 154, 158, 162, 167, 171, 175, 179, 181, 183, 184, 185, 184, 183, 182, 182, 181, - 182, 183, 185, 189, 194, 201, 208, 217, 225, 234, 241, 248, 253, 255, 255, 252, - 246, 237, 224, 209, 191, 171, 150, 127, 105, 84, 64, 46, 31, 18, 9, 2, - }; - - // ====== useful math functions - int positiveMod(int n, int d) { - return (((n % d) + d) % d); - } - - 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; - } - - 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); - - Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800); - - Rotary rotary = Rotary(ROT_PIN_A, ROT_PIN_B); - bool rotaryIsClicked = HIGH; // - bool rotaryWasClicked = HIGH; // - int8_t rotaryKnobTurns = 0; // - byte maxKnobTurns = 1; - - // Create an instance of the U8g2 graphics library. - U8G2_SH1107_SEEED_128X128_F_HW_I2C u8g2(U8G2_R2, /* reset=*/ U8X8_PIN_NONE); - - // Create menu object of class GEM_u8g2. Supply its constructor with reference to u8g2 object we created earlier - GEM_u8g2 menu( - u8g2, GEM_POINTER_ROW, GEM_ITEMS_COUNT_AUTO, - MENU_ITEM_HEIGHT, MENU_PAGE_SCREEN_TOP_OFFSET, MENU_VALUES_LEFT_OFFSET - ); - const byte defaultContrast = 63; // GFX default contrast - bool screenSaverOn = 0; // - uint64_t screenTime = 0; // GFX timer to count if screensaver should go on - const uint64_t screenSaverTimeout = (1u << 23); // 2^23 microseconds ~ 8 seconds - - const byte diagnostics = DIAGNOSTIC_ON; - - // Global time variables - uint64_t runTime = 0; // Program loop consistent variable for time in microseconds since power on - uint64_t lapTime = 0; // Used to keep track of how long each loop takes. Useful for rate-limiting. - uint64_t loopTime = 0; // Used to check speed of the loop in diagnostics mode 4 - - // animation variables E NE NW W SW SE - int8_t vertical[] = { 0,-1,-1, 0, 1, 1}; - int8_t horizontal[] = { 2, 1,-1,-2,-1, 1}; - - byte animationFPS = 32; // actually frames per 2^20 microseconds. close enough to 30fps - int32_t rainbowDegreeTime = 65'536; // microseconds to go through 1/360 of rainbow - - // Button matrix and LED locations (PROD unit only) - const byte mPin[] = { - MPLEX_1_PIN, MPLEX_2_PIN, MPLEX_4_PIN, MPLEX_8_PIN - }; - const byte cPin[] = { - COLUMN_PIN_0, COLUMN_PIN_1, COLUMN_PIN_2, COLUMN_PIN_3, - COLUMN_PIN_4, COLUMN_PIN_5, COLUMN_PIN_6, - COLUMN_PIN_7, COLUMN_PIN_8, COLUMN_PIN_9 - }; - const byte assignCmd[] = { - CMDBTN_0, CMDBTN_1, CMDBTN_2, CMDBTN_3, - CMDBTN_4, CMDBTN_5, CMDBTN_6 - }; - - // MIDI note layout tables overhauled procedure since v1.1 - - buttonDef h[LED_COUNT]; // a collection of all the buttons from 0 to 139 - // h[i] refers to the button with the LED address = i. - const byte layoutCount = sizeof(layoutOptions) / sizeof(layoutDef); - const byte scaleCount = sizeof(scaleOptions) / sizeof(scaleDef); - - // Tone and Arpeggiator variables - oscillator synth[POLYPHONY_LIMIT]; // maximum polyphony - std::queue<byte> MPEchQueue; - std::queue<byte> buzzChQueue; - const byte attenuation[] = {24,24,17,14,12,11,10,9,8}; // kind of a fast inverse square - - - byte arpeggiatingNow = UNUSED_NOTE; // if this is 255, buzzer set to off (0% duty cycle) - uint64_t arpeggiateTime = 0; // Used to keep track of when this note started buzzin - uint32_t arpeggiateLength = 65'536; // in microseconds - - byte scaleLock = 0; - byte perceptual = 1; - - int velWheelSpeed = 8; - int modWheelSpeed = 8; - int pbWheelSpeed = 1024; - - wheelDef modWheel = { false, true, // standard mode, sticky - &h[assignCmd[4]].btnState, &h[assignCmd[5]].btnState, &h[assignCmd[6]].btnState, - 0, 127, &modWheelSpeed, 0, 0, 0, 0 - }; - wheelDef pbWheel = { false, false, // standard mode, not sticky - &h[assignCmd[4]].btnState, &h[assignCmd[5]].btnState, &h[assignCmd[6]].btnState, - -8192, 8191, &pbWheelSpeed, 0, 0, 0, 0 - }; - wheelDef velWheel = { false, true, // standard mode, sticky - &h[assignCmd[0]].btnState, &h[assignCmd[1]].btnState, &h[assignCmd[2]].btnState, - 0, 127, &velWheelSpeed, 96, 96, 96, 0 - }; - bool toggleWheel = 0; // 0 for mod, 1 for pb - - - // 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 menuPageTuning("Tuning"); - GEMItem menuTuningBack("<< Back", menuPageMain); - GEMItem menuGotoTuning("Tuning", menuPageTuning); - GEMPage menuPageLayout("Layout"); - GEMItem menuGotoLayout("Layout", menuPageLayout); - GEMItem menuLayoutBack("<< Back", menuPageMain); - GEMPage menuPageScales("Scales"); - GEMItem menuGotoScales("Scales", menuPageScales); - GEMItem menuScalesBack("<< Back", menuPageMain); - GEMPage menuPageColors("Color options"); - GEMItem menuGotoColors("Color options", menuPageColors); - GEMItem menuColorsBack("<< Back", menuPageMain); - GEMPage menuPageSynth("Buzzer options"); - GEMItem menuGotoSynth("Buzzer options", menuPageSynth); - GEMItem menuSynthBack("<< Back", menuPageMain); - GEMPage menuPageControl("Control wheel"); - GEMItem menuGotoControl("Control wheel", menuPageControl); - GEMItem menuControlBack("<< Back", menuPageMain); - GEMPage menuPageTesting("Testing"); - GEMItem menuGotoTesting("Testing", menuPageTesting); - GEMItem menuTestingBack("<< Back", menuPageMain); - char* blank = ""; - GEMItem menuItemVersion("v0.6.0",blank,true); - - // the following get initialized in the setup() routine. - GEMItem* menuItemTuning[TUNINGCOUNT]; - GEMItem* menuItemLayout[layoutCount]; - GEMItem* menuItemScales[scaleCount]; - GEMSelect* selectKey[TUNINGCOUNT]; - GEMItem* menuItemKeys[TUNINGCOUNT]; - - byte paletteBeginsAtKeyCenter = 1; - - void setLEDcolorCodes(); // forward-declaration - SelectOptionByte optionByteYesOrNo[] = { { "No", 0 }, { "Yes" , 1 } }; - GEMSelect selectYesOrNo( sizeof(optionByteYesOrNo) / sizeof(SelectOptionByte), optionByteYesOrNo); - GEMItem menuItemScaleLock( "Scale lock?", scaleLock, selectYesOrNo); - GEMItem menuItemPercep( "Fix color:", perceptual, selectYesOrNo, setLEDcolorCodes); - GEMItem menuItemShiftColor( "ColorByKey", paletteBeginsAtKeyCenter, selectYesOrNo, setLEDcolorCodes); - - void resetBuzzers(); // forward declaration - byte playbackMode = BUZZ_POLY; - SelectOptionByte optionBytePlayback[] = { { "Off", BUZZ_OFF }, { "Mono", BUZZ_MONO }, { "Arp'gio", BUZZ_ARPEGGIO }, { "Poly", BUZZ_POLY } }; - GEMSelect selectPlayback(sizeof(optionBytePlayback) / sizeof(SelectOptionByte), optionBytePlayback); - GEMItem menuItemPlayback( "Buzzer:", playbackMode, selectPlayback, resetBuzzers); - - void changeTranspose(); // forward-declaration - int transposeSteps = 0; - // doing this long-hand because the STRUCT has problems accepting string conversions of numbers for some reason - SelectOptionInt optionIntTransposeSteps[] = { - {"-127",-127},{"-126",-126},{"-125",-125},{"-124",-124},{"-123",-123},{"-122",-122},{"-121",-121},{"-120",-120},{"-119",-119},{"-118",-118},{"-117",-117},{"-116",-116},{"-115",-115},{"-114",-114},{"-113",-113}, - {"-112",-112},{"-111",-111},{"-110",-110},{"-109",-109},{"-108",-108},{"-107",-107},{"-106",-106},{"-105",-105},{"-104",-104},{"-103",-103},{"-102",-102},{"-101",-101},{"-100",-100},{"- 99",- 99},{"- 98",- 98}, - {"- 97",- 97},{"- 96",- 96},{"- 95",- 95},{"- 94",- 94},{"- 93",- 93},{"- 92",- 92},{"- 91",- 91},{"- 90",- 90},{"- 89",- 89},{"- 88",- 88},{"- 87",- 87},{"- 86",- 86},{"- 85",- 85},{"- 84",- 84},{"- 83",- 83}, - {"- 82",- 82},{"- 81",- 81},{"- 80",- 80},{"- 79",- 79},{"- 78",- 78},{"- 77",- 77},{"- 76",- 76},{"- 75",- 75},{"- 74",- 74},{"- 73",- 73},{"- 72",- 72},{"- 71",- 71},{"- 70",- 70},{"- 69",- 69},{"- 68",- 68}, - {"- 67",- 67},{"- 66",- 66},{"- 65",- 65},{"- 64",- 64},{"- 63",- 63},{"- 62",- 62},{"- 61",- 61},{"- 60",- 60},{"- 59",- 59},{"- 58",- 58},{"- 57",- 57},{"- 56",- 56},{"- 55",- 55},{"- 54",- 54},{"- 53",- 53}, - {"- 52",- 52},{"- 51",- 51},{"- 50",- 50},{"- 49",- 49},{"- 48",- 48},{"- 47",- 47},{"- 46",- 46},{"- 45",- 45},{"- 44",- 44},{"- 43",- 43},{"- 42",- 42},{"- 41",- 41},{"- 40",- 40},{"- 39",- 39},{"- 38",- 38}, - {"- 37",- 37},{"- 36",- 36},{"- 35",- 35},{"- 34",- 34},{"- 33",- 33},{"- 32",- 32},{"- 31",- 31},{"- 30",- 30},{"- 29",- 29},{"- 28",- 28},{"- 27",- 27},{"- 26",- 26},{"- 25",- 25},{"- 24",- 24},{"- 23",- 23}, - {"- 22",- 22},{"- 21",- 21},{"- 20",- 20},{"- 19",- 19},{"- 18",- 18},{"- 17",- 17},{"- 16",- 16},{"- 15",- 15},{"- 14",- 14},{"- 13",- 13},{"- 12",- 12},{"- 11",- 11},{"- 10",- 10},{"- 9",- 9},{"- 8",- 8}, - {"- 7",- 7},{"- 6",- 6},{"- 5",- 5},{"- 4",- 4},{"- 3",- 3},{"- 2",- 2},{"- 1",- 1},{"+/-0", 0},{"+ 1", 1},{"+ 2", 2},{"+ 3", 3},{"+ 4", 4},{"+ 5", 5},{"+ 6", 6},{"+ 7", 7}, - {"+ 8", 8},{"+ 9", 9},{"+ 10", 10},{"+ 11", 11},{"+ 12", 12},{"+ 13", 13},{"+ 14", 14},{"+ 15", 15},{"+ 16", 16},{"+ 17", 17},{"+ 18", 18},{"+ 19", 19},{"+ 20", 20},{"+ 21", 21},{"+ 22", 22}, - {"+ 23", 23},{"+ 24", 24},{"+ 25", 25},{"+ 26", 26},{"+ 27", 27},{"+ 28", 28},{"+ 29", 29},{"+ 30", 30},{"+ 31", 31},{"+ 32", 32},{"+ 33", 33},{"+ 34", 34},{"+ 35", 35},{"+ 36", 36},{"+ 37", 37}, - {"+ 38", 38},{"+ 39", 39},{"+ 40", 40},{"+ 41", 41},{"+ 42", 42},{"+ 43", 43},{"+ 44", 44},{"+ 45", 45},{"+ 46", 46},{"+ 47", 47},{"+ 48", 48},{"+ 49", 49},{"+ 50", 50},{"+ 51", 51},{"+ 52", 52}, - {"+ 53", 53},{"+ 54", 54},{"+ 55", 55},{"+ 56", 56},{"+ 57", 57},{"+ 58", 58},{"+ 59", 59},{"+ 60", 60},{"+ 61", 61},{"+ 62", 62},{"+ 63", 63},{"+ 64", 64},{"+ 65", 65},{"+ 66", 66},{"+ 67", 67}, - {"+ 68", 68},{"+ 69", 69},{"+ 70", 70},{"+ 71", 71},{"+ 72", 72},{"+ 73", 73},{"+ 74", 74},{"+ 75", 75},{"+ 76", 76},{"+ 77", 77},{"+ 78", 78},{"+ 79", 79},{"+ 80", 80},{"+ 81", 81},{"+ 82", 82}, - {"+ 83", 83},{"+ 84", 84},{"+ 85", 85},{"+ 86", 86},{"+ 87", 87},{"+ 88", 88},{"+ 89", 89},{"+ 90", 90},{"+ 91", 91},{"+ 92", 92},{"+ 93", 93},{"+ 94", 94},{"+ 95", 95},{"+ 96", 96},{"+ 97", 97}, - {"+ 98", 98},{"+ 99", 99},{"+100", 100},{"+101", 101},{"+102", 102},{"+103", 103},{"+104", 104},{"+105", 105},{"+106", 106},{"+107", 107},{"+108", 108},{"+109", 109},{"+110", 110},{"+111", 111},{"+112", 112}, - {"+113", 113},{"+114", 114},{"+115", 115},{"+116", 116},{"+117", 117},{"+118", 118},{"+119", 119},{"+120", 120},{"+121", 121},{"+122", 122},{"+123", 123},{"+124", 124},{"+125", 125},{"+126", 126},{"+127", 127} - }; - GEMSelect selectTransposeSteps( 255, optionIntTransposeSteps); - GEMItem menuItemTransposeSteps( "Transpose:", transposeSteps, selectTransposeSteps, changeTranspose); - - byte colorMode = RAINBOW_MODE; - SelectOptionByte optionByteColor[] = { { "Rainbow", RAINBOW_MODE }, { "Tiered" , TIERED_COLOR_MODE }, {"Alt", ALTERNATE_COLOR_MODE} }; - GEMSelect selectColor( sizeof(optionByteColor) / sizeof(SelectOptionByte), optionByteColor); - GEMItem menuItemColor( "Color mode:", colorMode, selectColor, setLEDcolorCodes); - - byte animationType = ANIMATE_NONE; - SelectOptionByte optionByteAnimate[] = { { "None" , ANIMATE_NONE }, { "Octave", ANIMATE_OCTAVE }, - { "By Note", ANIMATE_BY_NOTE }, { "Star", ANIMATE_STAR }, { "Splash" , ANIMATE_SPLASH }, { "Orbit", ANIMATE_ORBIT } }; - GEMSelect selectAnimate( sizeof(optionByteAnimate) / sizeof(SelectOptionByte), optionByteAnimate); - GEMItem menuItemAnimate( "Animation:", animationType, selectAnimate); - - byte globalBrightness = BRIGHT_MID; - SelectOptionByte optionByteBright[] = { { "Dim", BRIGHT_DIM}, {"Low", BRIGHT_LOW}, {"Normal", BRIGHT_MID}, {"High", BRIGHT_HIGH}, {"THE SUN", BRIGHT_MAX } }; - GEMSelect selectBright( sizeof(optionByteBright) / sizeof(SelectOptionByte), optionByteBright); - GEMItem menuItemBright( "Brightness", globalBrightness, selectBright, setLEDcolorCodes); - - byte currWave = WAVEFORM_SAW; - SelectOptionByte optionByteWaveform[] = { { "Square", WAVEFORM_SQUARE }, { "Saw", WAVEFORM_SAW }, - {"Triangl", WAVEFORM_TRIANGLE}, {"Sine", WAVEFORM_SINE}, {"Strings", WAVEFORM_STRINGS}, {"Clrinet", WAVEFORM_CLARINET} }; - GEMSelect selectWaveform(sizeof(optionByteWaveform) / sizeof(SelectOptionByte), optionByteWaveform); - GEMItem menuItemWaveform( "Waveform:", currWave, selectWaveform); - - SelectOptionInt optionIntModWheel[] = { { "too slo", 1 }, { "Turtle", 2 }, { "Slow", 4 }, - { "Medium", 8 }, { "Fast", 16 }, { "Cheetah", 32 }, { "Instant", 127 } }; - GEMSelect selectModSpeed(sizeof(optionIntModWheel) / sizeof(SelectOptionInt), optionIntModWheel); - GEMItem menuItemModSpeed( "Mod wheel:", modWheelSpeed, selectModSpeed); - GEMItem menuItemVelSpeed( "Vel wheel:", velWheelSpeed, selectModSpeed); - - SelectOptionInt optionIntPBWheel[] = { { "too slo", 128 }, { "Turtle", 256 }, { "Slow", 512 }, - { "Medium", 1024 }, { "Fast", 2048 }, { "Cheetah", 4096 }, { "Instant", 16384 } }; - GEMSelect selectPBSpeed(sizeof(optionIntPBWheel) / sizeof(SelectOptionInt), optionIntPBWheel); - GEMItem menuItemPBSpeed( "PB wheel:", pbWheelSpeed, selectPBSpeed); - - - // put all user-selectable options into a class so that down the line these can be saved and loaded. - class presetDef { - public: - std::string presetName; - int tuningIndex; // instead of using pointers, i chose to store index value of each option, to be saved to a .pref or .ini or something - int layoutIndex; - int scaleIndex; - int keyStepsFromA; // what key the scale is in, where zero equals A. - int transpose; - // define simple recall functions - tuningDef tuning() { - return tuningOptions[tuningIndex]; - } - layoutDef layout() { - return layoutOptions[layoutIndex]; - } - scaleDef scale() { - return scaleOptions[scaleIndex]; - } - int layoutsBegin() { - if (tuningIndex == TUNING_12EDO) { - return 0; - } else { - int temp = 0; - while (layoutOptions[temp].tuning < tuningIndex) { - temp++; - } - return temp; - } - } - int keyStepsFromC() { - return tuning().spanCtoA() - keyStepsFromA; - } - int pitchRelToA4(int givenStepsFromC) { - return givenStepsFromC + tuning().spanCtoA() + transpose; - } - int keyDegree(int givenStepsFromC) { - return positiveMod(givenStepsFromC + keyStepsFromC(), tuning().cycleLength); - } - }; - - presetDef current = { - "Default", // name - TUNING_12EDO, // tuning - 0, // default to the first layout, wicki hayden - 0, // default to using no scale (chromatic) - -9, // default to the key of C, which in 12EDO is -9 steps from A. - 0 // default to no transposition - }; - -// ====== diagnostic wrapper - - void sendToLog(std::string msg) { - if (diagnostics) { - Serial.println(msg.c_str()); - } - } - -// ====== LED routines - - int16_t transformHue(float h) { - float D = fmod(h,360); - if (!perceptual) { - return 65536 * D / 360; - } else { - // red yellow green cyan blue - int hueIn[] = { 0, 9, 18, 102, 117, 135, 142, 155, 203, 240, 252, 261, 306, 333, 360}; - // #ff0000 #ffff00 #00ff00 #00ffff #0000ff #ff00ff - int hueOut[] = { 0, 3640, 5861,10922,12743,16384,21845,27306,32768,38229,43690,49152,54613,58254,65535}; - byte B = 0; - while (D - hueIn[B] > 0) { - B++; - } - float T = (D - hueIn[B - 1]) / (float)(hueIn[B] - hueIn[B - 1]); - return (hueOut[B - 1] * (1 - T)) + (hueOut[B] * T); - } - } - - uint32_t getLEDcode(colorDef c) { - return strip.gamma32(strip.ColorHSV(transformHue(c.hue),c.sat,c.val * globalBrightness / 255)); - } - - void setLEDcolorCodes() { // calculate color codes for each hex, store for playback - for (byte i = 0; i < LED_COUNT; i++) { - if (!(h[i].isCmd)) { - colorDef setColor; - byte paletteIndex = positiveMod(h[i].stepsFromC,current.tuning().cycleLength); - if (paletteBeginsAtKeyCenter) { - paletteIndex = current.keyDegree(paletteIndex); - } - switch (colorMode) { - case TIERED_COLOR_MODE: - setColor = palette[current.tuningIndex].getColor(paletteIndex); - break; - case RAINBOW_MODE: - setColor = - { 360.0 * ((float)paletteIndex / (float)current.tuning().cycleLength) - , SAT_VIVID - , VALUE_NORMAL - }; - break; - case ALTERNATE_COLOR_MODE: - float cents = current.tuning().stepSize * paletteIndex; - bool perf = 0; - float center = 0.0; - if (cents < 50) {perf = 1; center = 0.0;} - else if ((cents >= 50) && (cents < 250)) { center = 147.1;} - else if ((cents >= 250) && (cents < 450)) { center = 351.0;} - else if ((cents >= 450) && (cents < 600)) {perf = 1; center = 498.0;} - else if ((cents >= 600) && (cents <= 750)) {perf = 1; center = 702.0;} - else if ((cents > 750) && (cents <= 950)) { center = 849.0;} - else if ((cents > 950) && (cents <=1150)) { center = 1053.0;} - else if ((cents > 1150) && (cents < 1250)) {perf = 1; center = 1200.0;} - else if ((cents >=1250) && (cents < 1450)) { center = 1347.1;} - else if ((cents >=1450) && (cents < 1650)) { center = 1551.0;} - else if ((cents >=1650) && (cents < 1850)) {perf = 1; center = 1698.0;} - else if ((cents >=1800) && (cents <=1950)) {perf = 1; center = 1902.0;} - float offCenter = cents - center; - int16_t altHue = positiveMod((int)(150 + (perf * ((offCenter > 0) ? -72 : 72)) - round(1.44 * offCenter)), 360); - float deSaturate = perf * (abs(offCenter) < 20) * (1 - (0.02 * abs(offCenter))); - setColor = { (float)altHue, 255 - (255 * deSaturate), (cents ? VALUE_SHADE : VALUE_NORMAL) }; - break; - } - h[i].LEDcodeRest = getLEDcode(setColor); - h[i].LEDcodePlay = getLEDcode(setColor.tint()); - h[i].LEDcodeDim = getLEDcode(setColor.shade()); - setColor = {HUE_NONE,SAT_BW,VALUE_BLACK}; - h[i].LEDcodeOff = getLEDcode(setColor); // turn off entirely - h[i].LEDcodeAnim = h[i].LEDcodePlay; - } - } - sendToLog("LED codes re-calculated."); - } - - void resetVelocityLEDs() { - colorDef tempColor = - { (runTime % (rainbowDegreeTime * 360)) / rainbowDegreeTime - , SAT_MODERATE - , byteLerp(0,255,85,127,velWheel.curValue) - }; - strip.setPixelColor(assignCmd[0], getLEDcode(tempColor)); - - tempColor.val = byteLerp(0,255,42,85,velWheel.curValue); - strip.setPixelColor(assignCmd[1], getLEDcode(tempColor)); - - tempColor.val = byteLerp(0,255,0,42,velWheel.curValue); - strip.setPixelColor(assignCmd[2], getLEDcode(tempColor)); - } - void resetWheelLEDs() { - // middle button - int tempSat = SAT_BW; - colorDef tempColor = {HUE_NONE, tempSat, (toggleWheel ? VALUE_SHADE : VALUE_LOW)}; - strip.setPixelColor(assignCmd[3], getLEDcode(tempColor)); - if (toggleWheel) { - // pb red / green - tempSat = byteLerp(SAT_BW,SAT_VIVID,0,8192,abs(pbWheel.curValue)); - tempColor = {((pbWheel.curValue > 0) ? HUE_RED : HUE_CYAN), tempSat, VALUE_FULL}; - strip.setPixelColor(assignCmd[5], getLEDcode(tempColor)); - - tempColor.val = tempSat * (pbWheel.curValue > 0); - strip.setPixelColor(assignCmd[4], getLEDcode(tempColor)); - - tempColor.val = tempSat * (pbWheel.curValue < 0); - strip.setPixelColor(assignCmd[6], getLEDcode(tempColor)); - } else { - // mod blue / yellow - tempSat = byteLerp(SAT_BW,SAT_VIVID,0,64,abs(modWheel.curValue - 63)); - tempColor = {((modWheel.curValue > 63) ? HUE_YELLOW : HUE_INDIGO), tempSat, 127 + (tempSat / 2)}; - strip.setPixelColor(assignCmd[6], getLEDcode(tempColor)); - - if (modWheel.curValue <= 63) { - tempColor.val = 127 - (tempSat / 2); - } - strip.setPixelColor(assignCmd[5], getLEDcode(tempColor)); - - tempColor.val = tempSat * (modWheel.curValue > 63); - strip.setPixelColor(assignCmd[4], getLEDcode(tempColor)); - } - } - uint32_t applyNotePixelColor(byte x) { - if (h[x].animate) { return h[x].LEDcodeAnim; - } else if (h[x].MIDIch) { return h[x].LEDcodePlay; - } else if (h[x].inScale) { return h[x].LEDcodeRest; - } else if (scaleLock) { return h[x].LEDcodeOff; - } else { return h[x].LEDcodeDim; - } - } - -// ====== layout routines - - float freqToMIDI(float Hz) { // formula to convert from Hz to MIDI note - return 69.0 + 12.0 * log2f(Hz / 440.0); - } - float MIDItoFreq(float MIDI) { // formula to convert from MIDI note to Hz - return 440.0 * exp2((MIDI - 69.0) / 12.0); - } - float stepsToMIDI(int16_t stepsFromA) { // return the MIDI pitch associated - return freqToMIDI(CONCERT_A_HZ) + ((float)stepsFromA * (float)current.tuning().stepSize / 100.0); - } - - void 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 < LED_COUNT; i++) { - if (!(h[i].isCmd)) { - // steps is the distance from C - // the stepsToMIDI function needs distance from A4 - // it also needs to reflect any transposition, but - // NOT the key of the scale. - float N = stepsToMIDI(current.pitchRelToA4(h[i].stepsFromC)); - if (N < 0 || N >= 128) { - h[i].note = UNUSED_NOTE; - h[i].bend = 0; - h[i].frequency = 0.0; - } else { - h[i].note = ((N >= 127) ? 127 : round(N)); - h[i].bend = (ldexp(N - h[i].note, 13) / PITCH_BEND_SEMIS); - h[i].frequency = MIDItoFreq(N); - } - sendToLog( - "hex #" + std::to_string(i) + ", " + - "steps=" + std::to_string(h[i].stepsFromC) + ", " + - "isCmd? " + std::to_string(h[i].isCmd) + ", " + - "note=" + std::to_string(h[i].note) + ", " + - "bend=" + std::to_string(h[i].bend) + ", " + - "freq=" + std::to_string(h[i].frequency) + ", " + - "inScale? " + std::to_string(h[i].inScale) + "." - ); - } - } - sendToLog("assignPitches complete."); - } - void applyScale() { - sendToLog("applyScale was called:"); - for (byte i = 0; i < LED_COUNT; i++) { - if (!(h[i].isCmd)) { - if (current.scale().tuning == ALL_TUNINGS) { - h[i].inScale = 1; - } else { - byte degree = current.keyDegree(h[i].stepsFromC); - if (degree == 0) { - h[i].inScale = 1; // the root is always in the scale - } else { - byte tempSum = 0; - byte iterator = 0; - while (degree > tempSum) { - tempSum += current.scale().pattern[iterator]; - iterator++; - } // add the steps in the scale, and you're in scale - h[i].inScale = (tempSum == degree); // if the note lands on one of those sums - } - } - sendToLog( - "hex #" + std::to_string(i) + ", " + - "steps=" + std::to_string(h[i].stepsFromC) + ", " + - "isCmd? " + std::to_string(h[i].isCmd) + ", " + - "note=" + std::to_string(h[i].note) + ", " + - "inScale? " + std::to_string(h[i].inScale) + "." - ); - } - } - setLEDcolorCodes(); - sendToLog("applyScale complete."); - } - - void applyLayout() { // call this function when the layout changes - sendToLog("buildLayout was called:"); - for (byte i = 0; i < LED_COUNT; i++) { - if (!(h[i].isCmd)) { - int8_t distCol = h[i].coordCol - h[current.layout().hexMiddleC].coordCol; - int8_t distRow = h[i].coordRow - h[current.layout().hexMiddleC].coordRow; - h[i].stepsFromC = ( - (distCol * current.layout().acrossSteps) + - (distRow * ( - current.layout().acrossSteps + - (2 * current.layout().dnLeftSteps) - )) - ) / 2; - sendToLog( - "hex #" + std::to_string(i) + ", " + - "steps from C4=" + std::to_string(h[i].stepsFromC) + "." - ); - } - } - applyScale(); // when layout changes, have to re-apply scale and re-apply LEDs - assignPitches(); // same with pitches - u8g2.setDisplayRotation(current.layout().isPortrait ? U8G2_R2 : U8G2_R1); // and landscape / portrait rotation - sendToLog("buildLayout complete."); - } -// ====== buzzer routines - // the piezo buzzer is an on/off switch that can buzz as fast as the processor clock (133MHz) - // the processor is fast enough to emulate analog signals. - // the RP2040 has pulse width modulation (PWM) built into the hardware. - // it can output a %-on / %-off pattern at any percentage desired. - // at high enough frequencies, it sounds the same as an analog signal at that % volume. - // to emulate an 8-bit (0-255) analog sample, with phase-correction, we need a 9 bit (512) cycle. - // we can safely sample up to 260kHz (133MHz / 512) this way. - // the highest frequency note in MIDI is about 12.5kHz. - // it is theoretically possible to emulate waveforms with 4 bits resolution (260kHz / 12.5kHz) - // but we are limited by calculation time. - // the macro POLLING_INTERVAL_IN_MICROSECONDS is set to a value that is long enough - // that the audio output is accurate, but short enough to allow as much resolution as possible. - // currently, 32 microseconds appears to be sufficient (about 500 CPU cycles). - // - // 1) set a constant PWM signal at F_CPU/512 (260kHz) to play on pin 23 - // the PWM signal can emulate an analog value from 0 to 255. - // this is done in setup1(). - // 2) if a note is to be played on the buzzer, assign a channel (same as MPE mode for MIDI) - // and calculate the frequency. this might include pitch bends. - // this is done in buzz(). - // 3) the frequency is expressed as "amount you'd increment a counter every polling interval - // so that you roll over a 16-bit (65536) value at that frequency. - // example: 440Hz note, 32microS polling - // 65536 x 440/s x .000032s = an increment of 923 per poll - // this is done in buzz(). - // 4) the object called synth[] stores the increment and counter for each channel (0-14)=MIDI(2 thru 16) - // at every poll, each counter is incremented (will roll over since the type is 16-bit unsigned integer) - // and depending on the waveform, the 8-bit analog level is calculated. - // example: square waves return 0 if the counter is 0-32767, 255 if 32768-65535. - // saw waves return (counter / 256). - // 5) the analog levels are mixed. i use an attenuation function, basically (# of simultaneous notes) ^ -0.5, - // so the perceived volume is consistent. the velocity wheel is also multiplied in. - // 6) hardware timers are used because they will interrupt and run even if other code is active. - // otherwise, the subperiod is essentially floored at the length of the main loop() which is - // thousands of microseconds long! - // further, we can run this process on the 2nd core so it doesn't interrupt the user experience - // the implementation of 6) is to make a single timer that calls back an interrupt function called poll(). - // the callback function then resets the interrupt flag and resets the timer alarm. - // the timer is set to go off at the time of the last timer + the polling interval - - - // RUN ON CORE 2 - void poll() { - hw_clear_bits(&timer_hw->intr, 1u << ALARM_NUM); - timer_hw->alarm[ALARM_NUM] = timer_hw->timerawl + POLL_INTERVAL_IN_MICROSECONDS; - uint32_t mix = 0; - byte voices = POLYPHONY_LIMIT; - byte p; - byte level = 0; - for (byte i = 0; i < POLYPHONY_LIMIT; i++) { - if (synth[i].increment) { - synth[i].counter += synth[i].increment; // should loop from 65536 -> 0 - p = (synth[i].counter >> 8); - switch (currWave) { - case WAVEFORM_SAW: break; - case WAVEFORM_TRIANGLE: p = 2 * ((p < 128) ? p : (255 - p)); break; - case WAVEFORM_SQUARE: p = 0 - (p > (128 - modWheel.curValue)); break; - case WAVEFORM_SINE: p = sine[p]; break; - case WAVEFORM_STRINGS: p = strings[p]; break; - case WAVEFORM_CLARINET: p = clarinet[p]; break; - default: break; - } - mix += (p * synth[i].eq); // P[8bit] * EQ[8bit] =[16bit] - } else { - --voices; - } - } - mix = mix * attenuation[voices] * velWheel.curValue; // [16bit]*vel[7bit]=[23bit], poly+atten=[6bit] = [29bit] - level = mix >> 21; // [29bit] - [8bit] = [21bit] - pwm_set_chan_level(TONE_SL, TONE_CH, level); - } - // RUN ON CORE 1 - byte isoTwoTwentySix(float f) { - // a very crude implementation of ISO 226 - // equal loudness curves - // Hz dB Amp = sqrt(10^(dB/10)) - // 200 0 255 - // 800 -3 181 - // 1500 0 255 - // 3250 -6 127 - // 5000 0 255 - if ((f < 8.0) || (f > 12500.0)) { // really crude low- and high-pass - return 0; - } else { - if ((f <= 200.0) || (f >= 5000.0)) { - return 255; - } else { - if (f < 1500.0) { - return 181 + 74 * (float)(abs(f-800) / 700); - } else { - return 127 + 128 * (float)(abs(f-3250) / 1750); - } - } - } - } - void setBuzzer(float f, byte c) { - synth[c - 1].counter = 0; - float FwithPB = f * exp2(pbWheel.curValue * PITCH_BEND_SEMIS / 98304.0); - synth[c - 1].increment = round(FwithPB * POLL_INTERVAL_IN_MICROSECONDS * 0.065536); // cycle 0-65535 at resultant frequency - synth[c - 1].eq = isoTwoTwentySix(FwithPB); - } - - // USE THIS IN MONO OR ARPEG MODE ONLY - - byte findNextHeldNote() { - byte n = UNUSED_NOTE; - for (byte i = 1; i <= LED_COUNT; i++) { - byte j = positiveMod(arpeggiatingNow + i, LED_COUNT); - if ((h[j].MIDIch) && (!h[j].isCmd)) { - n = j; - break; - } - } - return n; - } - void replaceBuzzerWith(byte x) { - if (arpeggiatingNow != x) { - arpeggiatingNow = x; - if (arpeggiatingNow != UNUSED_NOTE) { - setBuzzer(h[arpeggiatingNow].frequency, 1); - } else { - setBuzzer(0, 1); - } - } - } - - void resetBuzzers() { - while (!buzzChQueue.empty()) { - buzzChQueue.pop(); - } - for (byte i = 0; i < POLYPHONY_LIMIT; i++) { - synth[i].increment = 0; - synth[i].counter = 0; - } - if (playbackMode == BUZZ_POLY) { - for (byte i = 0; i < POLYPHONY_LIMIT; i++) { - buzzChQueue.push(i + 1); - } - } - } -// ====== MIDI routines - void setPitchBendRange(byte Ch, byte semitones) { - MIDI.beginRpn(0, Ch); - MIDI.sendRpnValue(semitones << 7, Ch); - MIDI.endRpn(Ch); - sendToLog( - "set pitch bend range on ch " + - std::to_string(Ch) + " to be " + - std::to_string(semitones) + " semitones" - ); - } - void setMPEzone(byte masterCh, byte sizeOfZone) { - MIDI.beginRpn(6, masterCh); - MIDI.sendRpnValue(sizeOfZone << 7, masterCh); - MIDI.endRpn(masterCh); - sendToLog( - "tried sending MIDI msg to set MPE zone, master ch " + - std::to_string(masterCh) + ", zone of this size: " + std::to_string(sizeOfZone) - ); - } - void resetTuningMIDI() { - while (!MPEchQueue.empty()) { // empty the channel queue - MPEchQueue.pop(); - } - for (byte i = 1; i <= 16; i++) { - MIDI.sendControlChange(123, 0, i); - setPitchBendRange(i, PITCH_BEND_SEMIS); // force pitch bend back to the expected range of 2 semitones. - } - if (current.tuningIndex == TUNING_12EDO) { - setMPEzone(1, 0); - } else { - setMPEzone(1, 15); // MPE zone 1 = ch 2 thru 16 - for (byte i = 0; i < 15; i++) { - MPEchQueue.push(i + 2); - sendToLog("pushed ch " + std::to_string(i + 2) + " to the open channel queue"); - } - } - resetBuzzers(); - } - void chgModulation() { - MIDI.sendControlChange(1, modWheel.curValue, 1); - sendToLog("sent mod value " + std::to_string(modWheel.curValue) + " to ch 1"); - } - void chgUniversalPB() { - MIDI.sendPitchBend(pbWheel.curValue, 1); - for (byte i = 0; i < LED_COUNT; i++) { - if (!(h[i].isCmd)) { - if (h[i].buzzCh) { - setBuzzer(h[i].frequency,h[i].buzzCh); // rebuzz all notes if the pitch bend changes - } - } - sendToLog("sent pb wheel value " + std::to_string(pbWheel.curValue) + " to ch 1"); - } - } - -// ====== hex press routines - - void playNote(byte x) { - // this gets called on any non-command hex - // that is not scale-locked. - if (!(h[x].MIDIch)) { - if (current.tuningIndex == TUNING_12EDO) { - h[x].MIDIch = 1; - } else { - if (MPEchQueue.empty()) { // if there aren't any open channels - sendToLog("MPE queue was empty so did not play a midi note"); - } else { - h[x].MIDIch = MPEchQueue.front(); // value in MIDI terms (1-16) - MPEchQueue.pop(); - sendToLog("popped " + std::to_string(h[x].MIDIch) + " off the MPE queue"); - MIDI.sendPitchBend(h[x].bend, h[x].MIDIch); // ch 1-16 - } - } - if (h[x].MIDIch) { - MIDI.sendNoteOn(h[x].note, velWheel.curValue, h[x].MIDIch); // ch 1-16 - sendToLog( - "sent MIDI noteOn: " + std::to_string(h[x].note) + - " pb " + std::to_string(h[x].bend) + - " vel " + std::to_string(velWheel.curValue) + - " ch " + std::to_string(h[x].MIDIch) - ); - } - } - if (playbackMode != BUZZ_OFF) { - if (playbackMode == BUZZ_POLY) { - // operate independently of MIDI - if (buzzChQueue.empty()) { - sendToLog("synths all firing, so did not buzz"); - } else { - h[x].buzzCh = buzzChQueue.front(); - buzzChQueue.pop(); - sendToLog("popped " + std::to_string(h[x].buzzCh) + " off the synth queue"); - setBuzzer(h[x].frequency, h[x].buzzCh); - } - } else { - // operate in lockstep with MIDI - if (h[x].MIDIch) { - replaceBuzzerWith(x); - } - } - } - } - - void stopNote(byte x) { - // this gets called on any non-command hex - // that is not scale-locked. - if (h[x].MIDIch) { // but just in case, check - MIDI.sendNoteOff(h[x].note, velWheel.curValue, h[x].MIDIch); - sendToLog( - "sent note off: " + std::to_string(h[x].note) + - " pb " + std::to_string(h[x].bend) + - " vel " + std::to_string(velWheel.curValue) + - " ch " + std::to_string(h[x].MIDIch) - ); - if (current.tuningIndex != TUNING_12EDO) { - MPEchQueue.push(h[x].MIDIch); - sendToLog("pushed " + std::to_string(h[x].MIDIch) + " on the MPE queue"); - } - h[x].MIDIch = 0; - if (playbackMode && (playbackMode != BUZZ_POLY)) { - replaceBuzzerWith(findNextHeldNote()); - } - } - if (playbackMode == BUZZ_POLY) { - if (h[x].buzzCh) { - setBuzzer(0, h[x].buzzCh); - buzzChQueue.push(h[x].buzzCh); - h[x].buzzCh = 0; - } - } - } - void cmdOn(byte x) { // volume and mod wheel read all current buttons - switch (h[x].note) { - case CMDB + 3: - toggleWheel = !toggleWheel; - break; - default: - // the rest should all be taken care of within the wheelDef structure - break; - } - } - void cmdOff(byte x) { // pitch bend wheel only if buttons held. - switch (h[x].note) { - default: - break; // nothing; should all be taken care of within the wheelDef structure - } - } - -// ====== animations - uint64_t animFrame(byte x) { - if (h[x].timePressed) { // 2^20 microseconds is close enough to 1 second - return 1 + (((runTime - h[x].timePressed) * animationFPS) >> 20); - } else { - return 0; - } - } - void flagToAnimate(int8_t r, int8_t c) { - if (! - ( ( r < 0 ) || ( r >= ROWCOUNT ) - || ( c < 0 ) || ( c >= (2 * COLCOUNT) ) - || ( ( c + r ) & 1 ) - ) - ) { - h[(10 * r) + (c / 2)].animate = 1; - } - } - void animateMirror() { - for (byte i = 0; i < LED_COUNT; i++) { // check every hex - if ((!(h[i].isCmd)) && (h[i].MIDIch)) { // that is a held note - for (byte j = 0; j < LED_COUNT; j++) { // compare to every hex - if ((!(h[j].isCmd)) && (!(h[j].MIDIch))) { // that is a note not being played - int16_t temp = h[i].stepsFromC - h[j].stepsFromC; // look at difference between notes - if (animationType == ANIMATE_OCTAVE) { // set octave diff to zero if need be - temp = positiveMod(temp, current.tuning().cycleLength); - } - if (temp == 0) { // highlight if diff is zero - h[j].animate = 1; - } - } - } - } - } - } - - void animateOrbit() { - for (byte i = 0; i < LED_COUNT; i++) { // check every hex - if ((!(h[i].isCmd)) && (h[i].MIDIch) && ((h[i].inScale) || (!scaleLock))) { // that is a held note - byte tempDir = (animFrame(i) % 6); - flagToAnimate(h[i].coordRow + vertical[tempDir], h[i].coordCol + horizontal[tempDir]); // different neighbor each frame - } - } - } - - void animateRadial() { - for (byte i = 0; i < LED_COUNT; i++) { // check every hex - if (!(h[i].isCmd)) { // that is a note - uint64_t radius = animFrame(i); - if ((radius > 0) && (radius < 16)) { // played in the last 16 frames - byte steps = ((animationType == ANIMATE_SPLASH) ? radius : 1); // star = 1 step to next corner; ring = 1 step per hex - int8_t turtleRow = h[i].coordRow + (radius * vertical[HEX_DIRECTION_SW]); - int8_t turtleCol = h[i].coordCol + (radius * horizontal[HEX_DIRECTION_SW]); - for (byte dir = HEX_DIRECTION_EAST; dir < 6; dir++) { // walk along the ring in each of the 6 hex directions - for (byte i = 0; i < steps; i++) { // # of steps to the next corner - flagToAnimate(turtleRow,turtleCol); // flag for animation - turtleRow += (vertical[dir] * (radius / steps)); - turtleCol += (horizontal[dir] * (radius / steps)); - } - } - } - } - } - } - -// ====== menu routines - void menuHome() { - menu.setMenuPageCurrent(menuPageMain); - menu.drawMenu(); - } - void showOnlyValidLayoutChoices() { // re-run at setup and whenever tuning changes - for (byte L = 0; L < layoutCount; L++) { - menuItemLayout[L]->hide((layoutOptions[L].tuning != current.tuningIndex)); - } - sendToLog("menu: Layout choices were updated."); - } - void showOnlyValidScaleChoices() { // re-run at setup and whenever tuning changes - for (int S = 0; S < scaleCount; S++) { - menuItemScales[S]->hide((scaleOptions[S].tuning != current.tuningIndex) && (scaleOptions[S].tuning != ALL_TUNINGS)); - } - sendToLog("menu: Scale choices were updated."); - } - void showOnlyValidKeyChoices() { // re-run at setup and whenever tuning changes - for (int T = 0; T < TUNINGCOUNT; T++) { - menuItemKeys[T]->hide((T != current.tuningIndex)); - } - sendToLog("menu: Key choices were updated."); - } - void changeLayout(GEMCallbackData callbackData) { // when you change the layout via the menu - byte selection = callbackData.valByte; - if (selection != current.layoutIndex) { - current.layoutIndex = selection; - applyLayout(); - } - menuHome(); - } - void changeScale(GEMCallbackData callbackData) { // when you change the scale via the menu - int selection = callbackData.valInt; - if (selection != current.scaleIndex) { - current.scaleIndex = selection; - applyScale(); - } - menuHome(); - } - void changeKey() { // when you change the key via the menu - applyScale(); - } - void changeTranspose() { // when you change the transpose via the menu - current.transpose = transposeSteps; - assignPitches(); - } - void changeTuning(GEMCallbackData callbackData) { // not working yet - byte selection = callbackData.valByte; - if (selection != current.tuningIndex) { - current.tuningIndex = selection; - current.layoutIndex = current.layoutsBegin(); // reset layout to first in list - current.scaleIndex = 0; // reset scale to "no scale" - current.keyStepsFromA = current.tuning().spanCtoA(); // reset key to C - showOnlyValidLayoutChoices(); // change list of choices in GEM Menu - showOnlyValidScaleChoices(); // change list of choices in GEM Menu - showOnlyValidKeyChoices(); // change list of choices in GEM Menu - applyLayout(); // apply changes above - resetTuningMIDI(); // clear out MIDI queue - } - menuHome(); - } - void createTuningMenuItems() { - for (byte T = 0; T < TUNINGCOUNT; T++) { - menuItemTuning[T] = new GEMItem(tuningOptions[T].name.c_str(), changeTuning, T); - menuPageTuning.addMenuItem(*menuItemTuning[T]); - } - } - void createLayoutMenuItems() { - for (byte L = 0; L < layoutCount; L++) { // create pointers to all layouts - menuItemLayout[L] = new GEMItem(layoutOptions[L].name.c_str(), changeLayout, L); - menuPageLayout.addMenuItem(*menuItemLayout[L]); - } - showOnlyValidLayoutChoices(); - } - void createKeyMenuItems() { - for (byte T = 0; T < TUNINGCOUNT; T++) { - selectKey[T] = new GEMSelect(tuningOptions[T].cycleLength, tuningOptions[T].keyChoices); - menuItemKeys[T] = new GEMItem("Key:", current.keyStepsFromA, *selectKey[T], changeKey); - menuPageScales.addMenuItem(*menuItemKeys[T]); - } - showOnlyValidKeyChoices(); - } - void createScaleMenuItems() { - for (int S = 0; S < scaleCount; S++) { // create pointers to all scale items, filter them as you go - menuItemScales[S] = new GEMItem(scaleOptions[S].name.c_str(), changeScale, S); - menuPageScales.addMenuItem(*menuItemScales[S]); - } - showOnlyValidScaleChoices(); - } - -// ====== setup routines - void testDiagnostics() { - sendToLog("theHDM was here"); - } - 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() - resetTuningMIDI -(); - sendToLog("setupMIDI okay"); - } - void setupFileSystem() { - Serial.begin(115200); // Set serial to make uploads work without bootsel button - LittleFSConfig cfg; // Configure file system defaults - cfg.setAutoFormat(true); // Formats file system if it cannot be mounted. - LittleFS.setConfig(cfg); - LittleFS.begin(); // Mounts file system. - if (!LittleFS.begin()) { - sendToLog("An Error has occurred while mounting LittleFS"); - } else { - sendToLog("LittleFS mounted OK"); - } - } - void setupPins() { - for (byte p = 0; p < sizeof(cPin); p++) { // For each column pin... - pinMode(cPin[p], INPUT_PULLUP); // set the pinMode to INPUT_PULLUP (+3.3V / HIGH). - } - for (byte p = 0; p < sizeof(mPin); p++) { // For each column pin... - pinMode(mPin[p], OUTPUT); // Setting the row multiplexer pins to output. - } - Wire.setSDA(SDAPIN); - Wire.setSCL(SCLPIN); - pinMode(ROT_PIN_C, INPUT_PULLUP); - sendToLog("Pins mounted"); - } - void setupGrid() { - for (byte i = 0; i < LED_COUNT; i++) { - h[i].coordRow = (i / 10); - h[i].coordCol = (2 * (i % 10)) + (h[i].coordRow & 1); - h[i].isCmd = 0; - h[i].note = UNUSED_NOTE; - h[i].btnState = 0; - } - for (byte c = 0; c < CMDCOUNT; c++) { - h[assignCmd[c]].isCmd = 1; - h[assignCmd[c]].note = CMDB + c; - } - sendToLog("initializing hex grid..."); - applyLayout(); - } - void setupLEDs() { - strip.begin(); // INITIALIZE NeoPixel strip object - strip.show(); // Turn OFF all pixels ASAP - sendToLog("LEDs started..."); - setLEDcolorCodes(); - } - void setupMenu() { - menu.setSplashDelay(0); - menu.init(); - menuPageMain.addMenuItem(menuGotoTuning); - createTuningMenuItems(); - menuPageTuning.addMenuItem(menuTuningBack); - menuPageMain.addMenuItem(menuGotoLayout); - createLayoutMenuItems(); - menuPageLayout.addMenuItem(menuLayoutBack); - menuPageMain.addMenuItem(menuGotoScales); - createKeyMenuItems(); - menuPageScales.addMenuItem(menuItemScaleLock); - createScaleMenuItems(); - menuPageScales.addMenuItem(menuScalesBack); - menuPageMain.addMenuItem(menuGotoControl); - menuPageControl.addMenuItem(menuItemPBSpeed); - menuPageControl.addMenuItem(menuItemModSpeed); - menuPageControl.addMenuItem(menuItemVelSpeed); - menuPageControl.addMenuItem(menuControlBack); - menuPageMain.addMenuItem(menuGotoColors); - menuPageColors.addMenuItem(menuItemColor); - menuPageColors.addMenuItem(menuItemBright); - menuPageColors.addMenuItem(menuItemAnimate); - menuPageColors.addMenuItem(menuColorsBack); - menuPageMain.addMenuItem(menuGotoSynth); - menuPageSynth.addMenuItem(menuItemPlayback); - menuPageSynth.addMenuItem(menuItemWaveform); - menuPageSynth.addMenuItem(menuSynthBack); - menuPageMain.addMenuItem(menuItemTransposeSteps); - menuPageMain.addMenuItem(menuGotoTesting); - menuPageTesting.addMenuItem(menuItemVersion); - menuPageTesting.addMenuItem(menuItemPercep); - menuPageTesting.addMenuItem(menuItemShiftColor); - menuPageTesting.addMenuItem(menuTestingBack); - menuHome(); - } - void setupGFX() { - u8g2.begin(); // Menu and graphics setup - u8g2.setBusClock(1000000); // Speed up display - u8g2.setContrast(defaultContrast); // Set contrast - sendToLog("U8G2 graphics initialized."); - } - void setupPiezo() { - gpio_set_function(TONEPIN, GPIO_FUNC_PWM); // set that pin as PWM - pwm_set_phase_correct(TONE_SL, true); // phase correct sounds better - pwm_set_wrap(TONE_SL, 254); // 0 - 254 allows 0 - 255 level - pwm_set_clkdiv(TONE_SL, 1.0f); // run at full clock speed - pwm_set_chan_level(TONE_SL, TONE_CH, 0); // initialize at zero to prevent whining sound - pwm_set_enabled(TONE_SL, true); // ENGAGE! - hw_set_bits(&timer_hw->inte, 1u << ALARM_NUM); // initialize the timer - irq_set_exclusive_handler(ALARM_IRQ, poll); // function to run every interrupt - irq_set_enabled(ALARM_IRQ, true); // ENGAGE! - timer_hw->alarm[ALARM_NUM] = timer_hw->timerawl + POLL_INTERVAL_IN_MICROSECONDS; - sendToLog("buzzer is ready."); - } - -// ====== loop routines - void timeTracker() { - lapTime = runTime - loopTime; - loopTime = runTime; // Update previousTime variable to give us a reference point for next loop - runTime = timer_hw->timerawh; - runTime = (runTime << 32) + (timer_hw->timerawl); // Store the current time in a uniform variable for this program loop - } - void screenSaver() { - if (screenTime <= screenSaverTimeout) { - screenTime = screenTime + lapTime; - if (screenSaverOn) { - screenSaverOn = 0; - u8g2.setContrast(defaultContrast); - } - } else { - if (!screenSaverOn) { - screenSaverOn = 1; - u8g2.setContrast(1); - } - } - } - void readHexes() { - for (byte r = 0; r < ROWCOUNT; r++) { // Iterate through each of the row pins on the multiplexing chip. - for (byte d = 0; d < 4; d++) { - digitalWrite(mPin[d], (r >> d) & 1); - } - for (byte c = 0; c < COLCOUNT; c++) { // Now iterate through each of the column pins that are connected to the current row pin. - byte p = cPin[c]; // Hold the currently selected column pin in a variable. - pinMode(p, INPUT_PULLUP); // Set that row pin to INPUT_PULLUP mode (+3.3V / HIGH). - byte i = c + (r * COLCOUNT); - delayMicroseconds(6); // delay while column pin mode - bool didYouPressHex = (digitalRead(p) == LOW); // hex is pressed if it returns LOW. else not pressed - h[i].interpBtnPress(didYouPressHex); - if (h[i].btnState == 1) { - h[i].timePressed = runTime; // log the time - } - pinMode(p, INPUT); // Set the selected column pin back to INPUT mode (0V / LOW). - } - } - } - void actionHexes() { - for (byte i = 0; i < LED_COUNT; i++) { // For all buttons in the deck - switch (h[i].btnState) { - case 1: // just pressed - if (h[i].isCmd) { - cmdOn(i); - } else if (h[i].inScale || (!scaleLock)) { - playNote(i); - } - break; - case 2: // just released - if (h[i].isCmd) { - cmdOff(i); - } else if (h[i].inScale || (!scaleLock)) { - stopNote(i); - } - break; - case 3: // held - break; - default: // inactive - break; - } - } - } - void arpeggiate() { - if (playbackMode == BUZZ_ARPEGGIO) { - if (runTime - arpeggiateTime > arpeggiateLength) { - arpeggiateTime = runTime; - replaceBuzzerWith(findNextHeldNote()); - } - } - } - void updateWheels() { - velWheel.setTargetValue(); - bool upd = velWheel.updateValue(runTime); - if (upd) { - sendToLog("vel became " + std::to_string(velWheel.curValue)); - } - if (toggleWheel) { - pbWheel.setTargetValue(); - upd = pbWheel.updateValue(runTime); - if (upd) { - chgUniversalPB(); - } - } else { - modWheel.setTargetValue(); - upd = modWheel.updateValue(runTime); - if (upd) { - chgModulation(); - } - } - } - - void animateLEDs() { - for (byte i = 0; i < LED_COUNT; i++) { - h[i].animate = 0; - } - if (animationType) { - switch (animationType) { - case ANIMATE_STAR: case ANIMATE_SPLASH: - animateRadial(); - break; - case ANIMATE_ORBIT: - animateOrbit(); - break; - case ANIMATE_OCTAVE: case ANIMATE_BY_NOTE: - animateMirror(); - break; - default: - break; - } - } - } - void lightUpLEDs() { - for (byte i = 0; i < LED_COUNT; i++) { - if (!(h[i].isCmd)) { - strip.setPixelColor(i,applyNotePixelColor(i)); - } - } - resetVelocityLEDs(); - resetWheelLEDs(); - strip.show(); - } - void dealWithRotary() { - if (menu.readyForKey()) { - rotaryIsClicked = digitalRead(ROT_PIN_C); - if (rotaryIsClicked > rotaryWasClicked) { - menu.registerKeyPress(GEM_KEY_OK); - screenTime = 0; - } - rotaryWasClicked = rotaryIsClicked; - if (rotaryKnobTurns != 0) { - for (byte i = 0; i < abs(rotaryKnobTurns); i++) { - menu.registerKeyPress(rotaryKnobTurns < 0 ? GEM_KEY_UP : GEM_KEY_DOWN); - } - rotaryKnobTurns = 0; - screenTime = 0; - } - } - } - void readMIDI() { - MIDI.read(); - } - void keepTrackOfRotaryKnobTurns() { - switch (rotary.process()) { - case DIR_CW: rotaryKnobTurns++; break; - case DIR_CCW: rotaryKnobTurns--; break; - } - rotaryKnobTurns = ( - (rotaryKnobTurns > maxKnobTurns) ? maxKnobTurns : ( - (rotaryKnobTurns < -maxKnobTurns) ? -maxKnobTurns : rotaryKnobTurns - ) - ); - } - -// ====== setup() and loop() - - void setup() { - testDiagnostics(); // Print diagnostic troubleshooting information to serial monitor - #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(); - setupGrid(); - setupLEDs(); - setupGFX(); - setupMenu(); - for (byte i = 0; i < 5 && !TinyUSBDevice.mounted(); i++) { - delay(1); // wait until device mounted, maybe - } - } - void setup1() { // set up on second core - setupPiezo(); - } - 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 - } - void loop1() { // run on second core - keepTrackOfRotaryKnobTurns(); - } |