How To Create A New Duplex Application

While there is a good amount of documentation on how to work with control-maps and device configurations, there has been little documentation on how to write your own application from the ground up.
To adress this need, here is the first in a series of exercises, gradually becoming more complex with each step.

Part 1: Creating a simple Duplex application

This is a basic demonstration of how to write an application, and the techniques involved in getting proper bi-directional communication going with the Duplex framework.
For the sake of simplicity, we’ll be creating a simple application with just a few lines of code. We want to be able to control the metronome in Renoise, and we want the state of the Metronome to be reflected on the controller at all times.
Although you could write an application that doesn’t care what Renoise is doing, it’s simply more intuitive, and feel more “robust” to see a button reflect the correct state at all times - this is called bi-directional communication, and part of the reason why Duplex was born (as well as making a script across a wide range of devices, of course).

Let’s start with a bit of background info, and take a look at how an application appear on a controller in the first place.

1. How applications are tied in with the Duplex framework

We’ve got a special folder in Duplex called “Applications”. This is where all the applications are situated: Mixer.lua, Effect.lua, etc.
Any .lua files in this folder are automatically included on each startup, but you can’t just write an application, put it in this folder and expect your device to magically start using it - you need to create a configuration for it as well.

So let’s start with that.

Under normal circumstances, all of Duplex’ configurations are located inside the device classes. For the sake of this example, we’ll use the imaginary “R-Control” device.
Opening the R-Control class, we’ll see that it has existing configuration entries. We simply want to add our new one at the bottom, so we add the following:

duplex_configurations:insert {
  -- configuration properties
  name = "Metronome",
  pinned = true,
  -- device properties
  device = {
    display_name = "R-control",
    device_port_in = "USB R-control",
    device_port_out = "USB R-control",
    control_map = "Controllers/R-control/R-control.xml",
    thumbnail = "R-control.bmp",
    protocol = DEVICE_MIDI_PROTOCOL
  },
  applications = {
    Metronome = {
      mappings = {
        toggle = {
          group_name = "Switches",
          index = 1,
        }
      }
    },
  }
}

Actually, this is just a copy-paste of the previous entry (the device, class name etc.), with a single entry in the applications branch for our Metronome application (note that you are not limited to running a single application, you can add as many applications as you like in any single configuration - but you can only run a single configuration for each device).

Inside our Metronome application entry, we also created the mapping we want to control (“toggle”), and it has been assigned to the first button in the group called “Switches”. The group name is referring to a named group in the control-map - if you want, you can open the control-map file (R-Control.xml) to see where this group is actually located.

Now, if you start Renoise, the Duplex tool menu already contains a new preset for the R-Control, but it’s hardly functional yet.

2. Creating a functional application

To make our application work, we need to make sure that it has the special methods that is required for a Duplex application (special events like when a new document has been created, or continuous polling to create updates over time). We could of course write these methods ourselves, but thanks to the object-oriented nature of Lua, it’s possible to automatically inherit those methods from a parent class. To do this, we extend the Duplex Application class with our own Metronome “implementation” (this is what a class that inherit another class is called when programming in an object oriented language).

This is the skeleton for the class: Apart from extending the Application class, we also specify a table for it’s options (which, in this case, is empty as our Metronome has no options), and supply our mapping (“toggle”) with a useful description.

class 'Metronome' (Application)

Metronome.default_options = {}

function Metronome:__init(display,mappings,options,config_name)
 self.mappings = {
  toggle = {
   description = "Metronome: toggle on/off"
  }
 }
 -- initialize our parent class
 Application.__init(self,display,mappings,options,config_name)

end

Save the file into the Duplex/Applications folder as “Metronome.lua”.
It doesn’t do anything yet, but at least we no longer encounter an error message if we choose to run the application.

Now we really want to add the exciting stuff (a button), but to do this we need to initialize the application first. Let’s add the following:

-- called automatically when application is started
function Metronome:start_app()
 if not Application.start_app(self) then
  return
 end
end

