JAMES BRITT

Current album
Loosies

Get it here ...

Renoise send-track scripting with MIDI and Lua

A previous article explored using OSC to call Lua code to set the “Receiver” value of a send device.

The basic idea was that a given send device would be associated with some set of send tracks using a naming convention. The OSC handler code would then allow for changing that send device to use one of those associated send tracks.

It works great. But what if you want to use MIDI instead of OSC?

Custom MIDI handlers

An even earlier article showed how to use Lua scripting in Renoise to listen for particular MIDI devices and handle messages.

That code was devised to work with custom sysex messages. However, if you want to use an off-the-shelf MIDI controller, such as the Novation Launchpad, it helps to be able to handle standard MIDI note on/off messages.

The code used for sysex handling can be adapted to do the same dynamic method invocation based on note values.

Once that is in place the code used to handler OSC messages can be pilfered and reused.

Getting set up

The OSC example put everything into GlobalOscActions.lua. The alternative would have been to create a stand-alone tool, but that would require the code to run a second OSC server; it would not be adding a new handler into the /renoise/song/track address pattern.

Renoise has a GlobalMidiActions.lua file, but in this case a tool works well. It is unlikely you will always want to intercept various MIDI note on/off messages, and using a tool can make it easier to turn certain behavior on or off. That’s in principle. In practice the code shown here is always on. Adding a nice tool GUI with some clever behavior is a task for another day.

Renoise provides renoise.Midi.create_input_device for creating a reference to a MIDI device. As with the previous example this code takes an array of expected device names, loops over them, and holds on to the first one found.


local DEVICE_NAMES = {"Teensy MIDI NG", "Launchpad", "LoopBe Internal MIDI" }

local devices = renoise.Midi.available_input_devices()

for i,name in pairs(DEVICE_NAMES) do
  for i= 1, #devices do
  print(("Compare '%s' to '%s'"):format(name,  devices[i]))
    if string.find( devices[i], name) then
      print(("FOUND '%s'"):format(name))
      midi_in_device = renoise.Midi.create_input_device(devices[i], midi_handler, sysex_handler)
      midi_out_device = renoise.Midi.create_output_device(devices[i])
      break
    end
  end
end

Key here are the callback functions passed to create_input_device. These will be invoked when the particular device receives a MIDI or sysex message.

Naturally we need to define these functions. Each takes the received message as an argument; it’s up to the function to decide what to do next.

Once again the code will extract the message details and use them to construct a function name. That function will then be invoked using pcall.

midi_out_device is the same device as midi_in_device because later code will want to send MIDI messages back to the Launchpad. The reference to midi_in_device is not used in the code. You can even omit assigning the return value of renoise.Midi.create_input_device to anything.

Referencing trouble

You can invoke a (global) function by using a string as an index into the global environment table _G and then invoking it with pcall. That works great when that string refers to a defined function.

If you try referring to an undefined variable or function Lua gets made and raises an exception.

There are a few ways you can trap these errors; one is to careful wrap potentially troublesome calls in their own functions and call those functions using pcall. This would add an extra wrapper function to the code, and it is not such a bad idea (i.e it works).

Another way is to see if the desired function is defined, and only then try to invoke it.

The Lua Web site explains how to do this:


