A template for an OSC-based Renoise tool

Some edits have been since this was first published.



Seems the best way to learn a programming language is to publish something.



The minute something is live you are sure realize what you did wrong.

Neurogami has hacked about on assorted Renoise scripts.

There’s one that uses OSC to manipulate tracks and patterns.

Renoise comes with a built-in OSC server and a set of predefined message handlers. You can add your own by editing your local copy of GlobalOscActions.lua. Those changes will only be available to you; if you want to distribute your code you’ll do better by writing a tool.

There does not seem to be any tutorials specific to using OSC in Renoise tools, so this may be the first.

Renoise tool basics

A Renoise tool needs to have a at least two files. First, you need a manifest.xml that holds basic metadata about your tool. (By the way, there’s a Renoise tool for creating Renoise tools though it can be useful to know how to do this by hand.)

You will also want a main.lua file.

While main.lua can do many things, it doesn’t actually have to do anything in order to be part of a valid (if perhaps useless) tool. It can be empty. It just has to exist.

main.lua is typically where a tool does initialization work, such as loading in additional files and setting up tool menu entries.

It is often a good idea to break apart software into smaller files, each specific to a task or role. If your tool is particularly short you may find it easier to put all your code into main.lua . On the other hand, you may find that doing so makes it harder to maintain or debug.

.xrnx . That extension is for identification purposes; the files are in fact plain zip archives holding the tool files.



When you install a tool, Renoise unzips the file in your <HOME>/.renoise/<VERSION>/scripts/tools directory.



If you are hacking on your own tool you can bypass the zip process and just put your tool folder into that tools directory. Make sure you follow the naming requirements for your tool directory name (i.e. <something>.<something>.<ToolName>.xrnx ). Then be sure to



You can then play around with your code, reload it when you make changes, and watch for errors and other output in the scripting terminal.



If you want to be a little more slick about this you can do your development apart from that tools folder (ideally using a version management tool, such as git), then copy over the tool files when you want to try it out.



Renoise are distributed in the form of files that end with. That extension is for identification purposes; the files are in fact plain zip archives holding the tool files.When you install a tool, Renoise unzips the file in yourdirectory.If you are hacking on your own tool you can bypass the zip process and just put your tool folder into thatdirectory. Make sure you follow the naming requirements for your tool directory name (i.e.). Then be sure to enable the scripting developer tools in Renoise.You can then play around with your code, reload it when you make changes, and watch for errors and other output in the scripting terminal.If you want to be a little more slick about this you can do your development apart from that tools folder (ideally using a version management tool, such as git), then copy over the tool files when you want to try it out.

One plus to chunking up your code, though, is that you start seeing where you can reuse files in multiple tools. (There’s no official way to share code among different Renoise tools but there’s a hack you might consider.)

For example, once you know how to prepare a tool to handle OSC you can add that magic to all your tools, and if you kept your code reasonably clean it shouldn’t be too much work.

A basic OSC-enabled tool

This is a bare-bones tool that provides an OSC tool framework. You’ll likely want to adjust it to suit your own needs.

This is the manifest.xml :

<?xml version="1.0" encoding="UTF-8"?> <RenoiseScriptingTool doc_version="0"> <ApiVersion>3</ApiVersion> <AutoUpgraded>true</AutoUpgraded> <Id>com.neurogami.OscExample</Id> <Version>1.0</Version> <Author>Neurogami | james@neurogami.com </Author> <Name>Neurogami OSC Example</Name> <Category>In Progress</Category> <Description>Bare-bones tool with OSC</Description> <Homepage>http://neurogami.com</Homepage> </RenoiseScriptingTool>

Nothing special. You’ll change the details when you make your own tool, and perhaps change the ApiVersion value.

main.lua goes like this:

