From 9bd3f7d68479570614bbfc56d307486dc1ea29d4 Mon Sep 17 00:00:00 2001 From: Jared DeCook Date: Fri, 24 May 2024 16:43:34 -0400 Subject: HexBoard Firmware Golden Master MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I think that’s what they call it when the assumption is it’s ready to ship. Version 1.0.0 is ready! --- Hexperiment.ino | 2033 ------------------------------------------------------- 1 file changed, 2033 deletions(-) delete mode 100644 Hexperiment.ino (limited to 'Hexperiment.ino') 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 - #include - #include "LittleFS.h" - #include - #include - #define GEM_DISABLE_GLCD - #include - #include - #include - #include "hardware/pwm.h" - #include "hardware/timer.h" - #include "hardware/irq.h" - #include // std::queue construct to store open channels in microtonal mode - #include - - // 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",-29},{"vEb",-28},{"Eb",-27} - ,{" D#",-26},{"^D#",-25},{"E",-20},{"vF", -19} - ,{" F", -18},{"^F", -17},{">F",-16},{"vGb",-15},{"Gb",-14} - ,{" F#",-13},{"^F#",-12},{"G", -7},{"vAb", -6},{"Ab", -5} - ,{" G#", -4},{"^G#", -3},{"A", 2},{"vBb", 3},{"Bb", 4} - ,{" A#", 5},{"^A#", 6},{"C", -52},{" C+",-51},{"C#",-46},{" Dd",-45},{"D", -40},{" D+",-39},{"Eb",-34},{" Ed",-33},{"E", -28},{" E+",-27},{"F", -22},{" F+",-21},{"F#",-16},{" Gd",-15},{"G", -10},{" G+", -9},{"G#", -4},{" Ad", -3},{"A", 2},{" A+", 3},{"Bb", 8},{" Bd", 9},{"B", 14},{" Cd", 15},{" 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 MPEchQueue; - std::queue 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(); - } -- cgit 1.4.1