-- But now, to test whether a variable exists, we cannot simply compare it to nil; 
-- if it is nil, the  --access will throw an error. Instead, we use rawget, 
-- which avoids the metamethod:

    if rawget(_G, var) == nil then
      -- `var' is undeclared
      ...
    end

Therefore the MIDI handling code needs to check if the constructed function name refers to something not nil, and only then invoke it.


function midi_handle(message)

  local m = (HANDLER_PREFIX .. "%d_%d"):format(message[2], message[3])
  print(("MIDI handler has message:  %d %d %d"):format(message[1], message[2], message[3]))

  --   http://www.lua.org/pil/14.2.html
  if rawget(_G, m) ~= nil then
     print(" pcall " .. m )
     local success, result = pcall(_G[m], message, midi_out_device ) 
     if not success then
       print( ("There was an error calling %s: %s"):format( m, result) )
     end
  else
    print( ("* Cannot find %s in _G"):format(m) )
  end

end

With this in place the code can catch every MIDI message and skip invoking functions that are not defined.

Handling the message

The variable HANDLER_PREFIX is defined in Handlers.lua as =“handler_”. This is used to avoid accidentally creating a function name that collides with an existing Renoise or Lua function name. If you prefer a different prefix, change it.

The functions that end up handing this or that MIDI message will have names like handler_64_127 or handler_64_0. The Launchpad sends velocity values of 0 and 127, for key-down and key-up.

Send device changes will happen on key-down, so it’s a matter of deciding what notes map to what tracks. Looking at a Launchpad, do you think of sequencer tracks as running down in rows, or across in columns?

This seems like one of those things that are hard to tell in advance of actual usage, so let’s just go with down in rows. Interface design is tricky. Often things that seemed quite sensible in the abstract turn out unusable in practice. For example, not every sequencer track will have a send device and named-associated send tracks. Using a literal mapping of Launchpad row to sequencer track can mean wasted rows. On the other hand that literal mapping might make it easier to know what button to push.

The first button of the fifth row sends the note 64. To handle the “on” value using our handler function naming scheme there needs to be a function handler_64_127. The corresponding “off” handler is handler_64_0.

These functions will take two arguments: the actual MIDI message received, and a reference to a MIDI out device. The lights on a Launchpad are controlled using MIDI messages sent to the device. An easy way to give some feedback is to just send back the message received. This should cause a button to light up when pressed and go dark again when released.

Such functions are kept in a file named Handlers.lua that is loaded in main.lua.

Here’s an example


function handler_64_127(message, midi_out_device)
  send_switch(5, 1)
  midi_out_device:send(message)
end

send_switch is another custom method. It’s essentially the core code from the OSC version of send-track switching.

Volume, volume, volume!

It just so happens that the original Launchpad was not a just-plug-it-in class-compliant MIDI device. It required additional drivers, and getting it to work on Ubuntu (or anything other than OSX or Windows) is a challenge.

No matter; this simple tool can just listen for a different input device, such as the QuNexus.

Easy to do; just add “QuNexus MIDI 1” to the DEVICE_NAMES array.

You need to see how a device appears on any given machine before setting the names. The QuNexus just happens to come up as “QuNexus MIDI 1” on Ubuntu; it may be slightly different on Windows or OSX.

A good way to get the name is to see what device names appear in Renoise in the Preferences | MIDI screen.

The best situation would be tool GUI that allowed you to select from a list, but at the moment this tool is pretty bare-bones.

The code now responds to the QuNexus. And there’s a problem. The incoming QuNexus MIDI messages generate function names such as handler_64_98 and handler_64_102. The velocity value no longer just 0 or 127.

The Launchpad is not pressure-sensitive. The QuNexus is. As it happens you can turn this off, but suppose you want to use a device that doesn’t allow that. Or where turning off pressure sensitivity would interfere with something else.

What the MIDI handlers are looking care about is not a specific velocity of 127, but a velocity that is not zero. That is, a “note on” message. What’s really needed are handler functions like handler_64_on and handler_64_off.

First, a helper function:


function on_or_off(v)
  if v > 0 then return "on" else return "off" end
end

Then a change to how handler function names are constructed:


local m = (HANDLER_PREFIX .. "%d_%s"):format(message[2], on_or_off(message[3]))

Yeah, but …

There’s another problem with accommodating different MIDI devices. One reason controllers such as the Launchpad work well is that the layout maps easily to many DAWs. The QuNexus is a more traditional keyboard; the location of notes 64 and 65 do not have any positional affordance.

For this use-case I don’t know what a proper mapping would be with a standard MIDI keyboard. Perhaps sequence tracks should map to C-scale octaves; C == send track 1, D == send track 2, etc. Or should C# == send track 2?

Details aside, suppose we have various MIDI controllers and suitable, but different, mappings for each. One approach is to use the input device name in the construction of handler function names. When an inout device is located have the code store the name. Then, instead of handler_65_on it would be handler_launchpad_65_on or handler_qunexus_65_on.

This would allow you to define handlers crafted for different controller layouts.

Another way to manage different controllers is to dynamically load the handler function file. This is little more tricky.

There first needs to be a way to associate an input device name with a handler file name. One way might be to just assume the file has the same name as the device; the handlers for “QuNexus MIDI 1” would be in a file named “QuNexus MIDI 1.lua”.

Another way would be to assign explicit mappings between expected device names and the handler file names. That means setting up a table using strings (the device names) as keys.


local DEVICE_NAMES = {['Teensy MIDI NG']="Teensy", 
                      ['Launchpad']="Launchpad", 
                      ['QuNexus MIDI 1']="QuNexus", 
                      ['LoopBe Internal MIDI']="LoopBe" }

(For an explanation of this particular Lua syntax, see here.)

The .lua file extension is omitted because it is not used when calling require.

The nice thing about this is that you can reuse handler files for different controllers that are essentially the same but have different names.

Now the code for grabbing the input devices needs to change as well:


for name,mapping in pairs(DEVICE_NAMES) do
  for i= 1, #devices do
  print(("Compare '%s' to '%s' [handler file '%s']"):format(name,  devices[i],  mapping ))
    if string.find( devices[i], name) then
      print(("FOUND '%s'"):format(name))
      renoise.Midi.create_input_device(devices[i], midi_handler, sysex_handler)
      midi_out_device = renoise.Midi.create_output_device(devices[i])
      require( ("Handlers%s"):format( mapping ) )
      break
    end
  end
end

Lua lets you load files at runtime using string variables. Very slick.

There’s one more thing

This tool has a copy of the send-track switching code used in the OSC version. Having the same code in more than one place is a drag. What are the options here?

Ideally Renoise scripts would be able to load common files from a known location. (That’s possibly a bit flippant. It’s unclear whether this would introduce other problems. Let’s assume for now that it’s a perfectly good idea.)

Is this doable? Yes.

It’s the topic of a future post.

The code for this and some other Renoise stuff is at the Neurogami Renoise Github repo.

It’s in a steady state of flux.

At some point a proper tool will come out of this.

My Music

RNA

Loosies


American Electronic


Small Guitar Pieces

Maximum R&D



Love me on Tidal!!111!

Neurogami: Dance Noise


Neurogami: Maximum R&D