--[[====================================================== com.neurogami.OscExample.xrnx/main.lua =======================================================]]-- require 'OscExample/Utils' require 'OscExample/Core' require 'OscExample/OscDevice' require 'OscExample/Configuration' local osc_client, socket_error = nil local osc_server, server_socket_error = nil local osc_device = OscDevice() function create_osc_server() osc_server, server_socket_error = renoise.Socket.create_server( configuration.osc_settings.internal.ip.value, configuration.osc_settings.internal.port.value, renoise.Socket.PROTOCOL_UDP) if (server_socket_error) then renoise.app():show_warning(("Failed to start the " .. "OSC server. Error: '%s'"):format(socket_error)) return else print("OscExample has created a osc_server on port ", configuration.osc_settings.internal.port.value ) osc_server:run(osc_device) end end renoise.tool():add_menu_entry { name = "--- Main Menu:Tools:Neurogami OscExample:Start the OSC server ..", invoke = create_osc_server } require 'OscExample/Handlers' load_handlers(osc_device)

It just loads up the needed file, and adds a menu item for starting the OSC server.

This is Utils.lua :

--- Utils.lua for helper functions function clamp_value(value, min_value, max_value) return math.min(max_value, math.max(value, min_value)) end --[[ rPrint(struct, [limit], [indent]) Recursively print arbitrary data. Set limit (default 100) to stanch infinite loops. Indents tables as [KEY] VALUE, nested tables as [KEY] [KEY]...[KEY] VALUE Set indent ("") to prefix each line: Mytable [KEY] [KEY]...[KEY] VALUE --]] function rPrint(s, l, i) -- recursive Print (structure, limit, indent) l = (l) or 100; i = i or ""; -- default item limit, indent string if (l<1) then print "ERROR: Item limit reached."; return l-1 end; local ts = type(s); if (ts ~= "table") then print (i,ts,s); return l-1 end print (i,ts); -- print "table" for k,v in pairs(s) do -- print "[KEY] VALUE" l = rPrint(v, l, i.."\t["..tostring(k).."]"); if (l < 0) then break end end return l end

I’ve been using a Utils file as place for handy functions that don’t have a real home. This code was almost certainly picked up from a Lua tutorial or guide, or perhaps stolen from another tool; I apologize for not having the reference for it.

rPrint is nice for inspecting tables when you are not sure why you not getting what you expect, or for when you are not sure what a table might actually hold.

Please do not take this code as any sort of Lua “best practices” example.



Attempts have been made to write sensible code that follows what I’ve seen in other Lua programs, but that doesn’t guarantee anything.



Pretty much all the code is at the “try stuff out and see what happens” stage. There are no tests. Organization has improved but there’s room for more.



The code is not bad, but more effort has gone into making stuff do interesting things than into making exemplary Lua.



Corrections welcome.

Configuration.lua manages the collecting, saving, and loading of OSC IP addresses and port numbers. The first is what is used by the tool’s own OSC server; this how external OSC clients will communicate. The other is what is used by the built-in Renoise OSC server. It is on port 8000 by default. This tool needs to know this so that it can pass along OSC messages to Renoise itself.

Renoise provides the Document API which handles serialization of Lua tables as XML, making loading and saving a breeze.

