Making an Arduino MIDI controller
Have you ever wanted a touchscreen MIDI controller, but don't want to pay tonnes of money for one? Well now you can make your own!Hardware List
- 3.2" TFT LCD Touch Shield w/ILI9341 Capacitive Touch Panel (I used this one with a capacitive touch screen. If the resistive ones are anything like the LG Cookie, they are probably not worth having...)
- Arduino Mega 2560 (which can be bought with the screen above. I believe this tutorial will also work with an Uno as long as it has a USB port.)
- A USBASP (these can be found for super cheap on Ebay. Get the ones with the ribbon cable and the little 10 to 6 pin converter boards)
Reprogramming the ATmega16U2 chip (USB - Arduino bridge)
To use the Arduino as a MIDI device, we will need to re-flash the USB controller on the Arduino. This USB controller is what usually lets you reprogramme the main chip, however, it also is what tells the computer what type of device is plugged in and sends the USB data. We need it to show up as a MIDI device and send MIDI data, so this chip needs reflashing. To begin reprogramming the chip, first download AVRDUDESS, which is a GUI for AVRDUDE (it also requires you install "libUSB"). You will also need the HEX file from the mocoLUFA git repo. Once you have both of these, open AVRDUDESS and configure it to the following settings:
- Programmer (-c): "Any usbasp clone with correct VID/PID"
- MCU (-p): "ATmega16U2"
- Port (-P): "usb"
- Flash: This needs to be the location of "dualMoco.hex" and set to "Write"
- EEPROM: This needs to be the location of "dualMoco.eep" and set to "Write"
- L Bit: 0xFF
- H Bit: 0xD9
- E Bit: 0xF4
- LB Bit: 0x0F
- Set Fuses: True
- Set Lock: True
Then plug one end of your USBAVR into your USB port and the other end onto the programming pins of the ATmega16U2 chip. Then press the Program! button. Once it finishes, unplug the Arduino and plug it back in. It should now show up on Windows as "MocoLUFA" and this means the installation was successful.
Programming the controller
You may have noticed now that your Arduino doesn't show up on a virtual COM port in the Arduino IDE. Don't panic, this is just a side effect of it being a MIDI controller. Luckily, the smart guy who made the MIDI controller system put in a subsystem that allows the ATmega16U2 flash the main chip still - although it does require additional work, and a screwdriver, to activate it. To activate this mode, connect the bottom left pin, bottom centre pin and bottom right pin (I usually do this with a can tab). This should cause the board LEDs to go off. Then slide it so its only covering the bottom left and bottom centre pins. This should cause the board LEDs to come back on, and it to show up as an Arduino again in Windows, allowing it to be selected in the Arduino IDE to programme.
Communicating with its many parts
- Blackketter's FT6206 Library (I would use the one in the Arduino library manager, but it doesn't work with my buydisplay.com touch screen. This will be fixed when this fix is merged.)
- Adafruit ILI9341) (found in the Arduino library manager)
- Adafruit GFX (also found in the Arduino library manager)
- Arduino MIDI Library (installed via ZIP)
#include <Adafruit_GFX.h> // Core graphics library
#include <SPI.h> // this is needed for display
#include <Adafruit_FT6206.h> // this is needed for FT6206 but we need this version from Blackkettler: https://github.com/blackketter/Adafruit_FT6206_Library (This will be fixed by this https://github.com/adafruit/Adafruit_FT6206_Library/pull/5)
#include <Adafruit_ILI9341.h> // this is needed for the display
#include <MIDI.h> // MIDI stuff
Now we need to define any variables which will be used in both the "setup" method and the "loop" method (as Arduino programs made using the Arduino IDE are split into these two methods, rather than a singular "main" method, like other C/C++ programs). These need to be defined outside of the main method or they will be out of scope in the loop method.
Adafruit_FT6206 TouchScreen = Adafruit_FT6206(); //create an instance of the touchscreen
Adafruit_ILI9341 tft = Adafruit_ILI9341(9, 7); //create an instance of the display (9 is the chip select pin and 7 is the data / control pin)
TS_Point p = TS_Point(0, 0, 0); //create a point for the point of touch
TS_Point p_old = TS_Point(0, 0, 0); //create a point for the previous point of touch
MIDI_CREATE_DEFAULT_INSTANCE(); //create an instance for MIDI data to be sent over
We also should define a base or background color for the display here we can easily get it in the code later, but also allow it to be quickly changed should it need to be.
#define BACKGROUND_COLOR ILI9341_WHITE // colour of the background
Now we can write the "setup" method. It needs to initialize the display, touchscreen and MIDI and set whatever we want to display before the loop begins. The following code initializes the "tft" (display), sets the text size and color for later use in the loop, fills the screen with the base color we defined earlier and then initializes the touchscreen and the MIDI communication. Remember, this all needs to be inside void setup(void){}
tft.begin(); //initalize the display
tft.setTextSize(2); //set the display text size
tft.setTextColor(ILI9341_BLACK); //set the display color
tft.fillScreen(BACKGROUND_COLOR); //set the display background colour
TouchScreen.begin(30); // pass in sensitivity coefficient and initalize the touch screen
MIDI.begin(4); //initalize the MIDI
Considerations on the loop
Finally, we need to write the loop method. Remember, we want to cut down on the amount of stuff done per loop as much as possible - the longer each loop takes, the less often we can send MIDI data, which is the reason we are doing this in the first place. Every time a pixel is drawn by the Arduino, it takes some amount of CPU time and means it is longer untill our loop can get to its end, meaning longer until the next touchscreen data is received and the next MIDI data is sent. This means we need to re-draw as few pixels as possible. Using the fill screen function in the display libraries is not an option either; looking under the hood shows this function is just the same as setting every single pixel individually for every place on the entire screen. This is fine for when our device is starting up (which is why I used it in the setup method), but not for real-time applications such as in this loop.
So how do we update the screen then?
Well, we can cut down on the number of pixels we are updating by only updating the ones we changed. That's why earlier we made two variables which store the location of the touch on the touch screen. One for the latest touch location, and one for the touch location before that one. By saving the latest touch location to the "old touch location" when we are done with it, we can use this data when we get a new touch location to erase anything we placed at the previous location, without having to update the entire screen. We can also do this to update any text. Just draw a base color rectangle over any text that needs updating, and then draw the new text over that.
Putting it all together
So firstly, we need to know if the touchscreen has been touched. If it hasn't been touched, then we just want the loop to keep looping, until it has been touched. Luckily, we can use if ( TouchScreen.touched() )
to check this every loop. If it has been touched, we need to get the point where it was touched with p = TouchScreen.getPoint();
. As mentioned earlier, we don't want to update the MIDI or the display if this location is the same as before, so let us check for that now with if(p != p_old)
.
Secondly, we need some maths to take in the raw touch location from the touchscreen and output the MIDI effect value, which will be between 0 and 127. We need to get the raw input number on a particular axis, divide it by the length of the screen (320) or the width of the screen (240) depending on the axis and then multiply it by the maximum MIDI value of 127. This, when translated to code, looks more or less like this:
int Effect1 = ((float)p.x / (float)ILI9341_TFTWIDTH * (float)127);
int Effect2 = ((float)p.y / (float)ILI9341_TFTHEIGHT * (float)127);
Then, we need to send this as MIDI data to an appropriate channel. I chose the EffectControl1 and EffectControl2 channels since they seemed most fitting for this device. To do this we use the following statements:
MIDI.send(midi::ControlChange, 1, (int)Effect1, midi::EffectControl1);
MIDI.send(midi::ControlChange, 1, (int)Effect2, midi::EffectControl2);
Finally, we need to draw the screen. I elected to have it so that a pair of lines intersect where the point of touching is registered. These lines go from the left to the right of the screen, and from the top to the bottom of the screen. But first, we need to draw over any existing lines with our background color by doing the following:
tft.drawFastHLine(0, p_old.y, ILI9341_TFTWIDTH, BACKGROUND_COLOR);
tft.drawFastVLine(p_old.x, 0, ILI9341_TFTHEIGHT, BACKGROUND_COLOR);
and now we can draw our new lines:
tft.drawFastHLine(0, p.y, ILI9341_TFTWIDTH, ILI9341_BLACK);
tft.drawFastVLine(p.x, 0, ILI9341_TFTHEIGHT, ILI9341_BLACK);
I also thought it would be a good idea to have the effect amounts being sent to each MIDI appear somewhere on the screen. To do this, first draw a background color rectangle over any text that was in this location before, before then drawing any new text which is needed.
tft.fillRect(150, 150, 36, 36, BACKGROUND_COLOR); //remove the previous effect numbers
tft.setCursor(150 , 150); //set the location of the text output of Effect1
tft.print(Effect1); //print it
tft.setCursor(150 , 170); //set the location of the text output of Effect2
tft.print(Effect2); //print it
Finally, we can close the bracket opened by if(p != p_old)
and use the statement p_old = p;
to save our current point so we can make use of it next time. When you put it all together, it looks something like this...
#include <Adafruit_GFX.h> // Core graphics library
#include <SPI.h> // this is needed for display
#include <Adafruit_FT6206.h> // this is needed for FT6206 but we need this version from Blackkettler: https://github.com/blackketter/Adafruit_FT6206_Library (This will be fixed by this https://github.com/adafruit/Adafruit_FT6206_Library/pull/5)
#include <Adafruit_ILI9341.h> // this is needed for the display
#include <MIDI.h> // MIDI stuff
#define TFT_CS 9 // Chip select pin
#define TFT_DC 7 // Data / control pin
#define TFT_HEIGHT 315 // set the max height for the display
#define TFT_WIDTH 239 // set the max width for the display
#define BASE_COLOR ILI9341_WHITE // colour of the background
Adafruit_FT6206 TouchScreen = Adafruit_FT6206(); //create an instance of the touchscreen
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC); //create an instance of the display
TS_Point p = TS_Point(0, 0, 0); //create a point for the point of touch
TS_Point p_old = TS_Point(0, 0, 0); //create a point for the previous point of touch
MIDI_CREATE_DEFAULT_INSTANCE(); //create an instance for MIDI data to be sent over
void setup(void) {
tft.begin(); //initalize the display
tft.setTextSize(2); //set the display text size
tft.setTextColor(ILI9341_BLACK); //set the display color
tft.fillScreen(BASE_COLOR); //set the display background colour
if (!TouchScreen.begin(30)) { // pass in 'sensitivity' coefficient and initalize the touch screen
while (1); //wait for the touch screen to begin
}
MIDI.begin(4); //initalize the MIDI
}
void loop() {
if ( TouchScreen.touched()) { //if the touch screen was touched this loop
p = TouchScreen.getPoint(); //get the point of the touch
int Modulation1 = ((float)p.x / (float)TFT_WIDTH * (float)127); //work out the new modulations
int Modulation2 = ((float)p.y / (float)TFT_HEIGHT * (float)127);//work out the new modulations
if(p != p_old){ //if the point of touch has changed
//pitch, velo, channel
//1 is the modulation wheel
MIDI.send(midi::ControlChange, 1, (int)Modulation1, 0xC); //send the MIDI modulation 1 on 0xC
MIDI.send(midi::ControlChange, 1, (int)Modulation2, 0xD); //send the MIDI modulation 2 on 0xD
tft.drawFastHLine(0, p_old.y, TFT_WIDTH, BASE_COLOR); //remove the old line by drawing over it
tft.drawFastVLine(p_old.x, 0, TFT_HEIGHT, BASE_COLOR); //remove the old line by drawing over it
tft.drawFastHLine(0, p.y, TFT_WIDTH, ILI9341_BLACK); //draw a new line
tft.drawFastVLine(p.x, 0, TFT_HEIGHT, ILI9341_BLACK); //draw a new line
// Print out raw data from screen touch controller
tft.fillRect(150, 150, 36, 36, BASE_COLOR); //remove the previous modulation numbers
tft.setCursor(150 , 150); //set the location of the text output of mod1
tft.print(Modulation1); //print it
tft.setCursor(150 , 170); //set the location of the text output of mod2
tft.print(Modulation2); //print it
}
p_old = p; //set the old point to the current point
}
}
There go, you should have a working MIDI controller! Use MIDI-OX or your favourite DAW to test it. Yay.