The DivingBoard's controls are mostly read by the Arduino Nano, and relayed via USB serial to the Raspberry Pi which then outputs MIDI messages to the synth via USB MIDI, and pertinent information to the user via the character LCD.
User input is via a series of rotary encoders and rotary potentiometers. The Raspberry Pi doesn't have analogue pins and so can't read potentiometers easily, and can't keep up with the rapid-fire digital signals required to read encoders. My solution for this is to have an Arduino do the direct reading from these controls, and then relay that information to the Raspberry Pi over the serial connection.
The Arduino programme, controlReporter.ino
, maintains a list of the current values returned by each pin hooked up to a control, with all values initialised to 0. It reads each control in turn repeatedly, and when one is found to have changed beyond a threshold, sends a message over the serial connection in the form ,x,y,
, where x
is the individual number of that control, and y
is the new value of that control (more on the seemingly unnecessary extra commas shortly). Once the message is sent, the programme updates the list of control values with the new one, and then continues to iterate through the control pin reading routine.
To read the encoders, the programme uses the Encoder.h
library. As the encoders have infinite rotation, they don't have an absolute value and so instead of the y
part of the message to the Pi being a number, it's a plus or minus symbol, depending on what value the encoder has and had in the Arduino programme. The encoders also contain momentary switches- these inputs are handled by the Pi using the gpiozero
library.
On the Pi side of the serial connection, the Serial
library is used. Upon successful connection between the two devices, the Arduino sends a message once per second, which says b''
. This actually came in quite useful later, but is mainly ignored. Any message from the Arduino in the ,x,y,
format specified earlier arrives as a string like so: b'',x,y,\\r\\n
. Now the extra commas I added to the message hopefully start to make sense- using the Python split
operation and specifying the ,
character allows me to split the string into a list, and grab x
and y
directly from it as the second and third elements of that list. One quirk of this whole thing is that the encoders like to send four identical messages for every adjustment made to them. In order to make this workable for users, there's a buffer for encoder adjustments which only executes the action once at least four identical messages are recieved.
In the main Python programme, divingBoard.py
, the main body of the code runs in a while True
loop, which ends with the ser.readline()
command. This causes the programme to wait until a new message comes from the Arduino- either a control update, in which case appropriate MIDI messages will be sent to the synth- or one of those once-per-second b''
messages mentioned above. These are very useful, as they ensure that the main programme loop runs at least once per second, regardless of user input. The main way this has been used is in the 'quit' button- not something you'd want to accidentally trigger by brushing it. Instead, the programme checks to see if any buttons are depressed each time the loop runs. If the same button registers as depressed over two consecutive runs of the main loop, it's taken to have been held down for over a second, and the linked function executes.
The Python library Mido
is the backbone of the DivingBoard project. With it, ports can be opened to both send and recieve MIDI messages from connected USB MIDI devices, and messages can be converted into different formats and mutated, decontructed or built from scratch as desired. The DivingBoard learns the MIDI messages associated with a parameter on the synth by reading them directly from the synth, which has options to output every CC and Sysex message it sends, even those it would usually keep to itself. This method of parameter-learning should therefore be usable with any MIDI device that can output like this, making the DivingBoard potentially compatible with a range of synths/keyboards/etc.
To learn a parameter, the DivingBoard has the adder.py
programme, launched from a main menu. It instructs the user to navigate via the synth's menu system to the parameter they want 'learnt'. Once the user has confirmed this, the programme asks them to set the parameter to it's maximum, then press a button- then it's minimum, then press a button- and then back to it's maximum again, and then press a button a final time. The button presses are basically equivalent to starting and stopping recording, and the sweeps up and down through the parameter's range is in order to capture every MIDI message sent out by the synth. A diagram perhaps illustrates this more usefully:
As shown, the first press of the button starts recording a downwards sweep of parameter values (hence setting to maximum first- otherwise the user might start on the minimum value anyway). This is only done in order to catch the final message, which will be for the minimum value of the parameter- all other messages from this sweep are discarded. The second button press is to start recording an upwards sweep through all values of the parameter (as we start from the minimum value, the message saying 'set this parameter to the minimum value' won't be sent, which is the point of the initial sweep up and down). These values are then stored in a list. When the user wants to change a parameter, the value sent from the physical control (as described above) is mapped to the correct message in the list, which is then sent out to the synth in what is essentially a replay attack.
MIDI messages tend to be very similar. For most parameters, they'll be exactly the same for every value the parameter can take, minus one byte of hexidecimal containing the value the parameter should take, and another containing the checksum. So, why store every single possible message, rather than storing one version and just changing those two bytes? The answer is in two parts. Firstly, it's easier to replay the original correct checksum than it is to compute it every time, and secondly (and much more importantly), the maximum number of different values a single MIDI message ID can take is 127 (i.e. you can't send 'set parameter X to value 129', unless the minimum value is 3), and some parameters have many more values; the 'delay time' parameter on the JD-Xi can be anything from 1 to 2000 milliseconds. To get around this, the JD-Xi uses multiple different message IDs, with one picking up where another leaves off. It's much easier to store a complete sequence of messages than it is to record them all and then boil away all the unnecessary ones. Storing all the messages in a long list is definitely more expensive in both storage and speed of retrieval, but not so significantly as to detrimentally impact performance, so I've left it like that for now.
The DivingBoard stores parameter information in two ways: a CSV file long-term, and then in a specialised data structure when running. The data structure is built from the CSV file when divingBoard.py
runs, before the main programme loop. In the CSV file, each line stores information about a parameter in the following fashion:
Synth,Partial,Section,Name,messages,minimum,maximum,read-in index,Display Mode
The first three items in the list are for the organisation of the data structure and are hierarchal, like a file path. They map directly to the first three rotary encoders on the DivingBoard's interface. In the example diagram below, the first three items in the list might be D1,A,Filter
.
Name
is the name displayed to the user on the LCD. It has a limit of four characters, or four names next to each other on the LCD couldn't be guaranteed to fit next to each other. messages
is a period separated list of all of the MIDI messages which the parameter can send/recieve, and minimum
and maximum
are used to establish the range of messages. They were more useful in earlier version of the programme, but are still used to rescale the control values from the Arduino (from 0 to 1024) to the number of available states of the parameter (1 to 127 for most, but can be much larger or smaller). Display Mode
refers to the way the values of the parameter are displayed to the user: for normal numerical parameters (cutoff, delay time, etc.), no labels are used as the position of the potentiometer is label enough. For some parameters with only a few values (filter type, wave shape, etc.), a space separated list of possible values is stored and displayed as each different option is chosen. At the moment, this is the only useful information that can't be added to the DivingBoard without editing the CSV file manually, as inputting lots of meaningful text is quite difficult given the interface. read-in index
refers to an as yet unimplemented feature- the ability to press a button and see the current value of all parameters currently on screen. This is do-able but a little tedious- it involves getting the synth to do a memory dump of the current patch, which is received as a long hexidecimal string. It's possible to work out which data in this monolith refer to a specific parameter, and then to extract and convert the numbers for display. It's not a feature I'd use often, but I can see reasons for using it and I'll probably add it in at some point in the future.
The data structure that this information is stored in at runtime is just a great big nested list thing. Definitely not the most efficient solution, but I came at this problem to get it working, and it works. The 0th element of the list is it's name (EG D1
), and the rest of the elements are further lists, each storing further sub-lists until the bottom level, containing a list of objects of a class called Property
. These objects each hold the rest of the information about the parameter. An example of all this for a really simple version of the system where the entire DivingBoard CSV contains only a single parameter would look like this:
[JD-Xi[D1[A[Filter[Cutoff]]]]]
as built from a CSV containing the single line
D1,A,Filter,Cutoff,{long series of messages},0,126,6,1
This system is not the most efficient, I'm sure. Indeed, every time a parameter needs adjusting, several list traversals must be performed. First the nested list structure to find the current parameter being adjusted, and then the list of MIDI messages to send needs perusing until the correct one is found. When a control potentiometer is adjusted, this whole routine is performed again and again, which can't be fast. Switching to some other data structure would probably decrease latency significantly enough to allow the DivingBoard to run at acceptable speeds on a Raspberry Pi Zero.
That's pretty much how the majority of the system works, and why it's potentially decently equipped to work well with other MIDI devices too. I'm not very good at explaining stuff, and this page was written the best part of a year after I finished working on the current prototype, so some bits were initially a little muddled in my brain. Hopefully everything makes sense.