-- Configuration.lua configuration = nil local configuration_dialog = nil local view_osc_config_dialog = nil function load_osc_config() configuration = renoise.Document.create("OscExampleParameters") { osc_settings = { -- This is the OSC server so we can talk to the tool internal = { ip = "0.0.0.0", port = 8001, protocol = 2, -- 1 = TCP, 2 = UDP }, --- This should match what is used by Renoise -- so the tool can pass along messages using -- its own OSC client renoise = { ip = "0.0.0.0", port = 8000, protocol = 2, }, }, } configuration:load_from("config.xml") end function save_osc_config() if configuration ~= nil then configuration:save_as("config.xml") end end function configuration_dialog_keyhander(dialog, key) if key.name == "esc" then save_osc_config() configuration_dialog:close() else return key end end function init_osc_config_dialog() local vb = renoise.ViewBuilder() view_osc_config_dialog = vb:column { spacing = renoise.ViewBuilder.DEFAULT_CONTROL_SPACING, margin = renoise.ViewBuilder.DEFAULT_DIALOG_MARGIN, vb:horizontal_aligner { mode = "justify", vb:text { text = "Localhost: ", tooltip = "Localhost OSC server settings", }, vb:textfield { text = configuration.osc_settings.internal.ip.value, tooltip = "Internal OSC server IP", notifier = function(v) configuration.osc_settings.internal.ip.value = v end }, vb:valuebox { min = 4000, max = 65535, value = configuration.osc_settings.internal.port.value, tooltip = "Local OSC server port", notifier = function(v) configuration.osc_settings.internal.port.value = v end }, vb:popup { items = {"TCP", "UDP"}, value = configuration.osc_settings.internal.protocol.value, tooltip = "Local OSC server protocol", notifier = function(v) configuration.osc_settings.internal.protocol.value = v end }, }, vb:horizontal_aligner { mode = "justify", vb:text { text = "Renoise: ", tooltip = "Renoise OSC server settings", }, vb:textfield { text = configuration.osc_settings.renoise.ip.value, tooltip = "Renoise OSC server IP", notifier = function(v) configuration.osc_settings.renoise.ip.value = v end }, vb:valuebox { min = 4000, max = 65535, value = configuration.osc_settings.renoise.port.value, tooltip = "Renoise OSC server port", notifier = function(v) configuration.osc_settings.renoise.port.value = v end }, vb:popup { items = {"TCP", "UDP"}, value = configuration.osc_settings.renoise.protocol.value, tooltip = "Renoise OSC server protocol", notifier = function(v) configuration.osc_settings.renoise.protocol.value = v end }, }, vb:horizontal_aligner { mode = "justify", vb:button { text = "Save & Close", released = function() save_osc_config() configuration_dialog:close() renoise.app():show_status("OscExample configuration saved.") end }, }, } end function display_osc_config_dialog() if configuration_dialog then configuration_dialog = nil end load_osc_config() init_osc_config_dialog() configuration_dialog = renoise.app():show_custom_dialog("OscExample Preferences", view_osc_config_dialog, configuration_dialog_keyhander) end renoise.tool():add_menu_entry { name = "Main Menu:Tools:Neurogami OscExample:Configuration...", invoke = function() display_osc_config_dialog() end } load_osc_config()

The OSC devices

OscDevice.lua is more interesting. The core code was copied from the Duplex tool and modified.

There are two OSC “devices”. One is an OSC server and it’s what listens for client commands. The client might be your phone or tablet running something like TouchOSC or Control.

This OSC server will watch for matching custom address patterns and do magical cool stuff.

Renoise has its own OSC server for doing magical cool stuff. The Renoise OSC server and your tool’s OSC server will, of necessity, use different ports. Your OSC client will almost certainly being using just one port, presumably the one used by your tool.

The problem here is that by tying the client to the tool’s port it prevents it from calling all those sweet built-in Renoise OSC handlers.

The solution is to have your tool pass along any messages it cannot handle. For this, we need a second OSC device, an OSC client that talks to the Renoise OSC server.

The determination of “cannot handle” is sort of broad. In your OscDevice you should be setting a prefix value:

self.prefix = '/ng'

This tells the device code that it should only bother with OSC message whose address pattern start with that prefix. You can make this whatever you like; Renoise itself uses the /renoise prefix.

When your OSC server gets a message it looks at the address pattern to see if has that prefix. If not, it simply passes it on to the default Renoise OSC server.

if (self.prefix) then local prefix_str = string.sub(value_str,0,string.len(self.prefix)) if (prefix_str~=self.prefix) then print(" * * * * * Proxy on ", pattern, " * * * * ") self.osc_client:send(msg) return end -- strip the prefix before continuing value_str = string.sub(value_str,string.len(self.prefix)+1) pattern = string.sub(pattern,string.len(self.prefix)+1) end





