Firmware for HexBoard MIDI controller
-rw-r--r--HexBoard_V1.ino418
1 files changed, 418 insertions, 0 deletions
diff --git a/HexBoard_V1.ino b/HexBoard_V1.ino
new file mode 100644
index 0000000..4e142a4
--- /dev/null
+++ b/HexBoard_V1.ino
@@ -0,0 +1,418 @@
+// Hardware Information:
+// Teensy LC set to 48MHz with USB type MIDI
+
+//
+// Button matrix and LED locations
+// Portrait orientation top view:
+// 9 8 7 6 5 4 3 2 1
+// 20 19 18 17 16 15 14 13 12 11
+// 29 28 27 26 25 24 23 22 21
+// 40 39 38 37 36 35 34 33 32 31
+// 49 48 47 46 45 44 43 42 41
+// 60 59 58 57 56 55 54 53 52 51
+// 10 69 68 67 66 65 64 63 62 61
+// 30 80 79 78 77 76 75 74 73 72 71
+// 50 89 88 87 86 85 84 83 82 81
+// 70 100 99 98 97 96 95 94 93 92 91
+// 90 109 108 107 106 105 104 103 102 101
+//110 120 119 118 117 116 115 114 113 112 111
+// 130 129 128 127 126 125 124 123 122 121
+// 140 139 138 137 136 135 134 133 132 131
+
+//DIAGNOSTICS
+bool diagnostics = 0;
+
+// Define digital button matrix pins
+const byte columns[] = { 25, 24, 9, 8, 7, 6, 5, 4, 3, 2}; // Column pins in order from right to left
+const byte rows[] = {10, 11, 12, 13, 14, 15, 16, 18, 19, 20, 21, 22, 23, 26}; // Row pins in order from top to bottom
+// 16 & 17 reserved for lights.
+const byte columnCount = sizeof(columns); // The number of columns in the matrix
+const byte rowCount = sizeof(rows); // The number of rows in the matrix
+const byte elementCount = columnCount * rowCount;// The number of elements in the matrix
+
+// Since MIDI only uses 7 bits, we can give greater values special meanings.
+// (see commandPress)
+const int OCTAVEDOWN = 128;
+const int OCTAVEUP = 129;
+const int UNUSED = 255;
+
+// MIDI note value tables
+const byte wickiHaydenLayout[elementCount] = {
+ 78, 80, 82, 84, 86, 88, 90, 92, 94, OCTAVEUP,
+71, 73, 75, 77, 79, 81, 83, 85, 87, 89,
+ 66, 68, 70, 72, 74, 76, 78, 80, 82, OCTAVEDOWN,
+59, 61, 63, 65, 67, 69, 71, 73, 75, 77,
+ 54, 56, 58, 60, 62, 64, 66, 68, 70, UNUSED,
+47, 49, 51, 53, 55, 57, 59, 61, 63, 65,
+ 42, 44, 46, 48, 50, 52, 54, 56, 58, UNUSED,
+35, 37, 39, 41, 43, 45, 47, 49, 51, 53,
+ 30, 32, 34, 36, 38, 40, 42, 44, 46, UNUSED,
+23, 25, 27, 29, 31, 33, 35, 37, 39, 41
+};
+const byte harmonicTableLayout[elementCount] = {
+ 20, 27, 34, 41, 48, 55, 62, 69, 76, OCTAVEUP,
+17, 24, 31, 38, 45, 52, 59, 66, 73, 80,
+ 21, 28, 35, 42, 49, 56, 63, 70, 77, OCTAVEDOWN,
+18, 25, 32, 39, 46, 53, 60, 67, 74, 81,
+ 22, 29, 36, 43, 50, 57, 64, 71, 78, UNUSED,
+19, 26, 33, 40, 47, 54, 61, 68, 75, 82,
+ 23, 30, 37, 44, 51, 58, 65, 72, 79, UNUSED,
+20, 27, 34, 41, 48, 55, 62, 69, 76, 83,
+ 24, 31, 38, 45, 52, 59, 66, 73, 80, UNUSED,
+21, 28, 35, 42, 49, 56, 63, 70, 77, 84
+};
+const byte gerhardLayout[elementCount] = {
+ 20, 21, 22, 23, 24, 25, 26, 27, 28, OCTAVEUP,
+23, 24, 25, 26, 27, 28, 29, 30, 31, 32,
+ 27, 28, 29, 30, 31, 32, 33, 34, 35, OCTAVEDOWN,
+30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
+ 34, 35, 36, 37, 38, 39, 40, 41, 42, UNUSED,
+37, 38, 39, 40, 41, 42, 43, 44, 45, 46,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, UNUSED,
+44, 45, 46, 47, 48, 49, 50, 51, 52, 53,
+ 48, 49, 50, 51, 52, 53, 54, 55, 56, UNUSED,
+51, 52, 53, 54, 55, 56, 57, 58, 59, 60
+};
+
+//byte *currentLayout = &wickiHaydenLayout;
+
+// Global time variables
+unsigned long currentTime; // Program loop consistent variable for time in milliseconds since power on
+unsigned long timeBothPressed;
+const byte debounceTime = 2; // Global digital button debounce time in milliseconds
+
+// Variables for holding digital button states and activation times
+byte activeButtons[elementCount]; // Array to hold current note button states
+byte previousActiveButtons[elementCount]; // Array to hold previous note button states for comparison
+unsigned long activeButtonsTime[elementCount]; // Array to track last note button activation time for debounce
+
+// Control button states
+
+byte octaveUpState; // Top row (white) right
+byte octaveDownState; // Bottom row (white) right
+
+
+// MIDI channel assignment
+byte midiChannel = 0; // Current MIDI channel (changed via user input)
+
+// MIDI program variables
+byte midiProgram[16]; // MIDI program selection per channel (0-15)
+
+// Octave modifier
+int octave = 0;// Apply a MIDI note number offset (changed via user input in steps of 12)
+
+// Velocity levels
+byte velocity = 95; // Non-zero default velocity for testing; this will update via analog pot
+// END SETUP SECTION
+// ------------------------------------------------------------------------------------------------------------------------------------------------------------
+
+void setup()
+{
+ // Set pinModes for the digital button matrix.
+ for (int pinNumber = 0; pinNumber < columnCount; pinNumber++) // For each column pin...
+ {
+ pinMode(columns[pinNumber], INPUT_PULLUP); // set the pinMode to INPUT_PULLUP (+3.3V / HIGH).
+ }
+ for (int pinNumber = 0; pinNumber < rowCount; pinNumber++) // For each row pin...
+ {
+ pinMode(rows[pinNumber], INPUT); // Set the pinMode to INPUT (0V / LOW).
+ }
+}
+
+// ------------------------------------------------------------------------------------------------------------------------------------------------------------
+// START LOOP SECTION
+void loop()
+{
+ // Print diagnostic troubleshooting information to serial monitor
+ // diagnosticTest();
+
+ // Store the current time in a uniform variable for this program loop
+ currentTime = millis();
+
+ // Read and store the digital button states of the scanning matrix
+ readDigitalButtons();
+
+ // Set all states and values related to the control buttons and pots
+ // runControlModule(); //turned off for now
+
+ // Run the octave select function
+ // runOctave(); //wating until we get the basics sorted out
+
+ // Run the channel select function
+ //runChannelSelect();
+
+ // Send notes to the MIDI bus
+ playNotes();
+}
+// END LOOP SECTION
+// ------------------------------------------------------------------------------------------------------------------------------------------------------------
+
+
+// ------------------------------------------------------------------------------------------------------------------------------------------------------------
+// START FUNCTIONS SECTION
+
+void readDigitalButtons()
+{
+ // Button Deck
+ for (byte columnIndex = 0; columnIndex < columnCount; columnIndex++) // Iterate through each of the column pins.
+ {
+ if (diagnostics == 1){
+ Serial.println();
+ }
+ byte currentColumn = columns[columnIndex]; // Hold the currently selected column pin in a variable.
+ pinMode(currentColumn, OUTPUT); // Set that column pin to OUTPUT mode and...
+ digitalWrite(currentColumn, LOW); // set the pin state to LOW turning it into a temporary ground.
+ for (byte rowIndex = 0; rowIndex < rowCount; rowIndex++) // Now iterate through each of the row pins that are connected to the current column pin.
+ {
+ byte currentRow = rows[rowIndex]; // Hold the currently selected row pin in a variable.
+ pinMode(currentRow, INPUT_PULLUP); // Set that row pin to INPUT_PULLUP mode (+3.3V / HIGH).
+ byte buttonNumber = columnIndex + (rowIndex * columnCount); // Assign this location in the matrix a unique number.
+ if (diagnostics == 1){
+ Serial.print(buttonNumber);
+ Serial.print(" - ");
+ }
+ delayMicroseconds(50); // Delay to give the pin modes time to change state (false readings are caused otherwise).
+ byte buttonState = !digitalRead(currentRow); // Invert reading due to INPUT_PULLUP, and store the currently selected pin state.
+ if (buttonState == HIGH && (millis() - activeButtonsTime[buttonNumber]) > debounceTime) // If button is active and passes debounce
+ {
+ if (diagnostics == 1){
+ Serial.print("1, ");
+ }
+ activeButtons[buttonNumber] = 1; // write a 1 to the storage variable
+ activeButtonsTime[buttonNumber] = millis(); // and save the last button press time for later debounce comparison.
+ }
+ if (buttonState == LOW)
+ {
+ if (diagnostics == 1){
+ Serial.print("0, ");
+ }
+ activeButtons[buttonNumber] = 0; // Or if the button is inactive, write a 0.
+ }
+ pinMode(currentRow, INPUT); // Set the selected row pin back to INPUT mode (0V / LOW).
+ }
+ pinMode(currentColumn, INPUT); // Set the selected column pin back to INPUT mode (0V / LOW) and move onto the next column pin.
+ }
+}
+
+void runControlModule()
+{
+ // Digital Buttons
+ for (int buttonNumber = 9; buttonNumber < 30; buttonNumber++) // Limit to the 10 buttons in the control panel
+ {
+ if (activeButtons[buttonNumber] != previousActiveButtons[buttonNumber]) // Compare current button state to the previous state, and if a difference is found...
+ {
+ if (activeButtons[buttonNumber] == 1) // If the buttons is active
+ {
+ if (buttonNumber == 9) {
+ octaveUpState = HIGH;
+ }
+ if (buttonNumber == 29) {
+ octaveDownState = HIGH;
+ }
+ previousActiveButtons[buttonNumber] = 1; // Update the "previous" variable for comparison next loop
+ }
+ if (activeButtons[buttonNumber] == 0) // If the button is inactive
+ {
+
+ if (buttonNumber == 19) {
+ octaveUpState = LOW;
+ }
+ if (buttonNumber == 39) {
+ octaveDownState = LOW;
+ }
+ previousActiveButtons[buttonNumber] = 0; // Update the "previous" variable for conparison next loop
+ }
+ }
+ }
+}
+
+// TODO: We still want to be able to change octaves with the two buttons 19 and 39.
+void runOctave()
+{
+ /*if (metaKeyState == LOW) // If the meta key is not held
+ {
+ if (octaveUpState == HIGH && previousOctaveUpState == LOW && octave < 24) // Highest current Wicki-Hayden layout pitch is 94 - Keep pitch in bounds of 7 bit value range (0-127)
+ {
+ previousOctaveUpState = HIGH; // Lock input until released
+ for (int i = 10; i < elementCount; i++) // For all note buttons in the deck
+ {
+ activeButtons[i] = 0; // Pop any active notes to prevent hangs when abruptly changing octave modifier
+ }
+ playNotes();
+ octave = octave + 12; // Increment octave modifier
+ // LCD Update Octave Info
+ if (octave == 0) { lcd.setCursor(14,1); lcd.print(" 0"); }
+ if (octave == 12) { lcd.setCursor(14,1); lcd.write(0); lcd.print("1"); }
+ if (octave == 24) { lcd.setCursor(14,1); lcd.write(0); lcd.print("2"); }
+ if (octave == -12) { lcd.setCursor(14,1); lcd.write(1);lcd.print("1"); }
+ if (octave == -24) { lcd.setCursor(14,1); lcd.write(1);lcd.print("2"); }
+ }
+ if (octaveDownState == HIGH && previousOctaveDownState == LOW && octave > -23) // Lowest current Wicki-Hayden layout pitch is 30 - Keep pitch in bounds of 7 bit value range (0-127)
+ {
+ previousOctaveDownState = HIGH; // Lock input until released
+ for (int i = 10; i < elementCount; i++) // For all note buttons in the deck
+ {
+ activeButtons[i] = 0; // Pop any active notes to prevent hangs when abruptly changing octave modifier
+ }
+ playNotes();
+ octave = octave - 12; // Decrement octave modifier
+ // LCD Update Octave Info
+ if (octave == 0) { lcd.setCursor(14,1); lcd.print(" 0"); }
+ if (octave == 12) { lcd.setCursor(14,1); lcd.write(0); lcd.print("1"); }
+ if (octave == 24) { lcd.setCursor(14,1); lcd.write(0); lcd.print("2"); }
+ if (octave == -12) { lcd.setCursor(14,1); lcd.write(1);lcd.print("1"); }
+ if (octave == -24) { lcd.setCursor(14,1); lcd.write(1);lcd.print("2"); }
+ }
+ if (octaveUpState == HIGH && octaveDownState == HIGH && octave != 0) // If both keys are pressed simultaneously and octave is not default
+ {
+ for (int i = 10; i < elementCount; i++) // For all note buttons in the deck
+ {
+ activeButtons[i] = 0; // Pop any active notes to prevent hangs when abruptly changing octave modifier
+ }
+ playNotes();
+ octave = 0; // Reset octave modifier to 0
+ // LCD Update Octave Info
+ if (octave == 0) { lcd.setCursor(14,1); lcd.print(" 0"); }
+ if (octave == 12) { lcd.setCursor(14,1); lcd.write(0); lcd.print("1"); }
+ if (octave == 24) { lcd.setCursor(14,1); lcd.write(0); lcd.print("2"); }
+ if (octave == -12) { lcd.setCursor(14,1); lcd.write(1);lcd.print("1"); }
+ if (octave == -24) { lcd.setCursor(14,1); lcd.write(1);lcd.print("2"); }
+ }
+ }*/
+}
+
+
+
+void playNotes()
+{
+ for (int i = 0; i < elementCount; i++) // For all buttons in the deck
+ {
+ if (activeButtons[i] != previousActiveButtons[i]) // If a change is detected
+ {
+ if (activeButtons[i] == 1) // If the button is active (newpress)
+ {
+ if (wickiHaydenLayout[i] < 128) {
+ noteOn(midiChannel, (wickiHaydenLayout[i] + octave) % 128 , velocity);
+ } else {
+ commandPress(wickiHaydenLayout[i]);
+ }
+
+ previousActiveButtons[i] = 1; // Update the "previous" variable for comparison on next loop
+ }
+ if (activeButtons[i] == 0) // If the button is inactive (released)
+ {
+ if (wickiHaydenLayout[i] < 128) {
+ noteOff(midiChannel, (wickiHaydenLayout[i] + octave) % 128, 0);
+ } else {
+ commandRelease(wickiHaydenLayout[i]);
+ }
+
+ previousActiveButtons[i] = 0; // Update the "previous" variable for comparison on next loop
+ }
+ }
+ }
+}
+
+// MIDI PACKET FUNCTIONS
+
+// Send MIDI Note On
+// 1st byte = Event type (0x09 = note on, 0x08 = note off).
+// 2nd byte = Event type bitwise ORed with MIDI channel.
+// 3rd byte = MIDI note number.
+// 4th byte = Velocity (7-bit range 0-127)
+void noteOn(byte channel, byte pitch, byte velocity)
+{
+ usbMIDI.sendNoteOn(pitch, velocity, channel);
+}
+void loopNoteOn(byte channel, byte pitch, byte velocity)
+{
+
+}
+
+void commandPress(byte command)
+{
+ if(command == OCTAVEDOWN){
+ octave -= 12;
+ octaveDownState = HIGH;
+ } else if (command == OCTAVEUP){
+ octave += 12;
+ octaveUpState = HIGH;
+ }
+ if (octaveDownState && octaveUpState) {
+ timeBothPressed = currentTime;
+ } else {
+ timeBothPressed = 0;
+ }
+}
+void commandRelease(byte command)
+{
+ if(command == OCTAVEDOWN){
+ octaveDownState = LOW;
+ } else if (command == OCTAVEUP){
+ octaveUpState = LOW;
+ }
+ if (timeBothPressed && currentTime > timeBothPressed + 500){
+ octave = 0;
+ // also change modes
+ timeBothPressed = 0;
+ }
+}
+
+
+// Send MIDI Note Off
+// 1st byte = Event type (0x09 = note on, 0x08 = note off).
+// 2nd byte = Event type bitwise ORed with MIDI channel.
+// 3rd byte = MIDI note number.
+// 4th byte = Velocity (7-bit range 0-127)
+void noteOff(byte channel, byte pitch, byte velocity)
+{
+ usbMIDI.sendNoteOff(pitch, velocity, channel);
+}
+void loopNoteOff(byte channel, byte pitch, byte velocity)
+{
+
+}
+
+// Control Change
+// 1st byte = Event type (0x0B = Control Change).
+// 2nd byte = Event type bitwise ORed with MIDI channel.
+// 3rd byte = MIDI CC number (7-bit range 0-127).
+// 4th byte = Control value (7-bit range 0-127).
+void controlChange(byte channel, byte control, byte value)
+{
+
+}
+void loopControlChange(byte channel, byte control, byte value)
+{
+
+}
+
+// Program Change
+// 1st byte = Event type (0x0C = Program Change).
+// 2nd byte = Event type bitwise ORed with MIDI channel.
+// 3rd byte = Program value (7-bit range 0-127).
+void programChange(byte channel, byte value)
+{
+
+}
+
+// Pitch Bend
+// (14 bit value 0-16363, neutral position = 8192)
+// 1st byte = Event type (0x0E = Pitch bend change).
+// 2nd byte = Event type bitwise ORed with MIDI channel.
+// 3rd byte = The 7 least significant bits of the value.
+// 4th byte = The 7 most significant bits of the value.
+void pitchBendChange(byte channel, byte lowValue, byte highValue)
+{
+
+}
+void loopPitchBendChange(byte channel, byte lowValue, byte highValue)
+{
+
+}
+
+// END FUNCTIONS SECTION
+// ------------------------------------------------------------------------------------------------------------------------------------------------------------
+
+// END OF PROGRAM
+// ------------------------------------------------------------------------------------------------------------------------------------------------------------