├── circuit ├── theMIDInator-circuit.pdf └── theMIDInator-circuit.png ├── code └── theMIDInator │ ├── MIDImessage.ino │ ├── readRotSwitch.ino │ ├── readEncoder.ino │ ├── readPots.ino │ ├── readJoystick.ino │ ├── theMIDInator.ino │ └── readKeyPad.ino └── README.md /circuit/theMIDInator-circuit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwitchAndLever/theMIDInator/HEAD/circuit/theMIDInator-circuit.pdf -------------------------------------------------------------------------------- /circuit/theMIDInator-circuit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwitchAndLever/theMIDInator/HEAD/circuit/theMIDInator-circuit.png -------------------------------------------------------------------------------- /code/theMIDInator/MIDImessage.ino: -------------------------------------------------------------------------------- 1 | /* 2 | The MIDI message consists of three bytes, first setting the status 3 | of the message, and the following bytes denoting the action, which 4 | note number, velocity, pressure etc. The data bytes are different 5 | depending on the type of the message set by the status byte. 6 | 7 | Refer to the following chart for which bytes do what: 8 | 9 | https://www.midi.org/specifications-old/item/table-2-expanded-messages-list-status-bytes 10 | */ 11 | 12 | void MIDImessage(byte status, byte data1, byte data2) 13 | { 14 | Serial.write(status); 15 | Serial.write(data1); 16 | Serial.write(data2); 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # theMIDInator 2 | TheMIDInator is a MIDI controller based on an Arduino MEGA 2560. It uses a few potentiometers, rotary encoders, a joystick, switches and a keypad as inputs and sends MIDI commands via serial USB interface to the computer. For this controller to work properly a program such as Hairless MIDI to Serial Bridge needs to be running on the computer to receive the commands. This can then be piped into your DAW of choice. 3 | 4 | A video detailing the build of this MIDI controller can be found here: 5 | 6 | https://youtu.be/JZ5yPdoPooU 7 | 8 | [![Watch the video](https://img.youtube.com/vi/JZ5yPdoPooU/hqdefault.jpg)](https://youtu.be/JZ5yPdoPooU) -------------------------------------------------------------------------------- /code/theMIDInator/readRotSwitch.ino: -------------------------------------------------------------------------------- 1 | /* 2 | The rotary switch behaves just like any switch, it just has 3 | six positions rather than two or three like most switches. 4 | Reading the switch uses the internal pullup resistors in the 5 | Arduino, which means that the switch is normally HIGH when 6 | it's off, and normally LOW when it's on. 7 | 8 | The switch position is read and a value is stored in the 9 | cVal variable which is used by the rotary encoders to keep 10 | track of their values. 11 | */ 12 | 13 | void readRotSwitch() { // read position of rotary switch and assign it to cVal 14 | 15 | if (digitalRead(rotSwitch1) == LOW) { // inversed logic because of INPUT_PULLUP 16 | cVal = 4; 17 | } 18 | else if (digitalRead(rotSwitch2) == LOW) { 19 | cVal = 6; 20 | } 21 | else if (digitalRead(rotSwitch3) == LOW) { 22 | cVal = 8; 23 | } 24 | else if (digitalRead(rotSwitch4) == LOW) { 25 | cVal = 10; 26 | } 27 | else if (digitalRead(rotSwitch5) == LOW) { 28 | cVal = 12; 29 | } 30 | else if (digitalRead(rotSwitch6) == LOW) { 31 | cVal = 14; 32 | } else { 33 | 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /code/theMIDInator/readEncoder.ino: -------------------------------------------------------------------------------- 1 | /* 2 | The values of the two encoders are stored in an array. This 3 | allows the values to be remembered when changing inbetween 4 | encoders using the rotary switch. The rotary switch sets 5 | the cVal value which determines which position in the array 6 | the value is stored at, and retrieved from. 7 | 8 | While the rotary encoders can turn essentially forever their 9 | values are constrained to match MIDI note values (0-127). 10 | */ 11 | 12 | void readEncoder() { 13 | 14 | long newPos1 = myEnc1.read(); // reads both encoders 15 | long newPos2 = myEnc2.read(); 16 | 17 | if (newPos1 > position1) { // increases encoder #1 18 | position1 = newPos1; 19 | encVals[cVal]++; 20 | encVals[cVal] = constrain(encVals[cVal], 0, 127); 21 | 22 | MIDImessage(178, cVal, encVals[cVal]); 23 | } 24 | 25 | if (newPos1 < position1) { // decreases encoder #1 26 | position1 = newPos1; 27 | encVals[cVal]--; 28 | encVals[cVal] = constrain(encVals[cVal], 0, 127); 29 | 30 | MIDImessage(178, cVal, encVals[cVal]); 31 | } 32 | 33 | if (newPos2 > position2) { // increases encoder #2 34 | position2 = newPos2; 35 | encVals[cVal + 1]++; 36 | encVals[cVal + 1] = constrain(encVals[cVal + 1], 0, 127); 37 | 38 | MIDImessage(178, cVal + 1, encVals[cVal + 1]); 39 | } 40 | 41 | if (newPos2 < position2) { // decreases encoder #2 42 | position2 = newPos2; 43 | encVals[cVal + 1]--; 44 | encVals[cVal + 1] = constrain(encVals[cVal + 1], 0, 127); 45 | 46 | MIDImessage(178, cVal + 1, encVals[cVal + 1]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /code/theMIDInator/readPots.ino: -------------------------------------------------------------------------------- 1 | /* 2 | Because potentiometers, especially cheaper potentiometers, 3 | oftentimes can have a little bit of jitter, they may 4 | quickly switch between two adjacent values. Some care need 5 | to be taken to make sure to stabilize the values so not 6 | to constantly send MIDI messages, flooding communications 7 | and slowing down the program. 8 | */ 9 | 10 | void readPots() { 11 | 12 | int diff = 4; // difference amount 13 | 14 | // READ POTENTIOMETERS // 15 | 16 | potVal1 = analogRead(pot1); 17 | potVal2 = analogRead(pot2); 18 | potVal3 = analogRead(pot3); 19 | slidePotVal = analogRead(slidePot); 20 | 21 | // CALCULATE DIFFERENCE BETWEEN NEW VALUE AND LAST RECORDED VALUE // 22 | 23 | int potVal1diff = potVal1 - lastPotVal1; 24 | int potVal2diff = potVal2 - lastPotVal2; 25 | int potVal3diff = potVal3 - lastPotVal3; 26 | int slidePotValdiff = slidePotVal - lastSlidePotVal; 27 | 28 | // SEND MIDI MESSAGE // 29 | 30 | if (abs(potVal1diff) > diff) // execute only if new and old values differ enough 31 | { 32 | MIDImessage(177, 0, map(potVal1, 0, 1023, 0, 127)); // map sensor range to MIDI range 33 | lastPotVal1 = potVal1; // reset old value with new reading 34 | } 35 | 36 | if (abs(potVal2diff) > diff) 37 | { 38 | MIDImessage(177, 1, map(potVal2, 0, 1023, 0, 127)); 39 | lastPotVal2 = potVal2; 40 | } 41 | 42 | if (abs(potVal3diff) > diff) 43 | { 44 | MIDImessage(177, 2, map(potVal3, 0, 1023, 0, 127)); 45 | lastPotVal3 = potVal3; 46 | } 47 | 48 | if (abs(slidePotValdiff) > diff) 49 | { 50 | MIDImessage(177, 3, map(slidePotVal, 1023, 0, 0, 127)); 51 | lastSlidePotVal = slidePotVal; 52 | } 53 | 54 | delay(2); // small delay further stabilizes sensor readings 55 | 56 | } 57 | 58 | -------------------------------------------------------------------------------- /code/theMIDInator/readJoystick.ino: -------------------------------------------------------------------------------- 1 | /* 2 | The joystick is essentially two potentiometers, one 3 | in each direction. Due to their limited movement 4 | range and low range of resistance we are only 5 | reading a part of their range (~380-640) and 6 | therefore we have to map our output accordingly. 7 | Unfortunately that also means that the joystick 8 | will not have as high of a fidelity as a full range 9 | 10kOhm resistor, like we use elsewhere in The 10 | MIDInator, would have. 11 | 12 | It is also possible to turn off one or both axis of 13 | the joystick using the two toggle switches under the 14 | joystick. 15 | 16 | Because we are using the Y axis of the joystick to 17 | send a modulation MIDI message, like the modulation 18 | wheel you would find on many MIDI keyboards, some 19 | additional value splitting has to be done to be 20 | able to send a 14 bit value in two chunks. 21 | 22 | Because of inherent jitteryness of some potentiometers 23 | a 'difference' value is set up and checked. Events 24 | will only trigger if the value of the last recorded 25 | reading is higher than this value. Set to '1' if your 26 | potentiometer is very steady. A value of '2' will 27 | stop it from jittering between two adjacent numbers. 28 | If your potentiometer is very noisy a higher value 29 | may be required. Keep in mind that higher difference 30 | values will result in further less fidelity of the 31 | potentiometer reading. 32 | */ 33 | 34 | void readJoystick() { 35 | 36 | int diff = 2; 37 | 38 | // READ LEFT-RIGHT // 39 | if (digitalRead(Xswitch) == 0) { // execute only if X-switch is turned on 40 | 41 | joyXval = analogRead(joyX); 42 | int joyXvalDiff = joyXval - lastJoyXval; 43 | 44 | if (abs(joyXvalDiff) > diff) // send MIDI message only if new and old values differ enough 45 | { 46 | MIDImessage(176, 1, map(joyXval, 379, 637, 127, 0)); // map sensor range to MIDI range 47 | 48 | lastJoyXval = joyXval; // reset old value with new reading 49 | } 50 | delay(2); // for stability 51 | } 52 | 53 | 54 | // READ UP-DOWN FOR MODULATION // 55 | if (digitalRead(Yswitch) == 0) { 56 | 57 | joyYval = analogRead(joyY); 58 | int joyYvalDiff = joyYval - lastJoyYval; 59 | 60 | if (abs(joyYvalDiff) > diff) 61 | { 62 | // construct 14 bit modulation value and send over MIDI 63 | int modulation = map(joyYval, 361, 635, -8000, 8000); 64 | unsigned int change = 0x2000 + modulation; // 0x2000 == no Change 65 | unsigned char low = change & 0x7F; // low 7 bits 66 | unsigned char high = (change >> 7) & 0x7F; // high 7 bits 67 | 68 | MIDImessage(224, low, high); 69 | 70 | lastJoyYval = joyYval; 71 | } 72 | delay(2); 73 | } 74 | 75 | } 76 | 77 | -------------------------------------------------------------------------------- /code/theMIDInator/theMIDInator.ino: -------------------------------------------------------------------------------- 1 | // THE MIDINATOR // 2 | 3 | // Author: Daniel Jansson // Switch & Lever // switchandlever.com 4 | 5 | // This code is a companion to the YouTube video on how to build a MIDI controller (called 6 | // The MIDInator) linked below: 7 | 8 | // https://youtu.be/JZ5yPdoPooU 9 | 10 | // The MIDInator uses an Arduino MEGA 2560, connected to a keypad, a few potentiometers, rotary 11 | // encoders, switches and a joystick to send MIDI messages over USB serial to a Serial to 12 | // MIDI bridge software running on the computer. 13 | 14 | // COMPONENT LIST 15 | 16 | // 1x Arduino MEGA 2560 17 | // 3x 10kOhm (B10K) linear rotary potentiometer 18 | // 1x 10kOhm (B10K) linear slide potentiometer 19 | // 1x ALPS 6 position rotary switch (SRBM160700) 20 | // 2x ALPHA 12 step rotary encoder (RE130F-41-175F-12P) 21 | // 1x phone keypad from old Ericsson Diavox 22 | // 2x generic slide toggle switches 23 | // 1x generic (ON)-OFF-(ON) toggle switch 24 | // 1x remote control joystick (2 potentiometers) 25 | 26 | // The libraries used in this sketch can be found at: 27 | 28 | // Keypad.h :: https://github.com/Chris--A/Keypad 29 | // Encoder.h :: https://github.com/PaulStoffregen/Encoder 30 | 31 | // Please refer to the documentation for the individual libraries on their respective 32 | // operation and function. 33 | 34 | // This code is licensed under a GNU General Public License and may be freely modified 35 | // and redistributed under the same license terms. 36 | // https://www.gnu.org/licenses/gpl-3.0.en.html 37 | 38 | 39 | // KEYPAD // 40 | 41 | #include 42 | 43 | const byte ROWS = 4; // four rows 44 | const byte COLS = 3; // three columns 45 | char keys[ROWS][COLS] = { // keypad keys, 1-9, 0, S for star (asterisk) and P for pound (square) 46 | {'1', '2', '3'}, 47 | {'4', '5', '6'}, 48 | {'7', '8', '9'}, 49 | {'S', '0', 'P'} 50 | }; 51 | 52 | byte rowPins[ROWS] = {43, 41, 39, 35}; // keypad row pinouts 53 | byte colPins[COLS] = {33, 31, 37}; // keypad column pinouts 54 | 55 | Keypad kpd = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS ); 56 | 57 | int midC = 60; // MIDI note value for middle C on a standard keyboard 58 | 59 | const int transposePin1 = 22; // pins for the switch controlling transposing 60 | const int transposePin2 = 23; 61 | int transpose = 0; // if = 0 no transposing 62 | int transposeLeft = 0; 63 | int transposeRight = 0; 64 | int oldTransposeLeft = 0; 65 | int oldTransposeRight = 0; 66 | unsigned long transposeTimer = 0; // for debouncing 67 | 68 | 69 | // ROTARY ENCODER // 70 | 71 | #define ENCODER_DO_NOT_USE_INTERRUPTS 72 | #include 73 | 74 | Encoder myEnc1(26, 27); 75 | Encoder myEnc2(24, 25); 76 | long position1 = -999; 77 | long position2 = -999; 78 | int encVals[12] = {64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64}; // set initial value of encoder to mid range of 0-127 79 | 80 | 81 | // ROTARY SWITCH // 82 | 83 | const int rotSwitch1 = 30; // rotary switch pins 84 | const int rotSwitch2 = 32; 85 | const int rotSwitch3 = 34; 86 | const int rotSwitch4 = 36; 87 | const int rotSwitch5 = 38; 88 | const int rotSwitch6 = 40; 89 | int cVal = 1; 90 | 91 | 92 | // POTENTIOMETERS // 93 | 94 | const int pot1 = A0; // potentiometer pins 95 | const int pot2 = A1; 96 | const int pot3 = A2; 97 | const int slidePot = A3; 98 | 99 | int potVal1 = 0; 100 | int potVal2 = 0; 101 | int potVal3 = 0; 102 | int slidePotVal = 0; 103 | 104 | int lastPotVal1 = 0; 105 | int lastPotVal2 = 0; 106 | int lastPotVal3 = 0; 107 | int lastSlidePotVal = 0; 108 | 109 | 110 | // JOYSTICK // 111 | 112 | const int joyX = A5; // joystick pins 113 | const int joyY = A4; 114 | 115 | const int Xswitch = 52; // axis switche pins 116 | const int Yswitch = 50; 117 | 118 | int joyXval = 0; 119 | int joyYval = 0; 120 | int lastJoyXval = 0; 121 | int lastJoyYval = 0; 122 | 123 | 124 | void setup() { 125 | 126 | Serial.begin(9600); // enable serial communication 127 | 128 | pinMode(transposePin1, INPUT_PULLUP); // activate the input pullup resistor on all buttons/switches 129 | pinMode(transposePin2, INPUT_PULLUP); // means you won't need external resistors to read the buttons 130 | pinMode(rotSwitch1, INPUT_PULLUP); 131 | pinMode(rotSwitch2, INPUT_PULLUP); 132 | pinMode(rotSwitch3, INPUT_PULLUP); 133 | pinMode(rotSwitch4, INPUT_PULLUP); 134 | pinMode(rotSwitch5, INPUT_PULLUP); 135 | pinMode(rotSwitch6, INPUT_PULLUP); 136 | pinMode(Xswitch, INPUT_PULLUP); 137 | pinMode(Yswitch, INPUT_PULLUP); 138 | 139 | } 140 | 141 | 142 | void loop() { 143 | 144 | readRotSwitch(); // read roatary switch 145 | readEncoder(); // read roraty encoder 146 | readKeyPad(); // read keypad + transpose switch 147 | readPots(); // read potentiometers 148 | readJoystick(); // read joystick + on/off switches 149 | 150 | } 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /code/theMIDInator/readKeyPad.ino: -------------------------------------------------------------------------------- 1 | /* 2 | The keypad runs on the keypad.h library, which you also can use 3 | to run other similar keypads with similar pinouts, or even use 4 | regular micro switches in an array to build your own keypad. 5 | 6 | With the use of a bidirectional toggle switch the keypad 7 | mapping can also be transposed, or moved, up and down the 8 | scale. The keypad starts with middle C (note number 60) on the 9 | Nr 1 key. Using the transpose switch moves the keypad one entire 10 | octave up or down, within the full range (0 - 127) of available 11 | notes. 12 | */ 13 | 14 | int kpc = 144; // the function of the keypad, 144 = Channel 1 Note on 15 | 16 | void readKeyPad() { 17 | 18 | // TRANSPOSE SWITCH // 19 | 20 | transposeRight = digitalRead(transposePin1); 21 | 22 | if (oldTransposeRight != transposeRight && transposeRight == 0) { // detect switch change, and only do this once regardless how long it's held down 23 | if (millis() - transposeTimer > 1000) { // debounce so you can only do this once a second 24 | if (transpose < 48) transpose = transpose + 12; // only change transpose value if it's smaller than 48 25 | transposeTimer = millis();// reset debounce timer 26 | } 27 | } 28 | oldTransposeRight = transposeRight; 29 | 30 | transposeLeft = digitalRead(transposePin2); 31 | 32 | if (oldTransposeLeft != transposeLeft && transposeLeft == 0) { // same as above but to decrease the transpose value 33 | if (millis() - transposeTimer > 1000) { 34 | if (transpose > -60) transpose = transpose - 12; // only change transpose value if it's bigger than -60 35 | transposeTimer = millis(); 36 | } 37 | } 38 | oldTransposeLeft = transposeLeft; 39 | 40 | 41 | // KEYPAD // 42 | 43 | if (kpd.getKeys()) // supports up to ten simultaneous key presses 44 | { 45 | for (int i = 0; i < LIST_MAX; i++) // scan the whole key list 46 | { 47 | if ( kpd.key[i].stateChanged) // find keys that have changed state 48 | { 49 | if (kpd.key[i].kstate == PRESSED) // sends MIDI on message when keys are pressed 50 | { 51 | switch (kpd.key[i].kchar) { 52 | case '1': 53 | MIDImessage(kpc, midC + transpose + 0, 127); 54 | break; 55 | case '2': 56 | MIDImessage(kpc, midC + transpose + 1, 127); 57 | break; 58 | case '3': 59 | MIDImessage(kpc, midC + transpose + 2, 127); 60 | break; 61 | case '4': 62 | MIDImessage(kpc, midC + transpose + 3, 127); 63 | break; 64 | case '5': 65 | MIDImessage(kpc, midC + transpose + 4, 127); 66 | break; 67 | case '6': 68 | MIDImessage(kpc, midC + transpose + 5, 127); 69 | break; 70 | case '7': 71 | MIDImessage(kpc, midC + transpose + 6, 127); 72 | break; 73 | case '8': 74 | MIDImessage(kpc, midC + transpose + 7, 127); 75 | break; 76 | case '9': 77 | MIDImessage(kpc, midC + transpose + 8, 127); 78 | break; 79 | case 'S': 80 | MIDImessage(kpc, midC + transpose + 9, 127); 81 | break; 82 | case '0': 83 | MIDImessage(kpc, midC + transpose + 10, 127); 84 | break; 85 | case 'P': 86 | MIDImessage(kpc, midC + transpose + 11, 127); 87 | 88 | } 89 | } 90 | 91 | if (kpd.key[i].kstate == RELEASED) // sends MIDI off message when keys are released 92 | { 93 | switch (kpd.key[i].kchar) { 94 | case '1': 95 | MIDImessage(kpc - 16, midC + transpose + 0, 0); 96 | break; 97 | case '2': 98 | MIDImessage(kpc - 16, midC + transpose + 1, 0); 99 | break; 100 | case '3': 101 | MIDImessage(kpc - 16, midC + transpose + 2, 0); 102 | break; 103 | case '4': 104 | MIDImessage(kpc - 16, midC + transpose + 3, 0); 105 | break; 106 | case '5': 107 | MIDImessage(kpc - 16, midC + transpose + 4, 0); 108 | break; 109 | case '6': 110 | MIDImessage(kpc - 16, midC + transpose + 5, 0); 111 | break; 112 | case '7': 113 | MIDImessage(kpc - 16, midC + transpose + 6, 0); 114 | break; 115 | case '8': 116 | MIDImessage(kpc - 16, midC + transpose + 7, 0); 117 | break; 118 | case '9': 119 | MIDImessage(kpc - 16, midC + transpose + 8, 0); 120 | break; 121 | case 'S': 122 | MIDImessage(kpc - 16, midC + transpose + 9, 0); 123 | break; 124 | case '0': 125 | MIDImessage(kpc - 16, midC + transpose + 10, 0); 126 | break; 127 | case 'P': 128 | MIDImessage(kpc - 16, midC + transpose + 11, 0); 129 | 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | 137 | --------------------------------------------------------------------------------