// ====== Hexperiment
// Sketch to program the hexBoard to handle microtones
// March 2024, theHDM / Nicholas Fox
// major thanks to Zach and Jared!
// Arduino IDE setup:
// Board = Generic RP2040 (use the following additional board manager repo:
// https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json)
// Patches needed for U8G2, Rotary.h
// ==============================================================================
// list of things remaining to do:
// -- program the wheel -- OK!
// -- put back the animations -- OK!
// -- test MPE working on iPad garageband -- works on pianoteq
without MPE; need to get powered connection to iOS.
// -- volume control test on buzzer
// -- save and load presets
// -- sequencer restore
// ==============================================================================
//
#include <Arduino.h>
#include <Wire.h>
#include <LittleFS.h>
#include <queue> // std::queue construct to store open
channels in microtonal mode
const byte diagnostics = 1;
// ====== initialize timers
uint32_t runTime = 0; // Program loop consistent
variable for time in milliseconds since power on
uint32_t lapTime = 0; // Used to keep track of how
long each loop takes. Useful for rate-limiting.
uint32_t loopTime = 0; // Used to check speed of the
loop in diagnostics mode 4
// ====== initialize SDA and SCL pins for hardware I/O
const byte lightPinSDA = 16;
const byte lightPinSCL = 17;
// ====== initialize MIDI
#include <Adafruit_TinyUSB.h>
#include <MIDI.h>
Adafruit_USBD_MIDI usb_midi;
MIDI_CREATE_INSTANCE(Adafruit_USBD_MIDI, usb_midi, MIDI);
float concertA = 440.0; // tuning of A4 in Hz
byte MPE = 0; // microtonal mode. if zero then attempt to
self-manage multiple channels.
// if one then on certain synths that are MPE compatible
will send in that mode.
int16_t channelBend[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0 }; // what's the current note bend on this channel
byte channelPoly[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0 }; // how many notes are playing on this channel
std::queue<byte> openChannelQueue;
const byte defaultPBRange = 2;
// ====== initialize LEDs
#include <Adafruit_NeoPixel.h>
const byte multiplexPins[] = { 4, 5, 2, 3 }; // m1p, m2p, m4p, m8p
const byte rowCount = 14; // The number of rows
in the matrix
const byte columnPins[] = { 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
const byte colCount = sizeof(columnPins); // The number of columns
in the matrix
const byte hexCount = colCount * rowCount; // The number of
elements in the matrix
const byte LEDPin = 22;
Adafruit_NeoPixel strip(hexCount, LEDPin, NEO_GRB + NEO_KHZ800);
enum { NoAnim, StarAnim, SplashAnim, OrbitAnim, OctaveAnim, NoteAnim };
byte animationType = 0;
byte animationFPS = 32; // actually frames per 2^10 seconds. close
enough to 30fps
int16_t rainbowDegreeTime = 64; // ms to go through 1/360 of rainbow.
// ====== initialize hex state object
enum { Right, UpRight, UpLeft, Left, DnLeft, DnRight };
typedef struct {
int8_t row;
int8_t col;
} coordinates;
typedef struct {
byte keyState = 0; // binary 00 = off, 01 = just pressed,
10 = just released, 11 = held
coordinates coords = {0,0};
uint32_t timePressed = 0; // timecode of last press
uint32_t LEDcolorAnim = 0; //
uint32_t LEDcolorPlay = 0; //
uint32_t LEDcolorOn = 0; //
uint32_t LEDcolorOff = 0; //
bool animate = 0; // hex is flagged as part of the
animation in this frame
int16_t steps = 0; // number of steps from key center
(semitones in 12EDO; microtones if >12EDO)
bool isCmd = 0; // 0 if it's a MIDI note; 1 if it's a
MIDI control cmd
bool inScale = 0; // 0 if it's not in the selected
scale; 1 if it is
byte note = 255; // MIDI note or control parameter
corresponding to this hex
int16_t bend; // in microtonal mode, the pitch bend
for this note needed to be tuned correctly
byte channel; // what MIDI channel this note is playing on
float frequency; // what frequency to ring on the buzzer
void updateKeyState(bool keyPressed) {
keyState = (((keyState << 1) + keyPressed) & 3);
if (keyState == 1) {
timePressed = millis(); // log the time
};
};
uint32_t animFrame() {
if (timePressed) { // 2^10 milliseconds is close enough to 1 second
return 1 + (((runTime - timePressed) * animationFPS) >> 10);
} else {
return 0;
};
};
} buttonDef;
buttonDef h[hexCount]; // array of hex objects from 0 to 139
const byte assignCmd[] = { 0, 20, 40, 60, 80, 100, 120 };
const byte cmdCount = 7;
const byte cmdCode = 192;
// ====== initialize wheel emulation
const uint16_t ccMsgCoolDown = 20; // milliseconds between steps
typedef struct {
byte* topBtn;
byte* midBtn;
byte* botBtn;
int16_t minValue;
int16_t maxValue;
uint16_t stepValue;
int16_t defValue;
int16_t curValue;
int16_t targetValue;
uint32_t timeLastChanged;
void setTargetValue() {
if (*midBtn >> 1) { // middle button toggles target (0) vs. step (1) mode
int16_t temp = curValue;
if (*topBtn == 1) {temp += stepValue;};
if (*botBtn == 1) {temp -= stepValue;};
if (temp > maxValue) {temp = maxValue;}
else if (temp <= minValue) {temp = minValue;};
targetValue = temp;
} else {
switch (((*topBtn >> 1) << 1) + (*botBtn >> 1)) {
case 0b10: targetValue = maxValue; break;
case 0b11: targetValue = defValue; break;
case 0b01: targetValue = minValue; break;
default: targetValue = curValue; break;
};
};
};
bool updateValue() {
int16_t temp = targetValue - curValue;
if (temp != 0) {
if ((runTime - timeLastChanged) >= ccMsgCoolDown) {
timeLastChanged = runTime;
if (abs(temp) < stepValue) {
curValue = targetValue;
} else {
curValue = curValue + (stepValue * (temp / abs(temp)));
};
return 1;
} else {
return 0;
};
} else {
return 0;
};
};
} wheelDef;
wheelDef modWheel = {
&h[assignCmd[4]].keyState,
&h[assignCmd[5]].keyState,
&h[assignCmd[6]].keyState,
0, 127, 8,
0, 0, 0
};
wheelDef pbWheel = {
&h[assignCmd[4]].keyState,
&h[assignCmd[5]].keyState,
&h[assignCmd[6]].keyState,
-8192, 8192, 1024,
0, 0, 0
};
wheelDef velWheel = {
&h[assignCmd[0]].keyState,
&h[assignCmd[1]].keyState,
&h[assignCmd[2]].keyState,
0, 127, 8,
96, 96, 96
};
bool toggleWheel = 0; // 0 for mod, 1 for pb
// ====== initialize rotary knob
#include <Rotary.h>
const byte rotaryPinA = 20;
const byte rotaryPinB = 21;
const byte rotaryPinC = 24;
Rotary rotary = Rotary(rotaryPinA, rotaryPinB);
bool rotaryIsClicked = HIGH; //
bool rotaryWasClicked = HIGH; //
int8_t rotaryKnobTurns = 0; //
// ====== initialize GFX display
#include <GEM_u8g2.h>
#define GEM_DISABLE_GLCD
U8G2_SH1107_SEEED_128X128_F_HW_I2C u8g2(U8G2_R2, U8X8_PIN_NONE);
GEM_u8g2 menu(u8g2, GEM_POINTER_ROW, GEM_ITEMS_COUNT_AUTO, 10, 10,
78); // menu item height; page screen top offset; menu values left
offset
const byte defaultContrast = 63; // GFX default contrast
bool screenSaverOn = 0; //
uint32_t screenTime = 0; // GFX timer to count if
screensaver should go on
const uint32_t screenSaverMillis = 10'000; //
// ====== initialize piezo buzzer
//#include "RP2040_Volume.h" // simulated volume control on buzzer
const byte tonePin = 23;
//RP2040_Volume piezoBuzzer(tonePin, tonePin);
byte buzzer = 0; // buzzer state
byte currentBuzzNote = 255; // need to work on this
uint32_t currentBuzzTime = 0; // Used to keep track of when
this note started buzzin
uint32_t arpeggiateLength = 10; //
// ====== initialize tuning (microtonal) presets
typedef struct {
char* name;
byte cycleLength; // steps before repeat
float stepSize; // in cents, 100 = "normal" semitone.
} tuningDef;
enum {
Twelve, Seventeen, Nineteen, TwentyTwo,
TwentyFour, ThirtyOne, FortyOne, FiftyThree,
SeventyTwo, BohlenPierce,
CarlosA, CarlosB, CarlosG
};
tuningDef tuningOptions[] = {
// replaces the idea of const byte EDO[] = { 12, 17, 19, 22, 24,
31, 41, 53, 72 };
{ (char*)"12 EDO", 12, 100.0 },
{ (char*)"17 EDO", 17, 1200.0 / 17 },
{ (char*)"19 EDO", 19, 1200.0 / 19 },
{ (char*)"22 EDO", 22, 1200.0 / 22 },
{ (char*)"24 EDO", 24, 50.0 },
{ (char*)"31 EDO", 31, 1200.0 / 31 },
{ (char*)"41 EDO", 41, 1200.0 / 41 },
{ (char*)"53 EDO", 53, 1200.0 / 53 },
{ (char*)"72 EDO", 72, 100.0 / 6 },
{ (char*)"Bohlen-Pierce", 13, 1901.955 / 13 }, //
{ (char*)"Carlos Alpha", 9, 77.965 }, //
{ (char*)"Carlos Beta", 11, 63.833 }, //
{ (char*)"Carlos Gamma", 20, 35.099 }
};
const byte tuningCount = sizeof(tuningOptions) / sizeof(tuningDef);
// ====== initialize layout patterns
typedef struct {
char* name;
bool isPortrait;
byte rootHex;
int8_t acrossSteps;
int8_t dnLeftSteps;
byte tuning;
} layoutDef;
layoutDef layoutOptions[] = {
{ (char*)"Wicki-Hayden", 1, 64, 2, -7, Twelve },
{ (char*)"Harmonic Table", 0, 75, -7, 3, Twelve },
{ (char*)"Janko", 0, 65, -1, -1, Twelve },
{ (char*)"Gerhard", 0, 65, -1, -3, Twelve },
{ (char*)"Accordion C-sys.", 1, 75, 2, -3, Twelve },
{ (char*)"Accordion B-sys.", 1, 64, 1, -3, Twelve },
{ (char*)"Full Layout", 1, 65, -1, -9, Twelve },
{ (char*)"Bosanquet, 17", 0, 65, -2, -1, Seventeen },
{ (char*)"Full Layout", 1, 65, -1, -9, Seventeen },
{ (char*)"Bosanquet, 19", 0, 65, -1, -2, Nineteen },
{ (char*)"Full Layout", 1, 65, -1, -9, Nineteen },
{ (char*)"Bosanquet, 22", 0, 65, -3, -1, TwentyTwo },
{ (char*)"Full Layout", 1, 65, -1, -9, TwentyTwo },
{ (char*)"Bosanquet, 24", 0, 65, -1, -3, TwentyFour },
{ (char*)"Full Layout", 1, 65, -1, -9, TwentyFour },
{ (char*)"Bosanquet, 31", 0, 65, -2, -3, ThirtyOne },
{ (char*)"Full Layout", 1, 65, -1, -9, ThirtyOne },
{ (char*)"Bosanquet, 41", 0, 65, -4, -3, FortyOne }, //
forty-one #1
{ (char*)"Gerhard, 41", 0, 65, 3, -10, FortyOne }, //
forty-one #2
{ (char*)"Full Layout, 41", 0, 65, -1, -8, FortyOne }, //
forty-one #3
{ (char*)"Wicki-Hayden, 53", 1, 64, 9, -31, FiftyThree },
{ (char*)"Harmonic Tbl, 53", 0, 75, -31, 14, FiftyThree },
{ (char*)"Bosanquet, 53", 0, 65, -5, -4, FiftyThree },
{ (char*)"Full Layout, 53", 0, 65, -1, -9, FiftyThree },
{ (char*)"Full Layout, 72", 0, 65, -1, -9, SeventyTwo },
{ (char*)"Full Layout", 1, 65, -1, -9, BohlenPierce },
{ (char*)"Full Layout", 1, 65, -1, -9, CarlosA },
{ (char*)"Full Layout", 1, 65, -1, -9, CarlosB },
{ (char*)"Full Layout", 1, 65, -1, -9, CarlosG }
};
const byte layoutCount = sizeof(layoutOptions) / sizeof(layoutDef);
// ====== initialize list of supported scales / modes / raga / maqam
typedef struct {
char* name;
byte tuning;
byte step[16]; // 16 bytes = 128 bits, 1 = in scale; 0 = not
} scaleDef;
scaleDef scaleOptions[] = {
{ (char*)"None", 255, { 255, 255,
255,255,255,255,255,255,255,255,255,255,255,255,255,255} },
{ (char*)"Major", Twelve, { 0b10101101,
0b0101'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ (char*)"Minor, natural", Twelve, { 0b10110101,
0b1010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ (char*)"Minor, melodic", Twelve, { 0b10110101,
0b0101'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ (char*)"Minor, harmonic", Twelve, { 0b10110101,
0b1001'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ (char*)"Pentatonic, major", Twelve, { 0b10101001,
0b0100'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ (char*)"Pentatonic, minor", Twelve, { 0b10010101,
0b0010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ (char*)"Blues", Twelve, { 0b10010111,
0b0010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ (char*)"Double Harmonic", Twelve, { 0b11001101,
0b1001'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ (char*)"Phrygian", Twelve, { 0b11010101,
0b1010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ (char*)"Phrygian Dominant", Twelve, { 0b11001101,
0b1010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ (char*)"Dorian", Twelve, { 0b10110101,
0b0110'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ (char*)"Lydian", Twelve, { 0b10101011,
0b0101'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ (char*)"Lydian Dominant", Twelve, { 0b10101011,
0b0110'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ (char*)"Mixolydian", Twelve, { 0b10101101,
0b0110'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ (char*)"Locrian", Twelve, { 0b11010110,
0b1010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ (char*)"Whole tone", Twelve, { 0b10101010,
0b1010'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ (char*)"Octatonic", Twelve, { 0b10110110,
0b1101'0000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ (char*)"Rast maqam", TwentyFour, { 0b10001001,
0b00100010, 0b00101100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ (char*)"Rast makam", FiftyThree, { 0b10000000,
0b01000000, 0b01000010, 0b00000001,
0b00000000,
0b10001000, 0b10000'000, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
};
const byte scaleCount = sizeof(scaleOptions) / sizeof(scaleDef);
byte scaleLock = 0; // menu wants this to be an int, not a bool
// ====== initialize key coloring routines
enum colors { W, R, O, Y, L, G, C, B,
I, P, M,
r, o, y, l, g, c, b, i,
p, m };
enum { DARK = 0, VeryDIM = 1, DIM = 32, BRIGHT = 127, VeryBRIGHT = 255 };
enum { GRAY = 0, DULL = 127, VIVID = 255 };
float hueCode[] = { 0.0, 0.0, 36.0, 72.0, 108.0, 144.0, 180.0,
216.0, 252.0, 288.0, 324.0,
0.0, 36.0, 72.0, 108.0, 144.0, 180.0,
216.0, 252.0, 288.0, 324.0 };
byte satCode[] = { GRAY,
VIVID,VIVID,VIVID,VIVID,VIVID,VIVID,VIVID,VIVID,VIVID,VIVID,
DULL, DULL, DULL, DULL, DULL, DULL, DULL,
DULL, DULL, DULL };
byte colorMode = 0;
byte perceptual = 1;
enum {assignDefault = -1}; // auto-determine this component of color
// ====== initialize note labels in each tuning, also used for key signature
typedef struct {
char* name;
byte tuning;
int8_t offset; // steps from constant A4 to that key class
colors tierColor;
} keyDef;
keyDef keyOptions[] = {
// 12 EDO, whole tone = 2, #/b = 1
{ (char*)" C (B#)", Twelve, -9, W },
{ (char*)" C# / Db", Twelve, -8, I },
{ (char*)" D", Twelve, -7, W },
{ (char*)" D# / Eb", Twelve, -6, I },
{ (char*)" (Fb) E", Twelve, -5, W },
{ (char*)" F (E#)", Twelve, -4, W },
{ (char*)" Gb / F#", Twelve, -3, I },
{ (char*)" G", Twelve, -2, W },
{ (char*)" G# / Ab", Twelve, -1, I },
{ (char*)" A", Twelve, 0, W },
{ (char*)" A# / Bb", Twelve, 1, I },
{ (char*)"(Cb) B", Twelve, 2, W },
// 17 EDO, whole tone = 3, #/b = 2, +/d = 1
{ (char*)" C (B+)", Seventeen, -13, W },
{ (char*)" C+ / Db / B#", Seventeen, -12, R },
{ (char*)" C# / Dd", Seventeen, -11, I },
{ (char*)" D", Seventeen, -10, W },
{ (char*)" D+ / Eb", Seventeen, -9, R },
{ (char*)" Fb / D# / Ed", Seventeen, -8, I },
{ (char*)"(Fd) E", Seventeen, -7, W },
{ (char*)" F (E+)", Seventeen, -6, W },
{ (char*)" F+ / Gb / E#", Seventeen, -5, R },
{ (char*)" F# / Gd", Seventeen, -4, I },
{ (char*)" G", Seventeen, -3, W },
{ (char*)" G+ / Ab", Seventeen, -2, R },
{ (char*)" G# / Ad", Seventeen, -1, I },
{ (char*)" A", Seventeen, 0, W },
{ (char*)" Bb / A+", Seventeen, 1, R },
{ (char*)" Cb / Bd / A#", Seventeen, 2, I },
{ (char*)"(Cd) B" , Seventeen, 3, W },
// 19 EDO, whole tone = 3, #/b = 1
{ (char*)" C", Nineteen, -14, W },
{ (char*)" C#", Nineteen, -13, R },
{ (char*)" Db", Nineteen, -12, I },
{ (char*)" D", Nineteen, -11, W },
{ (char*)" D#", Nineteen, -10, R },
{ (char*)" Eb", Nineteen, -9, I },
{ (char*)" E", Nineteen, -8, W },
{ (char*)" E# / Fb", Nineteen, -7, m },
{ (char*)" F", Nineteen, -6, W },
{ (char*)" F#", Nineteen, -5, R },
{ (char*)" Gb", Nineteen, -4, I },
{ (char*)" G", Nineteen, -3, W },
{ (char*)" G#", Nineteen, -2, R },
{ (char*)" Ab", Nineteen, -1, I },
{ (char*)" A", Nineteen, 0, W },
{ (char*)" A#", Nineteen, 1, R },
{ (char*)" Bb", Nineteen, 2, I },
{ (char*)" B", Nineteen, 3, W },
{ (char*)" Cb / B#", Nineteen, 4, m },
// 22 EDO, whole tone = 4, #/b = 3, ^/v = 1
{ (char*)" C (^B)", TwentyTwo, -17, W },
{ (char*)" ^C / Db / vB#", TwentyTwo, -16, l },
{ (char*)" vC# / ^Db / B#", TwentyTwo, -15, C },
{ (char*)" C# / vD", TwentyTwo, -14, i },
{ (char*)" D", TwentyTwo, -13, W },
{ (char*)" ^D / Eb", TwentyTwo, -12, l },
{ (char*)" Fb / vD# / ^Eb", TwentyTwo, -11, C },
{ (char*)" ^Fb / D# / vE", TwentyTwo, -10, i },
{ (char*)"(vF) E", TwentyTwo, -9, W },
{ (char*)" F (^E)", TwentyTwo, -8, W },
{ (char*)" ^F / Gb / vE#", TwentyTwo, -7, l },
{ (char*)" vF# / ^Gb / E#", TwentyTwo, -6, C },
{ (char*)" F# / vG", TwentyTwo, -5, i },
{ (char*)" G", TwentyTwo, -4, W },
{ (char*)" ^G / Ab", TwentyTwo, -3, l },
{ (char*)" vG# / ^Ab", TwentyTwo, -2, C },
{ (char*)" G# / vA", TwentyTwo, -1, i },
{ (char*)" A", TwentyTwo, 0, W },
{ (char*)" Bb / ^A", TwentyTwo, 1, l },
{ (char*)" Cb / ^Bb / vA#", TwentyTwo, 2, C },
{ (char*)" ^Cb / vB / A#", TwentyTwo, 3, i },
{ (char*)"(vC) B", TwentyTwo, 4, W },
// 24 EDO, whole tone = 4, #/b = 2, +/d = 1
{ (char*)" C / B#", TwentyFour, -18, W },
{ (char*)" C+", TwentyFour, -17, r },
{ (char*)" C# / Db", TwentyFour, -16, I },
{ (char*)" Dd", TwentyFour, -15, g },
{ (char*)" D", TwentyFour, -14, W },
{ (char*)" D+", TwentyFour, -13, r },
{ (char*)" Eb / D#", TwentyFour, -12, I },
{ (char*)" Ed", TwentyFour, -11, g },
{ (char*)" E / Fb", TwentyFour, -10, W },
{ (char*)" E+ / Fd", TwentyFour, -9, y },
{ (char*)" E# / F", TwentyFour, -8, W },
{ (char*)" F+", TwentyFour, -7, r },
{ (char*)" Gb / F#", TwentyFour, -6, I },
{ (char*)" Gd", TwentyFour, -5, g },
{ (char*)" G", TwentyFour, -4, W },
{ (char*)" G+", TwentyFour, -3, r },
{ (char*)" G# / Ab", TwentyFour, -2, I },
{ (char*)" Ad", TwentyFour, -1, g },
{ (char*)" A", TwentyFour, 0, W },
{ (char*)" A+", TwentyFour, 1, r },
{ (char*)" Bb / A#", TwentyFour, 2, I },
{ (char*)" Bd", TwentyFour, 3, g },
{ (char*)" B / Cb", TwentyFour, 4, W },
{ (char*)" B+ / Cd", TwentyFour, 5, y },
// 31 EDO, whole tone = 5, #/b = 2, +/d = 1
{ (char*)" C", ThirtyOne, -23, W },
{ (char*)" C+", ThirtyOne, -22, R },
{ (char*)" C#", ThirtyOne, -21, Y },
{ (char*)" Db", ThirtyOne, -20, C },
{ (char*)" Dd", ThirtyOne, -19, I },
{ (char*)" D", ThirtyOne, -18, W },
{ (char*)" D+", ThirtyOne, -17, R },
{ (char*)" D#", ThirtyOne, -16, Y },
{ (char*)" Eb", ThirtyOne, -15, C },
{ (char*)" Ed", ThirtyOne, -14, I },
{ (char*)" E", ThirtyOne, -13, W },
{ (char*)" E+ / Fb", ThirtyOne, -12, L },
{ (char*)" E# / Fd", ThirtyOne, -11, M },
{ (char*)" F", ThirtyOne, -10, W },
{ (char*)" F+", ThirtyOne, -9, R },
{ (char*)" F#", ThirtyOne, -8, Y },
{ (char*)" Gb", ThirtyOne, -7, C },
{ (char*)" Gd", ThirtyOne, -6, I },
{ (char*)" G", ThirtyOne, -5, W },
{ (char*)" G+", ThirtyOne, -4, R },
{ (char*)" G#", ThirtyOne, -3, Y },
{ (char*)" Ab", ThirtyOne, -2, C },
{ (char*)" Ad", ThirtyOne, -1, I },
{ (char*)" A", ThirtyOne, 0, W },
{ (char*)" A+", ThirtyOne, 1, R },
{ (char*)" A#", ThirtyOne, 2, Y },
{ (char*)" Bb", ThirtyOne, 3, C },
{ (char*)" Bd", ThirtyOne, 4, I },
{ (char*)" B", ThirtyOne, 5, W },
{ (char*)" Cb / B+", ThirtyOne, 6, L },
{ (char*)" Cd / B#", ThirtyOne, 7, M },
// 41 EDO, whole tone = 7, #/b = 4, +/d = 2, ^/v = 1
{ (char*)" C (vB#)", FortyOne, -31, W },
{ (char*)" ^C / B#", FortyOne, -30, c },
{ (char*)" C+ ", FortyOne, -29, O },
{ (char*)" vC# / Db", FortyOne, -28, I },
{ (char*)" C# / ^Db", FortyOne, -27, R },
{ (char*)" Dd", FortyOne, -26, B },
{ (char*)" vD", FortyOne, -25, y },
{ (char*)" D", FortyOne, -24, W },
{ (char*)" ^D", FortyOne, -23, c },
{ (char*)" D+", FortyOne, -22, O },
{ (char*)" vD# / Eb", FortyOne, -21, I },
{ (char*)" D# / ^Eb", FortyOne, -20, R },
{ (char*)" Ed", FortyOne, -19, B },
{ (char*)" vE", FortyOne, -18, y },
{ (char*)" (^Fb) E", FortyOne, -17, W },
{ (char*)" Fd / ^E", FortyOne, -16, c },
{ (char*)" vF / E+", FortyOne, -15, y },
{ (char*)" F (vE#)", FortyOne, -14, W },
{ (char*)" ^F / E#", FortyOne, -13, c },
{ (char*)" F+", FortyOne, -12, O },
{ (char*)" Gb / vF#", FortyOne, -11, I },
{ (char*)" ^Gb / F#", FortyOne, -10, R },
{ (char*)" Gd", FortyOne, -9, B },
{ (char*)" vG", FortyOne, -8, y },
{ (char*)" G", FortyOne, -7, W },
{ (char*)" ^G", FortyOne, -6, c },
{ (char*)" G+", FortyOne, -5, O },
{ (char*)" vG# / Ab", FortyOne, -4, I },
{ (char*)" G# / ^Ab", FortyOne, -3, R },
{ (char*)" Ad", FortyOne, -2, B },
{ (char*)" vA", FortyOne, -1, y },
{ (char*)" A", FortyOne, 0, W },
{ (char*)" ^A", FortyOne, 1, c },
{ (char*)" A+", FortyOne, 2, O },
{ (char*)" vA# / Bb", FortyOne, 3, I },
{ (char*)" A# / ^Bb", FortyOne, 4, R },
{ (char*)" Bd", FortyOne, 5, B },
{ (char*)" vB", FortyOne, 6, y },
{ (char*)" (^Cb) B", FortyOne, 7, W },
{ (char*)" Cd / ^B", FortyOne, 8, c },
{ (char*)" vC / B+", FortyOne, 9, y },
// 53 EDO, whole tone = 9, #/b = 5, >/< = 2, ^/v = 1
{ (char*)" C (vB#)", FiftyThree, -40, W },
{ (char*)" ^C / B#", FiftyThree, -39, c },
{ (char*)" >C / <Db", FiftyThree, -38, l },
{ (char*)" <C# / vDb", FiftyThree, -37, O },
{ (char*)" vC# / Db", FiftyThree, -36, I },
{ (char*)" C# / ^Db", FiftyThree, -35, R },
{ (char*)" ^C# / >Db", FiftyThree, -34, B },
{ (char*)" >C# / <D", FiftyThree, -33, g },
{ (char*)" vD", FiftyThree, -32, y },
{ (char*)" D", FiftyThree, -31, W },
{ (char*)" ^D", FiftyThree, -30, c },
{ (char*)" >D / <Eb", FiftyThree, -29, l },
{ (char*)" <D# / vEb", FiftyThree, -28, O },
{ (char*)" vD# / Eb", FiftyThree, -27, I },
{ (char*)" D# / ^Eb", FiftyThree, -26, R },
{ (char*)" ^D# / >Eb", FiftyThree, -25, B },
{ (char*)" >D# / <E", FiftyThree, -24, g },
{ (char*)" Fb / vE", FiftyThree, -23, y },
{ (char*)"(^Fb) E", FiftyThree, -22, W },
{ (char*)"(>Fb) ^E", FiftyThree, -21, c },
{ (char*)" <F / >E", FiftyThree, -20, G },
{ (char*)" vF (<E#)", FiftyThree, -19, y },
{ (char*)" F (vE#)", FiftyThree, -18, W },
{ (char*)" ^F / E#", FiftyThree, -17, c },
{ (char*)" >F / <Gb", FiftyThree, -16, l },
{ (char*)" <F# / vGb", FiftyThree, -15, O },
{ (char*)" vF# / Gb", FiftyThree, -14, I },
{ (char*)" F# / ^Gb", FiftyThree, -13, R },
{ (char*)" ^F# / >Gb", FiftyThree, -12, B },
{ (char*)" >F# / <G", FiftyThree, -11, g },
{ (char*)" vG", FiftyThree, -10, y },
{ (char*)" G", FiftyThree, -9, W },
{ (char*)" ^G", FiftyThree, -8, c },
{ (char*)" >G / <Ab", FiftyThree, -7, l },
{ (char*)" <G# / vAb", FiftyThree, -6, O },
{ (char*)" vG# / Ab", FiftyThree, -5, I },
{ (char*)" G# / ^Ab", FiftyThree, -4, R },
{ (char*)" ^G# / >Ab", FiftyThree, -3, B },
{ (char*)" >G# / <A", FiftyThree, -2, g },
{ (char*)" vA", FiftyThree, -1, y },
{ (char*)" A", FiftyThree, 0, W },
{ (char*)" ^A", FiftyThree, 1, c },
{ (char*)" <Bb / >A", FiftyThree, 2, l },
{ (char*)" vBb / <A#", FiftyThree, 3, O },
{ (char*)" Bb / vA#", FiftyThree, 4, I },
{ (char*)" ^Bb / A#", FiftyThree, 5, R },
{ (char*)" >Bb / ^A#", FiftyThree, 6, B },
{ (char*)" <B / >A#", FiftyThree, 7, g },
{ (char*)" Cb / vB", FiftyThree, 8, y },
{ (char*)"(^Cb) B", FiftyThree, 9, W },
{ (char*)"(>Cb) ^B", FiftyThree, 10, c },
{ (char*)" <C / >B", FiftyThree, 11, G },
{ (char*)" vC (<B#)", FiftyThree, 12, y },
// 72 EDO, whole tone = 12, #/b = 6, +/d = 3, ^/v = 1
{ (char*)" C (B#)", SeventyTwo, -54, W },
{ (char*)" ^C", SeventyTwo, -53, g },
{ (char*)" vC+", SeventyTwo, -52, r },
{ (char*)" C+", SeventyTwo, -51, p },
{ (char*)" ^C+", SeventyTwo, -50, b },
{ (char*)" vC#", SeventyTwo, -49, y },
{ (char*)" C# / Db", SeventyTwo, -48, I },
{ (char*)" ^C# / ^Db", SeventyTwo, -47, g },
{ (char*)" vDd", SeventyTwo, -46, r },
{ (char*)" Dd", SeventyTwo, -45, p },
{ (char*)" ^Dd", SeventyTwo, -44, b },
{ (char*)" vD", SeventyTwo, -43, y },
{ (char*)" D", SeventyTwo, -42, W },
{ (char*)" ^D", SeventyTwo, -41, g },
{ (char*)" vD+", SeventyTwo, -40, r },
{ (char*)" D+", SeventyTwo, -39, p },
{ (char*)" ^D+", SeventyTwo, -38, b },
{ (char*)" vEb / vD#", SeventyTwo, -37, y },
{ (char*)" Eb / D#", SeventyTwo, -36, I },
{ (char*)" ^Eb / ^D#", SeventyTwo, -35, g },
{ (char*)" vEd", SeventyTwo, -34, r },
{ (char*)" Ed", SeventyTwo, -33, p },
{ (char*)" ^Ed", SeventyTwo, -32, b },
{ (char*)" vE (vFb)", SeventyTwo, -31, y },
{ (char*)" E (Fb)", SeventyTwo, -30, W },
{ (char*)" ^E (^Fb)", SeventyTwo, -29, g },
{ (char*)" vE+ / vFd", SeventyTwo, -28, r },
{ (char*)" E+ / Fd", SeventyTwo, -27, p },
{ (char*)" ^E+ / ^Fd", SeventyTwo, -26, b },
{ (char*)"(vE#) vF", SeventyTwo, -25, y },
{ (char*)" (E#) F", SeventyTwo, -24, W },
{ (char*)"(^E#) ^F", SeventyTwo, -23, g },
{ (char*)" vF+", SeventyTwo, -22, r },
{ (char*)" F+", SeventyTwo, -21, p },
{ (char*)" ^F+", SeventyTwo, -20, b },
{ (char*)" vGb / vF#", SeventyTwo, -19, y },
{ (char*)" Gb / F#", SeventyTwo, -18, I },
{ (char*)" ^Gb / ^F#", SeventyTwo, -17, g },
{ (char*)" vGd", SeventyTwo, -16, r },
{ (char*)" Gd", SeventyTwo, -15, p },
{ (char*)" ^Gd", SeventyTwo, -14, b },
{ (char*)" vG", SeventyTwo, -13, y },
{ (char*)" G", SeventyTwo, -12, W },
{ (char*)" ^G", SeventyTwo, -11, g },
{ (char*)" vG+", SeventyTwo, -10, r },
{ (char*)" G+", SeventyTwo, -9, p },
{ (char*)" ^G+", SeventyTwo, -8, b },
{ (char*)" vG# / vAb", SeventyTwo, -7, y },
{ (char*)" G# / Ab", SeventyTwo, -6, I },
{ (char*)" ^G# / ^Ab", SeventyTwo, -5, g },
{ (char*)" vAd", SeventyTwo, -4, r },
{ (char*)" Ad", SeventyTwo, -3, p },
{ (char*)" ^Ad", SeventyTwo, -2, b },
{ (char*)" vA", SeventyTwo, -1, y },
{ (char*)" A", SeventyTwo, 0, W },
{ (char*)" ^A", SeventyTwo, 1, g },
{ (char*)" vA+", SeventyTwo, 2, r },
{ (char*)" A+", SeventyTwo, 3, p },
{ (char*)" ^A+", SeventyTwo, 4, b },
{ (char*)" vBb / vA#", SeventyTwo, 5, y },
{ (char*)" Bb / A#", SeventyTwo, 6, I },
{ (char*)" ^Bb / ^A#", SeventyTwo, 7, g },
{ (char*)" vBd", SeventyTwo, 8, r },
{ (char*)" Bd", SeventyTwo, 9, p },
{ (char*)" ^Bd", SeventyTwo, 10, b },
{ (char*)" vB (vCb)", SeventyTwo, 11, y },
{ (char*)" B (Cb)", SeventyTwo, 12, W },
{ (char*)" ^B (^Cb)", SeventyTwo, 13, g },
{ (char*)" vB+ / vCd", SeventyTwo, 14, r },
{ (char*)" B+ / Cd", SeventyTwo, 15, p },
{ (char*)" ^B+ / ^Cd", SeventyTwo, 16, b },
{ (char*)"(vB#) vC", SeventyTwo, 17, y },
//
{ (char*)"n/a",BohlenPierce,0,W},
{ (char*)"n/a",BohlenPierce,0,W},
{ (char*)"n/a",BohlenPierce,0,W},
{ (char*)"n/a",BohlenPierce,0,W},
{ (char*)"n/a",BohlenPierce,0,W},
{ (char*)"n/a",BohlenPierce,0,W},
{ (char*)"n/a",BohlenPierce,0,W},
{ (char*)"n/a",BohlenPierce,0,W},
{ (char*)"n/a",BohlenPierce,0,W},
{ (char*)"n/a",BohlenPierce,0,W},
{ (char*)"n/a",BohlenPierce,0,W},
{ (char*)"n/a",BohlenPierce,0,W},
{ (char*)"n/a",BohlenPierce,0,W},
//
{ (char*)"n/a",CarlosA,0,W},
{ (char*)"n/a",CarlosA,0,W},
{ (char*)"n/a",CarlosA,0,W},
{ (char*)"n/a",CarlosA,0,W},
{ (char*)"n/a",CarlosA,0,W},
{ (char*)"n/a",CarlosA,0,W},
{ (char*)"n/a",CarlosA,0,W},
{ (char*)"n/a",CarlosA,0,W},
{ (char*)"n/a",CarlosA,0,W},
//
{ (char*)"n/a",CarlosB,0,W},
{ (char*)"n/a",CarlosB,0,W},
{ (char*)"n/a",CarlosB,0,W},
{ (char*)"n/a",CarlosB,0,W},
{ (char*)"n/a",CarlosB,0,W},
{ (char*)"n/a",CarlosB,0,W},
{ (char*)"n/a",CarlosB,0,W},
{ (char*)"n/a",CarlosB,0,W},
{ (char*)"n/a",CarlosB,0,W},
{ (char*)"n/a",CarlosB,0,W},
{ (char*)"n/a",CarlosB,0,W},
//
{ (char*)"n/a",CarlosG,0,W},
{ (char*)"n/a",CarlosG,0,W},
{ (char*)"n/a",CarlosG,0,W},
{ (char*)"n/a",CarlosG,0,W},
{ (char*)"n/a",CarlosG,0,W},
{ (char*)"n/a",CarlosG,0,W},
{ (char*)"n/a",CarlosG,0,W},
{ (char*)"n/a",CarlosG,0,W},
{ (char*)"n/a",CarlosG,0,W},
{ (char*)"n/a",CarlosG,0,W},
{ (char*)"n/a",CarlosG,0,W},
{ (char*)"n/a",CarlosG,0,W},
{ (char*)"n/a",CarlosG,0,W},
{ (char*)"n/a",CarlosG,0,W},
{ (char*)"n/a",CarlosG,0,W},
{ (char*)"n/a",CarlosG,0,W},
{ (char*)"n/a",CarlosG,0,W},
{ (char*)"n/a",CarlosG,0,W},
{ (char*)"n/a",CarlosG,0,W},
{ (char*)"n/a",CarlosG,0,W},
};
const int keyCount = sizeof(keyOptions) / sizeof(keyDef);
// ====== initialize structure to store and recall user preferences
typedef struct { // put all user-selectable options into a class so
that down the line these can be saved and loaded.
char* presetName;
int tuningIndex; // instead of using pointers, i chose to
store index value of each option, to be saved to a .pref or .ini or
something
int layoutIndex;
int scaleIndex;
int keyIndex;
int transpose;
// define simple recall functions
tuningDef tuning() {
return tuningOptions[tuningIndex];
};
layoutDef layout() {
return layoutOptions[layoutIndex];
};
scaleDef scale() {
return scaleOptions[scaleIndex];
};
keyDef key() {
return keyOptions[keyIndex];
};
int layoutsBegin() {
if (tuningIndex == Twelve) {
return 0;
} else {
int temp = 0;
while (layoutOptions[temp].tuning < tuningIndex) {
temp++;
};
return temp;
};
};
int keysBegin() {
if (tuningIndex == Twelve) {
return 0;
} else {
int temp = 0;
while (keyOptions[temp].tuning < tuningIndex) {
temp++;
};
return temp;
};
};
int findC() {
return keyOptions[keysBegin()].offset;
};
} presetDef;
presetDef current = {
(char*)"Default",
Twelve, // see the relevant enum{} statement
0, // default to the first layout, wicki hayden
0, // default to using no scale (chromatic)
0, // default to the key of C
0 // default to no transposition
};
// ====== functions
int positiveMod(int n, int d) {
return (((n % d) + d) % d);
}
coordinates indexToCoord(byte x) {
coordinates temp;
temp.row = (x / 10);
temp.col = (2 * (x % 10)) + (temp.row & 1);
return temp;
}
bool hexOOB(coordinates c) {
return (c.row < 0)
|| (c.row >= rowCount)
|| (c.col < 0)
|| (c.col >= (2 * colCount))
|| ((c.col + c.row) & 1);
}
byte coordToIndex(coordinates c) {
if (hexOOB(c)) {
return 255;
} else {
return (10 * c.row) + (c.col / 2);
};
}
coordinates hexVector(byte direction, byte distance) {
coordinates temp;
int8_t vertical[] = {0,-1,-1, 0, 1,1};
int8_t horizontal[] = {2, 1,-1,-2,-1,1};
temp.row = vertical[direction] * distance;
temp.col = horizontal[direction] * distance;
return temp;
}
coordinates hexOffset(coordinates a, coordinates b) {
coordinates temp;
temp.row = a.row + b.row;
temp.col = a.col + b.col;
return temp;
}
coordinates hexDistance(coordinates origin, coordinates destination) {
coordinates temp;
temp.row = destination.row - origin.row;
temp.col = destination.col - origin.col;
return temp;
}
float freqToMIDI(float Hz) { // formula to convert from
Hz to MIDI note
return 69.0 + 12.0 * log2f(Hz / 440.0);
}
float MIDItoFreq(float MIDI) { // formula to convert from
MIDI note to Hz
return 440.0 * exp2((MIDI - 69.0) / 12.0);
}
float stepsToMIDI(int16_t stepsFromA) { // return the MIDI pitch associated
return freqToMIDI(concertA) + ((float)stepsFromA *
(float)current.tuning().stepSize / 100.0);
}
// ====== diagnostic wrapper
void sendToLog(String msg) {
if (diagnostics) {
Serial.println(msg);
};
}
// ====== LED routines
int16_t transformHue(float D) {
if ((!perceptual) || (D > 360.0)) {
return 65536 * (D / 360.0);
} else {
// red yellow green
blue
int hueIn[] = { 0, 9, 18, 90, 108, 126, 135, 150,
198, 243, 252, 261, 306, 333, 360};
// #ff0000 #ffff00 #00ff00
#00ffff #0000ff #ff00ff
int hueOut[] = { 0, 3640,
5461,10922,12743,16384,21845,27306,32768,38229,43690,49152,54613,58254,65535};
byte B = 0;
while (D - hueIn[B] > 0) {
B++;
};
return hueOut[B - 1] + (hueOut[B] - hueOut[B - 1]) * ((D -
(float)hueIn[B - 1])/(float)(hueIn[B] - hueIn[B - 1]));
};
}
void resetHexLEDs() { // calculate color codes for each hex, store
for playback
int16_t hue;
float hueDegrees;
byte sat;
colors c;
for (byte i = 0; i < hexCount; i++) {
if (!(h[i].isCmd)) {
byte scaleDegree = positiveMod(h[i].steps +
current.key().offset - current.findC(),current.tuning().cycleLength);
switch (colorMode) {
case 1:
c = keyOptions[current.keysBegin() + scaleDegree].tierColor;
hueDegrees = hueCode[c];
sat = satCode[c];
break;
default:
hueDegrees = 360.0 * ((float)scaleDegree /
(float)current.tuning().cycleLength);
sat = 255;
break;
};
hue = transformHue(hueDegrees);
h[i].LEDcolorPlay = strip.gamma32(strip.ColorHSV(hue,sat,VeryBRIGHT));
h[i].LEDcolorOn = strip.gamma32(strip.ColorHSV(hue,sat,BRIGHT));
h[i].LEDcolorOff = strip.gamma32(strip.ColorHSV(hue,sat,DIM));
h[i].LEDcolorAnim = strip.ColorHSV(hue,0,255);
} else {
//
};
};
}
// ====== layout routines
void assignPitches() { // run this if the layout, key, or
transposition changes, but not if color or scale changes
sendToLog("assignPitch was called:");
for (byte i = 0; i < hexCount; i++) {
if (!(h[i].isCmd)) {
float N = stepsToMIDI(h[i].steps + current.key().offset +
current.transpose);
if (N < 0 || N >= 128) {
h[i].note = 255;
h[i].bend = 0;
h[i].frequency = 0.0;
} else {
h[i].note = ((N >= 127) ? 127 : round(N));
h[i].bend = (ldexp(N - h[i].note, 13) / defaultPBRange);
h[i].frequency = MIDItoFreq(N);
};
sendToLog(String(
"hex #" + String(i) + ", " +
"steps=" + String(h[i].steps) + ", " +
"isCmd? " + String(h[i].isCmd) + ", " +
"note=" + String(h[i].note) + ", " +
"bend=" + String(h[i].bend) + ", " +
"freq=" + String(h[i].frequency) + ", " +
"inScale? " + String(h[i].inScale) + "."
));
};
};
sendToLog("assignPitches complete.");
}
void applyScale() {
sendToLog("applyScale was called:");
for (byte i = 0; i < hexCount; i++) {
if (!(h[i].isCmd)) {
byte degree = positiveMod(h[i].steps, current.tuning().cycleLength);
byte whichByte = degree / 8;
byte bitShift = 7 - (degree - (whichByte << 3));
byte digitMask = 1 << bitShift;
h[i].inScale = (current.scale().step[whichByte] & digitMask)
>> bitShift;
sendToLog(String(
"hex #" + String(i) + ", " +
"steps=" + String(h[i].steps) + ", " +
"isCmd? " + String(h[i].isCmd) + ", " +
"note=" + String(h[i].note) + ", " +
"inScale? " + String(h[i].inScale) + "."
));
};
};
resetHexLEDs();
sendToLog("applyScale complete.");
}
void applyLayout() { // call this function when the layout changes
sendToLog("buildLayout was called:");
for (byte i = 0; i < hexCount; i++) {
if (!(h[i].isCmd)) {
coordinates dist =
hexDistance(h[current.layout().rootHex].coords, h[i].coords);
h[i].steps = (
(dist.col * current.layout().acrossSteps) +
(dist.row * (
current.layout().acrossSteps +
(2 * current.layout().dnLeftSteps)
))
) / 2;
sendToLog(String(
"hex #" + String(i) + ", " +
"steps=" + String(h[i].steps) + "."
));
};
};
applyScale(); // when layout changes, have to re-apply
scale and re-apply LEDs
assignPitches(); // same with pitches
u8g2.setDisplayRotation(current.layout().isPortrait ? U8G2_R2 :
U8G2_R1); // and landscape / portrait rotation
sendToLog("buildLayout complete.");
}
// ====== buzzer routines
byte nextHeldNote() {
byte n = 255;
for (byte i = 1; i < hexCount; i++) {
byte checkNote = positiveMod(currentBuzzNote + i, hexCount);
if ((h[checkNote].channel) && (!h[checkNote].isCmd)) {
n = checkNote;
break;
};
};
return n;
}
void buzz(byte x) { // send 128 or larger to turn off tone
currentBuzzNote = x;
if ((!(h[x].isCmd)) && (h[x].note < 128) && (h[x].frequency < 32767)) {
//piezoBuzzer.tone(h[x].frequency, (float)velWheel.curValue *
(100.0 / 128.0), 16384, TIME_MS);
tone(tonePin, h[x].frequency); // stock TONE library, but
frequency changed to float
} else {
//piezoBuzzer.stop_tone();
noTone(tonePin); // stock TONE library
};
}
// ====== MIDI routines
void setPitchBendRange(byte Ch, byte semitones) {
MIDI.beginRpn(0, Ch);
MIDI.sendRpnValue(semitones << 7, Ch);
MIDI.endRpn(Ch);
sendToLog(String(
"set pitch bend range on ch " +
String(Ch) + " to be " + String(semitones) + " semitones"
));
}
void setMPEzone(byte masterCh, byte sizeOfZone) {
MIDI.beginRpn(6, masterCh);
MIDI.sendRpnValue(sizeOfZone << 7, masterCh);
MIDI.endRpn(masterCh);
sendToLog(String(
"tried sending MIDI msg to set MPE zone, master ch " +
String(masterCh) + ", zone of this size: " + String(sizeOfZone)
));
}
void prepMIDIforMicrotones() {
bool makeZone = (MPE && (current.tuningIndex != Twelve)); // if
MPE flag is on and tuning <> 12EDO
setMPEzone(1, (8 * makeZone)); // MPE zone 1 = ch 2 thru 9 (or
reset if not using MPE)
delay(ccMsgCoolDown);
setMPEzone(16, (5 * makeZone)); // MPE zone 16 = ch 11 thru 15
(or reset if not using MPE)
delay(ccMsgCoolDown);
for (byte i = 1; i <= 16; i++) {
setPitchBendRange(i, defaultPBRange); // some synths try to set
PB range to 48 semitones.
delay(ccMsgCoolDown); // this forces it back to
the expected range of 2 semitones.
if ((i != 10) && ((!makeZone) || ((i > 1) && (i < 16)))) {
openChannelQueue.push(i);
sendToLog(String("pushed ch " + String(i) + " to the open
channel queue"));
};
channelBend[i - 1] = 0;
channelPoly[i - 1] = 0;
};
}
void chgModulation() {
if (current.tuningIndex == Twelve) {
MIDI.sendControlChange(1, modWheel.curValue, 1);
sendToLog(String("sent mod value " + String(modWheel.curValue) +
" to ch 1"));
} else if (MPE) {
MIDI.sendControlChange(1, modWheel.curValue, 1);
sendToLog(String("sent mod value " + String(modWheel.curValue) +
" to ch 1"));
MIDI.sendControlChange(1, modWheel.curValue, 16);
sendToLog(String("sent mod value " + String(modWheel.curValue) +
" to ch 16"));
} else {
for (byte i = 0; i < 16; i++) {
MIDI.sendControlChange(1, modWheel.curValue, i + 1);
sendToLog(String("sent mod value " + String(modWheel.curValue)
+ " to ch " + String(i+1)));
};
};
};
void chgUniversalPB() {
if (current.tuningIndex == Twelve) {
MIDI.sendPitchBend(pbWheel.curValue, 1);
sendToLog(String("sent pb value " + String(pbWheel.curValue) + "
to ch 1"));
} else if (MPE) {
MIDI.sendPitchBend(pbWheel.curValue, 1);
sendToLog(String("sent pb value " + String(pbWheel.curValue) + "
to ch 1"));
MIDI.sendPitchBend(pbWheel.curValue, 16);
sendToLog(String("sent pb value " + String(pbWheel.curValue) + "
to ch 16"));
} else {
for (byte i = 0; i < 16; i++) {
MIDI.sendPitchBend(channelBend[i] + pbWheel.curValue, i + 1);
sendToLog(String("sent pb value " + String(channelBend[i] +
pbWheel.curValue) + " to ch " + String(i+1)));
};
};
}
byte assignChannel(byte x) {
if (current.tuningIndex == Twelve) {
return 1;
} else {
byte temp = 17;
for (byte c = MPE; c < (16 - MPE); c++) { // MPE - look at ch 2
thru 15 [c 1-14]; otherwise ch 1 thru 16 [c 0-15]
if ((c + 1 != 10) && (h[x].bend == channelBend[c])) { // not
using drum channel ch 10 in either case
temp = c + 1;
sendToLog(String("found a matching channel: ch " +
String(temp) + " has pitch bend " + String(channelBend[c])));
break;
};
};
if (temp = 17) {
if (openChannelQueue.empty()) {
sendToLog(String("channel queue was empty so we didn't send
a note on"));
} else {
temp = openChannelQueue.front();
openChannelQueue.pop();
sendToLog(String("popped " + String(temp) + " off the queue"));
};
};
return temp;
};
}
// ====== hex press routines
void noteOn(byte x) {
byte c = assignChannel(x);
if (c <= 16) {
h[x].channel = c; // value is 1 - 16
if (current.tuningIndex != Twelve) {
MIDI.sendPitchBend(h[x].bend, c); // ch 1-16
};
MIDI.sendNoteOn(h[x].note, velWheel.curValue, c); // ch 1-16
sendToLog(String(
"sent note on: " + String(h[x].note) +
" pb " + String(h[x].bend) +
" vel " + String(velWheel.curValue) +
" ch " + String(c)
));
if (current.tuningIndex != Twelve) {
channelPoly[c - 1]++; // array is 0 - 15
};
if (buzzer) {
buzz(x);
};
};
}
void noteOff(byte x) {
byte c = h[x].channel;
if (c) {
h[x].channel = 0;
MIDI.sendNoteOff(h[x].note, velWheel.curValue, c);
sendToLog(String(
"sent note off: " + String(h[x].note) +
" pb " + String(h[x].bend) +
" vel " + String(velWheel.curValue) +
" ch " + String(c)
));
if (current.tuningIndex != Twelve) {
switch (channelPoly[c - 1]) {
case 1:
channelPoly[c - 1]--;
openChannelQueue.push(c);
break;
case 0:
break;
default:
channelPoly[c - 1]--;
break;
};
};
if (buzzer) {
buzz(nextHeldNote());
};
};
}
void cmdOn(byte x) { // volume and mod wheel read all current buttons
switch (h[x].note) {
case cmdCode + 3:
toggleWheel = !toggleWheel;
// recolorHex(x);
break;
default:
// the rest should all be taken care of within the wheelDef structure
break;
};
}
void cmdOff(byte x) { // pitch bend wheel only if buttons held.
// nothing; should all be taken care of within the wheelDef structure
}
// ====== animations
void flagToAnimate(coordinates C) {
if (!hexOOB(C)) {
h[coordToIndex(C)].animate = 1;
};
}
void animateMirror() {
for (byte i = 0; i < hexCount; i++) { // check every hex
if ((!(h[i].isCmd)) && (h[i].channel)) { // that is
a held note
for (byte j = 0; j < hexCount; j++) { // compare
to every hex
if ((!(h[j].isCmd)) && (!(h[j].channel))) { // that is
a note not being played
int16_t temp = h[i].steps - h[j].steps; // look at
difference between notes
if (animationType == OctaveAnim) { // set
octave diff to zero if need be
temp = positiveMod(temp, current.tuning().cycleLength);
};
if (temp == 0) { //
highlight if diff is zero
h[j].animate = 1;
};
};
};
};
};
}
void animateOrbit() {
for (byte i = 0; i < hexCount; i++) {
// check every hex
if ((!(h[i].isCmd)) && (h[i].channel)) {
// that is a held note
flagToAnimate(hexOffset(h[i].coords,hexVector((h[i].animFrame()
% 6),1))); // different neighbor each frame
};
};
}
void animateRadial() {
for (byte i = 0; i < hexCount; i++) {
// check every hex
if (!(h[i].isCmd)) {
// that is a note
uint32_t radius = h[i].animFrame();
if ((radius > 0) && (radius < 16)) {
// played in the last 16 frames
byte steps = ((animationType == SplashAnim) ? radius : 1);
// star = 1 step to next corner; ring = 1 step per hex
coordinates temp =
hexOffset(h[i].coords,hexVector(DnLeft,radius)); // start at one
corner of the ring
for (byte dir = 0; dir < 6; dir++) {
// walk along the ring in each of the 6 hex directions
for (byte i = 0; i < steps; i++) {
// # of steps to the next corner
flagToAnimate(temp);
// flag for animation
temp = hexOffset(temp, hexVector(dir,radius / steps));
// then next step
};
};
};
};
};
}
// ====== menu variables and routines
// must declare these variables globally for some reason
// doing so down here so we don't have to forward declare callback functions
//
SelectOptionByte optionByteYesOrNo[] = { { "No" , 0 },
{ "Yes" , 1 } };
SelectOptionByte optionByteBuzzer[] = { { "Off" , 0 },
{ "Mono" , 1 },
{ "Arp'gio", 2 } };
SelectOptionByte optionByteColor[] = { { "Rainbow", 0 },
{ "Tiered" , 1 } };
SelectOptionByte optionByteAnimate[] = { { "None" , NoAnim },
{ "Octave" , OctaveAnim},
{ "By Note", NoteAnim},
{ "Star" , StarAnim},
{ "Splash" , SplashAnim},
{ "Orbit" , OrbitAnim} };
GEMSelect selectYesOrNo(sizeof(optionByteYesOrNo) /
sizeof(SelectOptionByte), optionByteYesOrNo);
GEMSelect selectBuzzer( sizeof(optionByteBuzzer) /
sizeof(SelectOptionByte), optionByteBuzzer);
GEMSelect selectColor( sizeof(optionByteColor) /
sizeof(SelectOptionByte), optionByteColor);
GEMSelect selectAnimate(sizeof(optionByteAnimate) /
sizeof(SelectOptionByte), optionByteAnimate);
GEMPage menuPageMain("HexBoard MIDI Controller");
GEMPage menuPageTuning("Tuning");
GEMItem menuGotoTuning("Tuning", menuPageTuning);
GEMItem* menuItemTuning[tuningCount]; // dynamically generate item
based on tunings
GEMPage menuPageLayout("Layout");
GEMItem menuGotoLayout("Layout", menuPageLayout);
GEMItem* menuItemLayout[layoutCount]; // dynamically generate item
based on presets
GEMPage menuPageScales("Scales");
GEMItem menuGotoScales("Scales", menuPageScales);
GEMItem* menuItemScales[scaleCount]; // dynamically generate item
based on presets and if allowed in given EDO tuning
GEMPage menuPageKeys("Keys");
GEMItem menuGotoKeys("Keys", menuPageKeys);
GEMItem* menuItemKeys[keyCount]; // dynamically generate item
based on presets
GEMItem menuItemScaleLock( "Scale lock?", scaleLock, selectYesOrNo);
GEMItem menuItemMPE( "MPE Mode:", MPE,
selectYesOrNo, prepMIDIforMicrotones);
GEMItem menuItemBuzzer( "Buzzer:", buzzer, selectBuzzer);
GEMItem menuItemColor( "Color mode:", colorMode,
selectColor, resetHexLEDs);
GEMItem menuItemPercep( "Adjust color:", perceptual,
selectYesOrNo, resetHexLEDs);
GEMItem menuItemAnimate( "Animation:", animationType, selectAnimate);
void menuHome() {
menu.setMenuPageCurrent(menuPageMain);
menu.drawMenu();
}
void showOnlyValidLayoutChoices() { // re-run at setup and whenever
tuning changes
for (byte L = 0; L < layoutCount; L++) {
menuItemLayout[L]->hide((layoutOptions[L].tuning != current.tuningIndex));
};
sendToLog(String("menu: Layout choices were updated."));
}
void showOnlyValidScaleChoices() { // re-run at setup and whenever
tuning changes
for (int S = 0; S < scaleCount; S++) {
menuItemScales[S]->hide((scaleOptions[S].tuning !=
current.tuningIndex) && (scaleOptions[S].tuning != 255));
};
sendToLog(String("menu: Scale choices were updated."));
}
void showOnlyValidKeyChoices() { // re-run at setup and whenever
tuning changes
for (int K = 0; K < keyCount; K++) {
menuItemKeys[K]->hide((keyOptions[K].tuning != current.tuningIndex));
};
sendToLog(String("menu: Key choices were updated."));
}
void changeLayout(GEMCallbackData callbackData) { // when you
change the layout via the menu
byte selection = callbackData.valByte;
if (selection != current.layoutIndex) {
current.layoutIndex = selection;
applyLayout();
};
menuHome();
}
void changeScale(GEMCallbackData callbackData) { // when you
change the scale via the menu
int selection = callbackData.valInt;
if (selection != current.scaleIndex) {
current.scaleIndex = selection;
applyScale();
};
menuHome();
}
void changeKey(GEMCallbackData callbackData) { // when you
change the key via the menu
int selection = callbackData.valInt;
if (selection != current.keyIndex) {
current.keyIndex = selection;
assignPitches();
};
menuHome();
}
void changeTuning(GEMCallbackData callbackData) { // not working yet
byte selection = callbackData.valByte;
if (selection != current.tuningIndex) {
current.tuningIndex = selection;
current.layoutIndex = current.layoutsBegin();
current.scaleIndex = 0;
current.keyIndex = current.keysBegin();
showOnlyValidLayoutChoices();
showOnlyValidScaleChoices();
showOnlyValidKeyChoices();
applyLayout();
prepMIDIforMicrotones();
};
menuHome();
}
void buildMenu() {
menuPageMain.addMenuItem(menuGotoTuning);
for (byte T = 0; T < tuningCount; T++) { // create pointers to all
tuning choices
menuItemTuning[T] = new GEMItem(tuningOptions[T].name, changeTuning, T);
menuPageTuning.addMenuItem(*menuItemTuning[T]);
};
menuPageMain.addMenuItem(menuGotoLayout);
for (byte L = 0; L < layoutCount; L++) { // create pointers to all layouts
menuItemLayout[L] = new GEMItem(layoutOptions[L].name, changeLayout, L);
menuPageLayout.addMenuItem(*menuItemLayout[L]);
};
showOnlyValidLayoutChoices();
menuPageMain.addMenuItem(menuGotoScales);
for (int S = 0; S < scaleCount; S++) { // create pointers to all
scale items, filter them as you go
menuItemScales[S] = new GEMItem(scaleOptions[S].name, changeScale, S);
menuPageScales.addMenuItem(*menuItemScales[S]);
};
showOnlyValidScaleChoices();
menuPageMain.addMenuItem(menuGotoKeys);
for (int K = 0; K < keyCount; K++) {
menuItemKeys[K] = new GEMItem(keyOptions[K].name, changeKey, K);
menuPageKeys.addMenuItem(*menuItemKeys[K]);
};
showOnlyValidKeyChoices();
menuPageMain.addMenuItem(menuItemScaleLock);
menuPageMain.addMenuItem(menuItemColor);
menuPageMain.addMenuItem(menuItemBuzzer);
menuPageMain.addMenuItem(menuItemAnimate);
menuPageMain.addMenuItem(menuItemMPE);
menuPageMain.addMenuItem(menuItemPercep);
}
// ====== setup routines
void setupMIDI() {
usb_midi.setStringDescriptor("HexBoard MIDI"); // Initialize
MIDI, and listen to all MIDI channels
MIDI.begin(MIDI_CHANNEL_OMNI); // This will also
call usb_midi's begin()
}
void setupFileSystem() {
Serial.begin(115200); // Set serial to make uploads work
without bootsel button
LittleFSConfig cfg; // Configure file system defaults
cfg.setAutoFormat(true); // Formats file system if it cannot be mounted.
LittleFS.setConfig(cfg);
LittleFS.begin(); // Mounts file system.
if (!LittleFS.begin()) {
Serial.println("An Error has occurred while mounting LittleFS");
}
}
void setupPins() {
for (byte p = 0; p < sizeof(columnPins); p++) // For each column pin...
{
pinMode(columnPins[p], INPUT_PULLUP); // set the pinMode to
INPUT_PULLUP (+3.3V / HIGH).
}
for (byte p = 0; p < sizeof(multiplexPins); p++) // For each column pin...
{
pinMode(multiplexPins[p], OUTPUT); // Setting the row
multiplexer pins to output.
}
Wire.setSDA(lightPinSDA);
Wire.setSCL(lightPinSCL);
pinMode(rotaryPinC, INPUT_PULLUP);
}
void setupGrid() {
sendToLog(String("initializing hex grid..."));
for (byte i = 0; i < hexCount; i++) {
h[i].coords = indexToCoord(i);
h[i].isCmd = 0;
h[i].note = 255;
h[i].keyState = 0;
};
for (byte c = 0; c < cmdCount; c++) {
h[assignCmd[c]].isCmd = 1;
h[assignCmd[c]].note = cmdCode + c;
};
applyLayout();
}
void setupLEDs() { // need layout
strip.begin(); // INITIALIZE NeoPixel strip object
strip.show(); // Turn OFF all pixels ASAP
resetHexLEDs();
}
void setupMenu() { // need menu
menu.setSplashDelay(0);
menu.init();
buildMenu();
menuHome();
}
void setupGFX() {
u8g2.begin(); // Menu and graphics setup
u8g2.setBusClock(1000000); // Speed up display
u8g2.setContrast(defaultContrast); // Set contrast
}
void testDiagnostics() {
sendToLog(String("theHDM was here"));
}
// ====== loop routines
void timeTracker() {
lapTime = runTime - loopTime;
// sendToLog(String(lapTime)); // Print out the time it takes to
run each loop
loopTime = runTime; // Update previousTime variable to give us a
reference point for next loop
runTime = millis(); // Store the current time in a uniform
variable for this program loop
}
void screenSaver() {
if (screenTime <= screenSaverMillis) {
screenTime = screenTime + lapTime;
if (screenSaverOn) {
screenSaverOn = 0;
u8g2.setContrast(defaultContrast);
}
} else {
if (!screenSaverOn) {
screenSaverOn = 1;
u8g2.setContrast(1);
}
}
}
void readHexes() {
for (byte r = 0; r < rowCount; r++) { // Iterate through each of
the row pins on the multiplexing chip.
for (byte d = 0; d < 4; d++) {
digitalWrite(multiplexPins[d], (r >> d) & 1);
}
for (byte c = 0; c < colCount; c++) { // Now iterate through
each of the column pins that are connected to the current row pin.
byte p = columnPins[c]; // Hold the currently
selected column pin in a variable.
pinMode(p, INPUT_PULLUP); // Set that row pin to
INPUT_PULLUP mode (+3.3V / HIGH).
delayMicroseconds(10); // Delay to give the pin
modes time to change state (false readings are caused otherwise).
bool didYouPressHex = (digitalRead(p) == LOW); // hex is
pressed if it returns LOW. else not pressed
h[c + (r * colCount)].updateKeyState(didYouPressHex);
pinMode(p, INPUT); // Set the selected column pin back to
INPUT mode (0V / LOW).
}
}
}
void actionHexes() {
for (byte i = 0; i < hexCount; i++) { // For all buttons in the deck
switch (h[i].keyState) {
case 1: // just pressed
if (h[i].isCmd) {
cmdOn(i);
} else if (h[i].inScale || (!scaleLock)) {
noteOn(i);
};
break;
case 2: // just released
if (h[i].isCmd) {
cmdOff(i);
} else if (h[i].inScale || (!scaleLock)) {
noteOff(i);
};
break;
case 3: // held
break;
default: // inactive
break;
};
};
}
void arpeggiate() {
if (buzzer > 1) {
if (runTime - currentBuzzTime > arpeggiateLength) {
currentBuzzTime = millis();
byte nextNoteToBuzz = nextHeldNote();
if (nextNoteToBuzz < cmdCode) {
buzz(nextNoteToBuzz);
};
};
};
}
void updateWheels() {
velWheel.setTargetValue();
bool upd = velWheel.updateValue(); // this function returns a
boolean, gotta put it somewhere even if it isn't being used
if (upd) {
sendToLog(String("vel became " + String(velWheel.curValue)));
}
if (toggleWheel) {
pbWheel.setTargetValue();
upd = pbWheel.updateValue();
if (upd) {
chgUniversalPB();
};
} else {
modWheel.setTargetValue();
upd = modWheel.updateValue();
if (upd) {
chgModulation();
};
};
}
void animateLEDs() { // TBD
for (byte i = 0; i < hexCount; i++) {
h[i].animate = 0;
};
if (animationType) {
switch (animationType) {
case StarAnim: case SplashAnim:
animateRadial();
break;
case OrbitAnim:
animateOrbit();
break;
case OctaveAnim: case NoteAnim:
animateMirror();
break;
default:
break;
};
};
}
byte byteLerp(byte xOne, byte xTwo, float yOne, float yTwo, float y) {
float weight = (y - yOne) / (yTwo - yOne);
int temp = xOne + ((xTwo - xOne) * weight);
if (temp < xOne) {temp = xOne;};
if (temp > xTwo) {temp = xTwo;};
return temp;
}
void lightUpLEDs() {
for (byte i = 0; i < hexCount; i++) {
if (!(h[i].isCmd)) {
if (h[i].animate) {
strip.setPixelColor(i,h[i].LEDcolorAnim);
} else if (h[i].channel) {
strip.setPixelColor(i,h[i].LEDcolorPlay);
} else if (h[i].inScale) {
strip.setPixelColor(i,h[i].LEDcolorOn);
} else {
strip.setPixelColor(i,h[i].LEDcolorOff);
};
};
};
int16_t hueV = transformHue((runTime / rainbowDegreeTime) % 360);
strip.setPixelColor(assignCmd[0],strip.gamma32(strip.ColorHSV(
hueV,192,byteLerp(0,255,85,127,velWheel.curValue)
)));
strip.setPixelColor(assignCmd[1],strip.gamma32(strip.ColorHSV(
hueV,192,byteLerp(0,255,42,85,velWheel.curValue)
)));
strip.setPixelColor(assignCmd[2],strip.gamma32(strip.ColorHSV(
hueV,192,byteLerp(0,255,0,42,velWheel.curValue)
)));
if (toggleWheel) {
// pb red / green
int16_t hueP = transformHue((pbWheel.curValue > 0) ? 0 : 180);
byte satP = byteLerp(0,255,0,8192,abs(pbWheel.curValue));
strip.setPixelColor(assignCmd[3],strip.gamma32(strip.ColorHSV(
0,0,64
)));
strip.setPixelColor(assignCmd[4],strip.gamma32(strip.ColorHSV(
transformHue(0),satP * (pbWheel.curValue > 0),satP *
(pbWheel.curValue > 0)
)));
strip.setPixelColor(assignCmd[5],strip.gamma32(strip.ColorHSV(
hueP,satP,255
)));
strip.setPixelColor(assignCmd[6],strip.gamma32(strip.ColorHSV(
transformHue(180),satP * (pbWheel.curValue < 0),satP *
(pbWheel.curValue < 0)
)));
} else {
// mod blue / yellow
int16_t hueM = transformHue((modWheel.curValue > 63) ? 90 : 270);
byte satM = byteLerp(0,255,0,64,abs(modWheel.curValue - 63));
strip.setPixelColor(assignCmd[3],strip.gamma32(strip.ColorHSV(0,0,128)));
strip.setPixelColor(assignCmd[4],strip.gamma32(strip.ColorHSV(
hueM,satM,((modWheel.curValue > 63) ? satM : 0)
)));
strip.setPixelColor(assignCmd[5],strip.gamma32(strip.ColorHSV(
hueM,satM,((modWheel.curValue > 63) ? 127 + (satM / 2) : 127 -
(satM / 2))
)));
strip.setPixelColor(assignCmd[6],strip.gamma32(strip.ColorHSV(
hueM,satM,127 + (satM / 2)
)));
};
strip.show();
}
void dealWithRotary() {
if (menu.readyForKey()) {
rotaryIsClicked = digitalRead(rotaryPinC);
if (rotaryIsClicked > rotaryWasClicked) {
menu.registerKeyPress(GEM_KEY_OK);
screenTime = 0;
}
rotaryWasClicked = rotaryIsClicked;
if (rotaryKnobTurns != 0) {
for (byte i = 0; i < abs(rotaryKnobTurns); i++) {
menu.registerKeyPress(rotaryKnobTurns < 0 ? GEM_KEY_UP :
GEM_KEY_DOWN);
}
rotaryKnobTurns = 0;
screenTime = 0;
}
}
}
void readMIDI() {
MIDI.read();
}
void keepTrackOfRotaryKnobTurns() {
switch (rotary.process()) {
case DIR_CW:
rotaryKnobTurns++;
break;
case DIR_CCW:
rotaryKnobTurns--;
break;
}
}
// ====== setup() and loop()
void setup() {
#if (defined(ARDUINO_ARCH_MBED) && defined(ARDUINO_ARCH_RP2040))
TinyUSB_Device_Init(0); // Manual begin() is required on core
without built-in support for TinyUSB such as mbed rp2040
#endif
setupMIDI();
setupFileSystem();
setupPins();
testDiagnostics(); // Print diagnostic troubleshooting
information to serial monitor
setupGrid();
setupLEDs();
setupGFX();
setupMenu();
for (byte i = 0; i < 5 && !TinyUSBDevice.mounted(); i++) {
delay(1); // wait until device mounted, maybe
};
}
void setup1() {
//
};
void loop() { // run on first core
timeTracker(); // Time tracking functions
screenSaver(); // Reduces wear-and-tear on OLED panel
readHexes(); // Read and store the digital button states of
the scanning matrix
actionHexes(); // actions on hexes
arpeggiate(); // arpeggiate the buzzer
updateWheels(); // deal with the pitch/mod wheel
animateLEDs(); // deal with animations
lightUpLEDs(); // refresh LEDs
dealWithRotary(); // deal with menu
readMIDI();
}
void loop1() { // run on second core
keepTrackOfRotaryKnobTurns();
}