James Britt

Maximum R&D

Renoise send-track scripting with Lua and OSC

September 2014

As previously described, I am a big fan of Renoise, and a big fan of manipulating the send device.

Renoise has three kinds of tracks: sequencer, send, and a single master. A send track is an intermediary between a sequencer track (e.g., a “stem”, the track where you have the notes and other commands ) and the master output.

You can chain devices (e.g. reverb, gate, compression) on a sequencer track to change the sound of that track. You can also “send” that track to a (duh) send track, and the send track can also have a device chain.

One reason to use send tracks is apply a common set of device effects to multiple sequencer tracks. Perhaps you want most tracks to have the same amount and style of reverb. Set up that reverb, then send those tracks there.

Since different send tracks can, with variations in device chains, give the same source track very different output, they make for a handy way to quickly alter the sound of a track during a song by changing the “receiver” (i.e. the destination) of the send device.

In the previous article you saw how to use device automation to have the receiver of a send device change automatically at different parts of a song.

What if you want to control such changes during a live performance?

Lua to the rescue

Renoise can be controlled using both MIDI and OSC (Open Sound Control). It can also be scripted using Lua.

Renoise has MIDI mapping but if you want particularly clever behavior you may need to write some code to react to incoming MIDI or OSC messages.

In this example, OSC is used with a custom message designed to switch the send receiver. You can add this to your custom GlobalOscActions.lua file if you want this to be available to all the time.

Adding your own Renoise OSC track action handler

If you poke around GlobalOscActions.lua you will see how OSC message handlers are defined. It’s easy to add your own; copy an existing action handler and edit it.

But notice that there are different kinds of action handlers. If you want to define a custom OSC message handler that is meant to apply to the song as a whole you can use


add_global_action { 
   -- Body of code 
}

In this case, though, the OSC message we’re sending is meant to alter the value of a device on a specific track.

Here we can use a different method


add_track_action { 
  -- Body of code 
}

All Renoise OSC address patterns must begin with /renoise; in your global OSC handler code you need to specify the rest of the pattern. For example, to have Renoise handle the address pattern /renoise/song/restart you would need


