In this project, we’re going to build a MIDI box to test the CoolSoft synthesizer instruments and play a few notes. We’ll be using three sensors to modify MIDI messages. You’re free to add more elements to make a more complete MIDI controller.
Prerequisites: Creating a MIDI interface with Arduino
Equipment
- Arduino UNO
- potentiometer
- rotary encoder
- analog 4×4 keyboard
Features
To test MIDI communication and the synthesizer. We’re going to define a few simple functions:
- Using the keyboard, we’re going to send musical notes
- Le potentiometer permet d’ajuster la vitesse de la note
- And the encoder will modify the synthesized instrument
N.B.: We chose an analog keyboard for this project for its ease of wiring, so that we could quickly test the synthesizer. The disadvantage is that you can only press one key at a time. For a more effective MIDI controller, you can opt for a digital keyboard or arcade buttons to give feedback on the notes played.
Diagram
N.B.: For ease of reading, we’ve removed the 7-segment display. You can find the connection in this tutorial.
Code description
First, we’ll define functions to manage each sensor.
For the 4×4 keyboard, we assign each key an identifier that corresponds to a note. We create a function that returns the identifier when a key is pressed.
const int valThresh[nbABtn] = {1000, 900, 820, 750, 660, 620, 585, 540, 500, 475, 455, 425, 370, 300, 260, 200}; const int pitches[nbABtn] = {50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80 }
int getABtn() { /* function getABtn */
//// Read button states from keypad
int val = analogRead(abtnPin);
if (val <= 200) {
return 0;
} else {
for (int i = 0; i < 16; i++) {
if (val > valThresh[i]) {
delay(100);
return i + 1;
}
}
}
}
We define a readPot function which will modify the velocity value as a function of the potenciómetro value velVal.
void readPot(){ velVal = map(int(analogRead(potPin) / 10) * 10, 0, 1023, 0, 127); if (oldVel != velVal) { //Serial.println(velVal); velocity = velVal; } oldVel = velVal; }
The encoder function allows us to modify the musical instrument (rotVal identifier between 0 and 127).
void readRotary( ) { /* function readRotary */ ////Test routine for Rotary // gestion position clkState = digitalRead(clkPin); if ((clkLast == LOW) && (clkState == HIGH)) {//rotary moving if (digitalRead(dtPin) == HIGH) { rotVal = rotVal - 1; if ( rotVal < 0 ) { rotVal = 0; } } else { rotVal++; if ( rotVal > 127 ) { rotVal = 127; } } MIDImessage(prgmChg, rotVal, 0); number = rotVal; for (int i = 0; i < NUM_OF_DIGITS; i++) { digit_data[i] = number % 10; number /= 10; } delay(200); } clkLast = clkState; //gestion bouton swState = digitalRead(swPin); if (swState == LOW && swLast == HIGH) { delay(100);//debounce } swLast = swState; }
Once the instrument modification has been taken into account, we display its value on module 7 Segment.
void Display(int id, unsigned char num) { digitalWrite(latch, LOW); shiftOut(data, cs, MSBFIRST, table[num]); digitalWrite(latch, HIGH); for (int j = 0; j < NUM_OF_DIGITS; j++) digitalWrite(dPins[j], LOW); digitalWrite(dPins[id], HIGH); } void updateDigits() { for (int j = 0; j < NUM_OF_DIGITS; j++){ Display(j, digit_data[j]); delay(2); } }
Finally, we return to the function that allows us to send MIDI messages
void MIDImessage(byte command, byte MIDInote, byte MIDIvelocity) { Serial.write(command);//send note on or note off command Serial.write(MIDInote);//send pitch data if (command == noteON || command == noteOFF) { Serial.write(MIDIvelocity);//send velocity data } }
All that remains is to send the message when a button is pressed.
void readAbtn() { /* function readAbtn */ //// Read button states from keypad btnId = getABtn(); if (btnId) { if (lastBtnId != btnId) { MIDImessage(noteOFF, pitches[lastBtnId - 1], velocity); } MIDImessage(noteON, pitches[btnId - 1], velocity); //turn note on lastBtnId = btnId; } }
Full MIDIbox code
byte velocity = 50;//velocity of MIDI notes, must be between 0 and 127 byte noteON = 144;// = 10010000 , Note On byte noteOFF = 128;// = 10000000 , Note Off byte pitchBend = 224; // = 11100000, Pitch Bender Change byte polyKey = 160; // = 10100000, Polyphonic Key Pressure byte overallPr = 208; // = 11010000, Overall Pressure byte prgmChg = 192; // = 11000000, Program Change byte ctrlChg = 176; // = 10110000, Control Change //Keypad #define nbABtn 16 const int abtnPin = A1; const int valThresh[nbABtn] = {1000, 900, 820, 750, 660, 620, 585, 540, 500, 475, 455, 425, 370, 300, 260, 200}; const int pitches[nbABtn] = {50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80 }; bool btnPressed = false; //Potentiometer const int potPin = A0; // Pin connected to sensor int velVal = 0, oldVel = 0; // Analog value from the sensor //rotary var const int clkPin = 53; const int dtPin = 51; const int swPin = 49; int rotVal = 0, btnId = 0, lastBtnId = 0; bool clkState = LOW; bool clkLast = HIGH; bool swState = HIGH; bool swLast = HIGH; //4x7Segment #define NUM_OF_DIGITS 4 int latch = 4; //74HC595 pin 9 STCP int cs = 5; //74HC595 pin 10 SHCP int data = 3; //74HC595 pin 8 DS int dPins[4] = {8, 9, 10, 11}; // DP G F E D C B A //0: 1 1 0 0 0 0 0 0 0xc0 //1: 1 1 1 1 1 0 0 1 0xf9 //2: 1 0 1 0 0 1 0 0 0xa4 //3: 1 0 1 1 0 0 0 0 0xb0 //4: 1 0 0 1 1 0 0 1 0x99 //5: 1 0 0 1 0 0 1 0 0x92 //6: 1 0 0 0 0 0 1 0 0x82 //7: 1 1 1 1 1 0 0 0 0xf8 //8: 1 0 0 0 0 0 0 0 0x80 //9: 1 0 0 1 0 0 0 0 0x90 unsigned char table[] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90}; int digit_data[NUM_OF_DIGITS] = {0}; unsigned int number = 0; unsigned long previousUpdate = 0, updateTime = 200; void setup() { Serial.begin(115200); pinMode(potPin, INPUT); pinMode(clkPin, INPUT); pinMode(dtPin, INPUT); pinMode(swPin, INPUT_PULLUP); pinMode(latch, OUTPUT); pinMode(cs, OUTPUT); pinMode(data, OUTPUT); for (int j = 0; j < NUM_OF_DIGITS; j++) pinMode(dPins[j], OUTPUT); } void loop() { readRotary(); updateDigits(); readAbtn(); readPot(); } void MIDImessage(byte command, byte MIDInote, byte MIDIvelocity) { Serial.write(command);//send note on or note off command Serial.write(MIDInote);//send pitch data if (command == noteON || command == noteOFF) { Serial.write(MIDIvelocity);//send velocity data } } void readAbtn() { /* function readAbtn */ //// Read button states from keypad btnId = getABtn(); if (btnId) { if (lastBtnId != btnId) { MIDImessage(noteOFF, pitches[lastBtnId - 1], velocity); } MIDImessage(noteON, pitches[btnId - 1], velocity); //turn note on lastBtnId = btnId; } } int getABtn() { /* function getABtn */ //// Read button states from keypad int val = analogRead(abtnPin); if (val <= 200) { return 0; } else { for (int i = 0; i < 16; i++) { if (val > valThresh[i]) { delay(100); return i + 1; } } } } //Potentiometer void readPot(){ velVal = map(int(analogRead(potPin) / 10) * 10, 0, 1023, 0, 127); if (oldVel != velVal) { //Serial.println(velVal); velocity = velVal; } oldVel = velVal; } //7Segment void Display(int id, unsigned char num) { digitalWrite(latch, LOW); shiftOut(data, cs, MSBFIRST, table[num]); digitalWrite(latch, HIGH); for (int j = 0; j < NUM_OF_DIGITS; j++) digitalWrite(dPins[j], LOW); digitalWrite(dPins[id], HIGH); } void updateDigits() { for (int j = 0; j < NUM_OF_DIGITS; j++) { Display(j, digit_data[j]); delay(2); } } //rotary void readRotary( ) { /* function readRotary */ ////Test routine for Rotary // gestion position clkState = digitalRead(clkPin); if ((clkLast == LOW) && (clkState == HIGH)) {//rotary moving if (digitalRead(dtPin) == HIGH) { rotVal = rotVal - 1; if ( rotVal < 0 ) { rotVal = 0; } } else { rotVal++; if ( rotVal > 127 ) { rotVal = 127; } } MIDImessage(prgmChg, rotVal, 0); number = rotVal; for (int i = 0; i < NUM_OF_DIGITS; i++) { digit_data[i] = number % 10; number /= 10; } delay(200); } clkLast = clkState; //gestion bouton swState = digitalRead(swPin); if (swState == LOW && swLast == HIGH) { delay(100);//debounce } swLast = swState; }
Results
To test the code, open the hairless and VirtualMIDISynth programs as described in this article.
If everything is set up correctly, you should hear sounds coming from your computer when you press keys on the keyboard.
You can also change the note speed by playing the potentiometer.
Between two notes played, you can then select another synthesizer instrument using the rotary encoder.
Next steps
- Use of LEDs or illuminated buttons
- MIDI In management to light LEDs
- Use of an OLED screen for a more comprehensive menu