As you can see, the call to Application.start_app() return a boolean value. This is because the start_app() method is checking the available mappings, and if a problem is encountered (such as a missing or invalid mapping), the application should return a helpful error message.

Oh yeah, and start_app() will automatically call build_app() the first time it’s run. This is a special function where we construct our user-interface:

-- construct the user interface

function Metronome:_build_app()

 local c = UIToggleButton(self.display)
 c.group_name = self.mappings.toggle.group_name
 c:set_pos(self.mappings.toggle.index)
 c.on_change = function(obj)
  -- a stopped application should ignore any communication
  if not self.active then
   return false
  end
  renoise.song().transport.metronome_enabled = obj.active
 end
 self._toggle = c

 -- we successfully created the UI
 return true

end

We’ve got a couple of button types to choose from, for this purpose we chose the UIToggleButton, as we want a simple on/off button.
Now save, reload the tools and try to launch the “R-control Metronome” configuration, and hopefully it’s working! We’re getting close to something useful, but it’s not completely finished yet.

As updates to the button state happen when we press it, or when it’s changed in Renoise, there’s a third option to consider: what about when we start the application? The initial state needs to be correct as well.
We can solve this by calling a method which will simply set the button to the correct state, and make sure it’s always called when the application is started.

-- set button to current state
function Metronome:update()
 if self._toggle then
  self._toggle:set(renoise.song().transport.metronome_enabled)
 end
end

-- make sure update() is called when app is started

function Metronome:start_app()
 if not Application.start_app(self) then
  return
 end
 self:update()
end

Now, we can launch the R-Control configuration, and it will always display the correct state to begin with.

Next step is to listen to the changes in the metronome state from within Renoise. To pull this off, we need to attach a listener to the observable value, which tell us the present state of the metronome.
If we open “Renoise.Song.API.lua” and search for “metronome” we’ll see that we’ve got exactly what we need:

renoise.song().transport.metronome_enabled_observable

However, it’s get a little more complicated than that - because the property is a member of renoise.song() we also need to make sure that the notifier is recreated when we open a new song, or it would become invalid each time a new song was loaded or created. Basically, we need to re-attach the listener to our script each time this happens.
For this purpose, we’ll add two functions, on_new_document() and attach_to_song(). Note that the method on_new_document() is special, since it’s automatically called by our parent Application class whenever a new song becomes available.

-- called whenever a new document becomes available
function Metronome:on_new_document()
 self:_attach_to_song()
end

-- attach notifier to the song, handle changes
function Metronome:_attach_to_song()
 renoise.song().transport.metronome_enabled_observable:add_notifier(
  function()
   if self._toggle then
    self._toggle:set(renoise.song().transport.metronome_enabled)
   end
  end
 )

end

We will also add attach_to_song() to our build_app() method, to make sure that it’s called at initialization time.

And that’s it - we’re ready to include our faboulous Metronome into any device configuration…well, any device that features a button :slight_smile:

This is the full source for the Metronome application:

--[[============================================================================
-- Duplex.Application.Metronome
============================================================================]]--

--[[--

Take control of the Renoise metronome (tutorial).

--]]

--==============================================================================

class 'Metronome' (Application)

Metronome.default_options = {}
Metronome.available_mappings = {
  toggle = {
    description = "Metronome: toggle on/off"
  }
}
Metronome.default_palette = {
  enabled = { color = {0xFF,0x80,0x80}, text = "M", val=true },
  disabled = { color = {0x00,0x00,0x00}, text = "M", val=false }
}

--------------------------------------------------------------------------------

--- Constructor method
-- @param (VarArg)
-- @see Duplex.Application

function Metronome:__init(...)

  Application.__init(self,...)

end

--------------------------------------------------------------------------------

--- inherited from Application
-- @see Duplex.Application.start_app
-- @return bool or nil

function Metronome:start_app()

  if not Application.start_app(self) then
    return
  end
  self:update()

end

--------------------------------------------------------------------------------

--- inherited from Application
-- @see Duplex.Application._build_app
-- @return bool

