It’s no secret that Neurogami is a fan of Renoise. One of its most powerful features is that you can script it using Lua. There are countless add-in tools written by other Renoise lovers. The upcoming Neurogami album is being created with Renoise.
Part of making that album is the creation of music videos. Some of this was previously described. The secret sauce for that approach is to set up tracks with instruments configured to send out MIDI messages to virtual devices.
If you are always working on the same machine then you can set up the association between instruments and MIDI-out devices and be done. However, if you are working with the song on different machines then you run into a problem: Each machine has a different set of available MIDI devices. If you set up and save the song on one machine then load it on another you need to reassign the instrument MIDI devices.
Doing this a few times is annoying; keep doing it and it becomes really tiresome. Tiresome repetitive action leads one to stating wondering if there’s not some way to automate it. And there is.
Sharing Lua files in Renoise showed how to load code from outside a tool’s own folder. This same technique can be used to load configuration code.
This tool works in series of steps, and relies on some assumptions being true. The first assumption that the user has created a new folder, UserConfig
, in their $HOME/.renoise/[version]/Scripts
directory. The second assumption is that any song to be code-configured has a name set in the song comments section. This is critical, because the tool code will use this in order to locate a corresponding configuration file.
As an example, take the track A Temporary Lattice. Assuming the song name is entered as exactly that then the corresponding code configuration file would be UserConfig/A_Temporary_Lattice.lua
The tool (provisionally called “Configgy”) has this method to convert the song name to the code file name:
function song_slug()
local name = renoise.song().name:gsub(" ", "_")
print("Have song slug " .. name )
return name
end
A first version of this used the song file name (renoise.song().filename
), but that returns the complete path, not just the file name. Cleaning this up was too much work. Besides laziness, another reason to use the song name and not the file name is that it is not uncommon to have versioned copies of songs. Renoise xrns
files are zip files holding XML and often samples. This binary nature makes using something like git for version control clunky. It can be more pragmatic to save off a song under a different name while exploring different ideas. They’ll have different file names but can all have the same song name.
The basic way to load a Lua file from a non-standard location is to muck with the package path. Configgy does this right at the start:
package.path = os.currentdir() .. "../../UserConfig/?.lua;" .. package.path
This code assumes it is running in a tool folder directly under Scripts/Tools
, placing UserConfig
up two directories.
With this new location added to the package path it’s an easy matter to load any specific code config file to be found there.
Of course, not every song will have a corresponding config file. Nor, for that matter, a defined song name.
One way to approach this is to always attempt to load the config file and use exception handling to quietly ignore any errors. The first version of this tool did just that. It added a tool menu item, and would try to find and load the config file when clicked. The problem there is that you end up with a new tool menu item whether it can do anything useful or not. That seemed wrong.
Instead, the code looks for the config file and will only add the menu item if that file exists.
There is no built-in way to check for the existence of a file. What you can do is simply try to open a file and see if you get back something meaningful.
function have_config_file()
local file_name = os.currentdir() .. "../../UserConfig/" .. song_slug() .. ".lua"
local f=io.open(file_name,"r")
if f~=nil then io.close(f) return true else return false end
end
Once you know if the config file is there or not it should be a simple matter of optionally adding the menu item. Except it’s not. Tools are loaded before any song is loaded. When loaded, Configgy would never know what config file to look for because there would be no song name.
The trick is to add code that responds to the loading of a song (i.e., an observer). Luckily, that’s been worked out already.
The code at that link was grabbed and used with a few changes.
local function open_song()
if have_config_file() then
renoise.tool():add_menu_entry {
name = menu_name,
invoke = load_and_execute_config
}
end
end
menu_name
is defined earlier as local menu_name = "Main Menu:Tools:Neurogami Configgy"
When a song file is closed, the menu item is removed:
local function attempt_remove_menu()
renoise.tool():remove_menu_entry(menu_name)
end
local function close_song()
pcall(attempt_remove_menu)
end
Attempting to remove a menu item that is not there causes an exception. Putting that code into a separate function and using pcall
protects against this.
So far so good. If a song is loaded, and the song has a name defined, and that name maps to an existing configuration file, then the tools menu gets a new menu item. What does that menu item do?
Yet another assumption is that the song config file will implement a Lua function named configurate
.
Activating the tool invokes the function load_and_execute_config
:
function load_and_execute_config()
require(song_slug())
configurate();
end
configurate
is, of course, Lua code. This is why the configuration file has been occasionally refered to as the code configuration file.
When scheming up a configuration system you need to sort out the balance of power and convenience.
Here’s what configurate
looks like when the goal is to assign MIDI devices to instruments:
function configurate()
local device_name = "Virtual Raw MIDI 1-0: VirMIDI 1-0"
renoise.song().instruments[1].midi_output_properties.device_name = device_name
renoise.song().instruments[2].midi_output_properties.device_name = device_name
renoise.song().instruments[3].midi_output_properties.device_name = device_name
device_name = "Virtual Raw MIDI 1-1: VirMIDI 1-1"
renoise.song().instruments[4].midi_output_properties.device_name = device_name
end
For someone to do this on their own they would need to know some Lua, as well as be familiar with the Renoise API. At the moment, every user of this tool, all one of them, meets this criteria.
What’s the alternative? Configuration files are often simple “name = value” pairs. In many case this is much easier for the average use to manage, but there’s a cost. This approach works great for single, unambiguous, settings. For example, if you wanted a config file that allowed you to set the song BPM you could represent it as bpm = 121
. Simple.
But there’s no obvious way to represent instruments and their myriad settings. Since there can be multiple instruments, and each instrument has multiple settings (not to mention possible devices) perhaps some “2D” approach would work, maybe using YAML or JSON , like this:
instruments:
- 1:
midi_output_device_name: "Virtual Raw MIDI 1-0: VirMIDI 1-0"
- 2:
midi_output_device_name: "Virtual Raw MIDI 1-0: VirMIDI 1-0"
Or something. That may not even be valid YAML, but it’s not worth fixing because this is only marginally less complex than using Lua code. In both cases the user has to know something about the available options and make sure the syntax was correct.
Worse, turning that YAML or JSON into something usable by Renoise (i.e. Lua code) requires yet more tool code. More code means a greater change of bugs and unintended consequences.
This small gain in ease of use comes with a large loss in power. If you go with a non-programming configuration language then it’s limited by what the configuration parser implements.
This is not always a bad thing if one of your goals is to prevent the user from doing something stupid. It does raise the risk that the configuration parsing code will end up doing something stupid.
So far the actual use cases for this code offers zero reason to not use plain Lua. It’s stupid simple. It allows for endless customization. It has few moving parts. That’s a big win.
You can see the complete code here.