Sunday, May 17, 2020

Java Lighting Control Part 1 - Dual Launchpad Integration

Sunday, May 16th

I told you I'd have a post soon on my Java Lighting Control project so here I am. So far I've built the Art-Net backbone of the program, and so I decided that today's project was to implement MIDI support for both of my launchpads.

Quick primer - what is MIDI and why do I need it?

MIDI stands for Musical Instrument Digital Interface, and it's a technology that's been around for quite a while and something I am all to familiar with. It's basically a communication protocol that allows for MIDI devices to send channel, velocity, and pitch data from something like a MIDI keyboard to a computer based synthesizer without the keyboard itself doing the sound synthesis.

The reason I'm using MIDI as my control interface is because it will allow me to control the application with a pair of Launchpads. If you've spent any amount of time looking at electronic music covers on YouTube I can almost guarantee you've seen one of these before.

A launchpad outputs MIDI messages which I can read using a handy library I found for Java called TheMidiBus (available here for those who are curious). I settled on this Java library because it's nice and lightweight, and I couldn't get the default Java library working after three days of struggle. It doesn't offer nearly as many features as the default Java library, but in all honesty the added complexity of channels, tracks, and sequencers made the signal flow of the project a nightmare. This was the perfect solution.

There are two components of a MIDI message that I'm focused on in this project, which are the pitch and the velocity. On the input side, I'm only focused on the pitch since it tells me which button on the launchpad was pressed. However, in order to make the launchpad light up you have to send MIDI data back. The pitch tells the launchpad which button to light up, and the velocity tells it what color to light up.

It's a bit annoying not being able to use standard LED coloring with RGB values, but the easy solution to this problem is to write an object in Java that stores an RGB value for on-screen display alongside a velocity value to send to the Launchpad. That way I can read a color value to both the application and the launchpad, and both will have data to tell it what color it needs to use.

I've worked with MIDI before in various projects so this isn't my first time working with it. For example, I used Python to write a version of Pong that was controlled and outputted to my Launchpad MK2 (the code is available here for those who want to try it out for themselves. Though it only works for Launchpad MK2). Together with my friend Caden (who is a vastly better Java developer than I am), we wrote a version of this in Java to get the basics of MIDI input and output down. That code isn't publicly available yet but I imagine it will be at some point.

LaunchPong in Python
So I have figured out MIDI input with one device, however, with the way I'm building this control software I want to be able to run two Launchpads side by side to control various aspects of the lighting rig. So that was today's project.

My two Launchpads
On the left is my Launchpad MK2, the new guy on the block and the one featured in the above gif. I purchased it for reasons I will explain later, but it's gotten to see two gigs of lighting control before I ran out of gigs to DJ at. On the right is my Launchpad S, which I bought from a friend when I was first getting into music production. It's been running lights for almost the entire time I've had lights, and has been quite faithful, although it did have a problem.

The problem with the Launchpad S

The main problem I faced with the Launchpad S was due to the way that MIDI channels work. MIDI supports up to 16 different channels of data, which is important when you have, for example, several devices controlling different things. Whenever I would do DJ gigs, I would have two MIDI devices plugged into my laptop at any given time. The first was the Launchpad, which controlled lights via MIDI commands in a program called QLab (full post on this someday). The other was my DDJ-400, a DJ controller that ran Rekordbox, my DJing program of choice.

Both these devices work on the same MIDI channel (which defaults to channel 1 I believe). And while Rekordbox was good at making sure that the incoming data from the Launchpad didn't accidentally pause the music during a gig, QLab just took any MIDI commands that came in on the specified channel and controlled the lights regardless of what device the command had come from. I found myself accidentally turning all the lights green when I pushed play, or turning on strobe whenever I touched my EQ, which was not great. The worst part is, I was completely oblivious to this until I was DJing my Senior prom, and I was fortunate to have Caden as my lighting engineer for the night to correct my mistakes when I was DJing.

The solution was quite simple, all I had to do was change the MIDI channel that QLab was reading and the channel that the Launchpad was outputting. The problem was that Launchpad S is basically diet Launchpad, and while it allows you to change which device ID the Launchpad is (something that Ableton uses for distinguishing which device is which), it does not allow you to configure the MIDI channel. This is when I decided to purchase the Launchpad MK2 as my primary lighting controller since it allows the configuration of which MIDI channel it's outputting on.

This is actually one issue that I actually don't have to worry about in Java. When creating MIDI Bus objects using TheMidiBus library, you actually have to specify sending and receiving devices for that particular bus. Therefore I don't have to muck about with channels, I just have to make sure the correct devices are selected when setting them up.

The other problem with the Launchpad S

Most of my code for this test project was copied and pasted over from the LaunchPong Java project, which was written for the Launchpad MK2. I figured that the control mapping was the exact same on both devices, but I was dead wrong.


Control Mappings for Launchpad MK2
Above is a labeled diagram showing the pitch for each button. Starting in the bottom left with 11 and working up to 88 in the top right. This is really easy on me as far as control schemes go, because to get the X and Y coordinates of the button that was pressed, I just have to split the incoming pitch apart into row and column values. For example, a value of 36 would be row 3, column 6, or (6,3) in traditional Cartesian coordinates.


So I wrote a quick test project to implement both Launchpads controlling at once, and I discovered that I had a huge problem.

Control Mappings for Launchpad S
As you can see, the top left note is 0, and then they increment up by 1 until they reach note 8 at the end of the row. However, instead of continuing to increment up by 1 starting on the second row, it starts over at 16 and continues to do so. The next row starts at 32, and so on. In no way shape or form does it emulate the wonderful note mapping that is present on the MK2.

