Published August 20, 2017 by Christopher Lee

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

You will need...
  • 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)
What the Arduino with the shield on looks like.

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:

The ATmega16U2 chip on the board
  • 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
The programming pins of the ATmega16U2 with the USBASP attached.

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.

MocoLUFA showing up in "Devices and Printers"

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.

How to bridge the pins with a can tab

Communicating with its many parts

So now we need libraries to allow our code to talk to the touchscreen input and the display and MIDI on the Arduino. To do this, we need the following libraries: After these are all installed, we need to include their header files at the start of a new Arduino code file.

		#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.

Yay! It works!