add_global_action { 
  pattern = "/song/swap_volume",
  -- more stuff

But if you are adding a track action then Renoise helps you out. For track actions Renoise assumes the OSC address pattern will begin with /renoise/track/XXX, where XXX is the track index. Your action handling code then needs to specify the remaining part of the address pattern.

So, for an OSC handler that modifies the send device on a specific track, the code starts like this:


  -- /song/track/XXX/send_switch

  add_track_action { 
    pattern = "/send_switch", 
    description = "Change the receiver of the track's send device.\n" .. 
                   "Requires you follow a naming convention",

    arguments = { argument("send_index", "number") },

    handler = function(track_index, send_index)

The astute reader will notice that although arguments is defined for just one argument, handler is defined as a function taking two arguments. When using add_track_action Renoise will automagically extract the XXX value from the OSC message and pass it as the first argument to the handler function.

What the handler will do

The full code for this can be found at the Neurogami/track_send_switch.lua gist.

Here’s the operating logic:

A Renoise song can have any number of send tracks (though in practice there’s an upper limit on that). This code assumes that there is an explicit association between send devices and certain send tracks.

To make the use of this tool easier to conceptualize it seemed best to treat each group of associated send tracks as distinct from the complete set of send tracks.

In other words, if we have set up a group track for percussion, and have created two send tracks for it, then when switching between them it is easier to think of them as send tracks 1 and 2, even if they are not the first and second send tracks in the song. Likewise, if there is a guitar track with three associated send tracks, it is easier to think of switching the send receiver to 1, 2, or 3 instead of having to know their absolute position in the complete set of send tracks.

In order to help the code figure out what these associations are you need to follow a naming convention.

The send device must be renamed to send_???, where ??? is some identifying “tag.” In the example song provided, the percussion group send device has been named send_perc, making “perc” the tag.

Send tracks meant for association with this device then need to use that tag in their track name. The example song has send tracks named perc_1 and perc_2. The format is [tag]_[some-identifier].

When a request is made to alter the receiver for a send device on a track, the hander code:

The code relies on two top-level variables (not shown in the gist code):


local send_tracks = nil
local send_track_count = 0

These go someplace near the top of your GlobalOscActions.lua file.

The handler for the /send_switch pattern will check if send_tracks is nil, and if it is it will populate it with a list of send tracks. This means that any additional send tracks added perhaps by code or by other live interaction will not be stored.

The idea is to reduce the time needed to locate the send tracks on each subsequent call to the handler.

An example

You can download the demo xrns file here. It is a simplified version of the Neurogami track Age of Reason.

The track is pretty motorik with (for the most part) a single bass riff carrying through the entire song while things change around it.

For the break most of surrounding noise drops out and the percussion changes sound. This is done by switching the receiver of the send device on the Percussion group track.

There are five send tracks. Two for the bass track, two for the percussion group, and one intended for playing around with some additional guitar treatment.

To make this song play nice with the handler code the send device on the Percussion track (a group track, in this case) is named send_perc. The associated send tracks are names perc_1 and perc_2. A similar naming convention is in place for the bass track send device the and the bass send tracks.

The naming convention serves two purposes. One, it makes it easer to think about what associated send track to use. For the Percussion group there are only two send tracks that matter. When switching between them it’s easier to think of them as send tracks 1 and 2, rather than having to know that they are, in fact, send tracks 3 and 4.

Two, it helps avoid accidental send track switching. When the OSC message is handled it will only operate on a track if it contains a properly named send device, and if found will only allow switching among matching-named send tracks.

So, for example, if you send the OSC message /renoise/track/5/send_switch 1, the handler will go grab track 5. Renoise starts the track index at 1, so track 5 is the Percussion group track.

If the table of send tracks has not yet been populated the hander will do so. Send tracks always come after the master, so once the master is found all subsequent tracks are known to be send tracks.

The code then looks at the device chain on track 5 to find a send device whose name begins with “send_”. Here it will find the device named send_perc.

So far so good. Now the code will run through the list of send tracks to find any associated tracks. Two index values are important. The absolute index value of any associated send track, and the relative index of each associated send track.

The loop over the complete set of send tracks starts at 1 and counts up; table indexing in Lua is 1-indexed. When time comes to assign the index value to the device the code needs to subtract 1 because (for whatever Renoise reason) when automating the send device “Receiver” value send tracks indexing starts at 0.

The code will first find send track perc_1. It has the absolute send track index of 3 (since it’s the fourth send track, with zero-based indexing) but the relative index of 1 (because it is the first associated send track found).

Since the OSC message was sent with a send_index value of 1 this is the send track to use. To correctly update the send device the absolute index of 3 must be used.

If the OSC message was sent with a send_index value of 2 the code would have kept looking for a send associated send track. It would have found the track with the absolute index of 4, and that value would then have been assigned to the send device.

If for some reason a send_index value of 0 or 12 or anything but 1 or 2 were used then no matching send track would have been located and nothing would happen.

Conclusion

The code was written to explore an idea, and it works. It could probably use some adjustments, though in practice it is remarkably fast. For example the handler could store send tracks using a data structure that allows for indexing by tag. This way when, for example, the send device tag of “perc” is extracted it would be able to immediately reference just the two associated send tracks.

The code here is for OSC, but it can just as well be adapted for use with MIDI. This will be the next step.

One final thought: You could throw out all of the fancy naming and associations and just use raw values. For example, by striping down the code you can have a message such as /renosise/song/track/6/send_switch 3 simply set the “Receiver” value on track 6’s send device to 3. The code would still need to locate the send device (if any) but if found it gets set to whatever integer you pass along using whatever send track is at that location.

There’s a risk with this: if you try to assign a value out of range the send device receiver gets set to “none”. This has the effect of muting the track. This may be exactly what you want; it’s an interesting option to consider.