function Metronome:_build_app()

  local map = self.mappings.toggle
  local c = UIButton(self,map)
  c.on_press = function(obj)
    local enabled = renoise.song().transport.metronome_enabled
    renoise.song().transport.metronome_enabled = not enabled
    self:update()
  end
  self._toggle = c

  -- attach to song at first run
  self:_attach_to_song()

  return true

end

--------------------------------------------------------------------------------

--- set button to current state

function Metronome:update()
  if self._toggle then
    if renoise.song().transport.metronome_enabled then
      self._toggle:set(self.palette.enabled)
    else
      self._toggle:set(self.palette.disabled)
    end
  end
end

--------------------------------------------------------------------------------

--- inherited from Application
-- @see Duplex.Application.on_new_document

function Metronome:on_new_document()
  self:_attach_to_song()
end

--------------------------------------------------------------------------------

--- attach notifier to the song, handle changes

function Metronome:_attach_to_song()

  renoise.song().transport.metronome_enabled_observable:add_notifier(
    function()
      self:update()
    end
  )

end

Hi,

I thought it worth bumping this - I’m trying to learn about Duplex and Scripting and found this useful reading, so if others are curious this bump may save a few searches. Also if you know of other useful “getting started” posts for Duplex and/or scripting post 'em up :)

Cheers,
LW.

Hi, another very useful post about getting started with this stuff… How To Start Editing Duplex Files for your own controller.

Cheers,
LW.

Just want to express my gratitude, in finally took the time to learn the Renoise lua api and i have to say: i like it! Thanks

1 Like

I just finished reading this and I, too, wanted to express my appreciation that someone (lookin @ you, danoise) took the time to write this. Luckily, I have some coding experience, so lua doesn’t seem too much of a stretch. But finding the time to get behind it is, unfortunately. (Probably not until the end of the year) Being able to script the app was a major reason I jumped from ‘big, commercial, daw’ to Renoise, and this post just vindicates that reasoning. Now if I could just make my days 30hrs long…!

I have a question, not sure if this is the right place, but here it goes.

I like steppor. I want something like steppor for my APC 40 mkII. I would like to to make this happen using Duplex, as I have had good experience with Duplex in the past. The thing I am not sure about is this:

Is it possible to make Duplex display different data on a MIDI controller (i mean like a single button) dependent on the “mode” it’s in. I haven’t used Duplex in a while, but the way I remember it, it wouldn’t work that way. For example: Pattern mode makes the Duplex StepSequencer show up on the pads, and in Grid mode the pads work like GridPie. Can I do this with a configuration, or do I have to create a new application?

Thanks for this tutorial, and all your fantasticly helpful articles and tools danoise!

I like steppor. I want something like steppor for my APC 40 mkII.

Well, stepp0r is stepp0r … not Duplex :slight_smile:

I know Palo van Dalo personally, and his vision was to create a compact, self-contained “environment” for the Launchpad
Of course, he could have used Duplex for this but he decided to code things from the ground up.

So while you can come a long part of the way with Duplex, it will not be able to completely replicate stepp0r.

Is it possible to make Duplex display different data on a MIDI controller (i mean like a single button) dependent on the “mode” it’s in

Can I do this with a configuration, or do I have to create a new application?

Yes, since a few versions Duplex has had something called “states”. It’s doing what you are asking for, adding more flexibility to configurations.

States are relatively simple to specify (XML based instructions contained in the control-map), that can show and hide elements of the control surface depending on a value of a button or slider.

But they are limited when compared to what you can achieve with a full-blown application. For example, you can’t specify somehow that a given part of the Launchpad should show when focus in the matrix. Or In other words, states do not have “access” to such events, only those initiated by the user - pushing buttons and such.

Thanks for pointing me to states. I will have a look into it…

So while you can come a long part of the way with Duplex, it will not be able to completely replicate stepp0r.

Do you have something specific in mind that Stepp0r does that couldn’t be accomplished via Duplex?