This is a slightly trickier problem to solve, since my code is written to process things in terms of X and Y values. This doesn't play nicely with the fact that the MK2 and the S have completely different numbering schemes.

The best way to explain this is to go over how my code works. If you're familiar with Java you're welcome to criticize me (please go easy I'm still learning). If you don't know how programming works, Don't be alarmed! I'll do my best to explain so it makes sense.

My first idea was to write a MIDIHandler class for the Launchpad, which would hold functions such as doing something when it received MIDI input, and translating the note that came in to X and Y coordinates for use in the program later. This was a complex beast of code, and since I'm not sure how to paste code into Blogger's editor I'm just going to share screenshots.

The class constructor and MIDI Listener
Methods for translating from data to coordinates and vice versa
The first screenshot shows the class constructor and the MIDI Listener. The first line creates a new MidiBus with the input and output devices that I specify (in our case the Launchpad MK2). The note pressed simply stores the pitch of the last note pressed. The listener is exactly what it sounds like, it simply listens for any input data and when it receives some, it executes the code in the brackets which is to record the pitch of the message that just came in.

The lowest code block on the top screenshot is the constructor, a set of code that's always executed when a new object is created of this particular type. In this case, it just assigns the created listener to the bus specified in the class so the listener knows to listen on that bus.

The lower screenshot uses Java magic to convert a single MIDI pitch into two separate numbers, and the other combines two numbers into one. I'll be honest, Caden wrote these so I'm not fully sure how they work. I just know that they do work and that's all that matters.

So it was here I discovered that copying my previous class from my last project wasn't going to be a good solution. It was here that I decided I needed to add a generous amount of Object Oriented Programming magic.

My idea was to take most of the functionality from this class and make a base class out of it that I can extend to the different types of Launchpad. If you aren't sure what I'm talking about, imagine that the Launchpad MK2 is an eagle and the Launchpad S is a hummingbird. Both are birds and perform bird-like actions such as eating and flying, however the Eagle lets out a majestic screech while a hummingbird hums? (I'll be honest I don't know what sound a hummingbird makes). The idea is to make an object that handles all the things a bird might need to do, and then to create objects that inherit those things from the bird, but also modify or introduce their own functionality depending on what type of bird they are.

Translating this into programming, I have to create a base object for actions that all MIDI devices will need like using a listener, outputting MIDI messages, and things like that. Then, I need to create separate objects for the Launchpad MK2 and Launchpad S since they handle MIDI data differently. I know for sure that more work is going to go into the Launchpad S class since it has to handle translating seemingly arbitrary numbers into X and Y coordinates.

There are several ways I could've done this, but I ended up using an array approach and a whole bunch of if statements (If statements are simple code logic blocks, which see if something is true and if it is, does a thing), along with pre-defined arrays for each row on the Launchpad S.

Forgive my horrible use of white space, it's mainly for making the explanation clearer
The easiest explanation for these is that each array represents a row on the Launchpad (indexed with bottom being 1 so it's like the Launchpad MK2) and each spot in that array represents what column it is. For example, note 17 is row 7, column 2, or (2,7). I know computers start counting at 0 but because Launchpads start at 1, I will be 1-indexing in the MIDIHandler class (I will be 0-indexing everywhere else though).

Prepare for Spaghetti, and make it double
Calling this method will return an array containing the row in the 1st position and the column in the 2nd position. The way it accomplishes this is by checking if the note is within a specific range, and if it is, it runs through the row to see if the note is inside and if it is, it returns what slot it's in +1, since I'm starting my Cartesian coordinates at (1,1).

Is this the best solution? Absolutely not. Is it the solution I know how to program? Yes, it is. I'm sorry for subjecting your eyes to such madness, but I promise it works.

Thankfully, the method inside Launchpad MK2's handler is much more graceful.

This is the whole thing

Basically, when you ask for an array of coordinates from the Launchpad MK2, it simply converts the note into a string, grabs the first and second characters of the string and turns them back into notes, and then returns an array with those numbers as row and column coordinates. Super easy, not painful at all. This version of note to coordinate translation is my own method, because I wanted to prove I am capable of writing one.

After some testing, I've proven this successful, and I'll call it good for my MIDIHandlers for the Launchpad MK2 and the Launchpad S as far as note to coordinate translation is concerned!

The final problem with the Launchpad S

This one is more of a complaint about the design choice and how it is going to be limiting in terms of functionality. Remember when I called my Launchpad S the diet Launchpad? Well for whatever reason Novation decided not to include a blue LED in the Launchpad S's pads, so they're limited on the color range they can show to shades of green, orange, red, and yellow. This is way more limited than the Launchpad S's full RGB pads, which can show 127 different colors depending on what velocity value is passed.

This isn't a deal breaker for an initial test prototype, but it is annoying since I will absolutely be utilizing the full RGB capabilities of the Launchpad when it comes time to start putting together a more refined version of the software.

Final test project

So with all of this knowledge and work that I've done, I wanted to build a basic color and dimmer control for Art-Net output using both launchpads. The Launchpad MK2 will be taking on the job of setting the color for one of four lights, and the Launchpad S will take on the job of being the brightness control. It's not a super fancy application but it's simple enough to demonstrate proof-of-concept for MIDI to Art-Net out. Because the whole program is complex enough to be it's own post, I will be writing one on just that project alone and that will be here when it releases!

For now, thanks for reading! Keep on making things!
-Will

2 comments:

  1. Great work! Nice write up. When sharing code, sometimes I will create a “gist” of it on github and embed it in the blog.

    One of my favorite axioms is: “Start where you are. Use what you have. Do what you can.” This is how we level up in life. This post is a great example of that.

    ReplyDelete