Software and Multimedia R&D.

Neurogami ®

Dance Noise.
Out now.

Get it here ...

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.

Renoise are distributed in the form of files that end with .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 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
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, PureData, 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.

Listen




Love me on Spotify!11!1