Tuesday, May 19, 2020

Java Lighting Control Part 2 - Building a Simple Color and Brightness Controller

Monday, May 17th

In my previous post (available here), I explored integration of both of my Launchpads into Java. At the end, I teased a basic control project with both pads and explained I'd be posting about it. So here I am with the full project build process explained as best as I can.

The Goal

This is more of a first step in the ultimate goal of lighting control, rather than a functional project that I can actually use during gigs. My goal is to control up to 8 lights using both launchpads, one Launchpad to control the brightness and one to control the color.

Planning - Control Flow

The first thing I usually do when starting a Java project is draft the flow of data through the project so I can get an idea of what I'm trying to accomplish and what objects I will need to create. This usually takes the form of a whiteboard sketch.

From my initial brainstorming, I came up with the basics of what modules I need to build and how they communicate with each part of the program. I will need a way to create lighting fixture objects to store how many channels they have, what Art-Net universe they are on, and what their DMX address is. Then, I will need a fader class to act as a controller and middleman between the Launchpads and the lights. Finally, I will need to be able to store colors as both an RGB value for pushing to the lights and as a velocity value to push to the Launchpad.

As for the flow of data through the program, I ended up with two different control paths that I could use. The main difference between the two is how they handle pushing data to the lights.

First control flow idea
The first solution integrates everything I was talking about above, though the lines are a bit hard to follow. Let's start with the left of the program and work our way towards the right.

On the far left in pink are my two Launchpads, MIDI 1 being the MK2 and MIDI 2 being the S. Each push out data to one of two banks of faders, which are drawn in green. One bank of faders are single parameter control faders, which receive data from the Launchpad S and push back MIDI data to light up the Launchpad in a way that reflects the current state of the fader.

The second bank of faders, labeled '3fad', are responsible for holding the Red, Green, Blue, and White channels of the light they're assigned to. (To be fair, that's 4 parameters, it should be labeled '4fad'). This fader needs to be slightly more advanced so it can push RGBW commands to set the color of the light, as well as push color data back to the Launchpad MK2 display. The way it will do this is with the Color object I was talking about earlier that stores RGB(W) values alongside the velocity value for display on the launchpad.

In this solution, data from the faders is pushed to an array in each lighting object that stores the current value of each channel on that particular light. When it comes time to push that data over Art-Net, an object called a frame constructor puts together a list of commands for each channel on each light and pushes it to the Art-Net backbone of my program.

The Art-Net backbone is complex enough for it's own post (here when done), but for now it's important to explain the different types of data that it uses. The first is a DMXCommand, which is simply stores a channel, data value, and universe. The second is a DMXFrame, which is a collection of DMXCommands.

This was the first solution I came up with, but I realized that as more lights get added to the program it would start to take longer to update the universe since I would be generating new data based of the current state of the lights, even if nothing changed. This prompted me to generate a second solution that would be a bit more efficient at scale.

Second control flow idea
 Despite looking wildly different, there's actually a lot of similarities to the first solution here. MIDI input is handled exactly the same, and output to Art-Net is handled the same. The difference is that the faders send data both to the light objects and to the frame constructor, so that instead of rendering all channels all the time, the frame only contains changes that were made. The data is still stored to the lighting objects, however, just in case I need to get the current state of any given light in the future.

This should save vastly on processing time for larger projects, and even though the current project is a bit small, I feel it will be good to start building things efficiently the first time.

Coding Time

Fader Objects and MIDI Integration

I'll start by building the fader classes and setting up communication with MIDI devices. Upon starting this I realized I haven't implemented output for the Launchpad S or Launchpad MK2 classes, so I'll do a quick refresher of how I did that.

Turns out that translation from coordinates to values in the Launchpad S handler is actually super easy. I started by storing each row array into another array list. Now any time that I need to find a pitch value, I use the first value to find the row, and then the second value to find the number within that row.
I've also started commenting my code so it should be clearer
This is the whole code to go from a set of coordinates to a MIDI value with the Launchpad S. But it's clearly way more efficient than trying to go from a MIDI value to a set of coordinates.

