Firmware for HexBoard MIDI controller
Firmware 1.1 BETA 1
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | src/HexBoard.ino (renamed from HexBoard.ino) | 2454 |
2 files changed, 2144 insertions, 312 deletions
@@ -2,7 +2,7 @@ build/build.ino.uf2: build/build.ino arduino-cli compile -b rp2040:rp2040:generic:flash=16777216_0,freq=133,opt=Small,rtti=Disabled,stackprotect=Disabled,exceptions=Disabled,dbgport=Disabled,dbglvl=None,usbstack=tinyusb,boot2=boot2_generic_03h_2_padded_checksum --output-dir build build build/build.ino: HexBoard.ino - cp HexBoard.ino build/build.ino + cp src/HexBoard.ino build/build.ino /run/media/*/RPI-RP2/INFO_UF2.TXT: echo "Mounting device" diff --git a/HexBoard.ino b/src/HexBoard.ino index b67cfe5..a4881b5 100644 --- a/HexBoard.ino +++ b/src/HexBoard.ino @@ -1,7 +1,7 @@ // @readme /* HexBoard - Copyright 2022-2023 Jared DeCook and Zach DeCook + Copyright 2022-2025 Jared DeCook and Zach DeCook with help from Nicholas Fox Licensed under the GNU GPL Version 3. @@ -26,14 +26,14 @@ arduino-cli lib install "U8g2" # dependency for GEM arduino-cli lib install "Adafruit GFX Library" # dependency for GEM arduino-cli lib install "GEM" - sed -i 's@#include "config/enable-glcd.h"@//\0@g' ~/Arduino/libraries/GEM/src/config.h # remove dependency from GEM + sed -i 's@#include "config/enable-glcd.h"@//\0@g' ~/Arduino/libraries/GEM/src/config.h # remove dependency from GEM - I think this is unnecessary now. # Run Make to build the firmware make --------------------------- New to programming Arduino? --------------------------- Coding the Hexboard is, basically, done in C++. - + When the HexBoard is plugged in, it runs void setup() and void setup1(), then runs void loop() and void loop1() on an @@ -42,7 +42,7 @@ You can pretend that the compiler tosses these two routines inside an int main() for each processor. - + To #include libraries, the Arduino compiler expects them to be installed from a centralized repository. You can also bring @@ -59,9 +59,10 @@ future, it is easier to air-lift parts of the code into a library at that point. */ + // @init #include <Arduino.h> // this is necessary to talk to the Hexboard! - #include <Wire.h> // this is necessary to deal with the pins and wires + #include <Wire.h> // this is necessary to connect with I2C devices (such as the oled display) #define SDAPIN 16 #define SCLPIN 17 #include <GEM_u8g2.h> // library of code to create menu objects on the B&W display @@ -73,9 +74,37 @@ #define HARDWARE_V1_1 1 #define HARDWARE_V1_2 2 byte Hardware_Version = 0; // 0 = unknown, 1 = v1.1 board. 2 = v1.2 board. + + + +///////// global variables and defines /////////////////////////////////////////////// + + bool forceEnableMPE = false; + byte defaultMidiChannel = 1; + byte layoutRotation = 0; + + // Keyboard layout swapping + bool mirrorLeftRight = false; + bool mirrorUpDown = false; + + + // Helper, might be redundant + std::vector<byte> pressedKeyIDs = {}; + + + // Just Intonation related global variables + byte justIntonationBPM = 60; + byte justIntonationBPM_Multiplier = 1; + bool useJustIntonationBPM = false; + bool useDynamicJustIntonation = false; + +///////////////////////////////////////////////////////////////////////////////////////////////// + + + // @helpers /* - C++ returns a negative value for + C++ returns a negative value for negative N % D. This function guarantees the mod value is always positive. @@ -105,9 +134,9 @@ for user-editable options */ int transposeSteps = 0; - byte scaleLock = 0; - byte perceptual = 1; - byte paletteBeginsAtKeyCenter = 1; + bool scaleLock = 0; + bool perceptual = 1; + bool paletteBeginsAtKeyCenter = 1; byte animationFPS = 32; // actually frames per 2^20 microseconds. close enough to 30fps byte wheelMode = 0; // standard vs. fine tune mode @@ -130,30 +159,41 @@ #define WAVEFORM_HYBRID 7 #define WAVEFORM_SQUARE 8 #define WAVEFORM_SAW 9 - #define WAVEFORM_TRIANGLE 10 + #define WAVEFORM_TRIANGLE 10 byte currWave = WAVEFORM_HYBRID; #define RAINBOW_MODE 0 #define TIERED_COLOR_MODE 1 #define ALTERNATE_COLOR_MODE 2 + #define RAINBOW_OF_FIFTHS_MODE 3 + #define PIANO_ALT_COLOR_MODE 4 + #define PIANO_COLOR_MODE 5 + #define PIANO_INCANDESCENT_COLOR_MODE 6 byte colorMode = RAINBOW_MODE; #define ANIMATE_NONE 0 - #define ANIMATE_STAR 1 - #define ANIMATE_SPLASH 2 - #define ANIMATE_ORBIT 3 - #define ANIMATE_OCTAVE 4 + #define ANIMATE_STAR 1 + #define ANIMATE_SPLASH 2 + #define ANIMATE_ORBIT 3 + #define ANIMATE_OCTAVE 4 #define ANIMATE_BY_NOTE 5 + #define ANIMATE_BEAMS 6 + #define ANIMATE_SPLASH_REVERSE 7 + #define ANIMATE_STAR_REVERSE 8 byte animationType = ANIMATE_NONE; - + #define BRIGHT_MAX 255 #define BRIGHT_HIGH 210 #define BRIGHT_MID 180 #define BRIGHT_LOW 150 #define BRIGHT_DIM 110 - #define BRIGHT_DIMMER 50 + #define BRIGHT_DIMMER 70 + #define BRIGHT_DARK 50 // BRIGHT_DIMMEST + #define BRIGHT_DARKER 34 // Lowest brightness before backlight shuts down + #define BRIGHT_FAINT 33 // Highest brightness before backlight turns on + #define BRIGHT_FAINTER 24 // Lowest brightness before any highlighted button is lit in all color modes #define BRIGHT_OFF 0 - byte globalBrightness = BRIGHT_MID; + byte globalBrightness = BRIGHT_DIM; // @microtonal /* @@ -169,30 +209,39 @@ definition and smart pointer references to tuning presets without requiring an enumeration. */ - #define TUNINGCOUNT 13 #define TUNING_12EDO 0 - #define TUNING_17EDO 1 - #define TUNING_19EDO 2 - #define TUNING_22EDO 3 - #define TUNING_24EDO 4 - #define TUNING_31EDO 5 - #define TUNING_41EDO 6 - #define TUNING_53EDO 7 - #define TUNING_72EDO 8 - #define TUNING_BP 9 - #define TUNING_ALPHA 10 - #define TUNING_BETA 11 - #define TUNING_GAMMA 12 + #define TUNING_12EDO_ZETA 1 + #define TUNING_17EDO 2 + #define TUNING_19EDO 3 + #define TUNING_22EDO 4 + #define TUNING_24EDO 5 + #define TUNING_31EDO 6 + #define TUNING_31EDO_ZETA 7 + #define TUNING_41EDO 8 + #define TUNING_43EDO 9 + #define TUNING_46EDO 10 + #define TUNING_53EDO 11 + #define TUNING_58EDO 12 + #define TUNING_58EDO_ZETA 13 + #define TUNING_72EDO 14 + #define TUNING_72EDO_ZETA 15 + #define TUNING_80EDO 16 + #define TUNING_87EDO 17 + #define TUNING_BP 18 + #define TUNING_ALPHA 19 + #define TUNING_BETA 20 + #define TUNING_GAMMA 21 + #define TUNINGCOUNT 22 /* Note names and palette arrays are allocated in memory at runtime. Their usable size is based on the number - of steps (in standard tuning, semitones) in a tuning + of steps (in standard tuning, semitones) in a tuning system before a new period is reached (in standard tuning, the octave). This value provides a maximum array size that handles almost all useful tunings without wasting much space. */ - #define MAX_SCALE_DIVISIONS 72 + #define MAX_SCALE_DIVISIONS 87 /* A dictionary of musical scales is defined in the code. A scale is tied to one tuning system, with the exception @@ -241,46 +290,55 @@ expressing step sizes to six significant figures is sufficient to eliminate any detectable tuning artifacts due to rounding. - + The note names are formatted in an array specifically to match the format needed for the GEM Menu to accept directly as a spinner selection item. The number next to the note name is the number of steps from the anchor note A that key is. - + There are other ways the tuning could be calculated. Some microtonal players choose an anchor note other than A 440. Future versions will allow for more flexibility in anchor selection, which will also change the implementation of key options. - */ + */ + + /* + Sko: I felt like maximizing precision for just intonation purposes. + Values are precalculated by compiler, and MIDI 2.0 or later might benefit from it + */ tuningDef tuningOptions[] = { - { "12 EDO", 12, 100.000, + { "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, + { "12 EDO Zeta peak", 12, 99.8071515654111465, + {{"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, 1200.0/17.0, {{"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, + { "19 EDO", 19, 1200.0/19.0, {{"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, + }}, + { "22 EDO", 22, 1200.0/22.0, {{" 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, + { "24 EDO", 24, 1200.0/24.0, {{"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, + { "31 EDO", 31, 1200.0/31.0, {{"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} @@ -289,7 +347,16 @@ ,{"A", 0},{"A+", 1},{"A#", 2},{"Bb", 3},{"Bd", 4} ,{"B", 5},{"B+", 6} ,{"Cd", 7} }}, - { "41 EDO", 41, 29.2683, + { "31 EDO Zeta peak", 31, 1200.0/30.9783818789525220, + {{"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, 1200.0/41.0, {{" 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} @@ -298,7 +365,25 @@ ,{" 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, + { "43 EDO", 43, 1200.0/43.0, + {{" C",-32},{"C+1",-31},{"C+2",-30},{"C+3",-29},{"C+4",-28},{"C+5",-27},{"C+6",-26} + ,{" D",-25},{"D+1",-24},{"D+2",-23},{"D+3",-22},{"D+4",-21},{"D+5",-20},{"D+6",-19} + ,{" E",-18},{"E+1",-17},{"E+2",-16} ,{"E+3",-15} + ,{" F",-14},{"F+1",-13},{"F+2",-12},{"F+3",-11},{"F+4",-10},{"F+5", -9},{"F+6", -8} + ,{" G", -7},{"G+1", -6},{"G+2", -5},{"G+3", -4},{"G+4", -3},{"G+5", -2},{"G+6", -1} + ,{" A", 0},{"A+1", 1},{"A+2", 2},{"A+3", 3},{"A+4", 4},{"A+5", 5},{"A+6", 6} + ,{" B", 7},{"B+1", 8},{"B+2", 9},{"B+3", 10}, {"B+4", 11} + }}, + { "46 EDO", 46, 1200.0/46.0, + {{" C",-35},{"C+1",-34},{"C+2",-33},{"C+3",-32},{"C+4",-31},{"C+5",-30},{"C+6",-29},{"C+7",-28} + ,{" D",-27},{"D+1",-26},{"D+2",-25},{"D+3",-24},{"D+4",-23},{"D+5",-22},{"D+6",-21},{"D+7",-20} + ,{" E",-19},{"E+1",-18},{"E+2",-17} + ,{" F",-16},{"F+1",-15},{"F+2",-14},{"F+3",-13},{"F+4",-12},{"F+5",-11},{"F+6",-10},{"F+7", -9} + ,{" G", -8},{"G+1", -7},{"G+2", -6},{"G+3", -5},{"G+4", -4},{"G+5", -3},{"G+6", -2},{"G+7", -1} + ,{" A", 0},{"A+1", 1},{"A+2", 2},{"A+3", 3},{"A+4", 4},{"A+5", 5},{"A+6", 6},{"A+7", 7} + ,{" B", 8},{"B+1", 9},{"B+2", 10} + }}, + { "53 EDO", 53, 1200.0/53.0, {{" 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} @@ -312,7 +397,39 @@ ,{" A#", 5},{"^A#", 6},{"<B", 7},{"vB", 8} ,{" B", 9},{"^B", 10},{"<C", 11},{"vC", 12} }}, - { "72 EDO", 72, 16.6667, + { "58 EDO", 58, 1200.0/58.0, + {{" C",-44},{"C+1",-43},{"C+2",-42},{"C+3",-41},{"C+4",-40},{"C+5",-39},{"C+6",-38},{"C+7",-37},{"C+8",-36},{"C+8",-35} + ,{" D",-34},{"D+1",-33},{"D+2",-32},{"D+3",-31},{"D+4",-30},{"D+5",-29},{"D+6",-28},{"D+7",-27},{"D+8",-26},{"D+8",-25} + ,{" E",-24},{"E+1",-23},{"E+2",-22},{"E+3",-21} + ,{" F",-20},{"F+1",-19},{"F+2",-18},{"F+3",-17},{"F+4",-16},{"F+5",-15},{"F+6",-14},{"F+7",-13},{"F+8",-12},{"F+9",-11} + ,{" G",-10},{"G+1", -9},{"G+2", -8},{"G+3", -7},{"G+4", -6},{"G+5", -5},{"G+6", -4},{"G+7", -3},{"G+8", -2},{"G+9", -1} + ,{" A", 0},{"A+1", 1},{"A+2", 2},{"A+3", 3},{"A+4", 4},{"A+5", 5},{"A+6", 6},{"A+7", 7},{"A+8", 7},{"A+9", 7} + ,{" B", 10},{"B+1", 11},{"B+2", 12} + }}, + { "58 EDO Zeta peak", 58, 1200.0/58.066718758225889, + {{" C",-44},{"C+1",-43},{"C+2",-42},{"C+3",-41},{"C+4",-40},{"C+5",-39},{"C+6",-38},{"C+7",-37},{"C+8",-36},{"C+8",-35} + ,{" D",-34},{"D+1",-33},{"D+2",-32},{"D+3",-31},{"D+4",-30},{"D+5",-29},{"D+6",-28},{"D+7",-27},{"D+8",-26},{"D+8",-25} + ,{" E",-24},{"E+1",-23},{"E+2",-22},{"E+3",-21} + ,{" F",-20},{"F+1",-19},{"F+2",-18},{"F+3",-17},{"F+4",-16},{"F+5",-15},{"F+6",-14},{"F+7",-13},{"F+8",-12},{"F+9",-11} + ,{" G",-10},{"G+1", -9},{"G+2", -8},{"G+3", -7},{"G+4", -6},{"G+5", -5},{"G+6", -4},{"G+7", -3},{"G+8", -2},{"G+9", -1} + ,{" A", 0},{"A+1", 1},{"A+2", 2},{"A+3", 3},{"A+4", 4},{"A+5", 5},{"A+6", 6},{"A+7", 7},{"A+8", 7},{"A+9", 7} + ,{" B", 10},{"B+1", 11},{"B+2", 12} + }}, + { "72 EDO", 72, 1200.0/72.0, + {{" 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} + }}, + { "72 EDO Zeta peak", 72, 1200.0/71.9506066608606432, {{" 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} @@ -326,19 +443,37 @@ ,{" 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, + { "80 EDO", 80, 1200.0/80.0, + {{" C",-61},{"C+1",-60},{"C+2",-59},{"C+3",-58},{"C+4",-57},{"C+5",-56},{"C+6",-55},{"C+7",-54},{"C+8",-53},{"C+9",-52},{"C+10",-51},{"C+11",-50},{"C+12",-49},{"C+13",-48} + ,{" D",-47},{"D+1",-46},{"D+2",-45},{"D+3",-44},{"D+4",-43},{"D+5",-42},{"D+6",-41},{"D+7",-40},{"D+8",-39},{"D+9",-38},{"D+11",-37},{"D+12",-36},{"D+13",-35},{"D+14",-34} + ,{" E",-33},{"E+1",-32},{"E+2",-31},{"E+3",-30},{"E+4",-29} + ,{" F",-28},{"F+1",-27},{"F+2",-26},{"F+3",-25},{"F+4",-24},{"F+5",-23},{"F+6",-22},{"F+7",-21},{"F+8",-20},{"F+9",-19},{"F+11",-18},{"F+12",-17},{"F+13",-16},{"F+14",-15} + ,{" G",-14},{"G+1",-13},{"G+2",-12},{"G+3",-11},{"G+4",-10},{"G+5",-9},{"G+6",-8},{"G+7",-7},{"G+8",-6},{"G+9",-5},{"G+11",-4},{"G+12",-3},{"G+13",-2},{"G+14",-1} + ,{" A", 0},{"A+1", 1},{"A+2", 2},{"A+3", 3},{"A+4", 4},{"A+5", 5},{"A+6", 6},{"A+7", 7},{"A+8", 8},{"A+9", 9},{"A+10", 10},{"A+11", 11},{"A+12", 12},{"A+13", 13} + ,{" B", 14},{"B+1", 15},{"B+2", 16},{"B+3", 17},{"B+4", 18} + }}, + { "87 EDO", 87, 1200.0/87.0, + {{" C",-66},{"C+1",-65},{"C+2",-64},{"C+3",-63},{"C+4",-62},{"C+5",-61},{"C+6",-60},{"C+7",-59},{"C+8",-58},{"C+9",-57},{"C10",-57},{"C+11",-56},{"C+12",-55},{"C+13",-54},{"C+14",-53},{"C+15",-52} + ,{" D",-51},{"D+1",-50},{"D+2",-49},{"D+3",-48},{"D+4",-47},{"D+5",-46},{"D+6",-45},{"D+7",-44},{"D+8",-43},{"D+9",-42},{"D+10",-41},{"D+11",-40},{"D+12",-39},{"D+13",-38},{"D+14",-37} + ,{" E",-36},{"E+1",-35},{"E+2",-34},{"E+3",-33} + ,{" F",-30},{"F+1",-29},{"F+2",-28},{"F+3",-27},{"F+4",-26},{"F+5",-25},{"F+6",-24},{"F+7",-23},{"F+8",-22},{"F+9",-21},{"F+10",-20},{"F+11",-19},{"F+12",-18},{"F+13",-17},{"F+14",-16} + ,{" G",-15},{"G+1",-14},{"G+2",-13},{"G+3",-12},{"G+4",-11},{"G+5",-10},{"G+6",-9},{"G+7",-8},{"G+8",-7},{"G+9",-6},{"G+10",-5},{"G+11",-4},{"G+12",-3},{"G+13",-2},{"G+14",-1} + ,{" A", 0},{"A+1", 1},{"A+2", 2},{"A+3", 3},{"A+4", 4},{"A+5", 5},{"A+6", 6},{"A+7", 7},{"A+8", 8},{"A+9", 9},{"A+10", 10},{"A+11", 11},{"A+12", 12},{"A+13", 13},{"A+14", 14} + ,{" B", 15},{"B+1", 16},{"B+2", 17},{"B+3", 18} + }}, + { "Bohlen-Pierce", 13, (1200.0 * (log(3.0/1.0) / log(2.0)))/13.0, {{"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, + { "Carlos Alpha", 9, 77.964990, {{"I",0},{"I#",1},{"II-",2},{"II+",3},{"III",4} ,{"III#",5},{"IV-",6},{"IV+",7},{"Ib",8} }}, - { "Carlos Beta", 11, 63.8329, + { "Carlos Beta", 11, 63.832933, {{"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, + { "Carlos Gamma", 20, 35.0985422804, {{" 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} @@ -350,13 +485,13 @@ /* This section defines the different preset note layout options. - */ + */ /* This class provides the seed values needed to implement a given isomorphic note layout. From it, the map of buttons to note frequencies can be calculated. - + A layout is tied to a specific tuning. */ class layoutDef { @@ -375,63 +510,213 @@ steps to go up or down for the hex button across or down diagonally. */ + + // NOTE: Aside from adding new layouts, + // I have also rearranged them for personal use: + // - Wicki-Hayden first, if it manages to map all notes; + // - Compressed Janko second, if it maps all notes; + // - Full Janko otherwise; + // You might want to arrange them as seems fit for release, + // including all other layouts as I didn't put them in any particular order 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 }, + { "Janko", 0, 65, 1, -2, TUNING_12EDO }, + { "Bosanquet-Wilson", 0, 65, -1, -1, TUNING_12EDO }, + { "Compressed Janko", 0, 65, -1, -2, TUNING_12EDO }, + { "Compr. Bosanquet", 0, 65, -1, 3, 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 }, + { "Chromatic", 0, 75, 12, -1, TUNING_12EDO }, + { "Full Gamut", 1, 75, 1, -9, TUNING_12EDO }, + { "31 EDO Janko", 0, 65, 2, -5, TUNING_12EDO }, // These layouts are meant for + { "31 EDO Compr.Janko",0, 65, -3, -5, TUNING_12EDO }, // synths that do not support MPE + { "31 EDO Bos.Wilson", 0, 65, -2, -3, TUNING_12EDO }, // but support scala/tun files + { "31 EDO Wicki-Hayd.",1, 64, 5, -18, TUNING_12EDO }, + { "31 EDO Harm. Table",0, 75, -18, 8, TUNING_12EDO }, // I did not add many, as it would get VERY cluttered + { "41 EDO Janko", 0, 65, 4, -7, TUNING_12EDO }, + { "53 EDO Janko", 0, 65, 5, -9, TUNING_12EDO }, // A different microtuning option in firmware + { "58 EDO Janko", 0, 65, 3, -10, TUNING_12EDO }, // should mitigate the need for these layouts + + { "Wicki-Hayden", 1, 64, 2, -7, TUNING_12EDO_ZETA }, + { "Harmonic Table", 0, 75, -7, 3, TUNING_12EDO_ZETA }, + { "Janko", 0, 65, 1, -2, TUNING_12EDO_ZETA }, + { "Bosanquet-Wilson", 0, 65, -1, -1, TUNING_12EDO_ZETA }, + { "Compressed Janko", 0, 65, -1, -2, TUNING_12EDO_ZETA }, + { "Compr. Bosanquet", 0, 65, -1, 3, TUNING_12EDO_ZETA }, + { "Gerhard", 0, 65, -1, -3, TUNING_12EDO_ZETA }, + { "Accordion C-sys.", 1, 75, 2, -3, TUNING_12EDO_ZETA }, + { "Accordion B-sys.", 1, 64, 1, -3, TUNING_12EDO_ZETA }, + { "Chromatic", 0, 75, 12, -1, TUNING_12EDO_ZETA }, + { "Full Gamut", 1, 75, 1, -9, TUNING_12EDO_ZETA }, - { "Full Gamut", 1, 65, 1, -9, TUNING_17EDO }, + { "Compressed Janko", 0, 65, -1, -3, TUNING_17EDO }, + { "Compr. Bosanquet", 0, 65, -2, -1, TUNING_17EDO }, + { "Janko", 0, 65, 2, -3, TUNING_17EDO }, { "Bosanquet-Wilson", 0, 65, -2, -1, TUNING_17EDO }, { "Neutral Thirds A", 0, 65, -1, -2, TUNING_17EDO }, { "Neutral Thirds B", 0, 65, 1, -3, TUNING_17EDO }, + { "Full Gamut", 1, 75, 1, -9, TUNING_17EDO }, - { "Full Gamut", 1, 65, 1, -9, TUNING_19EDO }, + { "Wicki-Hayden", 1, 65, 3, -11, TUNING_19EDO }, + { "Compressed Janko", 0, 65, -2, -3, TUNING_19EDO }, + { "Compr. Bosanquet", 0, 65, -2, 5, TUNING_19EDO }, + { "Janko", 0, 65, 1, -3, TUNING_19EDO }, { "Bosanquet-Wilson", 0, 65, -1, -2, TUNING_19EDO }, + { "Harmonic Table", 0, 75, -11, 5, TUNING_19EDO }, { "Kleismic", 0, 65, -1, -4, TUNING_19EDO }, - - { "Full Gamut", 1, 65, 1, -8, TUNING_22EDO }, + { "Full Gamut", 1, 75, 1, -9, TUNING_19EDO }, + + { "Compressed Janko", 0, 65, -1, -4, TUNING_22EDO }, + { "Compr. Bosanquet", 0, 65, -1, 5, TUNING_22EDO }, + { "Janko", 0, 65, 3, -4, TUNING_22EDO }, { "Bosanquet-Wilson", 0, 65, -3, -1, TUNING_22EDO }, + { "Wicki-Hayden", 1, 64, 4, -13, TUNING_22EDO }, { "Porcupine", 0, 65, 1, -4, TUNING_22EDO }, - - { "Full Gamut", 1, 65, 1, -9, TUNING_24EDO }, - { "Bosanquet-Wilson", 0, 65, -1, -3, TUNING_24EDO }, - { "Inverted", 0, 65, 1, -4, TUNING_24EDO }, - - { "Full Gamut", 1, 65, 1, -7, TUNING_31EDO }, + { "Full Gamut", 1, 75, 1, -9, TUNING_22EDO }, + + { "Janko", 0, 65, 1, -4, TUNING_24EDO }, // Maybe call it "Quartertone Janko"? + { "Bosanquet-Wilson", 0, 65, -1, -3, TUNING_24EDO }, // Maybe call it "1/4 tone Bosanquet"? + { "Full Gamut", 1, 75, 1, -9, TUNING_24EDO }, + + { "Compressed Janko", 0, 65, -3, -5, TUNING_31EDO }, + { "Compr. Bosanquet", 0, 65, -3, 8, TUNING_31EDO }, + { "Janko", 0, 65, 2, -5, TUNING_31EDO }, { "Bosanquet-Wilson", 0, 65, -2, -3, TUNING_31EDO }, + { "Wicki-Hayden", 1, 64, 5, -18, TUNING_31EDO }, + { "5X -13Y", 1, 64, 5, -13, TUNING_31EDO }, // Unnamed layout, between Wicki-Hayd. and compressed Janko + { "Harmonic Table", 0, 75, -18, 8, TUNING_31EDO }, { "Double Bosanquet", 0, 65, -1, -4, TUNING_31EDO }, { "Anti-Double Bos.", 0, 65, 1, -5, TUNING_31EDO }, - - { "Full Gamut", 0, 65, 1, -8, TUNING_41EDO }, // forty-one #3 + { "Full Gamut", 1, 75, 1, -9, TUNING_31EDO }, + + { "Compressed Janko", 0, 65, -3, -5, TUNING_31EDO_ZETA }, + { "Compr. Bosanquet", 0, 65, -3, 8, TUNING_31EDO_ZETA }, + { "Janko", 0, 65, 2, -5, TUNING_31EDO_ZETA }, + { "Bosanquet-Wilson", 0, 65, -2, -3, TUNING_31EDO_ZETA }, + { "Wicki-Hayden", 1, 64, 5, -18, TUNING_31EDO_ZETA }, + { "5X -13Y", 1, 64, 5, -13, TUNING_31EDO_ZETA }, // Unnamed layout, between Wicki-Hayd. and compressed Janko + { "Harmonic Table", 0, 75, -18, 8, TUNING_31EDO_ZETA }, + { "Double Bosanquet", 0, 65, -1, -4, TUNING_31EDO_ZETA }, + { "Anti-Double Bos.", 0, 65, 1, -5, TUNING_31EDO_ZETA }, + { "Full Gamut", 1, 75, 1, -9, TUNING_31EDO_ZETA }, + + { "Compressed Janko", 0, 65, -3, -7, TUNING_41EDO }, + { "Compr. Bosanquet", 0, 65, -3, 10, TUNING_41EDO }, + { "Janko", 0, 65, 4, -7, TUNING_41EDO }, { "Bosanquet-Wilson", 0, 65, -4, -3, TUNING_41EDO }, // forty-one #1 + { "Harmonic Table", 0, 75, -24, 11, TUNING_41EDO }, + { "Wicki-Hayden", 1, 64, 7, -24, TUNING_41EDO }, { "Gerhard", 0, 65, 3, -10, TUNING_41EDO }, // forty-one #2 - { "Baldy", 0, 65, -1, -6, TUNING_41EDO }, - { "Rodan", 1, 65, -1, -7, TUNING_41EDO }, - - { "Wicki-Hayden", 1, 64, 9, -31, TUNING_53EDO }, + { "Baldy", 0, 65, -1, -6, TUNING_41EDO }, + { "Rodan", 1, 65, -1, -7, TUNING_41EDO }, + { "Full Gamut", 1, 75, 1, -9, TUNING_41EDO }, // forty-one #3 + + { "Janko", 0, 65, 3, -7, TUNING_43EDO }, + { "Bosanquet-Wilson", 0, 65, -3, -4, TUNING_43EDO }, + { "Wicki-Hayden", 1, 64, 7, -25, TUNING_43EDO }, + { "Harmonic Table", 0, 75, -25, 11, TUNING_43EDO }, + { "Full Gamut", 0, 75, 1, -9, TUNING_43EDO }, + + { "Janko", 0, 65, 5, -8, TUNING_46EDO }, + { "Bosanquet-Wilson", 0, 65, -5, -3, TUNING_46EDO }, + { "Harmonic Table", 0, 75, -27, 12, TUNING_46EDO }, + { "Echidnic", 0, 65, 5, -9, TUNING_46EDO }, + { "Full Gamut", 1, 75, 1, -9, TUNING_46EDO }, + + { "Janko", 0, 65, 5, -9, TUNING_53EDO }, { "Bosanquet-Wilson", 0, 65, -5, -4, TUNING_53EDO }, + { "Harmonic Table", 0, 75, -31, 14, TUNING_53EDO }, + { "Wicki-Hayden", 1, 64, 9, -31, TUNING_53EDO }, { "Kleismic A", 0, 65, -8, -3, TUNING_53EDO }, { "Kleismic B", 0, 65, -5, -3, TUNING_53EDO }, - { "Harmonic Table", 0, 75, -31, 14, TUNING_53EDO }, { "Buzzard", 0, 65, -9, -1, TUNING_53EDO }, - - { "Full Gamut", 1, 65, 1, -9, TUNING_72EDO }, + { "Compressed Janko", 1, 65, 9, -13, TUNING_53EDO }, // Can only fit vertically + { "Compr. Bosanquet", 1, 65, 9, 4, TUNING_53EDO }, // Can only fit vertically + { "Full Gamut", 1, 75, 1, -9, TUNING_53EDO }, + + { "Janko", 0, 64, 3, -10, TUNING_58EDO }, // Maybe call it "Quartertone Janko"? + { "Bosanquet-Wilson", 0, 64, 3, 7, TUNING_58EDO }, // Maybe call it "Quartertone Bosanquet"? + { "Hemififths", 0, 64, 4, -7, TUNING_58EDO }, + { "Hemififths mirror.",0, 64, -4, -3, TUNING_58EDO }, + { "Chromatic", 0, 64, -7, -5, TUNING_58EDO }, + { "Harmonic Table", 0, 75, -34, 15, TUNING_58EDO }, + { "Septimal H.T.", 0, 75, -34, 13, TUNING_58EDO }, + { "Diaschismic", 0, 64, 4, -9, TUNING_58EDO }, + { "4X -19Y", 0, 64, 4, -19, TUNING_58EDO }, // unnamed layout, efficient for major 7ths, 9s, #11s and so on + { "-27X 10Y", 1, 64, -27, 10, TUNING_58EDO }, // weird but efficient layout + { "Wicki-Hayd.(29EDO)",1, 64, 10, -34, TUNING_58EDO }, + { "Bos.Wilson (29EDO)",0, 65, -6, -4, TUNING_58EDO }, + { "Janko (29EDO)",0, 65, 6, -10, TUNING_58EDO }, // 29 EDO subset, each for two rings of fifths + { "Tridec.H.T.(29EDO)",0, 75, -34, 14, TUNING_58EDO }, + { "Full Gamut", 1, 75, 1, -9, TUNING_58EDO }, + + { "Janko", 0, 64, 3, -10, TUNING_58EDO_ZETA }, // Maybe call it "Quartertone Janko"? + { "Bosanquet-Wilson", 0, 64, 3, 7, TUNING_58EDO_ZETA }, // Maybe call it "1/4 tone Bosanquet"? + { "Hemififths", 0, 64, 4, -7, TUNING_58EDO_ZETA }, + { "Hemififths mirror.",0, 64, -4, -3, TUNING_58EDO_ZETA }, + { "Chromatic", 0, 64, -7, -5, TUNING_58EDO_ZETA }, + { "Harmonic Table", 0, 75, -34, 15, TUNING_58EDO_ZETA }, + { "Septimal H.T.", 0, 75, -34, 13, TUNING_58EDO_ZETA }, + { "Diaschismic", 0, 64, 4, -9, TUNING_58EDO_ZETA }, + { "4X -19Y", 0, 64, 4, -19, TUNING_58EDO_ZETA }, // unnamed layout, efficient for major 7ths, 9s, #11s and so on + { "-27X 10Y", 1, 64, -27, 10, TUNING_58EDO_ZETA }, // weird but efficient layout + { "Wicki-Hayd.(29EDO)",1, 64, 10, -34, TUNING_58EDO_ZETA }, + { "Bos.Wilson (29EDO)",0, 65, -6, -4, TUNING_58EDO_ZETA }, + { "Janko (29EDO)",0, 65, 6, -10, TUNING_58EDO_ZETA }, // 29 EDO subset, each for two rings of fifths + { "Tridec.H.T.(29EDO)",0, 75, -34, 14, TUNING_58EDO_ZETA }, + { "Full Gamut", 1, 75, 1, -9, TUNING_58EDO_ZETA }, + + { "Harmonic Table", 0, 75, -42, 19, TUNING_72EDO }, + { "-30X 19Y", 0, 75, -30, 19, TUNING_72EDO }, // unnamed layout. Like harmonic table but with fourths instead of fifths + { "Miracle mapping", 0, 65, -7, -2, TUNING_72EDO }, + { "Sept.H.T.(36EDO)", 0, 75, -42, 16, TUNING_72EDO }, // 36 EDO subset { "Expanded Janko", 0, 65, -1, -6, TUNING_72EDO }, - - { "Full Gamut", 1, 65, 1, -9, TUNING_BP }, + { "Full Gamut", 1, 65, 1, -9, TUNING_72EDO }, + + { "Harmonic Table", 0, 75, -42, 19, TUNING_72EDO_ZETA }, + { "-30X 19Y", 0, 75, -30, 19, TUNING_72EDO_ZETA }, // unnamed layout. Like harmonic table but with fourths instead of fifths + { "Miracle mapping", 0, 65, -7, -2, TUNING_72EDO_ZETA }, + { "Sept.H.T.(36EDO)", 0, 75, -42, 16, TUNING_72EDO_ZETA }, // 36 EDO subset + { "Expanded Janko", 0, 65, -1, -6, TUNING_72EDO_ZETA }, + { "Full Gamut", 1, 65, 1, -9, TUNING_72EDO_ZETA }, + + { "Janko", 0, 65, 9, -14, TUNING_80EDO }, // Janko mapping is still too large to map all notes (same for 87 EDO) + { "Bosanquet-Wilson", 0, 65, -9, -5, TUNING_80EDO }, // Same for Bosanquet-Wilson. Still usable + { "Compressed Janko", 0, 65, -5, -14, TUNING_80EDO }, + { "Full Gamut", 1, 75, 1, -9, TUNING_80EDO }, // So far this is the only one layout that maps every note + + { "Harmonic Table", 0, 75, -51, 23, TUNING_87EDO }, + { "Janko (good 3/2)", 0, 65, 5, -14, TUNING_87EDO }, + { "Bos.W.(good 3/2)", 0, 65, -5, -9, TUNING_87EDO }, + { "Wic.Hayd.nooctave", 1, 64, 14, -51, TUNING_87EDO }, // perfect thirds at the cost of losing an octave + { "Wic.Hayd. pyth.", 1, 64, 15, -51, TUNING_87EDO }, // pythagorean thirds, octave is preserved, note variety decreased + { "Janko (good 4/3)", 0, 65, 6, -14, TUNING_87EDO }, // Less efficient but allows perfect chord inversions + { "Bos.W.(good 4/3)", 0, 65, -6, -8, TUNING_87EDO }, + { "Bos.W.(26EDO)", 0, 65, -9, -6, TUNING_87EDO }, + { "Janko (26EDO)", 0, 65, 9, -15, TUNING_87EDO }, + { "Full Gamut", 1, 75, 1, -9, TUNING_87EDO }, + { "Standard", 0, 65, -2, -1, TUNING_BP }, - - { "Full Gamut", 1, 65, 1, -9, TUNING_ALPHA }, + { "Full Gamut", 1, 65, 1, -9, TUNING_BP }, + + { "Harmonic Table", 0, 75, -9, 5, TUNING_ALPHA }, { "Compressed", 0, 65, -2, -1, TUNING_ALPHA }, - - { "Full Gamut", 1, 65, 1, -9, TUNING_BETA }, - { "Compressed", 0, 65, -2, -1, TUNING_BETA }, - - { "Full Gamut", 1, 65, 1, -9, TUNING_GAMMA }, - { "Compressed", 0, 65, -2, -1, TUNING_GAMMA } + { "Full Gamut", 1, 65, 1, -9, TUNING_ALPHA }, + + { "Wicki-Hayden", 1, 65, 3, -11, TUNING_BETA }, // Carlos Beta has the same mappings as 19 EDO + { "Compressed Janko", 0, 65, -2, -3, TUNING_BETA }, + { "Compr. Bosanquet", 0, 65, -2, 5, TUNING_BETA }, + { "Janko", 0, 65, 1, -3, TUNING_BETA }, + { "Bosanquet-Wilson", 0, 65, -1, -2, TUNING_BETA }, + { "Harmonic Table", 0, 75, -11, 5, TUNING_BETA }, + { "Kleismic", 0, 65, -1, -4, TUNING_BETA }, + { "Full Gamut", 1, 75, 1, -9, TUNING_BETA }, + + { "Harmonic Table", 0, 75, -20, 9, TUNING_GAMMA }, // Same mappings as for 34 EDO + { "Compressed", 0, 65, -2, -1, TUNING_GAMMA }, // Difficult to map, has two rings of fifths + { "Full Gamut", 1, 65, 1, -9, TUNING_GAMMA } }; const byte layoutCount = sizeof(layoutOptions) / sizeof(layoutDef); // @scales @@ -442,7 +727,7 @@ between each degree of the scale. For example, the major scale in 12EDO is 2, 2, 1, 2, 2, 2, 1. - + A scale is tied to a specific tuning. */ class scaleDef { @@ -501,7 +786,7 @@ { "Pentatonic (Haba)", TUNING_24EDO, { 5,5,5,5,4 } }, { "Invert Pentatonic", TUNING_24EDO, { 6,3,6,6,3 } }, { "Rast maqam", TUNING_24EDO, { 4,3,3,4,4,2,1,3 } }, - { "Bayati maqam", TUNING_24EDO, { 3,3,4,4,2,1,3,4 } }, + { "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 } }, @@ -514,6 +799,15 @@ { "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 } }, + // 31 EDO ZETA PEAK; + { "Diatonic", TUNING_31EDO_ZETA, { 5,5,3,5,5,5,3 } }, + { "Pentatonic", TUNING_31EDO_ZETA, { 5,5,8,5,8 } }, + { "Harmonic", TUNING_31EDO_ZETA, { 5,5,4,4,4,3,3,3 } }, + { "Mavila", TUNING_31EDO_ZETA, { 5,3,3,3,5,3,3,3,3 } }, + { "Quartal", TUNING_31EDO_ZETA, { 2,2,7,2,2,7,2,7 } }, + { "Orwell", TUNING_31EDO_ZETA, { 4,3,4,3,4,3,4,3,3 } }, + { "Neutral", TUNING_31EDO_ZETA, { 4,4,4,4,4,4,4,3 } }, + { "Miracle", TUNING_31EDO_ZETA, { 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 } }, @@ -560,13 +854,13 @@ /* This section defines the code needed to determine colors for each hex. - */ + */ /* LED colors are defined in the code - on a perceptual basis. Instead of + on a perceptual basis. Instead of calculating RGB codes, the program uses an artist's color wheel approach. - + For value / brightness, two sets of named constants are defined. The BRIGHT_ series (see the defaults section above) @@ -579,7 +873,7 @@ to get the output brightness. */ #define VALUE_BLACK 0 - #define VALUE_LOW 127 + #define VALUE_LOW 80 #define VALUE_SHADE 164 #define VALUE_NORMAL 180 #define VALUE_FULL 255 @@ -597,7 +891,7 @@ /* Hues are angles from 0 to 360, starting at red and towards yellow->green->blue - when the hue angle increases. + when the hue angle increases. */ #define HUE_NONE 0.0 #define HUE_RED 0.0 @@ -634,7 +928,7 @@ colorDef shade() { colorDef temp; temp.hue = this->hue; - temp.sat = ((this->sat > SAT_DULL) ? SAT_DULL : this->sat); + temp.sat = ((this->sat > SAT_MODERATE) ? SAT_MODERATE : this->sat); temp.val = VALUE_LOW; return temp; } @@ -675,11 +969,13 @@ */ 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}}, + {{{HUE_NONE, SAT_BW, 64} + , {200, 60, VALUE_SHADE } + , {HUE_BLUE, SAT_VIVID, VALUE_SHADE} + , {230, 240, VALUE_NORMAL} + , {HUE_PURPLE, SAT_VIVID, VALUE_NORMAL} + , {270, SAT_VIVID, VALUE_NORMAL} + },{6,1,2,1,2,2,1,4,1,2,1,2}}, // 17 EDO {{{HUE_NONE, SAT_BW, VALUE_NORMAL} , {HUE_INDIGO, SAT_VIVID, VALUE_NORMAL} @@ -700,7 +996,7 @@ // 24 EDO {{{HUE_NONE, SAT_BW, VALUE_NORMAL} // n , {HUE_LIME, SAT_DULL, VALUE_SHADE } // + - , {HUE_CYAN, SAT_VIVID, VALUE_NORMAL} // #/b + , {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}}, @@ -723,6 +1019,16 @@ , {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}}, + // 43 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} // ^ @@ -786,9 +1092,10 @@ preferences / settings through the file system. */ - class presetDef { + + class presetDef { public: - std::string presetName; + 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; @@ -841,7 +1148,7 @@ optional sending of log messages to the Serial port */ - #define DIAGNOSTICS_ON true + #define DIAGNOSTICS_ON true void sendToLog(std::string msg) { if (DIAGNOSTICS_ON) { Serial.println(msg.c_str()); @@ -955,9 +1262,9 @@ whether the button is a command or not, whether the note is in the selected scale, whether the button is flagged to be animated, - and whether the note is currently + and whether the note is currently sounding on MIDI / the synth. - + Needless to say, this is an important class. */ class buttonDef { @@ -973,7 +1280,7 @@ int8_t coordRow = 0; // hex coordinates int8_t coordCol = 0; // hex coordinates uint64_t timePressed = 0; // timecode of last press - uint32_t LEDcodeAnim = 0; // calculate it once and store value, to make LED playback snappier + uint32_t 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 @@ -990,13 +1297,13 @@ }; /* This class is like a virtual wheel. - It takes references / pointers to + It takes references / pointers to the state of three command buttons, translates presses of those buttons into wheel turns, and converts these movements into corresponding values within a range. - + This lets us generalize the behavior of a virtual pitch bend wheel or mod wheel using the same @@ -1024,7 +1331,7 @@ int16_t temp = curValue; if (*topBtn == 1) {temp += *stepValue;} // tap button if (*botBtn == 1) {temp -= *stepValue;} // tap button - if (temp > maxValue) {temp = maxValue;} + if (temp > maxValue) {temp = maxValue;} else if (temp <= minValue) {temp = minValue;} targetValue = temp; } else { @@ -1066,41 +1373,41 @@ } else { return 0; } - } + } }; - const byte mPin[] = { - MPLEX_1_PIN, MPLEX_2_PIN, MPLEX_4_PIN, MPLEX_8_PIN + const byte mPin[] = { + MPLEX_1_PIN, MPLEX_2_PIN, MPLEX_4_PIN, MPLEX_8_PIN }; - const byte cPin[] = { + 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 + 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, + const byte assignCmd[] = { + CMDBTN_0, CMDBTN_1, CMDBTN_2, CMDBTN_3, CMDBTN_4, CMDBTN_5, CMDBTN_6 }; /* - define h, which is a collection of all the - buttons from 0 to 139. h[i] refers to the + define h, which is a collection of all the + buttons from 0 to 139. h[i] refers to the button with the LED address = i. */ buttonDef h[BTN_COUNT]; - + wheelDef modWheel = { &wheelMode, &modSticky, &h[assignCmd[4]].btnState, &h[assignCmd[5]].btnState, &h[assignCmd[6]].btnState, 0, 127, &modWheelSpeed, 0, 0, 0, 0 }; - wheelDef pbWheel = { &wheelMode, &pbSticky, + wheelDef pbWheel = { &wheelMode, &pbSticky, &h[assignCmd[4]].btnState, &h[assignCmd[5]].btnState, &h[assignCmd[6]].btnState, -8192, 8191, &pbWheelSpeed, 0, 0, 0, 0 }; - wheelDef velWheel = { &wheelMode, &velSticky, + wheelDef velWheel = { &wheelMode, &velSticky, &h[assignCmd[0]].btnState, &h[assignCmd[1]].btnState, &h[assignCmd[2]].btnState, 0, 127, &velWheelSpeed, 96, 96, 96, 0 }; - + bool toggleWheel = 0; // 0 for mod, 1 for pb void setupPins() { @@ -1141,8 +1448,7 @@ */ #include <Adafruit_NeoPixel.h> // library of code to interact with the LED array #define LED_PIN 22 - - Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800); + Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800); int32_t rainbowDegreeTime = 65'536; // microseconds to go through 1/360 of rainbow /* This is actually a hacked together approximation @@ -1152,7 +1458,7 @@ the LED class. This conversion is... not very OK... but does the job for now. A proper implementation of OKLAB is in the works. - + For transforming hues, the okLAB hue degree (0-360) is mapped to the RGB hue degree from 0 to 65535, using simple linear interpolation I created by hand comparing @@ -1175,6 +1481,95 @@ return (hueOut[B - 1] * (1 - T)) + (hueOut[B] * T); } } + + namespace incandescence + { + /* + const int fixed_shift = 16; + const int fixed_scale = (1 << fixed_shift); + + constexpr int32_t lambda_r = 700*256; + constexpr int32_t lambda_g = 550*256; + constexpr int32_t lambda_b = 450*256; + + constexpr uint32_t C1 = 374183; // W*m^2 + constexpr uint32_t C2 = 14388; // m*K + + int32_t fixed_exp(int32_t x) + { + return (fixed_scale + x + ((x*x) >> 1) + ((x*x*x)/6)); + } + int32_t planckRadiation(int32_t lambda, int32_t temp) + { + int32_t denom = (C2 / (lambda*temp >> fixed_shift)); + return (C1 / (pow(lambda,5))) / (fixed_exp(denom)); + } + */ + + constexpr float lambda_r = 625e-9; // average wavelengths of LED diodes + constexpr float lambda_g = 525e-9; + constexpr float lambda_b = 460e-9; + + constexpr float C1 = 3.74183e-16; // W*m^2 + constexpr float C2 = 1.4388e-2; // m*K + + float maxTemperature = 2400; + float brightnessCoefficient = 745000000.0f; + + float planckRadiation(float lambda, float temp) + { + return (C1 / (pow(lambda,5))) / (exp(C2/(lambda*temp))-1); + } + + float getCoefficient(float lambda, float maxTemperature) + { + float radiation = planckRadiation(lambda, maxTemperature); + return radiation/256.0f; + } + + float getTemperatureFromV(float value) + { + return value; + } + + colorDef getColor(int32_t temp) + { + float r = planckRadiation(lambda_r,temp); + float g = planckRadiation(lambda_g,temp); + float b = planckRadiation(lambda_b,temp); + + float maxVal = max(max(r,g),b); + + float minVal = min(min(r,g),b); + float delta = maxVal - minVal; + float h = 0, s = 0, v = 0; + + if(delta > 0.00001) + { + s = delta/maxVal; + if(maxVal == r) + { + h=60.0*fmodf(((g-b)/delta),6.0); + v = r / getCoefficient(lambda_r,maxTemperature); + } + else if(maxVal == g) + { + h=60.0*(((g-b)/delta)+2.0); + v = g / getCoefficient(lambda_g,maxTemperature); + } + else + { + h=60.0*(((g-b)/delta)+4.0); + v = b / getCoefficient(lambda_b,maxTemperature); + } + v=min(max(v,0),255); + } + + if(h < 0.0) h += 360.0; + return colorDef{h,(byte)(s*255),(byte)(v)}; + } + } + /* Saturation and Brightness are taken as is (already in a 0-255 range). The global brightness / 255 attenuates the resulting color for the @@ -1186,7 +1581,7 @@ } /* This function cycles through each button, and based on what color - palette is active, it calculates the LED color code in the palette, + palette is active, it calculates the LED color code in the palette, plus its variations for being animated, played, or out-of-scale, and stores it for recall during playback and animation. The color codes remain in the object until this routine is called again. @@ -1204,12 +1599,114 @@ setColor = palette[current.tuningIndex].getColor(paletteIndex); break; case RAINBOW_MODE: // This mode assigns the root note as red, and the rest as saturated spectrum colors across the rainbow. - setColor = + setColor = { 360 * ((float)paletteIndex / (float)current.tuning().cycleLength) , SAT_VIVID , VALUE_NORMAL }; break; + case RAINBOW_OF_FIFTHS_MODE: // This mode assigns the root note as red, and the rest as saturated spectrum colors across the rainbow. + { + float stepSize = current.tuning().stepSize; + float octaveCycleLength = 1200.0/current.tuning().stepSize; // This is to prevent non-octave colouring weirdness + float semipaletteIndex = fmodf(h[i].stepsFromC+(octaveCycleLength*256.0),octaveCycleLength); + float keyDegree = fmodf(semipaletteIndex + (current.tuning().spanCtoA() - current.keyStepsFromA), octaveCycleLength); + float fifthSize = ((ratioToCents(3.0/2.0))/stepSize); + float reverseFifth = fifthSize; + switch (current.tuningIndex) + { + case TUNING_17EDO: { reverseFifth = 12 ;}break; // reverse hash of (10*x)%17=x where 10 steps is a 17EDO fifth + case TUNING_19EDO: { reverseFifth = 7 ;}break; // reverse hash of (11*x)%19=x where 11 steps is a 19EDO fifth + case TUNING_22EDO: { reverseFifth = 17 ;}break; // reverse hash of (13*x)%22=x where 13 steps is a 22EDO fifth + case TUNING_24EDO: { reverseFifth = 11 ;}break; // hand-picked best-fit value. This tuning is very unruly + case TUNING_31EDO: { reverseFifth = 19 ;}break; // reverse hash of (18*x)%31=x where 18 steps is a 31EDO fifth + case TUNING_31EDO_ZETA: { reverseFifth = 19 ;}break; + case TUNING_41EDO: { reverseFifth = 12 ;}break; // reverse hash of (24*x)%41=x where 24 steps is a 41EDO fifth + case TUNING_43EDO: { reverseFifth = 31 ;}break; // reverse hash of (25*x)%43=x where 25 steps is a 43EDO fifth + case TUNING_46EDO: { reverseFifth = 29 ;}break; // reverse hash of (27*x)%46=x where 27 steps is a 46EDO fifth + case TUNING_53EDO: { reverseFifth = 12 ;}break; // reverse hash of (31*x)%53=x where 31 steps is a 53EDO fifth + case TUNING_58EDO: { reverseFifth = 12 ;}break; // reverse hash for 29EDO (2 chains of 29 EDO fifths in 58 EDO) + case TUNING_58EDO_ZETA: { reverseFifth = 12 ;}break; + case TUNING_72EDO: { reverseFifth = 7 ;}break; // reverse hash for 12EDO (6 chains of 12 EDO fifths in 72 EDO) + case TUNING_72EDO_ZETA: { reverseFifth = 7 ;}break; + case TUNING_80EDO: { reverseFifth = 63 ;}break; // reverse hash of (47*x)%80=x where 47 steps is an 80EDO fifth + case TUNING_87EDO: { reverseFifth = 41 ;}break; // A hand-picked value, seems to work. 46 also works + case TUNING_BP: { reverseFifth = 5 ;}break; // A hand-picked value; 23 and 64 also work + case TUNING_ALPHA: { reverseFifth = 5 ;}break; // A hand-picked value + case TUNING_BETA: { reverseFifth = 7 ;}break; // reverse hash of (11*x)%19=x where 11 steps is a 19EDO equivalent fifth + case TUNING_GAMMA: { reverseFifth = 12 ;}break; // reverse hash for 17EDO(2 chains of 17 EDO fifths in 34 EDO equivalent) + default: { reverseFifth = fifthSize;} // either the tuning has no fifths or scrambling colors using fifths works + } + + float paletteIndexOfFifths = fmodf((keyDegree*reverseFifth),octaveCycleLength); + setColor = + { 360.0f * (paletteIndexOfFifths/(1200.0f/stepSize)) + , SAT_VIVID + , VALUE_NORMAL + }; + } + break; + case PIANO_ALT_COLOR_MODE: + { + float octaveCycleLength = 1200.0/current.tuning().stepSize; // This is to prevent non-octave colouring weirdness + float semipaletteIndex = fmodf(h[i].stepsFromC+(octaveCycleLength*256.0),octaveCycleLength); + float keyDegree = (12.0f/octaveCycleLength)*semipaletteIndex; + if((int)round(keyDegree)%12 == 1 || (int)round(keyDegree)%12 == 3 || (int)round(keyDegree)%12 == 6 || (int)round(keyDegree)%12 == 8 || (int)round(keyDegree)%12 == 10) + { + float deviationFromDiatonic = (float)((int)round(keyDegree) - keyDegree)*180.0; // range from 180 to 360 + // +360 for proper fmodf; 180 is the opposite tint of 0; 30 is midway between yellow and red; + setColor = {fmodf(360.0+180.0+30.0+deviationFromDiatonic,360.0f),SAT_VIVID,VALUE_NORMAL}; + } + else // White key + { + float deviationFromDiatonic = (((float)((int)round(keyDegree))) - (keyDegree))*180.0; // from -60 to 120 + setColor = {fmodf(360.0+0.0+30.0+deviationFromDiatonic,360.0f),SAT_VIVID,VALUE_NORMAL}; + } + } + break; + case PIANO_COLOR_MODE: + { + float octaveCycleLength = 1200.0/current.tuning().stepSize; // This is to prevent non-octave colouring weirdness + float semipaletteIndex = fmodf(h[i].stepsFromC+(octaveCycleLength*256.0),octaveCycleLength); + float keyDegree = (12.0f/octaveCycleLength)*semipaletteIndex; + if((int)round(keyDegree)%12 == 1 || (int)round(keyDegree)%12 == 3 || (int)round(keyDegree)%12 == 6 || (int)round(keyDegree)%12 == 8 || (int)round(keyDegree)%12 == 10) + { + float deviationFromDiatonic = ((float)((int)round(keyDegree) - keyDegree) * 3072.0f)/12.0; + uint8_t tint = (uint8_t)(abs(round(deviationFromDiatonic))); + tint = strip.gamma8(tint); + setColor = {360 * (fmodf(round(keyDegree),12.0f) / 12.0f),SAT_TINT,VALUE_BLACK}; + } + else // White key + { + float deviationFromDiatonic = ((((float)((int)round(keyDegree))) - (keyDegree)) * 3072.0f)/12.0; + uint8_t tint = 255 - (uint8_t)(abs(round(deviationFromDiatonic))); + tint = strip.gamma8(tint); + setColor = {360 * (fmodf(round(keyDegree),12.0f) / 12.0f),SAT_TINT,VALUE_NORMAL}; + } + } + break; + case PIANO_INCANDESCENT_COLOR_MODE: + { + float octaveCycleLength = 1200.0/current.tuning().stepSize; // This is to prevent non-octave colouring weirdness + float semipaletteIndex = fmodf(h[i].stepsFromC+(octaveCycleLength*256.0),octaveCycleLength); + float keyDegree = (12.0f/octaveCycleLength)*semipaletteIndex; + float tint, deviationFromDiatonic; + if((int)round(keyDegree)%12 == 1 || (int)round(keyDegree)%12 == 3 || (int)round(keyDegree)%12 == 6 || (int)round(keyDegree)%12 == 8 || (int)round(keyDegree)%12 == 10) + { + deviationFromDiatonic = (round(keyDegree) - keyDegree); + deviationFromDiatonic = (abs(deviationFromDiatonic)); // from 0 to 0.5 + } + else // White key + { + deviationFromDiatonic = (round(keyDegree) - keyDegree); + deviationFromDiatonic = 1.0-abs(deviationFromDiatonic); // from 1 to 0.5 + } + auto baseTemperature = 800; + tint = ((sqrt(deviationFromDiatonic))) * (incandescence::maxTemperature-baseTemperature) + baseTemperature; + + setColor = incandescence::getColor(tint); + } + break; case ALTERNATE_COLOR_MODE: // This mode assigns each note a color based on the interval it forms with the root note. // This is an adaptation of an algorithm developed by Nicholas Fox and Kite Giedraitis. @@ -1231,15 +1728,15 @@ float offCenter = cents - center; int16_t altHue = positiveMod((int)(150 + (perf * ((offCenter > 0) ? -72 : 72)) - round(1.44 * offCenter)), 360); float deSaturate = perf * (abs(offCenter) < 20) * (1 - (0.02 * abs(offCenter))); - setColor = { - (float)altHue, - (byte)(255 - round(255 * deSaturate)), + setColor = { + (float)altHue, + (byte)(255 - round(255 * deSaturate)), (byte)(cents ? VALUE_SHADE : VALUE_NORMAL) }; break; } h[i].LEDcodeRest = getLEDcode(setColor); - h[i].LEDcodePlay = getLEDcode(setColor.tint()); - h[i].LEDcodeDim = getLEDcode(setColor.shade()); + 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; @@ -1249,7 +1746,7 @@ } void resetVelocityLEDs() { - colorDef tempColor = + colorDef tempColor = { (runTime % (rainbowDegreeTime * 360)) / (float)rainbowDegreeTime , SAT_MODERATE , byteLerp(0,255,85,127,velWheel.curValue) @@ -1258,7 +1755,7 @@ 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)); } @@ -1282,8 +1779,8 @@ // mod blue / yellow tempSat = byteLerp(SAT_BW,SAT_VIVID,0,64,abs(modWheel.curValue - 63)); tempColor = { - (float)((modWheel.curValue > 63) ? HUE_YELLOW : HUE_INDIGO), - tempSat, + (float)((modWheel.curValue > 63) ? HUE_YELLOW : HUE_INDIGO), + tempSat, (byte)(127 + (tempSat / 2)) }; strip.setPixelColor(assignCmd[6], getLEDcode(tempColor)); @@ -1292,7 +1789,7 @@ tempColor.val = 127 - (tempSat / 2); } strip.setPixelColor(assignCmd[5], getLEDcode(tempColor)); - + tempColor.val = tempSat * (modWheel.curValue > 63); strip.setPixelColor(assignCmd[4], getLEDcode(tempColor)); } @@ -1305,14 +1802,14 @@ } else { return h[x].LEDcodeDim; } } - void setupLEDs() { + void setupLEDs() { strip.begin(); // INITIALIZE NeoPixel strip object strip.show(); // Turn OFF all pixels ASAP - sendToLog("LEDs started..."); + sendToLog("LEDs started..."); setLEDcolorCodes(); } - void lightUpLEDs() { - for (byte i = 0; i < LED_COUNT; i++) { + void lightUpLEDs() { + for (byte i = 0; i < LED_COUNT; i++) { if (!(h[i].isCmd)) { strip.setPixelColor(i,applyNotePixelColor(i)); } @@ -1333,17 +1830,22 @@ These values support correct MIDI output. Note frequencies are converted to MIDI note and pitch bend messages assuming note 69 - equals concert A4, as defined below. + equals concert A4, as defined below. */ #define CONCERT_A_HZ 440.0 /* - Pitch bend messages are calibrated + Pitch bend messages are calibrated to a pitch bend range where - -8192 to 8191 = -200 to +200 cents, + -8192 to 8191 = -200 to +200 cents, or two semitones. */ #define PITCH_BEND_SEMIS 2 /* + We use pitch bends to retune notes in MPE mode. + Some setups can adjust to fit this, but some need us to adjust it. + */ + byte MPEpitchBendSemis = 48; + /* Create a new instance of the Arduino MIDI Library, and attach usb_midi as the transport. */ @@ -1361,7 +1863,7 @@ byte programChange = 0; std::queue<byte> MPEchQueue; - byte MPEpitchBendsNeeded; + byte MPEpitchBendsNeeded; float freqToMIDI(float Hz) { // formula to convert from Hz to MIDI note return 69.0 + 12.0 * log2f(Hz / 440.0); @@ -1386,7 +1888,7 @@ } sendToLog( "set pitch bend range on ch " + - std::to_string(Ch) + " to be " + + std::to_string(Ch) + " to be " + std::to_string(semitones) + " semitones" ); } @@ -1414,19 +1916,19 @@ MIDI works is via MPE (MIDI polyphonic expression). This assigns re-tuned notes to an independent channel so they can be pitched separately. - + if operating in a standard 12-EDO tuning, or in a tuning with steps that are all exact multiples of 100 cents, then MPE is not necessary. */ - if (current.tuning().stepSize == 100.0) { - MPEpitchBendsNeeded = 1; + if (current.tuning().stepSize == 100.0 && !useDynamicJustIntonation && !useJustIntonationBPM && !forceEnableMPE) { + MPEpitchBendsNeeded = 1; // Standard 12EDO, single-channel mode /* this was an attempt to allow unlimited polyphony for certain EDOs. doesn't work in Logic Pro. } else if (round(current.tuning().cycleLength * current.tuning().stepSize) == 1200) { MPEpitchBendsNeeded = current.tuning().cycleLength / std::gcd(12, current.tuning().cycleLength); */ } else { - MPEpitchBendsNeeded = 255; + MPEpitchBendsNeeded = 255; // Enables MPE mode when in Just Intonation or microtonal tuning } if (MPEpitchBendsNeeded > 15) { setMPEzone(1, 15); // MPE zone 1 = ch 2 thru 16 @@ -1444,7 +1946,7 @@ for (byte i = 1; i <= 16; i++) { if(midiD&MIDID_USB)UMIDI.sendControlChange(123, 0, i); if(midiD&MIDID_SER)SMIDI.sendControlChange(123, 0, i); - setPitchBendRange(i, PITCH_BEND_SEMIS); + setPitchBendRange(i, MPEpitchBendSemis); } } @@ -1459,39 +1961,629 @@ if(midiD&MIDID_SER)SMIDI.sendPitchBend(pbWheel.curValue, 1); sendToLog("sent pb wheel value " + std::to_string(pbWheel.curValue) + " to ch 1"); } + +////////////////////////////////////////////////////////////////// +// Dynamic just intonation code start + + + // HOW BPM SYNC WORKS: + // The idea is to round off the note frequency to a certain precision. + // If you round the note frequencies of a C-E-G chord to integer values (261.626Hz / 329.628Hz / 391.995Hz) - + // you'll get a chord with ratio of 262/294/392. + // As a result, because these frequency values are always a multiple of 1 Hz - + // they all will be guaranteed to finish their wave cycle in 1 second. + // Thus, this chord will beat at 1 Hz if not faster. + + // By knowing the pressed keys it is possible to pick better ratios, ideally having 262/327.5/393 (4/5/6) in this example + + // TODO: make BPM sync work with dynamic just intonation to make pure just intonation achieveable. + // Without it - this implementation provides you with n-EDO-sized independent JI rings, unconnected to eachother; + // TODO: replace floating point math with integer math; + // TODO: replace std::pair<byte,byte> ratios with precomputed floating(or fixed) point ratios; + // TODO: generate the table of ratios with a constexpr function rather than holding a huge block of hardcoded values in the code; + // TODO: It is a good idea to octave-reduce the ratios, and adjust the code to calculate pitchbend against the octave reduced set of ratios for significant performance improvement; + // TODO: implement dynamic just intonation for buzzer + // TODO: There is an issue with tuning error in this line: + // UMIDI.sendPitchBend(h[x].bend + justIntonationRetune(x), h[x].MIDIch); + // The retuning is done twice, and rounding errors will add up. This can be heard if you play a unison with different pitches that collapse into 1/1 by dynamic just intonation (or if you use BPM sync with a really high frequency). You will sometimes hear extremely slow beating despite both notes being forced into the same pitch + + + // This is a list of ratios sorted from the simplest ones to the most complex ones. The code searches for a first match that's good enough within 1/4 of an EDO step, literally bruteforcing through the list. As a result - the simplest ratio is chosen before more comples ones, prioritising consonant ratios first. In case not a single good ratio is found - the best one found so far is chosen instead - void tryMIDInoteOn(byte x) { - // this gets called on any non-command hex - // that is not scale-locked. - if (!(h[x].MIDIch)) { - if (MPEpitchBendsNeeded == 1) { - h[x].MIDIch = 1; - } else if (MPEpitchBendsNeeded <= 15) { - h[x].MIDIch = 2 + positiveMod(h[x].stepsFromC, MPEpitchBendsNeeded); - } else { - if (MPEchQueue.empty()) { // if there aren't any open channels - sendToLog("MPE queue was empty so did not play a midi note"); - } else { - h[x].MIDIch = MPEchQueue.front(); // value in MIDI terms (1-16) - MPEchQueue.pop(); - sendToLog("popped " + std::to_string(h[x].MIDIch) + " off the MPE queue"); + // byte pair was chosen to preserve space. The ratio is "unpacked" later + std::vector<std::pair<byte,byte>> ratios = + { + {1,1}, + {1,2}, + {2,1}, + {3,1}, + {1,3}, + {1,4}, + {2,3}, + {1,4}, + {4,1}, + {3,2}, + {1,5}, + {5,1}, + {5,2}, + {1,6}, + {3,4}, + {5,2}, + {4,3}, + {6,1}, + {2,5}, + {5,3}, + {1,7}, + {7,1}, + {3,5}, + {2,7}, + {8,1}, + {5,4}, + {1,8}, + {4,5}, + {7,2}, + {9,1}, + {7,3}, + {1,9}, + {3,7}, + {1,9}, + {1,10}, + {10,1}, + {7,4}, + {3,8}, + {8,3}, + {6,5}, + {1,10}, + {8,3}, + {4,7}, + {2,9}, + {9,2}, + {5,6}, + {11,1}, + {7,5}, + {1,11}, + {5,7}, + {5,8}, + {3,10}, + {4,9}, + {3,10}, + {2,11}, + {11,2}, + {12,1}, + {1,12}, + {9,4}, + {5,8}, + {1,12}, + {8,5}, + {10,3}, + {6,7}, + {7,6}, + {12,1}, + {9,5}, + {1,13}, + {3,11}, + {11,3}, + {9,5}, + {5,9}, + {13,1}, + {14,1}, + {13,2}, + {11,4}, + {1,14}, + {2,13}, + {8,7}, + {7,8}, + {4,11}, + {9,7}, + {11,5}, + {7,9}, + {5,11}, + {13,3}, + {3,13}, + {15,1}, + {1,15}, + {4,13}, + {2,15}, + {10,7}, + {2,15}, + {11,6}, + {8,9}, + {16,1}, + {12,5}, + {3,14}, + {7,10}, + {5,12}, + {14,3}, + {9,8}, + {15,2}, + {13,4}, + {1,16}, + {6,11}, + {17,1}, + {1,17}, + {5,13}, + {13,5}, + {4,15}, + {17,2}, + {9,10}, + {2,17}, + {9,10}, + {12,7}, + {10,9}, + {11,8}, + {16,3}, + {3,16}, + {13,6}, + {14,5}, + {15,4}, + {18,1}, + {8,11}, + {1,18}, + {4,15}, + {5,14}, + {6,13}, + {7,12}, + {19,1}, + {11,9}, + {17,3}, + {3,17}, + {9,11}, + {1,19}, + {5,16}, + {20,1}, + {8,13}, + {10,11}, + {20,1}, + {19,2}, + {1,20}, + {11,10}, + {2,19}, + {13,8}, + {17,4}, + {4,17}, + {16,5}, + {1,20}, + {13,9}, + {21,1}, + {7,15}, + {9,13}, + {19,3}, + {17,5}, + {3,19}, + {5,17}, + {15,7}, + {1,21}, + {13,10}, + {3,20}, + {12,11}, + {21,2}, + {18,5}, + {6,17}, + {15,8}, + {3,20}, + {20,3}, + {19,4}, + {5,18}, + {14,9}, + {9,14}, + {8,15}, + {2,21}, + {1,22}, + {17,6}, + {22,1}, + {10,13}, + {11,12}, + {4,19}, + {5,19}, + {1,23}, + {19,5}, + {23,1}, + {18,7}, + {8,17}, + {21,4}, + {22,3}, + {3,22}, + {7,18}, + {6,19}, + {12,13}, + {19,6}, + {2,23}, + {9,16}, + {17,8}, + {24,1}, + {13,12}, + {1,24}, + {23,2}, + {4,21}, + {16,9}, + {9,17}, + {1,25}, + {5,21}, + {25,1}, + {15,11}, + {17,9}, + {3,23}, + {23,3}, + {11,15}, + {21,5}, + {17,10}, + {10,17}, + {19,8}, + {5,22}, + {20,7}, + {22,5}, + {23,4}, + {7,20}, + {1,26}, + {8,19}, + {25,2}, + {26,1}, + {2,25}, + {4,23}, + {5,23}, + {9,19}, + {1,27}, + {13,15}, + {3,25}, + {15,13}, + {23,5}, + {19,9}, + {27,1}, + {25,3}, + {25,4}, + {14,15}, + {27,2}, + {9,20}, + {2,27}, + {26,3}, + {20,9}, + {17,12}, + {1,28}, + {24,5}, + {10,19}, + {12,17}, + {23,6}, + {21,8}, + {11,18}, + {19,10}, + {5,24}, + {4,25}, + {5,24}, + {3,26}, + {18,11}, + {28,1}, + {8,21}, + {6,23}, + {15,14}, + {1,29}, + {29,1}, + {23,8}, + {24,7}, + {7,24}, + {6,25}, + {10,21}, + {30,1}, + {5,26}, + {25,6}, + {11,20}, + {30,1}, + {1,30}, + {16,15}, + {8,23}, + {4,27}, + {2,29}, + {26,5}, + {9,22}, + {29,2}, + {27,4}, + {28,3}, + {15,16}, + {20,11}, + {18,13}, + {22,9}, + {21,10}, + {13,18}, + {12,19}, + {19,12}, + {3,28}, + {17,15}, + {15,17}, + {29,3}, + {31,1}, + {27,5}, + {3,29}, + {9,23}, + {23,9}, + {1,31}, + {5,27}, + {13,20}, + {2,31}, + {28,5}, + {1,32}, + {29,4}, + {25,8}, + {20,13}, + {4,29}, + {8,25}, + {23,10}, + {10,23}, + {5,28}, + {31,2}, + {32,1}, + {33,1}, + {3,31}, + {1,33}, + {5,29}, + {15,19}, + {9,25}, + {31,3}, + {19,15}, + {25,9}, + {29,5}, + {33,2}, + {6,29}, + {17,18}, + {34,1}, + {2,33}, + {32,3}, + {26,9}, + {31,4}, + {27,8}, + {1,34}, + {4,31}, + {18,17}, + {29,6}, + {8,27}, + {12,23}, + {11,24}, + {3,32}, + {9,26}, + {23,12}, + {24,11}, + {5,31}, + {31,5}, + {35,1}, + {1,35}, + {1,36}, + {30,7}, + {24,13}, + {18,19}, + {36,1}, + {6,31}, + {28,9}, + {34,3}, + {36,1}, + {15,22}, + {7,30}, + {8,29}, + {1,36}, + {17,20}, + {29,8}, + {4,33}, + {12,25}, + {10,27}, + {32,5}, + {20,17}, + {3,34}, + {25,12}, + {5,32}, + {2,35}, + {33,4}, + {22,15}, + {9,28}, + {13,24}, + {27,10}, + {35,2}, + {19,18}, + {31,6}, + {9,29}, + {35,3}, + {29,9}, + {5,33}, + {23,15}, + {33,5}, + {15,23}, + {37,1}, + {3,35}, + {1,37}, + {10,29}, + {31,8}, + {5,34}, + {4,35}, + {1,38}, + {35,4}, + {29,10}, + {34,5}, + {37,2}, + {2,37}, + {19,20}, + {8,31}, + {20,19}, + {38,1}, + {31,9}, + {39,1}, + {37,3}, + {3,37}, + {9,31}, + {1,39}, + {38,3}, + {11,30}, + {9,32}, + {26,15}, + {31,10}, + {29,12}, + {32,9}, + {20,21}, + {2,39}, + {35,6}, + {33,8}, + {5,36}, + {37,4}, + {21,20}, + {15,26}, + {40,1}, + {8,33}, + {10,31}, + {30,11}, + {12,29}, + {23,18}, + {17,24}, + {36,5}, + {40,1}, + {1,40}, + {4,37}, + {24,17}, + {39,2}, + {6,35}, + {18,23}, + {3,38}, + {41,1}, + {37,5}, + {5,37}, + {1,41}, + {33,10}, + {3,40}, + {4,39}, + {1,42}, + {37,6}, + {13,30}, + {12,31}, + {42,1}, + {10,33}, + {7,36}, + {36,7}, + {9,34}, + {41,2}, + {35,8}, + {40,3}, + {8,35}, + {5,38}, + {2,41}, + {39,4}, + {38,5} + }; + + int16_t centsToRelativePitchBend(float cents){ + return round(cents * (8192.0/(100.0*MPEpitchBendSemis))); + } + + float ratioToCents(float ratio){ + return 1200.0 * (std::log(ratio) / std::log(2.0)); + } + + int16_t justIntonationRetune(byte x) + { + if(useDynamicJustIntonation == false && useJustIntonationBPM == false) + { + return 0; + } + int16_t pitchAdjustment = 0; + float pitchAdjustmentCents = 0; + float basePitchOffset = 0; + //int16_t degree = (current.keyDegree(h[x].stepsFromC + current.transpose + current.tuning().spanCtoA())); + //float buttonStepsFromA = degree; + if(useJustIntonationBPM) + { + float buttonStepsFromA = -current.tuning().spanCtoA() - h[x].stepsFromC; + // It was planned to use integer math but floating point arithmetics works fast enough so far + float rounding = ((float)justIntonationBPM / 60.0 * justIntonationBPM_Multiplier); + pitchAdjustmentCents = (buttonStepsFromA * current.tuning().stepSize) - + ratioToCents(round(440.0 / rounding) / round(h[x].frequency / rounding)); + + if(pressedKeyIDs.size() > 1 && useDynamicJustIntonation) + { + basePitchOffset = ((-current.tuning().spanCtoA() - h[pressedKeyIDs[0]].stepsFromC) * current.tuning().stepSize) - + ratioToCents(round(440.0 / rounding) / round(h[pressedKeyIDs[0]].frequency / rounding)); + } + else + { + pitchAdjustment += centsToRelativePitchBend(pitchAdjustmentCents); + } + } + if(useDynamicJustIntonation && pressedKeyIDs.size() > 1) + { + //bool ratioFound = false; // I might need this one later + bool preferSmallRatios = true; // if false - the closest found ratio will be chosen from the ratio table + + // detune within a 1/4 of a step, avoid wild detuning but cover the entire pitch range + float errorThreshold = current.tuning().stepSize / 4.0; + float deviation = INFINITY; + float EDOCents = ratioToCents(h[pressedKeyIDs[0]].frequency / h[x].frequency); + std::pair<byte,byte> selectedRatio; + + for(int i = 0; i < ratios.size();i++) + { + auto ratio = ratios[i]; + float ratio0 = ratio.first; + float ratio1 = ratio.second; + //if(h[pressedKeyIDs[0]].note < h[x].note) + //{ + // std::swap(ratio1,ratio0); + //} + float ratioCents = ratioToCents(ratio0/ratio1); + + if(std::abs(deviation) > std::abs(ratioCents - EDOCents)) + { + deviation = (EDOCents - ratioCents); + selectedRatio.first = ratio0; + selectedRatio.second = ratio1; + if(preferSmallRatios && std::abs(deviation) < errorThreshold) + { + //ratioFound = true; + break; + } } } - if (h[x].MIDIch) { - if(midiD&MIDID_USB)UMIDI.sendNoteOn(h[x].note, velWheel.curValue, h[x].MIDIch); // ch 1-16 - if(midiD&MIDID_SER)SMIDI.sendNoteOn(h[x].note, velWheel.curValue, h[x].MIDIch); // ch 1-16 + //if(ratioFound) + { + pitchAdjustment += centsToRelativePitchBend(deviation + basePitchOffset); + } + } + return pitchAdjustment; + } - if(midiD&MIDID_USB)UMIDI.sendPitchBend(h[x].bend, h[x].MIDIch); // ch 1-16 - if(midiD&MIDID_SER)SMIDI.sendPitchBend(h[x].bend, h[x].MIDIch); // ch 1-16 - sendToLog( - "sent MIDI noteOn: " + std::to_string(h[x].note) + - " pb " + std::to_string(h[x].bend) + - " vel " + std::to_string(velWheel.curValue) + - " ch " + std::to_string(h[x].MIDIch) - ); - } + +void tryMIDInoteOn(byte x) { + // This gets called on any non-command hex that is not scale-locked. + if (!(h[x].MIDIch)) { + if (MPEpitchBendsNeeded == 1) { + h[x].MIDIch = defaultMidiChannel; + } else if (MPEpitchBendsNeeded <= 15) { + h[x].MIDIch = 2 + positiveMod(h[x].stepsFromC, MPEpitchBendsNeeded); + } else { + if (MPEchQueue.empty()) { // If there aren't any open channels + sendToLog("MPE queue was empty so did not play a MIDI note"); + } else { + h[x].MIDIch = MPEchQueue.front(); // Value in MIDI terms (1-16) + MPEchQueue.pop(); + sendToLog("Popped " + std::to_string(h[x].MIDIch) + " off the MPE queue"); + } + } + + if (h[x].MIDIch) { + pressedKeyIDs.push_back(x); // Dynamic JI pressed key tracking + // First, send the pitch bend (if applicable) + if (MPEpitchBendsNeeded != 1) { + if (midiD & MIDID_USB) UMIDI.sendPitchBend(h[x].bend + justIntonationRetune(x), h[x].MIDIch); // ch 1-16 + if (midiD & MIDID_SER) SMIDI.sendPitchBend(h[x].bend + justIntonationRetune(x), h[x].MIDIch); // ch 1-16 + } + + // Then, send the note-on message + if (midiD & MIDID_USB) UMIDI.sendNoteOn(h[x].note, velWheel.curValue, h[x].MIDIch); // ch 1-16 + if (midiD & MIDID_SER) SMIDI.sendNoteOn(h[x].note, velWheel.curValue, h[x].MIDIch); // ch 1-16 + + sendToLog( + "Sent MIDI pitch bend: " + std::to_string((MPEpitchBendsNeeded != 1) ? h[x].bend + justIntonationRetune(x) : 0) + + " to ch " + std::to_string(h[x].MIDIch) + ); + sendToLog( + "Sent MIDI noteOn: " + std::to_string(h[x].note) + + " vel " + std::to_string(velWheel.curValue) + + " ch " + std::to_string(h[x].MIDIch) + ); + } } - } +} void tryMIDInoteOff(byte x) { // this gets called on any non-command hex @@ -1499,13 +2591,14 @@ if (h[x].MIDIch) { // but just in case, check if(midiD&MIDID_USB)UMIDI.sendNoteOff(h[x].note, velWheel.curValue, h[x].MIDIch); if(midiD&MIDID_SER)SMIDI.sendNoteOff(h[x].note, velWheel.curValue, h[x].MIDIch); + pressedKeyIDs.pop_back(); // Dynamic JI pressed key tracking sendToLog( "sent note off: " + std::to_string(h[x].note) + " pb " + std::to_string(h[x].bend) + " vel " + std::to_string(velWheel.curValue) + " ch " + std::to_string(h[x].MIDIch) ); - if (MPEpitchBendsNeeded > 15) { + if (MPEpitchBendsNeeded > 15 && h[x].MIDIch > 1) { MPEchQueue.push(h[x].MIDIch); sendToLog("pushed " + std::to_string(h[x].MIDIch) + " on the MPE queue"); } @@ -1556,58 +2649,58 @@ no wave movement. */ byte sine[] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 3, - 4, 5, 6, 7, 8, 9, 10, 12, 13, 15, 16, 18, 19, 21, 23, 25, - 27, 29, 31, 33, 35, 37, 39, 42, 44, 46, 49, 51, 54, 56, 59, 62, - 64, 67, 70, 73, 76, 79, 81, 84, 87, 90, 93, 96, 99, 103, 106, 109, - 112, 115, 118, 121, 124, 127, 131, 134, 137, 140, 143, 146, 149, 152, 156, 159, - 162, 165, 168, 171, 174, 176, 179, 182, 185, 188, 191, 193, 196, 199, 201, 204, - 206, 209, 211, 213, 216, 218, 220, 222, 224, 226, 228, 230, 232, 234, 236, 237, - 239, 240, 242, 243, 245, 246, 247, 248, 249, 250, 251, 252, 252, 253, 254, 254, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 254, 253, 252, 252, - 251, 250, 249, 248, 247, 246, 245, 243, 242, 240, 239, 237, 236, 234, 232, 230, - 228, 226, 224, 222, 220, 218, 216, 213, 211, 209, 206, 204, 201, 199, 196, 193, - 191, 188, 185, 182, 179, 176, 174, 171, 168, 165, 162, 159, 156, 152, 149, 146, - 143, 140, 137, 134, 131, 127, 124, 121, 118, 115, 112, 109, 106, 103, 99, 96, - 93, 90, 87, 84, 81, 79, 76, 73, 70, 67, 64, 62, 59, 56, 54, 51, - 49, 46, 44, 42, 39, 37, 35, 33, 31, 29, 27, 25, 23, 21, 19, 18, + 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, + 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, + 0, 0, 2, 7, 14, 21, 30, 38, 47, 54, 61, 66, 70, 72, 73, 74, + 73, 73, 72, 71, 70, 71, 72, 74, 76, 80, 84, 88, 93, 97, 101, 105, + 109, 111, 113, 114, 114, 114, 113, 112, 111, 110, 109, 109, 109, 110, 112, 114, + 116, 118, 121, 123, 126, 127, 128, 129, 128, 127, 126, 123, 121, 118, 116, 114, + 112, 110, 109, 109, 109, 110, 111, 112, 113, 114, 114, 114, 113, 111, 109, 105, + 101, 97, 93, 88, 84, 80, 76, 74, 72, 71, 70, 71, 72, 73, 73, 74, + 73, 72, 70, 66, 61, 54, 47, 38, 30, 21, 14, 7, 2, 0, 0, 2, + 9, 18, 31, 46, 64, 84, 105, 127, 150, 171, 191, 209, 224, 237, 246, 252, + 255, 255, 253, 248, 241, 234, 225, 217, 208, 201, 194, 189, 185, 183, 182, 181, + 182, 182, 183, 184, 185, 184, 183, 181, 179, 175, 171, 167, 162, 158, 154, 150, + 146, 144, 142, 141, 141, 141, 142, 143, 144, 145, 146, 146, 146, 145, 143, 141, + 139, 136, 134, 132, 129, 128, 127, 126, 127, 128, 129, 132, 134, 136, 139, 141, + 143, 145, 146, 146, 146, 145, 144, 143, 142, 141, 141, 141, 142, 144, 146, 150, + 154, 158, 162, 167, 171, 175, 179, 181, 183, 184, 185, 184, 183, 182, 182, 181, + 182, 183, 185, 189, 194, 201, 208, 217, 225, 234, 241, 248, 253, 255, 255, 252, + 246, 237, 224, 209, 191, 171, 150, 127, 105, 84, 64, 46, 31, 18, 9, 2, }; /* The hybrid synth sound blends between @@ -1631,18 +2724,18 @@ poll is too short then the code will not have enough time to calculate the new sample and the resulting audio becomes unstable and - inaccurate. + inaccurate. */ #define POLL_INTERVAL_IN_MICROSECONDS 24 /* - Eight voice polyphony can be simulated. + Eight voice polyphony can be simulated. Any more voices and the resolution is too low to distinguish; also, the code becomes too slow to keep up with the poll interval. This value can be safely reduced below eight if there are issues. - + Note this is NOT the same as the MIDI polyphony limit, which is 15 (based on using channel 2 through 16 for @@ -1663,7 +2756,7 @@ By default it's off but you can change this flag to "true" to enable it. This may also - be moved to a Testing menu option. + be moved to a Advanced menu option. */ #define EQUAL_LOUDNESS_ADJUST true /* @@ -1671,10 +2764,10 @@ It stores an oscillation frequency in the form of an increment value, which is how much a counter would have to be increased - every time the poll() interval is reached, + every time the poll() interval is reached, such that a counter overflows from 0 to 65,535 back to zero at some frequency per second. - + The value of the counter is useful for reading a waveform sample, so that an analog signal can be emulated by reading the sample at each @@ -1711,7 +2804,7 @@ 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 + synth[i].counter += synth[i].increment; // should loop from 65536 -> 0 p = synth[i].counter; t = p >> 8; switch (currWave) { @@ -1738,7 +2831,7 @@ } } mix *= attenuation[(playbackMode == SYNTH_POLY) * voices]; // [19bit]*atten[6bit] = [25bit] - mix *= velWheel.curValue; // [25bit]*vel[7bit]=[32bit], poly+ + mix *= velWheel.curValue; // [25bit]*vel[7bit]=[32bit], poly+ level = mix >> 24; // [32bit] - [8bit] = [24bit] if(audioD&AUDIO_PIEZO)pwm_set_chan_level(PIEZO_SLICE, PIEZO_CHNL, level); if(audioD&AUDIO_AJACK)pwm_set_chan_level(AJACK_SLICE, AJACK_CHNL, level); @@ -1750,7 +2843,7 @@ equal loudness curves Hz dB Amp ~ sqrt(10^(dB/10)) 200 0 8 - 800 -3 6 + 800 -3 6 1500 0 8 3250 -6 4 5000 0 8 @@ -1853,7 +2946,7 @@ if(midiD&MIDID_USB)UMIDI.sendProgramChange(programChange - 1, 1); if(midiD&MIDID_SER)SMIDI.sendProgramChange(programChange - 1, 1); } - + void updateSynthWithNewFreqs() { if(midiD&MIDID_USB)UMIDI.sendPitchBend(pbWheel.curValue, 1); if(midiD&MIDID_SER)SMIDI.sendPitchBend(pbWheel.curValue, 1); @@ -1865,7 +2958,7 @@ } } } - + void trySynthNoteOn(byte x) { if (playbackMode != SYNTH_OFF) { if (playbackMode == SYNTH_POLY) { @@ -1878,7 +2971,7 @@ sendToLog("popped " + std::to_string(h[x].synthCh) + " off the synth queue"); setSynthFreq(h[x].frequency, h[x].synthCh); } - } else { + } else { // operate in lockstep with MIDI if (h[x].MIDIch) { replaceMonoSynthWith(x); @@ -1950,7 +3043,7 @@ int8_t vertical[] = { 0,-1,-1, 0, 1, 1}; int8_t horizontal[] = { 2, 1,-1,-2,-1, 1}; - uint64_t animFrame(byte x) { + 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 { @@ -1958,7 +3051,7 @@ } } void flagToAnimate(int8_t r, int8_t c) { - if (! + if (! ( ( r < 0 ) || ( r >= ROWCOUNT ) || ( c < 0 ) || ( c >= (2 * COLCOUNT) ) || ( ( c + r ) & 1 ) @@ -1969,7 +3062,7 @@ } 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 + 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 @@ -1980,11 +3073,11 @@ 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 @@ -1993,6 +3086,91 @@ } } } +*/ + void animateOrbit() { //BETTER ORBIT + const byte ORBIT_RADIUS = 2; // Radius of the orbit + const byte SLOW_FACTOR = 1; // Slowdown factor for animation + + for (byte i = 0; i < LED_COUNT; i++) { // Check every hex + if ((!(h[i].isCmd)) && (h[i].MIDIch) && // That is a held note + ((h[i].inScale) || (!scaleLock))) { // And is in scale or scale is unlocked + + byte frame = animFrame(i) / SLOW_FACTOR; // Slow down the animation + byte currentStep = frame % 12; // Determine position in the 12-light orbit + + // Determine row and column adjustments for the 12 possible directions + int8_t rowOffsets[12]; + int8_t colOffsets[12]; + + // Fill offsets for the 6 primary directions + for (byte dir = 0; dir < 6; dir++) { + rowOffsets[dir * 2] = ORBIT_RADIUS * vertical[dir]; + colOffsets[dir * 2] = ORBIT_RADIUS * horizontal[dir]; + + // Fill the intermediate (diagonal) positions + rowOffsets[dir * 2 + 1] = ORBIT_RADIUS * (vertical[dir] + vertical[(dir + 1) % 6]) / 2; + colOffsets[dir * 2 + 1] = ORBIT_RADIUS * (horizontal[dir] + horizontal[(dir + 1) % 6]) / 2; + } + + // Calculate light positions + int8_t light1Row = h[i].coordRow + rowOffsets[currentStep]; + int8_t light1Col = h[i].coordCol + colOffsets[currentStep]; + + byte oppositeStep = (currentStep + 6) % 12; // Opposite position in the 12-light ring + int8_t light2Row = h[i].coordRow + rowOffsets[oppositeStep]; + int8_t light2Col = h[i].coordCol + colOffsets[oppositeStep]; + + // Flag both lights for animation + flagToAnimate(light1Row, light1Col); + flagToAnimate(light2Row, light2Col); + } + } +} + +void animateStaticBeams() { + const byte MAX_BEAM_LENGTH = 13; // Maximum distance the beam can travel + static byte lastDirection[LED_COUNT] = {255}; // Track the last direction for each button (255 = uninitialized) + + for (byte i = 0; i < LED_COUNT; i++) { // Check every hex + // Skip buttons that are not in the playable area + if (h[i].isCmd || (!h[i].inScale && scaleLock)) { + continue; + } + + if (h[i].btnState == BTN_STATE_NEWPRESS) { // Button was just pressed + uint64_t clockValue = readClock(); // Get system clock + + // Choose a new random direction, excluding the last one + byte newDirection; + do { + newDirection = clockValue % 3; // Randomly pick 0, 1, or 2 + clockValue /= 3; // Update clockValue for a new seed + } while (newDirection == lastDirection[i]); // Exclude last direction + + lastDirection[i] = newDirection; // Store new direction + } + + if (h[i].btnState == BTN_STATE_HELD || h[i].btnState == BTN_STATE_NEWPRESS) { // Active button + byte baseDirection = lastDirection[i] * 2; // Convert to hex direction (0, 2, or 4) + byte oppositeDirection = (baseDirection + 3) % 6; // Opposite direction + + // Light up the entire beam in both directions + for (byte length = 1; length <= MAX_BEAM_LENGTH; length++) { + // Beam in primary direction + int8_t beam1Row = h[i].coordRow + (length * vertical[baseDirection]); + int8_t beam1Col = h[i].coordCol + (length * horizontal[baseDirection]); + + // Beam in opposite direction + int8_t beam2Row = h[i].coordRow + (length * vertical[oppositeDirection]); + int8_t beam2Col = h[i].coordCol + (length * horizontal[oppositeDirection]); + + // Flag both beams for animation + flagToAnimate(beam1Row, beam1Col); + flagToAnimate(beam2Row, beam2Col); + } + } + } +} void animateRadial() { for (byte i = 0; i < LED_COUNT; i++) { // check every hex @@ -2003,22 +3181,45 @@ 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 + for (byte i = 0; i < steps; i++) { // # of steps to the next corner flagToAnimate(turtleRow,turtleCol); // flag for animation turtleRow += (vertical[dir] * (radius / steps)); turtleCol += (horizontal[dir] * (radius / steps)); } } } - } - } + } + } } - void animateLEDs() { - for (byte i = 0; i < LED_COUNT; i++) { + + void animateRadialReverse() { //inverted splash/star + #define MAX_RADIUS 5 + for (byte i = 0; i < LED_COUNT; i++) { // Check every hex + if (!(h[i].isCmd) && (h[i].inScale || !scaleLock)) { // That is a note + uint64_t frame = animFrame(i); // Current animation frame + if ((frame > 0) && (frame < MAX_RADIUS)) { // Played in the last X frames + uint8_t reverseRadius = MAX_RADIUS - frame; // Calculate reverse radius + byte steps = ((animationType == ANIMATE_SPLASH_REVERSE) ? reverseRadius : 1); // Steps depend on animation type + int8_t turtleRow = h[i].coordRow + (reverseRadius * vertical[HEX_DIRECTION_SW]); + int8_t turtleCol = h[i].coordCol + (reverseRadius * horizontal[HEX_DIRECTION_SW]); + for (byte dir = HEX_DIRECTION_EAST; dir < 6; dir++) { // Walk along the ring in 6 hex directions + for (byte j = 0; j < steps; j++) { // Steps to the next corner + flagToAnimate(turtleRow, turtleCol); // Flag for animation + turtleRow += (vertical[dir] * (reverseRadius / steps)); + turtleCol += (horizontal[dir] * (reverseRadius / steps)); + } + } + } + } + } +} + + void animateLEDs() { + for (byte i = 0; i < LED_COUNT; i++) { h[i].animate = 0; } if (animationType) { - switch (animationType) { + switch (animationType) { case ANIMATE_STAR: case ANIMATE_SPLASH: animateRadial(); break; @@ -2027,7 +3228,13 @@ break; case ANIMATE_OCTAVE: case ANIMATE_BY_NOTE: animateMirror(); - break; + break; + case ANIMATE_BEAMS: + animateStaticBeams(); + break; + case ANIMATE_SPLASH_REVERSE: case ANIMATE_STAR_REVERSE: + animateRadialReverse(); + break; default: break; } @@ -2042,7 +3249,7 @@ of the hex grid. */ // run this if the layout, key, or transposition changes, but not if color or scale changes - void assignPitches() { + void assignPitches() { sendToLog("assignPitch was called:"); for (byte i = 0; i < LED_COUNT; i++) { if (!(h[i].isCmd)) { @@ -2057,16 +3264,16 @@ 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].bend = (ldexp(N - h[i].note, 13) / MPEpitchBendSemis); 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) + ", " + + "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) + "." ); } @@ -2080,7 +3287,7 @@ if (current.scale().tuning == ALL_TUNINGS) { h[i].inScale = 1; } else { - byte degree = current.keyDegree(h[i].stepsFromC); + byte degree = current.keyDegree(h[i].stepsFromC); if (degree == 0) { h[i].inScale = 1; // the root is always in the scale } else { @@ -2097,7 +3304,7 @@ "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) + ", " + + "note=" + std::to_string(h[i].note) + ", " + "inScale? " + std::to_string(h[i].inScale) + "." ); } @@ -2107,17 +3314,39 @@ } void applyLayout() { // call this function when the layout changes sendToLog("buildLayout was called:"); +/////////////////////////////////////////////////////////////////////////////////////// + int8_t acrossSteps = current.layout().acrossSteps; // x + int8_t dnLeftSteps = current.layout().dnLeftSteps; // y + if(mirrorUpDown) + { + dnLeftSteps = -(acrossSteps + dnLeftSteps); // y = -(x + y) + } + if(mirrorLeftRight) + { + dnLeftSteps = acrossSteps + dnLeftSteps; // y = x + y + acrossSteps = -acrossSteps; // x = -x + } + for(byte rotations = 0; rotations < layoutRotation; rotations++) + { + byte keyOffsetY = dnLeftSteps; + byte keyOffsetX = acrossSteps; + dnLeftSteps = keyOffsetX + keyOffsetY; + keyOffsetY = dnLeftSteps; + dnLeftSteps = -acrossSteps; + acrossSteps = keyOffsetY; + } +//////////////////////////////////////////////////////////////////////////////////////// for (byte i = 0; i < LED_COUNT; i++) { - if (!(h[i].isCmd)) { + 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) + + (distCol * acrossSteps) + (distRow * ( - current.layout().acrossSteps + - (2 * current.layout().dnLeftSteps) + acrossSteps + + (2 * dnLeftSteps) )) - ) / 2; + ) / 2; sendToLog( "hex #" + std::to_string(i) + ", " + "steps from C4=" + std::to_string(h[i].stepsFromC) + "." @@ -2174,17 +3403,17 @@ 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, + u8g2, GEM_POINTER_ROW, GEM_ITEMS_COUNT_AUTO, MENU_ITEM_HEIGHT, MENU_PAGE_SCREEN_TOP_OFFSET, MENU_VALUES_LEFT_OFFSET ); - bool screenSaverOn = 0; + bool screenSaverOn = 0; uint64_t screenTime = 0; // GFX timer to count if screensaver should go on const uint64_t screenSaverTimeout = (1u << 23); // 2^23 microseconds ~ 8 seconds /* - Create menu page object of class GEMPage. + Create menu page object of class GEMPage. Menu page holds menu items (GEMItem) and represents menu level. Menu can have multiple menu pages (linked to each other) with multiple menu items each. - + GEMPage constructor creates each page with the associated label. GEMItem constructor can create many different sorts of menu items. The items here are navigation links. @@ -2196,10 +3425,10 @@ GEMItem menuGotoTuning("Tuning", menuPageTuning); GEMItem menuTuningBack("<< Back", menuPageMain); GEMPage menuPageLayout("Layout"); - GEMItem menuGotoLayout("Layout", menuPageLayout); + GEMItem menuGotoLayout("Layout", menuPageLayout); GEMItem menuLayoutBack("<< Back", menuPageMain); GEMPage menuPageScales("Scales"); - GEMItem menuGotoScales("Scales", menuPageScales); + GEMItem menuGotoScales("Scales", menuPageScales); GEMItem menuScalesBack("<< Back", menuPageMain); GEMPage menuPageColors("Color options"); GEMItem menuGotoColors("Color options", menuPageColors); @@ -2207,12 +3436,15 @@ GEMPage menuPageSynth("Synth options"); GEMItem menuGotoSynth("Synth options", menuPageSynth); GEMItem menuSynthBack("<< Back", menuPageMain); + GEMPage menuPageMIDI("MIDI options"); + GEMItem menuGotoMIDI("MIDI options", menuPageMIDI); + GEMItem menuMIDIBack("<< Back", menuPageMain); GEMPage menuPageControl("Control wheel"); GEMItem menuGotoControl("Control wheel", menuPageControl); GEMItem menuControlBack("<< Back", menuPageMain); - GEMPage menuPageTesting("Advanced"); - GEMItem menuGotoTesting("Advanced", menuPageTesting); - GEMItem menuTestingBack("<< Back", menuPageMain); + GEMPage menuPageAdvanced("Advanced"); + GEMItem menuGotoAdvanced("Advanced", menuPageAdvanced); + GEMItem menuAdvancedBack("<< Back", menuPageMain); GEMPage menuPageReboot("Ready to flash firmware!"); /* We haven't written the code for some procedures, @@ -2230,7 +3462,7 @@ To be honest I don't know how to get just a plain text line to show here other than this! */ void fakeButton() {} - GEMItem menuItemVersion("v1.0.1", fakeButton); + GEMItem menuItemVersion("1.1 beta", fakeButton); SelectOptionByte optionByteHardware[] = { { "V1.1", HARDWARE_UNKNOWN }, { "V1.1" , HARDWARE_V1_1 }, { "V1.2", HARDWARE_V1_2 } @@ -2248,7 +3480,7 @@ manually type in menu objects for those pre-loaded values. Instead, we will use routines to construct menu items automatically. - + These lines are forward declarations for the menu objects we will make later. This allocates space in memory with @@ -2257,41 +3489,48 @@ the pre-loaded tuning/layout/etc. definitions we defined above. */ - GEMItem* menuItemTuning[TUNINGCOUNT]; - GEMItem* menuItemLayout[layoutCount]; - GEMItem* menuItemScales[scaleCount]; - GEMSelect* selectKey[TUNINGCOUNT]; - GEMItem* menuItemKeys[TUNINGCOUNT]; + GEMItem* menuItemTuning[TUNINGCOUNT]; + GEMItem* menuItemLayout[layoutCount]; + GEMItem* menuItemScales[scaleCount]; + GEMSelect* selectKey[TUNINGCOUNT]; + GEMItem* menuItemKeys[TUNINGCOUNT]; /* We are now creating some GEMItems that let you 1) select a value from a list of options, 2) update a given variable based on what was chosen, 3) if necessary, run a procedure as well once the value's chosen. - + The list of options is in the form of a 2-d array. There are A arrays, one for each option. Each is 2 entries long. First entry is the label for that choice, second entry is the value associated. - + These arrays go into a typedef that depends on the type of the variable being selected (i.e. Byte for small positive integers; Int for sign-dependent and large integers). - + Then that typeDef goes into a GEMSelect object, with parameters equal to the number of entries in the array, and the storage size of one element in the array. The GEMSelect object is basically just a pointer to the array of choices. The GEMItem then takes the GEMSelect pointer as a parameter. - + The fact that GEM expects pointers and references makes it tricky to work with if you are new to C++. */ + SelectOptionByte optionByteMPEpitchBend[] = { { "2", 2}, {"12", 12}, {"24", 24}, {"48", 48}, {"96", 96} }; + GEMSelect selectMPEpitchBend( sizeof(optionByteMPEpitchBend) / sizeof(SelectOptionByte), optionByteMPEpitchBend); + GEMItem menuItemMPEpitchBend( "MPE Bend:", MPEpitchBendSemis, selectMPEpitchBend, assignPitches); + SelectOptionByte optionByteYesOrNo[] = { { "No", 0 }, { "Yes" , 1 } }; GEMSelect selectYesOrNo( sizeof(optionByteYesOrNo) / sizeof(SelectOptionByte), optionByteYesOrNo); - GEMItem menuItemScaleLock( "Scale lock?", scaleLock, selectYesOrNo); - GEMItem menuItemPercep( "Fix color:", perceptual, selectYesOrNo, setLEDcolorCodes); - GEMItem menuItemShiftColor( "ColorByKey", paletteBeginsAtKeyCenter, selectYesOrNo, setLEDcolorCodes); + GEMItem menuItemScaleLock( "Scale lock?", scaleLock); + GEMItem menuItemPercep( "Fix color:", perceptual, setLEDcolorCodes); + GEMItem menuItemShiftColor( "ColorByKey", paletteBeginsAtKeyCenter, setLEDcolorCodes); GEMItem menuItemWheelAlt( "Alt wheel?", wheelMode, selectYesOrNo); + bool rotaryInvert = false; + GEMItem menuItemRotary( "Invert Encoder:", rotaryInvert); + SelectOptionByte optionByteWheelType[] = { { "Springy", 0 }, { "Sticky", 1} }; GEMSelect selectWheelType( sizeof(optionByteWheelType) / sizeof(SelectOptionByte), optionByteWheelType); GEMItem menuItemPBBehave( "Pitch bend", pbSticky, selectWheelType); @@ -2308,6 +3547,528 @@ GEMSelect selectAudioD( sizeof(optionByteAudioD) / sizeof(SelectOptionByte), optionByteAudioD); GEMItem menuItemAudioD("SynthOutput:", audioD, selectAudioD); +//////////////////////////////////////////////////////////////// + + SelectOptionByte optionByteBPM[] = { + {"1 BPM", 1}, + {"2 BPM", 2}, + {"3 BPM", 3}, + {"4 BPM", 4}, + {"5 BPM", 5}, + {"6 BPM", 6}, + {"7 BPM", 7}, + {"8 BPM", 8}, + {"9 BPM", 9}, + {"10 BPM", 10}, + {"11 BPM", 11}, + {"12 BPM", 12}, + {"13 BPM", 13}, + {"14 BPM", 14}, + {"15 BPM", 15}, + {"16 BPM", 16}, + {"17 BPM", 17}, + {"18 BPM", 18}, + {"19 BPM", 19}, + {"20 BPM", 20}, + {"21 BPM", 21}, + {"22 BPM", 22}, + {"23 BPM", 23}, + {"24 BPM", 24}, + {"25 BPM", 25}, + {"26 BPM", 26}, + {"27 BPM", 27}, + {"28 BPM", 28}, + {"29 BPM", 29}, + {"30 BPM", 30}, + {"31 BPM", 31}, + {"32 BPM", 32}, + {"33 BPM", 33}, + {"34 BPM", 34}, + {"35 BPM", 35}, + {"36 BPM", 36}, + {"37 BPM", 37}, + {"38 BPM", 38}, + {"39 BPM", 39}, + {"40 BPM", 40}, + {"41 BPM", 41}, + {"42 BPM", 42}, + {"43 BPM", 43}, + {"44 BPM", 44}, + {"45 BPM", 45}, + {"46 BPM", 46}, + {"47 BPM", 47}, + {"48 BPM", 48}, + {"49 BPM", 49}, + {"50 BPM", 50}, + {"51 BPM", 51}, + {"52 BPM", 52}, + {"53 BPM", 53}, + {"54 BPM", 54}, + {"55 BPM", 55}, + {"56 BPM", 56}, + {"57 BPM", 57}, + {"58 BPM", 58}, + {"59 BPM", 59}, + {"60 BPM", 60}, + {"61 BPM", 61}, + {"62 BPM", 62}, + {"63 BPM", 63}, + {"64 BPM", 64}, + {"65 BPM", 65}, + {"66 BPM", 66}, + {"67 BPM", 67}, + {"68 BPM", 68}, + {"69 BPM", 69}, + {"70 BPM", 70}, + {"71 BPM", 71}, + {"72 BPM", 72}, + {"73 BPM", 73}, + {"74 BPM", 74}, + {"75 BPM", 75}, + {"76 BPM", 76}, + {"77 BPM", 77}, + {"78 BPM", 78}, + {"79 BPM", 79}, + {"80 BPM", 80}, + {"81 BPM", 81}, + {"82 BPM", 82}, + {"83 BPM", 83}, + {"84 BPM", 84}, + {"85 BPM", 85}, + {"86 BPM", 86}, + {"87 BPM", 87}, + {"88 BPM", 88}, + {"89 BPM", 89}, + {"90 BPM", 90}, + {"91 BPM", 91}, + {"92 BPM", 92}, + {"93 BPM", 93}, + {"94 BPM", 94}, + {"95 BPM", 95}, + {"96 BPM", 96}, + {"97 BPM", 97}, + {"98 BPM", 98}, + {"99 BPM", 99}, + {"100 BPM", 100}, + {"101 BPM", 101}, + {"102 BPM", 102}, + {"103 BPM", 103}, + {"104 BPM", 104}, + {"105 BPM", 105}, + {"106 BPM", 106}, + {"107 BPM", 107}, + {"108 BPM", 108}, + {"109 BPM", 109}, + {"110 BPM", 110}, + {"111 BPM", 111}, + {"112 BPM", 112}, + {"113 BPM", 113}, + {"114 BPM", 114}, + {"115 BPM", 115}, + {"116 BPM", 116}, + {"117 BPM", 117}, + {"118 BPM", 118}, + {"119 BPM", 119}, + {"120 BPM", 120}, + {"121 BPM", 121}, + {"122 BPM", 122}, + {"123 BPM", 123}, + {"124 BPM", 124}, + {"125 BPM", 125}, + {"126 BPM", 126}, + {"127 BPM", 127}, + {"128 BPM", 128}, + {"129 BPM", 129}, + {"130 BPM", 130}, + {"131 BPM", 131}, + {"132 BPM", 132}, + {"133 BPM", 133}, + {"134 BPM", 134}, + {"135 BPM", 135}, + {"136 BPM", 136}, + {"137 BPM", 137}, + {"138 BPM", 138}, + {"139 BPM", 139}, + {"140 BPM", 140}, + {"141 BPM", 141}, + {"142 BPM", 142}, + {"143 BPM", 143}, + {"144 BPM", 144}, + {"145 BPM", 145}, + {"146 BPM", 146}, + {"147 BPM", 147}, + {"148 BPM", 148}, + {"149 BPM", 149}, + {"150 BPM", 150}, + {"151 BPM", 151}, + {"152 BPM", 152}, + {"153 BPM", 153}, + {"154 BPM", 154}, + {"155 BPM", 155}, + {"156 BPM", 156}, + {"157 BPM", 157}, + {"158 BPM", 158}, + {"159 BPM", 159}, + {"160 BPM", 160}, + {"161 BPM", 161}, + {"162 BPM", 162}, + {"163 BPM", 163}, + {"164 BPM", 164}, + {"165 BPM", 165}, + {"166 BPM", 166}, + {"167 BPM", 167}, + {"168 BPM", 168}, + {"169 BPM", 169}, + {"170 BPM", 170}, + {"171 BPM", 171}, + {"172 BPM", 172}, + {"173 BPM", 173}, + {"174 BPM", 174}, + {"175 BPM", 175}, + {"176 BPM", 176}, + {"177 BPM", 177}, + {"178 BPM", 178}, + {"179 BPM", 179}, + {"180 BPM", 180}, + {"181 BPM", 181}, + {"182 BPM", 182}, + {"183 BPM", 183}, + {"184 BPM", 184}, + {"185 BPM", 185}, + {"186 BPM", 186}, + {"187 BPM", 187}, + {"188 BPM", 188}, + {"189 BPM", 189}, + {"190 BPM", 190}, + {"191 BPM", 191}, + {"192 BPM", 192}, + {"193 BPM", 193}, + {"194 BPM", 194}, + {"195 BPM", 195}, + {"196 BPM", 196}, + {"197 BPM", 197}, + {"198 BPM", 198}, + {"199 BPM", 199}, + {"200 BPM", 200}, + {"201 BPM", 201}, + {"202 BPM", 202}, + {"203 BPM", 203}, + {"204 BPM", 204}, + {"205 BPM", 205}, + {"206 BPM", 206}, + {"207 BPM", 207}, + {"208 BPM", 208}, + {"209 BPM", 209}, + {"210 BPM", 210}, + {"211 BPM", 211}, + {"212 BPM", 212}, + {"213 BPM", 213}, + {"214 BPM", 214}, + {"215 BPM", 215}, + {"216 BPM", 216}, + {"217 BPM", 217}, + {"218 BPM", 218}, + {"219 BPM", 219}, + {"220 BPM", 220}, + {"221 BPM", 221}, + {"222 BPM", 222}, + {"223 BPM", 223}, + {"224 BPM", 224}, + {"225 BPM", 225}, + {"226 BPM", 226}, + {"227 BPM", 227}, + {"228 BPM", 228}, + {"229 BPM", 229}, + {"230 BPM", 230}, + {"231 BPM", 231}, + {"232 BPM", 232}, + {"233 BPM", 233}, + {"234 BPM", 234}, + {"235 BPM", 235}, + {"236 BPM", 236}, + {"237 BPM", 237}, + {"238 BPM", 238}, + {"239 BPM", 239}, + {"240 BPM", 240}, + {"241 BPM", 241}, + {"242 BPM", 242}, + {"243 BPM", 243}, + {"244 BPM", 244}, + {"245 BPM", 245}, + {"246 BPM", 246}, + {"247 BPM", 247}, + {"248 BPM", 248}, + {"249 BPM", 249}, + {"250 BPM", 250}, + {"251 BPM", 251}, + {"252 BPM", 252}, + {"253 BPM", 253}, + {"254 BPM", 254}, + {"255 BPM", 255} + }; + + SelectOptionByte optionByteBPM_Multiplier[] = { + {"x1", 1}, + {"x2", 2}, + {"x3", 3}, + {"x4", 4}, + {"x5", 5}, + {"x6", 6}, + {"x7", 7}, + {"x8", 8}, + {"x9", 9}, + {"x10", 10}, + {"x11", 11}, + {"x12", 12}, + {"x13", 13}, + {"x14", 14}, + {"x15", 15}, + {"x16", 16}, + {"x17", 17}, + {"x18", 18}, + {"x19", 19}, + {"x20", 20}, + {"x21", 21}, + {"x22", 22}, + {"x23", 23}, + {"x24", 24}, + {"x25", 25}, + {"x26", 26}, + {"x27", 27}, + {"x28", 28}, + {"x29", 29}, + {"x30", 30}, + {"x31", 31}, + {"x32", 32}, + {"x33", 33}, + {"x34", 34}, + {"x35", 35}, + {"x36", 36}, + {"x37", 37}, + {"x38", 38}, + {"x39", 39}, + {"x40", 40}, + {"x41", 41}, + {"x42", 42}, + {"x43", 43}, + {"x44", 44}, + {"x45", 45}, + {"x46", 46}, + {"x47", 47}, + {"x48", 48}, + {"x49", 49}, + {"x50", 50}, + {"x51", 51}, + {"x52", 52}, + {"x53", 53}, + {"x54", 54}, + {"x55", 55}, + {"x56", 56}, + {"x57", 57}, + {"x58", 58}, + {"x59", 59}, + {"x60", 60}, + {"x61", 61}, + {"x62", 62}, + {"x63", 63}, + {"x64", 64}, + {"x65", 65}, + {"x66", 66}, + {"x67", 67}, + {"x68", 68}, + {"x69", 69}, + {"x70", 70}, + {"x71", 71}, + {"x72", 72}, + {"x73", 73}, + {"x74", 74}, + {"x75", 75}, + {"x76", 76}, + {"x77", 77}, + {"x78", 78}, + {"x79", 79}, + {"x80", 80}, + {"x81", 81}, + {"x82", 82}, + {"x83", 83}, + {"x84", 84}, + {"x85", 85}, + {"x86", 86}, + {"x87", 87}, + {"x88", 88}, + {"x89", 89}, + {"x90", 90}, + {"x91", 91}, + {"x92", 92}, + {"x93", 93}, + {"x94", 94}, + {"x95", 95}, + {"x96", 96}, + {"x97", 97}, + {"x98", 98}, + {"x99", 99}, + {"x100", 100}, + {"x101", 101}, + {"x102", 102}, + {"x103", 103}, + {"x104", 104}, + {"x105", 105}, + {"x106", 106}, + {"x107", 107}, + {"x108", 108}, + {"x109", 109}, + {"x110", 110}, + {"x111", 111}, + {"x112", 112}, + {"x113", 113}, + {"x114", 114}, + {"x115", 115}, + {"x116", 116}, + {"x117", 117}, + {"x118", 118}, + {"x119", 119}, + {"x120", 120}, + {"x121", 121}, + {"x122", 122}, + {"x123", 123}, + {"x124", 124}, + {"x125", 125}, + {"x126", 126}, + {"x127", 127}, + {"x128", 128}, + {"x129", 129}, + {"x130", 130}, + {"x131", 131}, + {"x132", 132}, + {"x133", 133}, + {"x134", 134}, + {"x135", 135}, + {"x136", 136}, + {"x137", 137}, + {"x138", 138}, + {"x139", 139}, + {"x140", 140}, + {"x141", 141}, + {"x142", 142}, + {"x143", 143}, + {"x144", 144}, + {"x145", 145}, + {"x146", 146}, + {"x147", 147}, + {"x148", 148}, + {"x149", 149}, + {"x150", 150}, + {"x151", 151}, + {"x152", 152}, + {"x153", 153}, + {"x154", 154}, + {"x155", 155}, + {"x156", 156}, + {"x157", 157}, + {"x158", 158}, + {"x159", 159}, + {"x160", 160}, + {"x161", 161}, + {"x162", 162}, + {"x163", 163}, + {"x164", 164}, + {"x165", 165}, + {"x166", 166}, + {"x167", 167}, + {"x168", 168}, + {"x169", 169}, + {"x170", 170}, + {"x171", 171}, + {"x172", 172}, + {"x173", 173}, + {"x174", 174}, + {"x175", 175}, + {"x176", 176}, + {"x177", 177}, + {"x178", 178}, + {"x179", 179}, + {"x180", 180}, + {"x181", 181}, + {"x182", 182}, + {"x183", 183}, + {"x184", 184}, + {"x185", 185}, + {"x186", 186}, + {"x187", 187}, + {"x188", 188}, + {"x189", 189}, + {"x190", 190}, + {"x191", 191}, + {"x192", 192}, + {"x193", 193}, + {"x194", 194}, + {"x195", 195}, + {"x196", 196}, + {"x197", 197}, + {"x198", 198}, + {"x199", 199}, + {"x200", 200}, + {"x201", 201}, + {"x202", 202}, + {"x203", 203}, + {"x204", 204}, + {"x205", 205}, + {"x206", 206}, + {"x207", 207}, + {"x208", 208}, + {"x209", 209}, + {"x210", 210}, + {"x211", 211}, + {"x212", 212}, + {"x213", 213}, + {"x214", 214}, + {"x215", 215}, + {"x216", 216}, + {"x217", 217}, + {"x218", 218}, + {"x219", 219}, + {"x220", 220}, + {"x221", 221}, + {"x222", 222}, + {"x223", 223}, + {"x224", 224}, + {"x225", 225}, + {"x226", 226}, + {"x227", 227}, + {"x228", 228}, + {"x229", 229}, + {"x230", 230}, + {"x231", 231}, + {"x232", 232}, + {"x233", 233}, + {"x234", 234}, + {"x235", 235}, + {"x236", 236}, + {"x237", 237}, + {"x238", 238}, + {"x239", 239}, + {"x240", 240}, + {"x241", 241}, + {"x242", 242}, + {"x243", 243}, + {"x244", 244}, + {"x245", 245}, + {"x246", 246}, + {"x247", 247}, + {"x248", 248}, + {"x249", 249}, + {"x250", 250}, + {"x251", 251}, + {"x252", 252}, + {"x253", 253}, + {"x254", 254}, + {"x255", 255} + }; + + GEMSelect selectFrequencyOfJI(sizeof(optionByteBPM) / sizeof(SelectOptionByte), optionByteBPM); + GEMSelect selectBPM_MultiplierOfJI(sizeof(optionByteBPM) / sizeof(SelectOptionByte), optionByteBPM_Multiplier); +/////////////////////////////////////////////////////////////////// + // Roland MT-32 mode (1987) SelectOptionByte optionByteRolandMT32[] = { // Piano @@ -2446,17 +4207,43 @@ }; GEMSelect selectTransposeSteps( 255, optionIntTransposeSteps); GEMItem menuItemTransposeSteps( "Transpose:", transposeSteps, selectTransposeSteps, changeTranspose); + +////////////////////////////////////////////////////////////////////////////////////////////////////// + // MIDI Channel selection + SelectOptionByte optionByteMIDIChannel[] = {{": 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}}; + GEMSelect selectMIDIchannel(16,optionByteMIDIChannel); + GEMItem menuItemSelectMIDIChannel( "MIDI Channel", defaultMidiChannel, selectMIDIchannel); + + // MIDI force MPE option toggle + GEMItem menuItemToggleForceMPEChannels ("Force MPE", forceEnableMPE, resetTuningMIDI); + + // Layout rotation selection + SelectOptionByte optionByteLayoutRotation[] = {{"0 deg",0},{"60 deg",1},{"120 deg",2},{"180 deg",3},{"240 deg",4},{"300 deg",5},{"360 deg",6}}; + GEMSelect selectLayoutRotation(7,optionByteLayoutRotation); + GEMItem menuItemSelectLayoutRotation( "Rotate: ", layoutRotation, selectLayoutRotation, updateLayoutAndRotate); + + // Layout mirroring toggles + GEMItem mirrorLeftRightGEMItem("Mirror vert.", mirrorLeftRight, updateLayoutAndRotate); + GEMItem mirrorUpDownGEMItem ("Mirror hor." , mirrorUpDown, updateLayoutAndRotate); - SelectOptionByte optionByteColor[] = { { "Rainbow", RAINBOW_MODE }, { "Tiered" , TIERED_COLOR_MODE }, {"Alt", ALTERNATE_COLOR_MODE} }; + // Dynamic just intonation toggles and parameters + GEMItem menuItemToggleJI_BPM ("JI align BPM", useJustIntonationBPM,resetTuningMIDI); + GEMItem menuItemSetJI_BPM ("Beating BPM",justIntonationBPM,selectFrequencyOfJI); + GEMItem menuItemSetJI_BPM_Multiplier ("BPM mult.",justIntonationBPM_Multiplier,selectBPM_MultiplierOfJI); + GEMItem menuItemToggleDynamicJI ("Dynamic JI", useDynamicJustIntonation, resetTuningMIDI); + +////////////////////////////////////////////////////////////////////////////////////////////////////// + + SelectOptionByte optionByteColor[] = { { "Rainbow", RAINBOW_MODE }, { "Tiered" , TIERED_COLOR_MODE }, { "Alt", ALTERNATE_COLOR_MODE }, { "Fifths", RAINBOW_OF_FIFTHS_MODE }, { "Alt piano", PIANO_ALT_COLOR_MODE }, { "Piano", PIANO_COLOR_MODE }, { "Filament", PIANO_INCANDESCENT_COLOR_MODE } }; GEMSelect selectColor( sizeof(optionByteColor) / sizeof(SelectOptionByte), optionByteColor); GEMItem menuItemColor( "Color mode:", colorMode, selectColor, setLEDcolorCodes); SelectOptionByte optionByteAnimate[] = { { "None" , ANIMATE_NONE }, { "Octave", ANIMATE_OCTAVE }, - { "By Note", ANIMATE_BY_NOTE }, { "Star", ANIMATE_STAR }, { "Splash" , ANIMATE_SPLASH }, { "Orbit", ANIMATE_ORBIT } }; + { "By Note", ANIMATE_BY_NOTE }, { "Star", ANIMATE_STAR }, { "Splash" , ANIMATE_SPLASH }, { "Orbit", ANIMATE_ORBIT }, {"Beams", ANIMATE_BEAMS}, {"rSplash", ANIMATE_SPLASH_REVERSE}, {"rStar", ANIMATE_STAR_REVERSE} }; GEMSelect selectAnimate( sizeof(optionByteAnimate) / sizeof(SelectOptionByte), optionByteAnimate); GEMItem menuItemAnimate( "Animation:", animationType, selectAnimate); - SelectOptionByte optionByteBright[] = { { "Off", BRIGHT_OFF}, {"Dimmer", BRIGHT_DIMMER}, {"Dim", BRIGHT_DIM}, {"Low", BRIGHT_LOW}, {"Normal", BRIGHT_MID}, {"High", BRIGHT_HIGH}, {"THE SUN", BRIGHT_MAX } }; + SelectOptionByte optionByteBright[] = { { "Off", BRIGHT_OFF}, {"the end", BRIGHT_FAINTER}, {"Blackout", BRIGHT_FAINT}, {"Darker", BRIGHT_DARKER}, {"Dark", BRIGHT_DARK}, {"Dimmer", BRIGHT_DIMMER}, {"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); @@ -2465,13 +4252,13 @@ GEMSelect selectWaveform(sizeof(optionByteWaveform) / sizeof(SelectOptionByte), optionByteWaveform); GEMItem menuItemWaveform( "Waveform:", currWave, selectWaveform, resetSynthFreqs); - SelectOptionInt optionIntModWheel[] = { { "too slo", 1 }, { "Turtle", 2 }, { "Slow", 4 }, + 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 }, + 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); @@ -2493,11 +4280,11 @@ This procedure sets each layout menu item to be either visible if that layout is available in the current tuning, or hidden if not. - + It should run once after the layout menu items are generated, and then once any time the tuning changes. */ - void showOnlyValidLayoutChoices() { + void showOnlyValidLayoutChoices() { for (byte L = 0; L < layoutCount; L++) { menuItemLayout[L]->hide((layoutOptions[L].tuning != current.tuningIndex)); } @@ -2507,7 +4294,7 @@ This procedure sets each scale menu item to be either visible if that scale is available in the current tuning, or hidden if not. - + It should run once after the scale menu items are generated, and then once any time the tuning changes. */ @@ -2521,11 +4308,11 @@ This procedure sets each key spinner menu item to be either visible if the key names correspond to the current tuning, or hidden if not. - + It should run once after the key selectors are generated, and then once any time the tuning changes. */ - void showOnlyValidKeyChoices() { + void showOnlyValidKeyChoices() { for (int T = 0; T < TUNINGCOUNT; T++) { menuItemKeys[T]->hide((T != current.tuningIndex)); } @@ -2598,7 +4385,7 @@ quite a few items are reset, refreshed, and redone when the tuning changes. */ - void changeTuning(GEMCallbackData callbackData) { + void changeTuning(GEMCallbackData callbackData) { byte selection = callbackData.valByte; if (selection != current.tuningIndex) { current.tuningIndex = selection; @@ -2622,14 +4409,14 @@ each index. What's nice about doing this in an array is, we do not have to assign a variable name to each object; we just refer to it by its index in the array. - + The constructor "new GEMItem" is populated with the different variables in the preset objects we defined earlier. Then the menu item is added to the associated page. The item must be entered with the asterisk operator because an array index technically returns an address in memory pointing to the object; the addMenuItem procedure wants - the contents of that item, which is what the * beforehand does. + the contents of that item, which is what the * beforehand does. */ void createTuningMenuItems() { for (byte T = 0; T < TUNINGCOUNT; T++) { @@ -2660,7 +4447,7 @@ showOnlyValidScaleChoices(); } - void setupMenu() { + void setupMenu() { menu.setSplashDelay(0); menu.init(); /* @@ -2670,9 +4457,16 @@ */ menuPageMain.addMenuItem(menuGotoTuning); createTuningMenuItems(); + menuPageTuning.addMenuItem(menuItemToggleJI_BPM); + menuPageTuning.addMenuItem(menuItemSetJI_BPM); + menuPageTuning.addMenuItem(menuItemSetJI_BPM_Multiplier); + menuPageTuning.addMenuItem(menuItemToggleDynamicJI); menuPageTuning.addMenuItem(menuTuningBack); menuPageMain.addMenuItem(menuGotoLayout); createLayoutMenuItems(); + menuPageLayout.addMenuItem(mirrorLeftRightGEMItem); + menuPageLayout.addMenuItem(mirrorUpDownGEMItem); + menuPageLayout.addMenuItem(menuItemSelectLayoutRotation); menuPageLayout.addMenuItem(menuLayoutBack); menuPageMain.addMenuItem(menuGotoScales); createKeyMenuItems(); @@ -2690,23 +4484,29 @@ menuPageColors.addMenuItem(menuItemAnimate); menuPageColors.addMenuItem(menuColorsBack); menuPageMain.addMenuItem(menuGotoSynth); - menuPageSynth.addMenuItem(menuItemPlayback); + menuPageSynth.addMenuItem(menuItemPlayback); menuPageSynth.addMenuItem(menuItemWaveform); // menuItemAudioD added here for hardware V1.2 menuPageSynth.addMenuItem(menuItemRolandMT32); menuPageSynth.addMenuItem(menuItemGeneralMidi); menuPageSynth.addMenuItem(menuSynthBack); + menuPageMain.addMenuItem(menuGotoMIDI); + menuPageMIDI.addMenuItem(menuItemSelectMIDIChannel); + menuPageMIDI.addMenuItem(menuItemMPEpitchBend); + menuPageMIDI.addMenuItem(menuItemToggleForceMPEChannels); + menuPageMIDI.addMenuItem(menuMIDIBack); menuPageMain.addMenuItem(menuItemTransposeSteps); - menuPageMain.addMenuItem(menuGotoTesting); - menuPageTesting.addMenuItem(menuItemVersion); - menuPageTesting.addMenuItem(menuItemHardware); - menuPageTesting.addMenuItem(menuItemPercep); - menuPageTesting.addMenuItem(menuItemShiftColor); - menuPageTesting.addMenuItem(menuItemWheelAlt); - menuPageTesting.addMenuItem(menuItemPBBehave); - menuPageTesting.addMenuItem(menuItemModBehave); - menuPageTesting.addMenuItem(menuItemUSBBootloader); - menuPageTesting.addMenuItem(menuTestingBack); + menuPageMain.addMenuItem(menuGotoAdvanced); + menuPageAdvanced.addMenuItem(menuItemVersion); + menuPageAdvanced.addMenuItem(menuItemHardware); + menuPageAdvanced.addMenuItem(menuItemRotary); + menuPageAdvanced.addMenuItem(menuItemPercep); + menuPageAdvanced.addMenuItem(menuItemShiftColor); + menuPageAdvanced.addMenuItem(menuItemWheelAlt); + menuPageAdvanced.addMenuItem(menuItemPBBehave); + menuPageAdvanced.addMenuItem(menuItemModBehave); + menuPageAdvanced.addMenuItem(menuItemUSBBootloader); + menuPageAdvanced.addMenuItem(menuAdvancedBack); menuHome(); } void setupGFX() { @@ -2726,6 +4526,10 @@ if (!screenSaverOn) { screenSaverOn = 1; u8g2.setContrast(CONTRAST_SCREENSAVER); + //if(globalBrightness == BRIGHT_OFF) + { + u8g2.clear(); + } } } } @@ -2753,7 +4557,7 @@ passing through all the valid states above, at which point action should be taken depending on the direction of the turn. - + The variable rotaryState stores all of this data and refreshes it each loop of the 2nd processor. Value Meaning @@ -2771,9 +4575,10 @@ {0,5,1,0},{6,5,0,0},{6,5,7,0},{6,0,7,16} }; byte storeRotaryTurn = 0; - bool rotaryClicked = HIGH; + bool rotaryClicked = HIGH; void readHexes() { + /* This is the original way of reading buttons. multiplexer is doing the least movement. May be faster? 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); @@ -2790,6 +4595,29 @@ } pinMode(p, INPUT); // Set the selected column pin back to INPUT mode (0V / LOW). } + }*/ + // trying out a new way which may reduce rf noise (and increase reliability) by reducing the ammount of times the columns get energized + for (byte c = 0; c < COLCOUNT; c++) { // Iterate through each of the column pins. + byte p = cPin[c]; // Hold the currently selected column pin in a variable. + pinMode(p, INPUT_PULLUP); // Set that column pin to INPUT_PULLUP mode (+3.3V / HIGH). + delayMicroseconds(0); // delay to energize column and stabilize (may need adjustment) + for (byte r = 0; r < ROWCOUNT; r++) { // Then iterate through each of the row pins on the multiplexing chip for the selected column. + for (byte d = 0; d < 4; d++) { + digitalWrite(mPin[d], (r >> d) & 1); // Selected multiplexer channel is pulled to ground. + } + byte i = c + (r * COLCOUNT);/* + byte tempSat = SAT_BW; + colorDef tempColor = {HUE_NONE, tempSat, (byte)(toggleWheel ? VALUE_SHADE : VALUE_LOW)}; + strip.setPixelColor(i, getLEDcode(tempColor)); + strip.show();*/ + delayMicroseconds(14); // Delay to allow signal to settle and improve reliability (found this number by experimentation) + bool didYouPressHex = (digitalRead(p) == LOW); // hex is pressed if it returns LOW. else not pressed + h[i].interpBtnPress(didYouPressHex); + if (h[i].btnState == BTN_STATE_NEWPRESS) { + h[i].timePressed = runTime; // log the time + } + } + pinMode(p, INPUT); // Set the selected column pin back to INPUT mode (0V / LOW). } for (byte i = 0; i < BTN_COUNT; i++) { // For all buttons in the deck switch (h[i].btnState) { @@ -2806,7 +4634,7 @@ cmdOff(i); } else if (h[i].inScale || (!scaleLock)) { tryMIDInoteOff(i); - trySynthNoteOff(i); + trySynthNoteOff(i); } break; case BTN_STATE_HELD: // held @@ -2816,7 +4644,7 @@ } } } - void updateWheels() { + void updateWheels() { velWheel.setTargetValue(); bool upd = velWheel.updateValue(runTime); if (upd) { @@ -2859,7 +4687,9 @@ } rotaryClicked = temp; if (storeRotaryTurn != 0) { - menu.registerKeyPress((storeRotaryTurn == 8) ? GEM_KEY_UP : GEM_KEY_DOWN); + if (rotaryInvert == true) { + menu.registerKeyPress((storeRotaryTurn == 8) ? GEM_KEY_DOWN : GEM_KEY_UP); + } else {menu.registerKeyPress((storeRotaryTurn == 8) ? GEM_KEY_UP : GEM_KEY_DOWN);} storeRotaryTurn = 0; screenTime = 0; } @@ -2872,6 +4702,8 @@ audioD = AUDIO_PIEZO | AUDIO_AJACK; menuPageSynth.addMenuItem(menuItemAudioD, 2); globalBrightness = BRIGHT_DIM; + setLEDcolorCodes(); + rotaryInvert = true; } } |