This would allow the tool to pass messages to other instances of Renoise (each configured to unique ports), PureData, If you wanted to be especially clever you could have the tool configured for multiple OSC severs and reroute messages to the appropriate server based on prefix.This would allow the tool to pass messages to other instances of Renoise (each configured to unique ports), Reaper Processing sketches, any software that can handle OSC.

Something else happens here that has gone through a few changes. Originally the code would copy the values from the first two message arguments and use them as parameters to the message handler. It worked, but only by coincidence. “Works by coincidence” is a major risk when copying code without a suitable set of tests in place; it’s an easy thing to miss.

All usage of this code was for OSC handlers that did in fact take two arguments. But the minute there’s a handler that expects a different number of arguments then stuff starts breaking.

The first fix (i.e. hack) was to require that all OSC handler functions take exactly one argument. This will be a table of values. The handler code would then pull out the values and use them as needed.

It worked, it solved that problem. The downside was that message handlers became opaque. You could no longer look at the argument list to know what is being passed. In the original example code, values were assigned to local named variables so that their meaning is explicit. But it’s busy work; the code would work just as well if the values were passed directly to the underlying core functions.

Some other workarounds were suggested, but since that first hack a better way was found.

Lua has the unpack function. It’s similar to the “splat” operator in Ruby.

The code takes the table of OSC arguments and converts it into a simpler table of values. Then unpack is used with pcall , passing in all the arguments without having to know the right number for each call.

Here’s the full code of OscDevice.lua :