Once you have the MIDI note value, all that's left is to output it to the Launchpad. This is handled super easily by one line, which takes a channel (always 0 in this case), pitch, and velocity and pushes it as a MIDI message to the desired bus.
Command to output a MIDI message to the desired bus
As you can imagine, the code for the Launchpad MK2 is a lot better since the MIDI notes are already a concatenation of the row and column.

The next step is to start writing the fader base class, which will be inherited by every different type of fader we will be creating. The base class stores the reference to the particular MIDI Handler we're using, the resolution of the fader, and the column of the launchpad which will serve as the control interface and display for the fader.

Once the base class was finished, I moved onto writing a single-parameter fader class. In addition to inheriting all the base functionality of the base class, this integrates functions for displaying the current state of the fader to the Launchpad in a pre-set velocity color, and updating the display and fader according to input from the MIDI bus.

The way I handle display is by creating an array called display which stores the state of each pixel in the form of a 1 for on, or 0 for off, with the index position representing the Y value of the pixel. The class also includes methods for displaying the data in that array, clearing that array, and updating it according to MIDI input.

In any given frame (representing one cycle of the program), the fader reaches to the MIDI bus it was assigned to when it was created to get the coordinates of the latest note pressed. If the column of the fader aligns with the X value of the data, then it clears the display and turns on any pixels at or beneath the Y value from the data. Finally, it checks through the display array and sends appropriate data to the Launchpad.

The best part is I wrote it in such a way where you can create as many faders as you want by specifying different column IDs. To demonstrate this, I created 8 faders and assigned each to a different column on the launchpad. The result looked something like this:

Faders in operation on the Launchpad S
 As you can see, you can control each fader by tapping on the corresponding button in any given column, and the display reacts accordingly. On the lighting control side, the higher a bar is, the brighter the light is going to be. Even though I haven't implemented Art-Net control to each fader quite yet, this is still a major milestone in actually using MIDI to control the program.

The next step is to write a 4-channel fader for controlling the color of each light. Thankfully, I have some experience now with writing the single-channel fader, however there's still work that needs to go into making this one. Instead of being one color, each pixel on the display has to be a different color, and rather than updating and changing the height of a bar, it simply needs to turn the pixel green when it is pressed to show that it's the current active color on that fader.

The way I plan to implement this is by creating a type of object called a LaunchColor. This stores a velocity value for use with a Launchpad, as well as Red, Green, Blue, and White values for use with Art-Net. I'll then create eight different displayable colors that I can reference and use with this particular fader.

The fader has two arrays, one is for the MIDI display and is purely for showing which color is in each slot. This array needs to be able to change to update the state of the display, which includes which color is active. However, because we also need an array to fall back on when we change the active color, and to continue sending up-to-date Art-Net data. For this reason, I will also be creating a default display array which is just to store what each slot is assigned to functionally.

There is also a method to set a pad color, which simply puts a color in the desired slot. This is so we can set what color is assigned to what pad, and change it if needed. Additionally, the updateDisplay and showDisplay methods are almost identical to the single fader, but output the display color and update the active display color instead of changing the pixels that are lit up.

Once I finished, I realized that running a test with 8 faders would require almost 20 lines of setup per fader. I decided to write a FaderBank class for each type of fader to store references to all my faders. This allows me to create all 8 4-channel faders at the same time, and also allows me to update and show each fader by calling the bank instead of calling each separately. I don't know what the impact on efficiency is, but it at least allows me to save time by not having to write as much code.

After that was done, I finally had my faders built, and it was time to build the actual lighting control side of things.
Finished Color and Brightness Faders
Lighting Fixture Objects and a Primer on DMX

The first thing to do is to create a lighting fixture class to hold data like the number of channels, current channel states, default channel states, and DMX starting address. Therefore, I can create a lighting fixture object for each light in my setup.

I'm sure you're wondering what all this actually is, so allow me to give you a quick primer in the ways of DMX.

DMX (Digital MultipleXing) is a common protocol used for controlling stage lights. It's capable of controlling 512 channels with values between 0 and 255 on a single line. This means that you can daisy-chain lights together with DMX, and as long as the total number of channels is less than or equal to 512, you can control each channel individually.

