Firmware for HexBoard MIDI controller
Updates since Apr 22:
BUZZER / SYNTH
1) lowered polyphony count to 8 on the buzzer
2) added more synth wavetypes (can theoretically do any kind of sample now)
3) mod wheel adjusts the duty cycle on the square wave (no effect on other wavetypes tho)
4) improved resolution of the calculations so there should be less static/noise
5) added baseline EQ to the buzzer (see https://en.wikipedia.org/wiki/Equal-loudness_contour)
LEDS
6) the key colors now move when you change keys (i.e. original method), tho but the alternate way where the keys stay the same color is in the Testing menu
7) animations now occur even on scale-locked keys
8) scale-locked keys out of scale are dark; and are dim when scale lock is off
9) brightness options have returned
10) calibrated perceptual color slightly
MISC
11) menu reorganized a little bit
12) fixed a bug with the key center not being reset properly to C when you change tuning
13) hopefully fixed MIDI so that in 12-EDO everything is sent to channel 1 and in not-12-EDO mode the notes go to ch 2-16 while the pitch bend and mod go to channel 1 (MPE mode). I didn't test this yet.
Known issues and wishlist before release:
1) mono buzzer mode isn't quite back to its normal form yet
2) additional palettes. in particular i want to add a color choice for each tuning so that JUST the root key is in white, and all other keys are in a color, as opposed to all the "white keys" being white.
3) i want to research and add scales for 53-EDO, 72-EDO, and if possible to Bohlen Pierce and the Wendy Carlos tunings.
Swag wishlist:
4) a "squishy" animation effect ,where the highlight of the key stays stuck for an extra bit and then fades back
5) idle animations similar to what RGB keyboards have --i.e. a "rain" effect, or a spectrum swirl kind of like what i have on the velocity control
| -rw-r--r-- | Hexperiment.ino | 1331 |
1 files changed, 1080 insertions, 251 deletions
diff --git a/Hexperiment.ino b/Hexperiment.ino index 9b93007..255d91f 100644 --- a/Hexperiment.ino +++ b/Hexperiment.ino @@ -1,5 +1,6 @@ // ====== Hexperiment v1.2 // Copyright 2022-2023 Jared DeCook and Zach DeCook + // with help from Nicholas Fox // Licensed under the GNU GPL Version 3. // Hardware Information: // Generic RP2040 running at 133MHz with 16MB of flash @@ -10,14 +11,14 @@ // // 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 // - // Arduino IDE setup: - // Board = Generic RP2040 (use the following additional board manager repo: - // https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json) - // // Patches needed for U8G2, Rotary.h + // + // Wishlist: + // * multiple palettes per tuning + // * customize wheel placement and order + // * // ============================================================================== #include <Arduino.h> @@ -35,9 +36,733 @@ #include <queue> // std::queue construct to store open channels in microtonal mode #include <string> - #include "Constants.h" // preprocessor constants / macros - #include "Classes.h" // type definitions - #include "Presets.h" // pre-load tuning, scale, palette, layout definitions + // 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 212 + #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 + + // 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_12EDO }, + + { "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_17EDO }, + + { "Bosanquet-Wilson", 0, 65, -1, -2, TUNING_19EDO }, + { "Kleismic", 0, 65, -1, -4, TUNING_19EDO }, + { "Full Gamut", 1, 65, -1, -9, TUNING_19EDO }, + + { "Bosanquet-Wilson", 0, 65, -3, -1, TUNING_22EDO }, + { "Porcupine", 0, 65, 1, -4, TUNING_22EDO }, + { "Full Gamut", 1, 65, 1, -8, TUNING_22EDO }, + + { "Bosanquet-Wilson", 0, 65, -1, -3, TUNING_24EDO }, + { "Inverted", 0, 65, 1, -4, TUNING_24EDO }, + { "Full Gamut", 1, 65, -1, -9, TUNING_24EDO }, + + { "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", 1, 65, -1, -7, TUNING_31EDO }, + + { "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 }, + { "Full Gamut", 0, 65, -1, -8, TUNING_41EDO }, // forty-one #3 + + { "Bosanquet-Wilson", 0, 65, -5, -4, TUNING_53EDO }, + { "Kleismic A", 0, 65, -8, -3, TUNING_53EDO }, + { "Kleismic B", 0, 65, -5, -3, TUNING_53EDO }, + { "Wicki-Hayden", 1, 64, 9, -31, TUNING_53EDO }, + { "Harmonic Table", 0, 75, -31, 14, TUNING_53EDO }, + { "Buzzard", 0, 65, -9, -1, TUNING_53EDO }, + + { "Expanded Janko", 0, 65, -1, -6, TUNING_72EDO }, + { "Full Gamut", 1, 65, -1, -9, TUNING_72EDO }, + + { "Standard", 0, 65, -2, -1, TUNING_BP }, + { "Full Gamut", 1, 65, -1, -9, TUNING_BP }, + + { "Compressed", 0, 65, -2, -1, TUNING_ALPHA }, + { "Full Gamut", 1, 65, -1, -9, TUNING_ALPHA }, + + { "Compressed", 0, 65, -2, -1, TUNING_BETA }, + { "Full Gamut", 1, 65, -1, -9, TUNING_BETA }, + + { "Compressed", 0, 65, -2, -1, TUNING_GAMMA }, + { "Full Gamut", 1, 65, -1, -9, 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 (Hába)", 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 } }, + // 72 EDO + { "Diatonic", TUNING_72EDO, { 12,12,6,12,12,12,6 } }, + { "Pentatonic", TUNING_72EDO, { 12,12,18,12,18 } }, + // BP + // Alpha + // Beta + // Gamma + }; + + 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) { @@ -47,8 +772,8 @@ 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;}; + if (temp < xOne) {temp = xOne;} + if (temp > xTwo) {temp = xTwo;} return temp; } @@ -110,15 +835,15 @@ 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. - byte enableMIDI = 1; const byte layoutCount = sizeof(layoutOptions) / sizeof(layoutDef); const byte scaleCount = sizeof(scaleOptions) / sizeof(scaleDef); // Tone and Arpeggiator variables oscillator synth[POLYPHONY_LIMIT]; // maximum polyphony - byte poly = 0; // current polyphony - std::queue<byte> openChannelQueue; - const byte attenuation[] = {67,67,48,39,34,30,28,26,24,23,22,21,20,19,18,17}; + 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 @@ -131,10 +856,10 @@ int modWheelSpeed = 8; int pbWheelSpeed = 1024; - wheelDef modWheel = { false, false, // standard mode, not sticky + 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 @@ -145,6 +870,7 @@ }; 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 @@ -159,10 +885,21 @@ 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]; @@ -170,16 +907,20 @@ GEMSelect* selectKey[TUNINGCOUNT]; GEMItem* menuItemKeys[TUNINGCOUNT]; - void resetHexLEDs(); // forward-declaration + 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, resetHexLEDs); - + 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); + GEMItem menuItemPlayback( "Buzzer:", playbackMode, selectPlayback, resetBuzzers); void changeTranspose(); // forward-declaration int transposeSteps = 0; @@ -209,7 +950,7 @@ byte colorMode = TIERED_COLOR_MODE; SelectOptionByte optionByteColor[] = { { "Rainbow", RAINBOW_MODE }, { "Tiered" , TIERED_COLOR_MODE } }; GEMSelect selectColor( sizeof(optionByteColor) / sizeof(SelectOptionByte), optionByteColor); - GEMItem menuItemColor( "Color mode:", colorMode, selectColor, resetHexLEDs); + GEMItem menuItemColor( "Color mode:", colorMode, selectColor, setLEDcolorCodes); byte animationType = ANIMATE_NONE; SelectOptionByte optionByteAnimate[] = { { "None" , ANIMATE_NONE }, { "Octave", ANIMATE_OCTAVE }, @@ -217,8 +958,14 @@ 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 } }; + 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); @@ -233,6 +980,7 @@ 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: @@ -245,13 +993,13 @@ // 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; @@ -259,19 +1007,19 @@ int temp = 0; while (layoutOptions[temp].tuning < tuningIndex) { temp++; - }; + } return temp; - }; - }; + } + } int keyStepsFromC() { - return -(tuning().spanCtoA() + keyStepsFromA); - }; + return tuning().spanCtoA() - keyStepsFromA; + } int pitchRelToA4(int givenStepsFromC) { - return givenStepsFromC - tuning().spanCtoA() + transpose; - }; + return givenStepsFromC + tuning().spanCtoA() + transpose; + } int keyDegree(int givenStepsFromC) { return positiveMod(givenStepsFromC + keyStepsFromC(), tuning().cycleLength); - }; + } }; presetDef current = { @@ -279,7 +1027,7 @@ TUNING_12EDO, // tuning 0, // default to the first layout, wicki hayden 0, // default to using no scale (chromatic) - 0, // default to the key of C, which in 12EDO is -9 steps from A. + -9, // default to the key of C, which in 12EDO is -9 steps from A. 0 // default to no transposition }; @@ -288,34 +1036,41 @@ void sendToLog(std::string msg) { if (diagnostics) { Serial.println(msg.c_str()); - }; + } } // ====== LED routines int16_t transformHue(float h) { - // red yellow green blue - int hueIn[] = { 0, 9, 18, 90, 108, 126, 135, 150, 198, 243, 252, 261, 306, 333, 360}; - // #ff0000 #ffff00 #00ff00 #00ffff #0000ff #ff00ff - int hueOut[] = { 0, 3640, 5461,10922,12743,16384,21845,27306,32768,38229,43690,49152,54613,58254,65535}; float D = fmod(h,360); - 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); + 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)); + return strip.gamma32(strip.ColorHSV(transformHue(c.hue),c.sat,c.val * globalBrightness / 255)); } - void resetHexLEDs() { // calculate color codes for each hex, store for playback + 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); @@ -327,14 +1082,15 @@ , VALUE_NORMAL }; break; - }; - h[i].LEDcolorOn = getLEDcode(setColor); - h[i].LEDcolorPlay = getLEDcode(setColor.mixWithWhite()); // "mix with white" + } + 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].LEDcolorOff = getLEDcode(setColor); // turn off entirely - h[i].LEDcolorAnim = h[i].LEDcolorPlay; - }; - }; + h[i].LEDcodeOff = getLEDcode(setColor); // turn off entirely + h[i].LEDcodeAnim = h[i].LEDcodePlay; + } + } sendToLog("LED codes re-calculated."); } @@ -381,14 +1137,15 @@ tempColor.val = tempSat * (modWheel.curValue > 63); strip.setPixelColor(assignCmd[4], getLEDcode(tempColor)); - }; + } } - uint32_t applyNotePixelColor(byte x){ - if (h[x].animate) { return h[x].LEDcolorAnim; - } else if (h[x].channel) { return h[x].LEDcolorPlay; - } else if (h[x].inScale) { return h[x].LEDcolorOn; - } else { return h[x].LEDcolorOff; - }; + 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 @@ -420,7 +1177,7 @@ 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) + ", " + @@ -430,8 +1187,8 @@ "freq=" + std::to_string(h[i].frequency) + ", " + "inScale? " + std::to_string(h[i].inScale) + "." ); - }; - }; + } + } sendToLog("assignPitches complete."); } void applyScale() { @@ -450,10 +1207,10 @@ while (degree > tempSum) { tempSum += current.scale().pattern[iterator]; iterator++; - }; // add the steps in the scale, and you're in scale + } // 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) + ", " + @@ -461,9 +1218,9 @@ "note=" + std::to_string(h[i].note) + ", " + "inScale? " + std::to_string(h[i].inScale) + "." ); - }; - }; - resetHexLEDs(); + } + } + setLEDcolorCodes(); sendToLog("applyScale complete."); } @@ -484,8 +1241,8 @@ "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 @@ -537,101 +1294,135 @@ void poll() { hw_clear_bits(&timer_hw->intr, 1u << ALARM_NUM); timer_hw->alarm[ALARM_NUM] = timer_hw->timerawl + POLL_INTERVAL_IN_MICROSECONDS; - uint16_t lvl = 0; + uint32_t mix = 0; + byte voices = POLYPHONY_LIMIT; byte p; + byte level = 0; for (byte i = 0; i < POLYPHONY_LIMIT; i++) { - synth[i].counter += synth[i].increment; // should loop from 65536 -> 0 - switch (currWave) { - case WAVEFORM_SQUARE: - p = 0 - ((synth[i].counter & 0x8000) >> 15); // grab first bit -> 0 or -1 (255) - break; - case WAVEFORM_SAW: - p = (synth[i].counter >> 8); // 0 thru 255 - break; - default: - p = 0; - break; - }; - lvl += p; // for polyphony=15, cap=255*15=3825 - }; - lvl = (lvl * attenuation[poly]) >> 8; // cap = 3825 * 17 / 256 = 254 - lvl = (lvl * velWheel.curValue) >> 7; - pwm_set_chan_level(TONE_SL, TONE_CH, lvl); + 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 - void buzz(byte x, bool p) { - byte ch = h[x].channel - 2; - synth[ch].counter = 0; - if (p) { - synth[ch].increment = h[x].frequency // note frequency - * exp2(pbWheel.curValue * PITCH_BEND_SEMIS / 98304.0) // adjusted for global pitch bend - * ((POLL_INTERVAL_IN_MICROSECONDS << 16) / 1000000); // cycle 0-65535 at resultant frequency + 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 { - synth[ch].increment = 0; // zero effectively silences the channel - }; - } - -// ====== MIDI routines - void setAllNotesOff(byte Ch) { - if (enableMIDI) { - MIDI.sendControlChange(123, 0, Ch); + 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 + void replaceBuzzerWith(byte x) { + setBuzzer(0, 1); + arpeggiatingNow = x; + setBuzzer(h[arpeggiatingNow].frequency, 1); + } +// ====== MIDI routines void setPitchBendRange(byte Ch, byte semitones) { - if (enableMIDI) { // MIDI mode only - 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" - ); - } + 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) { - if (enableMIDI) { // MIDI mode only - 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) - ); + 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 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); + } } } - void applyMPEmode() { - while (!openChannelQueue.empty()) { // empty the channel queue - openChannelQueue.pop(); - }; + void resetTuningMIDI() { + while (!MPEchQueue.empty()) { // empty the channel queue + MPEchQueue.pop(); + } for (byte i = 1; i <= 16; i++) { - setAllNotesOff(i); // turn off all notes + MIDI.sendControlChange(123, 0, i); setPitchBendRange(i, PITCH_BEND_SEMIS); // force pitch bend back to the expected range of 2 semitones. - }; - setMPEzone(1, POLYPHONY_LIMIT); // MPE zone 1 = ch 2 thru 16 - for (byte i = 0; i < POLYPHONY_LIMIT; i++) { - openChannelQueue.push(i + 2); - sendToLog("pushed ch " + std::to_string(i + 2) + " to the open channel queue"); - }; + } + 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() { - if (enableMIDI) { // MIDI mode only - MIDI.sendControlChange(1, modWheel.curValue, 1); - sendToLog("sent mod value " + std::to_string(modWheel.curValue) + " to ch 1"); - }; + MIDI.sendControlChange(1, modWheel.curValue, 1); + sendToLog("sent mod value " + std::to_string(modWheel.curValue) + " to ch 1"); } void chgUniversalPB() { - if (enableMIDI) { // MIDI mode only - MIDI.sendPitchBend(pbWheel.curValue, 1); - for (byte i = 0; i < LED_COUNT; i++) { - if (!(h[i].isCmd)) { - if (h[i].channel) { - buzz(i,true); // rebuzz all notes if the pitch bend changes - }; - }; - }; + 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 @@ -639,52 +1430,79 @@ void playNote(byte x) { // this gets called on any non-command hex // that is not scale-locked. - if (!(h[x].channel)) { // but just in case, check - if (openChannelQueue.empty()) { // if there aren't any open channels - sendToLog("channel queue was empty so did not play"); + if (!(h[x].MIDIch)) { + if (current.tuningIndex == TUNING_12EDO) { + h[x].MIDIch = 1; } else { - h[x].channel = openChannelQueue.front(); // value in MIDI terms (1-16) - openChannelQueue.pop(); - sendToLog("popped " + std::to_string(h[x].channel) + " off the queue"); - if (!(playbackMode == BUZZ_OFF)) { - buzz(x, true); - }; - if (enableMIDI) { - MIDI.sendPitchBend(h[x].bend, h[x].channel); // ch 1-16 - MIDI.sendNoteOn(h[x].note, velWheel.curValue, h[x].channel); // 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].channel) - ); - }; - }; - }; - } + 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); + } + } + } + } + + // check - full death if over 8 poly. + void stopNote(byte x) { // this gets called on any non-command hex // that is not scale-locked. - if (h[x].channel) { // but just in case, check - openChannelQueue.push(h[x].channel); - sendToLog("pushed " + std::to_string(h[x].channel) + " on the queue"); - if (!(playbackMode == BUZZ_OFF)) { - buzz(x, false); - }; - if (playbackMode == BUZZ_ARPEGGIO) { + 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 == BUZZ_OFF) && !(playbackMode == BUZZ_POLY)) { + setBuzzer(0, 1); arpeggiateTime = 0; // trigger arpeggiate function early if any note changes - }; - if (enableMIDI) { - MIDI.sendNoteOff(h[x].note, velWheel.curValue, h[x].channel); - 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].channel) - ); - }; - h[x].channel = 0; - }; + } + } + 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) { @@ -694,13 +1512,13 @@ 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 @@ -709,7 +1527,7 @@ return 1 + (((runTime - h[x].timePressed) * animationFPS) >> 20); } else { return 0; - }; + } } void flagToAnimate(int8_t r, int8_t c) { if (! @@ -719,38 +1537,38 @@ ) ) { 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].channel)) { // that is a held note + 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].channel))) { // that is a note not being played + 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].channel) && ((h[i].inScale) || (!scaleLock))) { // that is a held note + 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 + 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 @@ -761,11 +1579,11 @@ flagToAnimate(turtleRow,turtleCol); // flag for animation turtleRow += (vertical[dir] * (radius / steps)); turtleCol += (horizontal[dir] * (radius / steps)); - }; - }; - }; - }; - }; + } + } + } + } + } } // ====== menu routines @@ -776,19 +1594,19 @@ 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 @@ -796,7 +1614,7 @@ if (selection != current.layoutIndex) { current.layoutIndex = selection; applyLayout(); - }; + } menuHome(); } void changeScale(GEMCallbackData callbackData) { // when you change the scale via the menu @@ -804,7 +1622,7 @@ if (selection != current.scaleIndex) { current.scaleIndex = selection; applyScale(); - }; + } menuHome(); } void changeKey() { // when you change the key via the menu @@ -825,21 +1643,22 @@ showOnlyValidScaleChoices(); // change list of choices in GEM Menu showOnlyValidKeyChoices(); // change list of choices in GEM Menu applyLayout(); // apply changes above - applyMPEmode(); // clear out MIDI queue - }; + 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() { @@ -847,14 +1666,14 @@ 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(); } @@ -865,7 +1684,8 @@ 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() - applyMPEmode(); + resetTuningMIDI +(); sendToLog("setupMIDI okay"); } void setupFileSystem() { @@ -899,11 +1719,11 @@ 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(); } @@ -911,7 +1731,7 @@ strip.begin(); // INITIALIZE NeoPixel strip object strip.show(); // Turn OFF all pixels ASAP sendToLog("LEDs started..."); - resetHexLEDs(); + setLEDcolorCodes(); } void setupMenu() { menu.setSplashDelay(0); @@ -932,11 +1752,21 @@ 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(menuItemColor); - menuPageMain.addMenuItem(menuItemPlayback); - menuPageMain.addMenuItem(menuItemWaveform); - menuPageMain.addMenuItem(menuItemAnimate); + menuPageMain.addMenuItem(menuGotoTesting); + menuPageTesting.addMenuItem(menuItemVersion); + menuPageTesting.addMenuItem(menuItemPercep); + menuPageTesting.addMenuItem(menuItemShiftColor); + menuPageTesting.addMenuItem(menuTestingBack); menuHome(); } void setupGFX() { @@ -994,7 +1824,7 @@ 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). } } @@ -1007,21 +1837,21 @@ 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() { @@ -1031,17 +1861,17 @@ byte n = UNUSED_NOTE; for (byte i = 1; i < LED_COUNT; i++) { byte j = positiveMod(arpeggiatingNow + i, LED_COUNT); - if ((h[j].channel) && (!h[j].isCmd)) { + if ((h[j].MIDIch) && (!h[j].isCmd)) { n = j; break; - }; - }; + } + } arpeggiatingNow = n; if (n != UNUSED_NOTE) { - buzz(n, true); - }; - }; - }; + replaceBuzzerWith(n); + } + } + } } void updateWheels() { velWheel.setTargetValue(); @@ -1054,20 +1884,20 @@ 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: @@ -1081,15 +1911,15 @@ 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(); @@ -1142,7 +1972,7 @@ 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(); @@ -1157,7 +1987,6 @@ animateLEDs(); // deal with animations lightUpLEDs(); // refresh LEDs dealWithRotary(); // deal with menu - readMIDI(); } void loop1() { // run on second core keepTrackOfRotaryKnobTurns(); |