in the step-by-step manual on how to create a Duplex application (point 2) there is a small mistake:

local c = UIToggleButton(self.display)

-> it has to be “UIButton” instead of “UIToggleButton”.

In the complete code (which is written on the bottom of the post), it is correct but this cost me some time and maybe it will also for others :wink:

Hi, where can i find the duplex luadoc reference?
this link in here is broken

Indeed, link is gone.
I will see if I can find the files somewhere else.

1 Like

did you manage to find the files?

A few years later, but I think I found it, and it migjt help someone else having troubles finding it.

https://renoise.github.io/libraries/duplex/index.html

1 Like

thznk’s it could be helpfull

Edit : I found below reason and a way to avoid it. That reason is that when activating daw mode of LPX, the midi input device MIDIIN2 (LPX MIDI) is disapper just a moment. Then the variables of the midi input device instance are inserted nil into. But MIDIIN2 reappers and starts to work agin. GC understands MIDIIN2 is not refered from any variables, so it will be closed by GC. So I write a code to re-create the midi input device again after activating daw mode and insert the midi input device to launchpadx.midi_in again. Now it is working as I expect it to.
Thank you for giving your time.


Hi, there. I’m creating a new duplex midi device of Launchpad X(LPX).
Could you have an idea for avoiding premature garbage collection for a midi input device in Duplex?

Assumption, LPX has 2 midi interfaces and communicates with DAW(Renoise) by switching them depending on its mode. After activating these midi interface in two Duplex processes, only one midi input device is closed in a little while. Of course, I don’t write any code calling device:release() (it is strange that only one midi input device is closed). Even if I don’t act anyting in Renoise after reloading all scripts, it will happen.

It is log clip when happen it,

~~~ load some tools ~~~

ScriptingTools: Initializing Scripting Tool: 'C:\Users\user\AppData\Roaming\Renoise\V3.4.4\Scripts\Tools\com.renoise.Duplex.xrnx\'...

MIDI: Opening MME Midi-In device 'LPX MIDI'
MIDI: Opening MME Midi-Out device 'LPX MIDI'
MIDI: Opening MME Midi-In device 'MIDIIN2 (LPX MIDI)'
MIDI: Opening MME Midi-Out device 'MIDIOUT2 (LPX MIDI)'

~~~ finish loading all tools, and few seconds later ~~~~

MIDI: Closing MME Midi-In device 'MIDIIN2 (LPX MIDI)'

I tried the propsed method which is declaring global variable to avoid gc. It has similar purpose of my problem.

I declare global variable of MidiInputDevice like this:

class "LaunchpadX" (MidiDevice)

LPXmidiin = nil
LPXmidiout = nil

function LaunchpadX:open()

  local input_devices = renoise.Midi.available_input_devices()
  local output_devices = renoise.Midi.available_output_devices()

  if table.find(input_devices, self.port_in) then
    local midiin = nil
    if self.interface_type == "midi" and LPXmidiin == nil then
       LPXmidiin = renoise.Midi.create_input_device(self.port_in,
        {self, _G[type(self)].midi_callback},
        {self, _G[type(self)].sysex_callback}
      )
      midiin = LPXmidiin
    else
      midiin = renoise.Midi.create_input_device(self.port_in,
        {self, _G[type(self)].midi_callback},
        {self, _G[type(self)].sysex_callback}
      )
    end

    self.midi_in = midiin
  else
    LOG("Notice: Could not create MIDI input device ", self.port_in)
  end

  if table.find(output_devices, self.port_out) then
    local midiout = nil
    if self.interface_type == "midi" and not "LPXmidiout" == nil then
      LPXmidiout = renoise.Midi.create_output_device(self.port_out)
      midiout = LPXmidiout
    else
      midiout = renoise.Midi.create_output_device(self.port_out)
    end

    self.midi_out = midiout
  else
    LOG("Notice: Could not create MIDI output device ", self.port_out)
  end

end

But it did not work. Its reason may be declaring in a MidiDevice of Duplex.
Any thoughts would be greatly appreciated.