I have several different kinds of lights, though for this project I'll be using a couple of XPC Pars, which are cheap lights I bought in a bundle on Amazon (link here. They're good lights for what they are but bear in mind, you get what you pay for). Each light has 8 channels (dimmer, strobe, macro 1, macro 2, red, green, blue, and white) which means the total number of lights I can put up to 64 of these on a single DMX chain and still be able to control all 8 channels individually for each light. Light 1 would be address 1 on a chain, light 2 is address 9, light 3 is address 17, etc because each light covers it's starting address and as many channel addresses as it has channels.

The way I control my DMX lights using Java is with a protocol called Art-Net, which I've mentioned previously. Art-Net is a protocol that allows for lighting control using IP networking, which allows for more than 30,000 universes (sets of 512 channels) can be controlled individually. I'll go into more detail about how I implement this into my programs when I cover the Art-Net backbone, but for now just know that I control lights by pushing data to a DMX chain using Art-Net.

DMXCommands from Faders

Referring back to the control flow at the beginning of the project, each fader has to store the channel that it's going to be controlling, and output a value associated with that channel to Art-Net. In the case of the quad-channel fader, it has to be able to output data to 4 different channels at once. These come in the form of DMXCommands, a simple message that basically says "Set this channel to this value on this universe".

On the single-channel fader side of things, all I have to do is assign a light, channel, and universe to each fader so it knows what to control. Then I have to program a way for the MIDI commands to generate data to push to that channel and universe. In this case, the brightness should slowly increase as the Y value of the fader increases.

The good news is that I have a way to get the total Y value of the fader, and that's by adding up the value of all the integers in the display array. Being the math nerd that I am, I decided to hop on over to desmos.com/calculator, one of the greatest free resources on the whole of the internet to see if I could figure out what that equation is.

Graph depicting relationship of brightness to button pressed
I started by graphing 2 points to represent button 1 being 0 brightness and button 8 being full brightness (255). The equation for this line is y = 36x - 36. This means that the value output needs to be 36 less than 36 times the number of the button pressed. However, since Art-Net and DMX really don't like decimal numbers, we have to make sure we cast the result as a whole integer.

From there all we have to do is calculate what channel on the universe this data will be going to since we assign the channel to the fader using the channel per fixture, rather than per universe. This is mainly to keep my head straight, and it's a simple calculation to figure out which channel on the universe we are trying to control.

Calculation of channel in universe relative to channel in fixture.
And there you go, that's how you use math to control lights. After we have that information we just write a method to output the result of our above equation to the channel we get in the lower equation in the universe we get from the fixture that was defined on that fader.

Code for sending DMX data based on the Y value of the single fader
That's all for the single fader, and the quad fader doesn't have to do nearly as much math. However, it does have to output a series of commands for the Red, Green, Blue, and White channels of the fixture it's controlling. This is going to be much easier since we have an array full of colors already that we can pull the RGBW values from.

Code for sending DMX data based on active color of the 4-channel fader
All that's left is to find somewhere to push this data to, and that's where the Frame Constructor comes into play.

DMXFrame Constructor

The frame constructor is the final link between the faders and the Art-Net output. The first step of making this work is creating references to an active frame in both types of faders. Then we create an empty DMXFrame object and add it to all of the faders in our banks so that when the faders output data, it goes to that frame.

Finally, the frame is pushed to a newly created ArtnetRouter, an object that is part of the Art-Net backbone for this program and I will discuss in detail in my post about it. From there it's pushed to my Art-Net node and then out to my lights on the DMX chains!

The Finished Project

After adding all of the object creation to the main function of the program, the program should be fully operational! And there you have it! A simple color and brightness controller for up to 8 different lights written entirely in Java utilizing both Launchpads! This project took me two days to get working, but I think it's an excellent first step in my journey to create my own lighting control application.
Final project demonstration - brightness inconsistency due to phone camera being bad
Thanks for reading this far! I hope you enjoyed! Keep on making things!
-Will

No comments:

Post a Comment