Firmware for HexBoard MIDI controller
Diffstat (limited to 'HexBoard_V1.1.ino')
-rw-r--r--HexBoard_V1.1.ino321
1 files changed, 254 insertions, 67 deletions
diff --git a/HexBoard_V1.1.ino b/HexBoard_V1.1.ino
index 82669a2..ccdeee5 100644
--- a/HexBoard_V1.1.ino
+++ b/HexBoard_V1.1.ino
@@ -12,6 +12,7 @@
#include <Arduino.h>
#include <Adafruit_TinyUSB.h>
+#include "LittleFS.h"
#include <MIDI.h>
#include <Adafruit_NeoPixel.h>
#define GEM_DISABLE_GLCD
@@ -29,9 +30,10 @@ MIDI_CREATE_INSTANCE(Adafruit_USBD_MIDI, usb_midi, MIDI);
#define LED_PIN 22
#define LED_COUNT 140
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_RGB + NEO_KHZ800);
-int defaultBrightness = 50;
-int dimBrightness = 15;
-int pressedBrightness = 180;
+int stripBrightness = 110;
+int defaultBrightness = 70;
+int dimBrightness = 20;
+int pressedBrightness = 255;
// ENCODER SETUP //
@@ -46,6 +48,7 @@ uint8_t encoder_state;
// Create an instance of the U8g2 graphics library.
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R2, /* reset=*/U8X8_PIN_NONE);
+int screenBrightness = stripBrightness / 2;
//
// Button matrix and LED locations
@@ -69,6 +72,7 @@ U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R2, /* reset=*/U8X8_PIN_NONE);
// 1 = Full button test (1 and 0)
// 2 = Button test (button number)
// 3 = MIDI output test
+// 4 = Loop timing readout in milliseconds
int diagnostics = 0;
// BUTTON MATRIX PINS //
@@ -158,29 +162,37 @@ const byte gerhardLayout[elementCount] = {
const byte *currentLayout = wickiHaydenLayout;
const unsigned int pitches[128] = {
- 16,17,18,19,21,22,23,25,26,28,29,31, // Octave 0
- 33,35,37,39,41,44,46,49,52,55,58,62, // Octave 1
- 65, 69, 73, 78, 82, 87, 93, 98,104,110,117,123, // Octave 2
- 131,139,147,156,165,175,185,196,208,220,233,247, // Octave 3
- 262,277,294,311,330,349,370,392,415,440,466,494, // Octave 4
- 523,554,587,622,659,698,740,784,831,880,932,988, // Octave 5
- 1047,1109,1175,1245,1319,1397,1480,1568,1661,1760,1865,1976, // Octave 6
- 2093,2217,2349,2489,2637,2794,2960,3136,3322,3520,3729,3951, // Octave 7
- 4186,4435,4699,4978,5274,5588,5920,6272,6645,7040,7459,7902, // Octave 8
- 8372,8870,9397,9956,10548,11175,11840,12544,13290,14080,14917,15804, //9
- 16744, // C10
- 17740, // C#10
- 18795, // D10
- 19912, // D#10
- 21096, // E10
- 22350, // F10
- 23680 // F#10
+ 16, 17, 18, 19, 21, 22, 23, 25, 26, 28, 29, 31, // Octave 0
+ 33, 35, 37, 39, 41, 44, 46, 49, 52, 55, 58, 62, // Octave 1
+ 65, 69, 73, 78, 82, 87, 93, 98, 104, 110, 117, 123, // Octave 2
+ 131, 139, 147, 156, 165, 175, 185, 196, 208, 220, 233, 247, // Octave 3
+ 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, // Octave 4
+ 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, // Octave 5
+ 1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, // Octave 6
+ 2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951, // Octave 7
+ 4186, 4435, 4699, 4978, 5274, 5588, 5920, 6272, 6645, 7040, 7459, 7902, // Octave 8
+ 8372, 8870, 9397, 9956, 10548, 11175, 11840, 12544, 13290, 14080, 14917, 15804, //9
+ 16744, // C10
+ 17740, // C#10
+ 18795, // D10
+ 19912, // D#10
+ 21096, // E10
+ 22350, // F10
+ 23680 // F#10
};
#define TONEPIN 23
// Global time variables
-unsigned long currentTime; // Program loop consistent variable for time in milliseconds since power on
-const byte debounceTime = 2; // Global digital button debounce time in milliseconds
+unsigned long currentTime = 0; // Program loop consistent variable for time in milliseconds since power on
+unsigned long previousTime = 0; // Used to check speed of the loop in diagnostics mode 4
+int loopTime = 0; // Used to keep track of how long each loop takes. Useful for rate-limiting.
+int screenTime = 0; // Used to dim screen after a set time to prolong the lifespan of the OLED
+
+// Pitch bend variables
+int pitchBendNeutral = 0; // The center position for the pitch bend "wheel." Could be adjusted for global tuning?
+int pitchBendTime = 0; // Used to rate-limit pitch bend updates
+int pitchBendPosition = 0; // The actual pitch bend variable used for sending MIDI
+int pitchBendSpeed = 2048; // The ammount the pitch bend moves every time pitchBendTime hits it's limit.
// Variables for holding digital button states and activation times
byte activeButtons[elementCount]; // Array to hold current note button states
@@ -220,26 +232,33 @@ GEMSelect selectTranspose(sizeof(selectTransposeOptions) / sizeof(SelectOptionBy
void validateTranspose(); // Forward declaration
GEMItem menuItemTranspose("Transpose:", transpose, selectTranspose, validateTranspose);
-//bool highlightScale = true; // whether the black keys should be dimmer REMOVING THIS SOON
-//GEMItem menuItemHighlightScale("Scale Light:", highlightScale, setLayoutLEDs);
+void resetPitchBend();
+SelectOptionInt selectBendSpeedOptions[] = { { "SO SLOW", 256 }, { "Slow", 512 }, { "Medium", 1024 }, { "Fast", 2048 }, { "Cheetah", 4096 } };
+GEMSelect selectBendSpeed(sizeof(selectBendSpeedOptions) / sizeof(SelectOptionInt), selectBendSpeedOptions);
+GEMItem menuItemBendSpeed("Pitch Bend:", pitchBendSpeed, selectBendSpeed, resetPitchBend);
+
+void setBrightness(); //Forward declaration
+SelectOptionByte selectBrightnessOptions[] = { { "Dim", 30 }, { "Low", 70 }, { "Medium", 110 }, { "High", 160 }, { "Highest", 210 }, { "MAX(!!)", 255 } };
+GEMSelect selectBrightness(sizeof(selectBrightnessOptions) / sizeof(SelectOptionByte), selectBrightnessOptions);
+GEMItem menuItemBrightness("Brightness:", stripBrightness, selectBrightness, setBrightness);
+bool buzzer = false; // For enabling built-in buzzer for sound generation without a computer
+GEMItem menuItemBuzzer("Buzzer:", buzzer);
// Create menu object of class GEM_u8g2. Supply its constructor with reference to u8g2 object we created earlier
byte menuItemHeight = 10;
byte menuPageScreenTopOffset = 10;
-byte menuValuesLeftOffset = 86;
+byte menuValuesLeftOffset = 78;
GEM_u8g2 menu(u8g2, GEM_POINTER_ROW, GEM_ITEMS_COUNT_AUTO, menuItemHeight, menuPageScreenTopOffset, menuValuesLeftOffset);
// MIDI channel assignment
byte midiChannel = 1; // Current MIDI channel (changed via user input)
-
// Velocity levels
byte midiVelocity = 100; // Default velocity
-bool buzzer = 0;
// END SETUP SECTION
// ------------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -261,6 +280,15 @@ void setup() {
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");
+ }
+
+
// Set pinModes for the digital button matrix.
for (int pinNumber = 0; pinNumber < columnCount; pinNumber++) // For each column pin...
{
@@ -271,20 +299,22 @@ void setup() {
pinMode(m4p, OUTPUT);
pinMode(m8p, OUTPUT);
- strip.begin(); // INITIALIZE NeoPixel strip object
- strip.show(); // Turn OFF all pixels ASAP
- strip.setBrightness(255); // Set BRIGHTNESS (max = 255)
+ strip.begin(); // INITIALIZE NeoPixel strip object
+ strip.show(); // Turn OFF all pixels ASAP
+ strip.setBrightness(stripBrightness); // Set BRIGHTNESS (max = 255)
setCMD_LEDs();
- strip.setPixelColor(cmdBtn1, strip.ColorHSV(65536 / 12, 255, defaultBrightness));
+ strip.setPixelColor(cmdBtn1, strip.ColorHSV(65536 / 12, 255, pressedBrightness));
setLayoutLEDs();
u8g2.begin(); //Menu and graphics setup
+ u8g2.setContrast(stripBrightness / 2);
+ menu.setSplashDelay(0);
menu.init();
setupMenu();
menu.drawMenu();
// wait until device mounted, maybe
- for (int i=0; i<5 && !TinyUSBDevice.mounted();i++) delay(1);
+ for (int i = 0; i < 5 && !TinyUSBDevice.mounted(); i++) delay(1);
// Print diagnostic troubleshooting information to serial monitor
diagnosticTest();
@@ -299,8 +329,12 @@ void setup1() { //Second core exclusively runs encoder
// ------------------------------------------------------------------------------------------------------------------------------------------------------------
// START LOOP SECTION
void loop() {
- // Store the current time in a uniform variable for this program loop
- currentTime = millis();
+
+ // Time tracking function
+ timeTracker();
+
+ // Reduces wear-and-tear on OLED panel
+ screenSaver();
// Read and store the digital button states of the scanning matrix
readDigitalButtons();
@@ -308,6 +342,9 @@ void loop() {
// Act on those buttons
playNotes();
+ // Pitch bend stuff
+ pitchBend();
+
// Held buttons
heldButtons();
@@ -317,26 +354,12 @@ void loop() {
// Read any new MIDI messages
MIDI.read();
- // Read menu functions
- if (menu.readyForKey()) {
- encoderState = digitalRead(encoderClick);
- if (encoderState > encoderLastState) {
- menu.registerKeyPress(GEM_KEY_OK);
- }
- encoderLastState = encoderState;
- if (encoder_val < 0) {
- menu.registerKeyPress(GEM_KEY_UP);
- encoder_val = 0;
- }
- if (encoder_val > 0) {
- menu.registerKeyPress(GEM_KEY_DOWN);
- encoder_val = 0;
- }
- }
+ // Read menu navigation functions
+ menuNavigation();
}
void loop1() {
- rotate();
+ rotate(); // Reads the encoder on the second core to avoid missed steps
//readEncoder();
}
// END LOOP SECTION
@@ -346,6 +369,17 @@ void loop1() {
// ------------------------------------------------------------------------------------------------------------------------------------------------------------
// START FUNCTIONS SECTION
+void timeTracker() {
+ loopTime = currentTime - previousTime;
+ if (diagnostics == 4) { //Print out the time it takes to run each loop
+ Serial.println(loopTime);
+ }
+ // Update previouTime variable to give us a reference point for next loop
+ previousTime = currentTime;
+ // Store the current time in a uniform variable for this program loop
+ currentTime = millis();
+}
+
void diagnosticTest() {
if (diagnostics > 0) {
Serial.println("Zach was here");
@@ -356,20 +390,17 @@ void commandPress(byte command) {
if (command == CMDB_1) {
midiVelocity = 100;
setCMD_LEDs();
- strip.setPixelColor(cmdBtn1, strip.ColorHSV(65536 / 12, 255, defaultBrightness));
- strip.setBrightness(255); // Set BRIGHTNESS (max = 255)
+ strip.setPixelColor(cmdBtn1, strip.ColorHSV(65536 / 12, 255, pressedBrightness));
}
if (command == CMDB_2) {
midiVelocity = 60;
setCMD_LEDs();
- strip.setPixelColor(cmdBtn2, strip.ColorHSV(65536 / 3, 255, defaultBrightness));
- strip.setBrightness(127); // Set BRIGHTNESS (max = 255)
+ strip.setPixelColor(cmdBtn2, strip.ColorHSV(65536 / 3, 255, pressedBrightness));
}
if (command == CMDB_3) {
midiVelocity = 20;
setCMD_LEDs();
- strip.setPixelColor(cmdBtn3, strip.ColorHSV(65536 / 2, 255, defaultBrightness));
- strip.setBrightness(63); // Set BRIGHTNESS (max = 255)
+ strip.setPixelColor(cmdBtn3, strip.ColorHSV(65536 / 2, 255, pressedBrightness));
}
if (command == CMDB_4) {
}
@@ -378,13 +409,121 @@ void commandPress(byte command) {
if (command == CMDB_6) {
}
if (command == CMDB_7) {
- buzzer = !buzzer;
- strip.setPixelColor(cmdBtn7, strip.ColorHSV(65536 / 2, 255, 2*defaultBrightness*buzzer));
}
}
void commandRelease(byte command) {
}
+void pitchBend() { //todo: possibly add a check where if no notes are active, make the pitch bend instant. Add LED updates.
+
+ if (activeButtons[89] && !activeButtons[109] && !activeButtons[129]) { // Whole pitch up
+ pitchBendTime = pitchBendTime + loopTime;
+ if (pitchBendTime >= 20) {
+ pitchBendTime = 0;
+ if (pitchBendPosition < 8192) {
+ pitchBendPosition = pitchBendPosition + pitchBendSpeed;
+ if (pitchBendPosition > 8191) { // This is a hack to prevent going over the maximum number (8191) that MIDI pitchbend can go without messing up my math.
+ MIDI.sendPitchBend(8191, midiChannel);
+ }
+ if (pitchBendPosition <= 8191) {
+ MIDI.sendPitchBend(pitchBendPosition, midiChannel);
+ }
+ }
+ }
+ }
+ if (activeButtons[89] && activeButtons[109] && !activeButtons[129]) { // Half pitch up
+ pitchBendTime = pitchBendTime + loopTime;
+ if (pitchBendTime >= 20) {
+ pitchBendTime = 0;
+ if (pitchBendPosition > 4096) {
+ pitchBendPosition = pitchBendPosition - pitchBendSpeed;
+ MIDI.sendPitchBend(pitchBendPosition, midiChannel);
+ }
+ if (pitchBendPosition < 4096) {
+ pitchBendPosition = pitchBendPosition + pitchBendSpeed;
+ MIDI.sendPitchBend(pitchBendPosition, midiChannel);
+ }
+ }
+ }
+ if (!activeButtons[89] && activeButtons[109] && activeButtons[129]) { // Half pitch down
+ pitchBendTime = pitchBendTime + loopTime;
+ if (pitchBendTime >= 20) {
+ pitchBendTime = 0;
+ if (pitchBendPosition > -4096) {
+ pitchBendPosition = pitchBendPosition - pitchBendSpeed;
+ MIDI.sendPitchBend(pitchBendPosition, midiChannel);
+ }
+ if (pitchBendPosition < -4096) {
+ pitchBendPosition = pitchBendPosition + pitchBendSpeed;
+ MIDI.sendPitchBend(pitchBendPosition, midiChannel);
+ }
+ }
+ }
+ if (!activeButtons[89] && !activeButtons[109] && activeButtons[129]) { // Whole pitch down
+ pitchBendTime = pitchBendTime + loopTime;
+ if (pitchBendTime >= 20) {
+ pitchBendTime = 0;
+ if (pitchBendPosition > -8192) {
+ pitchBendPosition = pitchBendPosition - pitchBendSpeed;
+ MIDI.sendPitchBend(pitchBendPosition, midiChannel);
+ }
+ }
+ }
+ if (!activeButtons[89] && !activeButtons[129]) { // Neutral pitch
+ if (pitchBendTime != 200) {
+ pitchBendTime = pitchBendTime + loopTime;
+ if (pitchBendTime >= 20) {
+ pitchBendTime = 0;
+ if (pitchBendPosition > 0) {
+ pitchBendPosition = pitchBendPosition - pitchBendSpeed;
+ MIDI.sendPitchBend(pitchBendPosition, midiChannel);
+ }
+ if (pitchBendPosition < 0) {
+ pitchBendPosition = pitchBendPosition + pitchBendSpeed;
+ MIDI.sendPitchBend(pitchBendPosition, midiChannel);
+ }
+ }
+ if (pitchBendPosition == 0) {
+ pitchBendTime = 200;
+ }
+ }
+ }
+ if (activeButtons[89] && activeButtons[109] && activeButtons[129]) { // Neutral pitch case two where all buttons are pressed - kinda hacky
+ if (pitchBendTime != 200) {
+ pitchBendTime = pitchBendTime + loopTime;
+ if (pitchBendTime >= 20) {
+ pitchBendTime = 0;
+ if (pitchBendPosition > 0) {
+ pitchBendPosition = pitchBendPosition - pitchBendSpeed;
+ MIDI.sendPitchBend(pitchBendPosition, midiChannel);
+ }
+ if (pitchBendPosition < 0) {
+ pitchBendPosition = pitchBendPosition + pitchBendSpeed;
+ MIDI.sendPitchBend(pitchBendPosition, midiChannel);
+ }
+ }
+ if (pitchBendPosition == 0) {
+ pitchBendTime = 200;
+ }
+ }
+ }
+ if (pitchBendPosition > 0) {
+ strip.setPixelColor(cmdBtn5, strip.ColorHSV(0, 255, ((pitchBendPosition / 32) - 1)));
+ strip.setPixelColor(cmdBtn6, strip.ColorHSV(0, 255, (-pitchBendPosition / 32)));
+ strip.setPixelColor(cmdBtn7, strip.ColorHSV(0, 255, 0));
+ }
+ if (pitchBendPosition == 0) {
+ strip.setPixelColor(cmdBtn5, strip.ColorHSV(0, 255, 0));
+ strip.setPixelColor(cmdBtn6, strip.ColorHSV(0, 255, 255));
+ strip.setPixelColor(cmdBtn7, strip.ColorHSV(0, 255, 0));
+ }
+ if (pitchBendPosition < 0) {
+ strip.setPixelColor(cmdBtn5, strip.ColorHSV(0, 255, 0));
+ strip.setPixelColor(cmdBtn6, strip.ColorHSV(0, 255, (pitchBendPosition / 32)));
+ strip.setPixelColor(cmdBtn7, strip.ColorHSV(0, 255, ((-pitchBendPosition / 32) - 1)));
+ }
+}
+
// BUTTONS //
void readDigitalButtons() {
if (diagnostics == 1) {
@@ -415,7 +554,6 @@ void readDigitalButtons() {
// newpress time
activeButtonsTime[buttonNumber] = millis();
}
- // TODO: Implement debounce?
activeButtons[buttonNumber] = 1;
} else {
// Otherwise, the button is inactive, write a 0.
@@ -488,17 +626,18 @@ void noteOn(byte channel, byte pitch, byte velocity) {
Serial.println(channel);
}
if (buzzer) {
- tone(TONEPIN, pitches[pitch], 1000);
+ tone(TONEPIN, pitches[pitch]);
}
}
// Send Note Off
void noteOff(byte channel, byte pitch, byte velocity) {
MIDI.sendNoteOff(pitch, velocity, channel);
noTone(TONEPIN);
- if(buzzer) {
+ if (buzzer) {
byte anotherPitch = getHeldNote();
if (anotherPitch < 128) {
- tone(TONEPIN, pitches[anotherPitch], 1000);
+ tone(TONEPIN, pitches[anotherPitch]);
+ } else {
}
}
}
@@ -509,9 +648,6 @@ void setCMD_LEDs() {
strip.setPixelColor(cmdBtn2, strip.ColorHSV(65536 / 3, 255, dimBrightness));
strip.setPixelColor(cmdBtn3, strip.ColorHSV(65536 / 2, 255, dimBrightness));
strip.setPixelColor(cmdBtn4, strip.ColorHSV(0, 255, defaultBrightness));
- strip.setPixelColor(cmdBtn5, strip.ColorHSV(0, 255, defaultBrightness));
- strip.setPixelColor(cmdBtn6, strip.ColorHSV(0, 255, defaultBrightness));
- strip.setPixelColor(cmdBtn7, strip.ColorHSV(0, 255, defaultBrightness));
}
void setLayoutLEDs() {
@@ -608,13 +744,36 @@ void encoder_init() {
}
// MENU //
+void menuNavigation() {
+ if (menu.readyForKey()) {
+ encoderState = digitalRead(encoderClick);
+ if (encoderState > encoderLastState) {
+ menu.registerKeyPress(GEM_KEY_OK);
+ screenTime = 0;
+ }
+ encoderLastState = encoderState;
+ if (encoder_val < 0) {
+ menu.registerKeyPress(GEM_KEY_UP);
+ encoder_val = 0;
+ screenTime = 0;
+ }
+ if (encoder_val > 0) {
+ menu.registerKeyPress(GEM_KEY_DOWN);
+ encoder_val = 0;
+ screenTime = 0;
+ }
+ }
+}
+
void setupMenu() {
// Add menu items to Main menu page
menuPageMain.addMenuItem(menuItemLayout);
menuPageMain.addMenuItem(menuItemKey);
menuPageMain.addMenuItem(menuItemScale);
- //menuPageMain.addMenuItem(menuItemHighlightScale); REMOVING SOON
menuPageMain.addMenuItem(menuItemTranspose);
+ menuPageMain.addMenuItem(menuItemBendSpeed);
+ menuPageMain.addMenuItem(menuItemBrightness);
+ menuPageMain.addMenuItem(menuItemBuzzer);
// Add menu items to Layout Select page
menuPageLayout.addMenuItem(menuItemWickiHayden);
menuPageLayout.addMenuItem(menuItemHarmonicTable);
@@ -629,22 +788,29 @@ void setupMenu() {
void wickiHayden() {
currentLayout = wickiHaydenLayout;
setLayoutLEDs();
+ //u8g2.setDisplayRotation(U8G2_R2); IMPLEMENT ROTATION WITH NEXT HARDWARE REVISION USING 128*128 SCREEN
menu.setMenuPageCurrent(menuPageMain);
menu.drawMenu();
}
void harmonicTable() {
currentLayout = harmonicTableLayout;
setLayoutLEDs();
+ //u8g2.setDisplayRotation(U8G2_R1);
menu.setMenuPageCurrent(menuPageMain);
menu.drawMenu();
}
void gerhard() {
currentLayout = gerhardLayout;
setLayoutLEDs();
+ //u8g2.setDisplayRotation(U8G2_R1);
menu.setMenuPageCurrent(menuPageMain);
menu.drawMenu();
}
+void setBrightness() {
+ strip.setBrightness(stripBrightness);
+ setLayoutLEDs();
+}
// Validation routine of transpose variable
void validateTranspose() {
@@ -656,4 +822,25 @@ void validateTranspose() {
setLayoutLEDs();
}
+//Resets pitch bend to zero to avoid glitches when changing speed mid-bend
+void resetPitchBend() {
+ pitchBendPosition = pitchBendNeutral;
+ MIDI.sendPitchBend(pitchBendPosition, midiChannel);
+}
+
+void screenSaver() {
+ if (screenTime <= 10000) {
+ screenTime = screenTime + loopTime;
+ if (screenBrightness != stripBrightness / 2) {
+ screenBrightness = stripBrightness / 2;
+ u8g2.setContrast(screenBrightness);
+ }
+ }
+ if (screenTime > 10000)
+ if (screenBrightness != 1) {
+ screenBrightness = 1;
+ u8g2.setContrast(screenBrightness);
+ }
+}
+
// END FUNCTIONS SECTION