Work on Maximum R&D continues, and part of the larger project is some innovative packaging.
Towards that end there have been some explorations using a custom Renoise tool designed to work with a special USB MIDI controller.
A few caveats. First, this is being done with Renoise 2.8.2, even through the current official release is 3.0. When all is done the results are expected to work with Renoise 3, but that has not yet been tested.
Second, if you are writing code to help generate music or manipulate some audio processing you often need to consider execution speed. The current code uses a technique that adds overhead. It may be fast enough, but that will have to be verified as development continues.
The key point is that the code to be shown should be viewed as one way to do something, hopefully a correct way but possibly not the best way, at least not for all circumstances.
At the very least it will show you how to get started writing custom MIDI input handlers for Renoise using dynamic dispatching.
Renoise is a very sweet DAW. Technically it’s a tracker, but it’s a long way from the early programs of that genre.
One its powerful features is the ability to script the application using Lua.
Renoise can listen for (and send) both OSC and MIDI messages, and Lua scripts can be made to handle such messages.
There are a few places you can put the code for this. When you install Renoise it gives you a few default script files. Some default MIDI handling code goes in [installation-folder]/Resources/Scripts/GlobalMidiActions.lua
.
There’s a similar file for global OSC actions.
if you want to alter this file the recommended way is to create a copy and place it in the ~/.renoise/[version]/Scripts/
directory, and edit that copy. Otherwise you will lose your customizations if you reinstall or upgrade Renoise.
Scripts in these files will get loaded every time you run Renoise.
Another way to load scripts is to bundle them into a tool. A Renoise tool is a zipped folder that contains (at the minimum) a manifest.xml
file describing the tool and a main.lua
with tool code. Your tool can contain other files as well, and if you are creating anything sizable you would do well to break it up into smaller files for easier code management.
There are many tools for Renoise and if you want to learn how to create your own, there is tool creation documentation. You will also learn a great deal by looking at other people’s code.
This article is not about tool creation as such, so that part is skipped and we’ll jump right into code samples.
For your code to respond to incoming MIDI message it will first need to locate a MIDI device. You can see what devices are available to Renoise if you open up (from the menu) “Options → Preferences → MIDI”.
This same information is available in a Renoise script using renoise.Midi.available_input_devices
.
To use any of these inputs you call renoise.Midi.create_input_device
.
This function takes a few arguments, the first being a device name.
(You can get the details for this and other Renoise MIDI scripting functions from the Renoise MIDI APAI docs.)
The custom code here is intended to work with a Teensy 3.1 board with USB MIDI.
The code should only kick in if this specific device is detected. Since by default all Teensy boards will report the same device name I went as far as altering usb_desc.h
to create a custom device name.
The name was set to “Teensy MIDI NG”, so the Lua code needed to use that name to grab that device.
Here’s where you need to be careful. The first version of the code just assumed that the desired device would be available, and for about an hour that was a sensible plan.
I then tried launching Renoise without the Teensy plugged in, and right away I got a pop-up window with an error message. While the code was part of a tool, the tool was not using any user activation. All installed tools will get loaded when Renoise loads. Most tools will add a menu item, and typically require the user to click on something to trigger running of code. But a tool can skip all that and go straight to code execution. If you take that route you need to be sure that this auto-execution will not raise any errors or otherwise interfere with normal use of Renoise (well, unless the point of the tool is to alter the normal behavior of Renoise).
I realized I needed to change the initialization code to check the list of available devices for the target input. But there was another issue: The name of a device can vary. On Windows 7 Renoise listed the renamed Teensy as “Teensy MIDI NG” (good) , but on Ubuntu it showed up as “Teensy MIDI NG MIDI 1” (less good).
Two things were needed. One, a loop over the list of available devices, and two, a substring search.
The use of the short-lived variable devices
is not a requirement but it makes the following code a bit shorter and, to my eyes, easier to read. It may also be marginally faster since it avoids repeated calls to looking up the available devices but since this is only run once, at load time, it’s not a big deal.
The “hash” character (i.e. #
) in front of a table returns the size of the table. Lua indices start at 1, so this iterates over the items in the table and checks each device name to see if it contains the name of the target input device.
If a match is found then the device is grabbed and some callback functions assigned.
If no match is found then, well, nothing. No other code will run since that’s predicated on callback functions being registered to an input device. And no error message (at least not for any missing devices).
There are no provisions for there being multiple devices that might match. Again, this is based on some assumptions about how this will be used; you’ll need to decide what works for your situation.
renoise.Midi.create_input_device takes a device name and, optionally, a callback for MIDI messages, and a callback for sysex messages.
At first the idea was to grab MIDI messages and re-purpose them for something other than straight-up note playing. This is doable, but there’s a possible problem. If the input device used by the script is also used by Renoise proper (that is, it has been selected as a MIDI input device through the options menu) then Renoise will apply its own default handling. Your own script will still run, but if you have MIDI notes arriving from some external device those notes my trigger actual sounds.
What’s somewhat amusing is that I had been selecting my custom input device via the options menu to ensure that it could be detected by Renoise; early on my code was not doing what I had hoped and I wasn’t sure where the trouble was. And this is how I noticed that even when my script was reacting to MIDI notes, if I had selected an instrument then I would hear notes play as well.
What I didn’t think of until later was that if I am using a custom MIDI device then there’s no reason Renoise, all by itself, will have it as one of the selected input devices. Renoise does not go and locate all attached devices and then automagically start listening to them for input. You need to do that yourself.
Your code can locate all available devices and grab one without any of those devices being otherwise used by Renoise. My concerns were largely for nothing. But serendipity prevails, because it motivated me to explore sysex (aka “system exclusive”) messages.
MIDI is pretty focused on sending music data. Note on, note off, sustain, etc. Sometimes, though, a devices may need to send or receive something more complex, something that does not have a predefined MIDI message.
In a nutshell, using a sysex message your device (or program) can send almost arbitrary messages. There can be some constraints on the maximum size (for example) but the content can be almost anything. A sysex message is a series of bytes, bookended by two special values (0xF and 0xF7).
This makes the sending of MIDI messages a bit more like OSC messages. One of the nicer things about OSC is that it is (or can be) self-descriptive. If you’ve sent up a system where a message can trigger a song to start playing you can, if you so arrange, use an OSC message such as /transport/start
. (Indeed, that’s what Renoise does).
This is much nicer than having to map MIDI note values to command names. Since a sysex message can hold text (albeit in the form of specific byte values) then can encode commands more or less in plain English (or French or German or whatever you want).
For example, if I want the Teensy to send a message telling Renoise to jump to a particular pattern number, the message can be the sequence of bytes that corresponds to “jump 2” (or whatever the pattern number should be).
On the Teensy end this looks like this:
byte sysex[]={0xF0, 'N','G','j','u','m','p',' ','2',' ','1' , 0xF7};
usbMIDI.sendSysEx(sizeof(sysex), sysex);
(Demo code. In more final code that integer would be dynamically inserted.)
There are two digits in there because the actual code allows for jumping to a given pattern and then scheduling the next pattern to loop over. (This was taken from the Renoise OSC tool OscJumper.xrnx.)
But what’s with that leading “NG”? That’s a straight-up hack.
Code was initially done on Ubuntu and then resumed on Windows 7. What worked fine on one OS was wonky on the other. On one of them the Lua code parsing the sysex byte array was always getting the same wrong values for bytes 2 and 3. Always. I could not figure out why. Was it Renoise? Was it the OS? Some driver?
I hate it when I set out to solve one problem and end up wasting time solving some tangential issue because something is buggy or otherwise misbehaving. I do want to know what is happening; it may very well be me doing something wrong. If you, dear reader, see me doing something boneheaded, or have an idea what I should look at, please tell me.
My more immediate concern was sorting out general sysex handling, so I opted to pad every sysex message with two disposable characters. Yeah, it’s sort of cringe-worthy.
A callback function passed to renoise.Midi.create_input_device
needs to take one argument, which will be the message.
The structure of the message arg is just a series of bytes. This makes it easy to parse. Of course, those bytes could contain something meant to represent some other structure (such as JSON). However, this is where you may want to think about processing overhead. Not only will you Lua code need to extract the bytes but it will then have to convert the resulting string into yet another type of object. Perhaps your code will do this fast enough for your purposes.
For my use case all I needed was a simple way to encode a function name and function arguments. My sysex handler therefore makes a number of assumptions (again, because the code is defined for a specific use case where I control the data).
We know that the first and last bytes are bookend values. My code assumes that if a message makes it far enough to be passed to my handler that the message is at least well-formed. My code also assumes the first two non-bookend bytes are to be ignored. (By the way, if and when I figure out why those few bytes get corrupted the use of some sort of leading tag is not a bad idea. It can act as a message namespace. It is common for device manufactures to use the first byte after the 0xF0 as an ID. I just happen to be using two bytes. )
The hander function skips the first three bytes (0xF0, ‘N’, and ‘G’) and then converts the rest into chars added to a string, omitting the very last byte (the 0xF7).
It then splits the resulting string on white space and creates a table whose first item is a function name and all the rest are function arguments.
That first value is assign to a variable then removed from the table. This allows the table to be passed as a method argument with no extraneous values.
Here’s the best part: Lua allows for dynamic function invocation given a string holding a function name. It isn’t allows as straightforward as shown here, depending on how your target functions are defined.
_G
is the global namespace and all the target functions for this code are in that global namespace. The risk with adding your methods to the global namespace is that you may end up colliding with an existing function. If you decide to use your own namespace then the dynamic look-up code requires an extra step.
In this case, to help avoid name collisions, all the functions to be dynamically invoked are prefixed with “handler_” (the HANDER_PREFIX
value used in this function). Whether or not this is a better approach than using a proper namespace is up to you; this works for me at the moment, though as the code grows I may reconsider.
Now what remains is defining handler functions for the possible commands embedded in sysex messages.
Here’s one (also stolen from OscJumper.xrnx):
Aside from the code that does the useful work, the main thing to note is the conversion of a single table
argument to a set of usable values. Any handler like this will know what to expect and will need to extract and convert the contents of the given table.
After that it’s no different than any other function you might have in a Renoise script.
In playing around with the code I found that the absence of robust error handling could be a problem. I’m intending this code to be used with specific tracks, so (again with the assumptions) there should always be sufficient patterns for setting loop ranges and jump points. But maybe not; perhaps some errant value will make its way in there, so something needs to be added that first checks if what’s requested is possible.
At the moment it’s mostly proof-of-concept, play-around-with-ideas code, but hopefully it will help others jump off and explore their own scripts and tools.
What I really like about this approach is that, on the Teensy end (or Processing, or whatever I may want to use as the MIDI source) I can use meaningful message data and not have to manage some mapping of names and MIDI values. In my Renoise code I can just write code that straight-up responds to these meaningful names and, again, not manage mappings of arbitrary MIDI values.
Of course this only works when sending sysex messages, and for some devices that may not be an option. And it may yet turn out that the overhead introduces unacceptable delays. But so far this looks quite workable.
If you see something wrong in the code, or have an idea on how to do something better, please leave a comment.