-- OscDevices.lua class 'OscDevice' function OscDevice:__init() print(" * * * * * OscExample - OscDevice:__init() * * * * * " ) self.prefix = '/ng' self.client = nil self.server = nil self.osc_client = OscClient(configuration.osc_settings.renoise.ip.value, configuration.osc_settings.renoise.port.value) if (self.osc_client == nil ) then renoise.app():show_warning("Warning: OscExample failed to start the internal OSC client") self.osc_client = nil else print("We have self.osc_client = ", self.osc_client ) end self.message_queue = nil self.bundle_messages = false self.handlers = table.create{} self:open() end function OscDevice:open() print("OscDevice:open()") end function OscDevice:map_args(osc_args) local arg_vals = {} for k,v in ipairs(osc_args) do table.insert(arg_vals, v.value) end return arg_vals end function OscDevice:_msg_to_string(msg) print("OscDevice:_msg_to_string()",msg) local rslt = msg.pattern for k,v in ipairs(msg.arguments) do rslt = ("%s %s"):format(rslt, tostring(v.value)) end return rslt end function OscDevice:socket_error(error_message) print("OscDevice:socket_error(error_message): %s", error_message) -- An error happened in the servers background thread. end function OscDevice:socket_accepted(socket) print("OscDevice:socket_accepted(socker)") -- FOR TCP CONNECTIONS ONLY: called as soon as a new client -- connected to your server. The passed socket is a ready to use socket -- object, representing a connection to the new socket. end --[[ Stuff stolen from Duplex/OscDevice ]]-- -------------------------------------------------------------------------------- -- look up value, once we have unpacked the message function OscDevice:receive_osc_message(value_str) -- local param,val,w_idx,r_char = self.control_map:get_osc_param(value_str) --print("*** OscDevice: param,val,w_idx,r_char",param,val,w_idx,r_char) if (param) then -- take copy before modifying stuff -- local xarg = table.rcopy(param["xarg"]) -- if w_idx then -- -- insert the wildcard index -- xarg["index"] = tonumber(r_char) -- --print('*** OscDevice: wildcard replace param["xarg"]["value"]',xarg["value"]) -- end local message = Message() message.context = OSC_MESSAGE message.is_osc_msg = true -- cap to the range specified in the control-map for k,v in pairs(val) do val[k] = clamp_value(v,xarg.minimum,xarg.maximum) end --rprint(xarg) -- multiple messages are tables, single value a number... message.value = (#val>1) and val or val[1] --print("*** OscDevice:receive_osc_message - message.value",message.value) -- self:_send_message(message,xarg) end end -------------------------------------------------------------------------------- function OscDevice:release() --[[ if (self.client) and (self.client.is_open) then self.client:close() self.client = nil end ]]-- if (self.server) and (self.server.is_open) then if (self.server.is_running) then self.server:stop() end self.server:close() self.server = nil end end -------------------------------------------------------------------------------- -- set prefix for this device (pattern is appended to all outgoing traffic, -- and also act as a filter for incoming messages). -- @param prefix (string), e.g. "/my_device" function OscDevice:set_device_prefix(prefix) if (not prefix) then self.prefix = "" else self.prefix = prefix end end function OscDevice:_unpack_messages(message_or_bundle, messages) if (type(message_or_bundle) == "Message") then messages:insert(message_or_bundle) elseif (type(message_or_bundle) == "Bundle") then for _,element in pairs(message_or_bundle.elements) do -- bundles may contain messages or other bundles self:_unpack_messages(element, messages) end else error("Internal Error: unexpected argument for unpack_messages: ".. "expected an osc bundle or message") end end -------------------------------------------------------------------------------- -- create string representation of OSC message: -- e.g. "/this/is/the/pattern 1 2 3" function OscDevice:_msg_to_string(msg) local rslt = msg.pattern for k,v in ipairs(msg.arguments) do rslt = ("%s %s"):format(rslt, tostring(v.value)) end return rslt end function OscDevice:add_message_handler(pattern, func) --if (self.handlers) then self.handlers[pattern] = func -- end end function OscDevice:socket_message(socket, binary_data) print("OscDevice:socket_message(socket, binary_data), %s",binary_data) --- local prefix = '/renoise' -- A message was received from a client: The passed socket is a ready -- to use connection for TCP connections. For UDP, a "dummy" socket is -- passed, which can only be used to query the peer address and port -- -> socket.port and socket.address -- local message_or_bundle, osc_error = renoise.Osc.from_binary_data(binary_data) print("Have message_or_bundle ",message_or_bundle) if (message_or_bundle) then local messages = table.create() self:_unpack_messages(message_or_bundle, messages) for _,msg in pairs(messages) do local value_str = self:_msg_to_string(msg) local pattern = msg.pattern -- (only if defined) check the prefix: -- ignore messages that doesn't match our prefix if (self.prefix) then local prefix_str = string.sub(value_str,0,string.len(self.prefix)) if (prefix_str~=self.prefix) then print(" * * * * * Proxy on ", pattern, " * * * * ") self.osc_client:send(msg) return end -- strip the prefix before continuing value_str = string.sub(value_str,string.len(self.prefix)+1) pattern = string.sub(pattern,string.len(self.prefix)+1) end if value_str then print(" value_str = ",value_str ) ---- Now we need to parse the string stuff and act on it. -- Suppose we have a hash that maps patterns to methods. Can Lua call -- methods dynamically? -- if(self.handlers[pattern]) then print("Have a handler match on ", pattern) local vals = OscDevice:map_args(msg.arguments) local res, err = pcall( self.handlers[pattern], unpack(vals) ) if res then print("Handler worked!"); else print("Handler error: ", err); end else print(" * * * * * No handler for ", pattern, " * * * * ") end end end else print(("OscDevice: Got invalid OSC data, or data which is not " .. "OSC data at all. Error: '%s'"):format(osc_error)) end end --- Glommed from Duplex -- class 'OscClient' function OscClient:__init(osc_host,osc_port) print("OscExample - OscClient:__init!") -- the socket connection, nil if not established self._connection = nil local client, socket_error = renoise.Socket.create_client(osc_host, osc_port, renoise.Socket.PROTOCOL_UDP) if (socket_error) then renoise.app():show_warning("Warning: OscExample failed to start the internal OSC client") self._connection = nil else self._connection = client print("+ + + OscExample started the internal OscClient",osc_host,osc_port) end end function OscClient:send(osc_msg) self._connection:send(osc_msg) end

Handlers.lua is where you define what address patterns to handle. However, this is not where the work gets done; the role of these handlers is to grab parameters from the OSC message and pass them off to helper functions defined elsewhere.

The example here calls out to some code that alters the current play location, schedule patterns, and set loops. The handler functions create explicit local variables so that the meaning is more clear.

-- Handlers.lua -- Suggestion: do not put core logic here; try to put that in Core.lua, and -- just invoke their functions from here. -- That way those core functions can be used more easily elsewhere, -- such as by a MIDI-mapping interface. -- Some example handlers. They invoke methods defined in Core.lua handlers = { { -- Marks a pattern loop range and then sets the start of the loop as the next pattern to play pattern = "/loop/schedule", handler = function(range_start, range_end) OscExample.loop_schedule(range_start, range_end) end }, { -- Instantly jumps from the current pattern/line to given pattern and relative next line. -- If the second arg is greater than -1 it schedules that as the next pattern to play, and turns on -- block loop for that pattern. pattern = "/pattern/into", handler = function(pattern_index, stick_to) OscExample.pattern_into(pattern_index, stick_to) end } } -- end of handlers function load_handlers(osc_device) for i, h in ipairs(handlers) do osc_device:add_message_handler( h.pattern, h.handler ) end end

The address patterns do not include the prefix defined in your OscDevice instance. This way can change that prefix without having to update the handler code as well.

Each handler is simply a paring of a address pattern (minus the tag types) and a function. The function will be assumed to handle all the arguments that are sent along with the OSC message.

There’s nothing in place to compare tag types and the actual values sent. That is, if you have a handler function designed for message with the pattern and tag type of /foo/bar ii but a client sends /foo/bar ss , the code will still find a handler match on the address pattern; it will attempt to call the paired function with two strings. Since the function is expecting two integers, there will be an exception.

Protection

Previous versions of this code would blow up; the OSC server would be killed by the error, and all the fun went away.

To guard against this, the use of pcall was added.

local vals = OscDevice:map_args(msg.arguments) local res, res = pcall( self.handlers[pattern], unpack(vals) ) if res then print("Handler worked!"); else print("Handler error: ", res); end

Now the tool keeps running even if gets janky OSC.

Keep your distance

Rather than couple your song-manipulation code inside the OSC handler code you might do better by keeping all that tool-specific logic in its own place. If you’ve been learning about Renoise OSC programming by hacking around in GlobalOscActions.lua you probably copied what’s done there and bulked up each handler with code. That’s not a bad way to get started, especially if it gives you enough good results to make you want to do more. But there at least two reasons you might not want to carry this over into your tool code.

Reason one: Depending on the complexity of the code it will be easier to manage by placing it into it’s own class or module. Even just being in a separate file makes it easier to look after.

Related reason two is that a tool (and programs in general) has two parts: the stuff it does and the ways it interacts with the outside world. This example tool is using OSC, but there’s no reason it can’t also be controlled using MIDI. Or a Websocket. In each of those cases there will be some interface code that will end up calling the same core code. Keeping things nicely partitioned makes adding any of these things simpler.

Bonus reason: If you want to write tests for your code (and you should, present code notwithstanding) it’s just better when you have things nice and clean.

You may not need it all this structure; I’m not a fan of trying to future-proof code. On the other hand this sort of code separation is a generally good habit to follow. After a while it becomes second (well, maybe third) nature and makes your coding life easier.

To the core

So what is this core code? First, the file is called Core.lua because that fits with its general role in this and similar projects. (In fact, these files were generated using a templating tool and the details are still in flux.) It may not be the best name for any particular tool since it really doesn’t describe it very well. Change it as you think best.

As to the code, this example does very little. However it tries to do it in a sensible way. Not so much in the details of each specific function, but by trying to package up these functions in a nice way.

Given that these function might be called from assorted other code, a namespace is used. In Lua this means a table, and code that wants to use these functions needs to use this namespace. It helps avoid function name collisions. (You should know, though, that there is a lot more to namespaces and packages than what is being done here. See this.)

-- Core.lua OscExample = {} function OscExample.loop_schedule(range_start, range_end) local song = renoise.song print("/loop/schedule! ", range_start, " ", range_end) song().transport:set_scheduled_sequence(clamp_value(range_start, 1, song().transport.song_length.sequence)) local pos_start = song().transport.loop_start pos_start.line = 1; pos_start.sequence = clamp_value(range_start, 1, song().transport.song_length.sequence) local pos_end = song().transport.loop_end pos_end.line = 1; pos_end.sequence = clamp_value(range_end + 1, 1, song().transport.song_length.sequence + 1) song().transport.loop_range = {pos_start, pos_end} end function OscExample.pattern_into(pattern_index, stick_to) print("pattern into ", pattern_index) local song = renoise.song local pos = renoise.song().transport.playback_pos pos.sequence = pattern_index song().transport.playback_pos = pos if stick_to > -1 then renoise.song().transport.loop_pattern = true local pos_start = song().transport.loop_start pos_start.line = 1; pos_start.sequence = clamp_value(stick_to, 1, song().transport.song_length.sequence) local pos_end = renoise.song().transport.loop_end pos_end.line = 1; pos_end.sequence = clamp_value(stick_to + 1, 1, song().transport.song_length.sequence + 1) renoise.song().transport.loop_range = {pos_start, pos_end} renoise.song().transport:set_scheduled_sequence(clamp_value(stick_to, 1, renoise.song().transport.song_length.sequence)) else renoise.song().transport.loop_pattern = false -- Seems that if you pass it 0,0 it clears the pattern. local range_start = 0 local range_end = 0 local pos_start = renoise.song().transport.loop_start pos_start.line = 1; pos_start.sequence = clamp_value(range_start, 1, renoise.song().transport.song_length.sequence) local pos_end = renoise.song().transport.loop_end pos_end.line = 1; pos_end.sequence = clamp_value(range_end + 1, 1, renoise.song().transport.song_length.sequence + 1) renoise.song().transport.loop_range = {pos_start, pos_end} end end

There are two functions, one for scheduling a loop, and another for jumping to a pattern.

loop_schedule takes a pattern range and basically tells Renoise, finish playing the current pattern, then jump to the first pattern of the newly-defined pattern loop. It makes use of the util function clamp_value so that the pattern range falls within the number of available patterns.

OscExample.pattern_into will move the “playback head” from its current position to the next relative location in another pattern. For example, suppose you have patterns of 32 lines each. If the current pattern line is 12, then when the pattern jump happens the playback is set to line 13 of this other pattern. The results should be a smooth beat even though you have changed patterns. You can get some interesting effects jumping around this way.

The first argument is the pattern to jump into. The second is to tell Renoise if that pattern should also be set to loop.

Wrap up

The funny thing about writing about code is that it makes you reevaluate it all. You see a whole bunch of things that maybe should be changed. main.lua , for example, should probably not contain create_osc_server , since that is more related to what’s in (the dubiously named) Core.lua . But moving that entails shifting around a few other things while still keeping the initial control in main.lua .

There’s an inclination to partition tool files such that were it used as a template for a new tool you would have to make changes in as few files as possible. You can get the number of files to change down to two if you put all of the code in main.lua , but that gets you other problems.

In any event, this should be a useful place to start your own OSC-enabled Renoise tool.