Norns: scripting best practices

Rather than a question oriented thread I’d like to put this out as a place to post or discuss snippets of code which are handy/helpful for other script writers.

Hopefully to develop a central repository of tips/tricks/best-practices.

A couple helpful links to that end:
https://github.com/monome/norns/wiki/coding-style-(lua)
https://github.com/monome/norns/wiki/syntax-2.0

8 Likes

As a first example, a number of scripts set the midi object to the default device midi.connect() (device slot 1) and don’t allow for that to be changed via a parameter. (which is a problem if you have a midi device you want to use in slot number 3 for example)

So… create a paramter for that.

local mo = midi.connect(1) -- defaults to port 1
mo.event = midi_event

-- process incoming midi
local midi_event = function(data) 
  d = midi.to_msg(data)
  -- etc.
end

function init()
  -- create a parameter for midi_device
  params:add{type = "number", id = "midi_device", name = "MIDI Device", min = 1, max = 4, default = 1, action = function(value)
    mo.event = nil
    mo = midi.connect(value)
    mo.event = midi_event
  end}
end

Code borrowed/adapted from @markeats

9 Likes

Should it tho? the SYSTEM > MIDI interface is designed especially to handle this. I’m not sure if using params for something that is so explicitly controlled via the system interface is better.

thinks for a moment

In the case where multiple devices are present, sending to .connect(1), by default, might be sending to the wrong device if the user actually want to send to .connect(2) And if the target device is not explicitly displayed on the interface it might be hard for the user to figure out why theirs device is not receiving any notes.

Okay, I can see the use of that, I will make it common practice into my own code, I will also update the tutorial.

Actually this could be a lot better, do you know how to get the name of the midi devices? Displaying a number means nothing to the user, unless they go through the SYSTEM > MIDI interface, instead if the params were explicitly:

  • MIDI Input: Roland UM-One
  • MIDI Output: Polivoks

I think that would be better.

1 Like

Different things. The system menu lets you allocate devices to the different “slots” for the whole device. The params are for a specific script instance. It’s up to the script to make use of those devices. The problem I see in many scripts (resulting in many “help thread” posts) is that the default .connect() goes to slot #1 and the user would then have to edit the script to use a different slot (because they want to use the device in slot #2) .

Multiple devices gets a bit more complicated and might not be well covered by this example.

Each device has .name.
Let me improve that code snippet.

   for x,y in pairs(midi.vports) do
       print (x .. ": " .. y.name)
   end
   print("-midi device details-")
    for i,v in pairs(midi.devices) do
      tab.print(midi.devices[i])
      print("-")
    end

Also a complication - you can’t have a string type as a param (yet). (I’m apparently confused about this)

the OPTION param type is a list of strings.

1 Like

Really? This seems to be working for me:

I use the option type, and the vport.name list as choices.

2 Likes

there may be some confusion about the prospective “string” param type, which is meant to be a user-input string (ie like the wifi password) for naming something, etc.

2 Likes

Manage MIDI device as parameters, by name

local viewport = { width = 128, height = 64 }
local devices = {}

-- Main

function init()
  -- Render Style
  screen.level(15)
  screen.aa(0)
  screen.line_width(1)
  -- Get a list of midi devices
  for id,device in pairs(midi.vports) do
    devices[id] = device.name
  end
  -- Render
  redraw()
  params:add{type = "option", id = "midi_output", name = "Midi Output", options = devices, default = 1, action=set_midi_output}
  params:add{type = "option", id = "midi_input", name = "Midi Input", options = devices, default = 2, action=set_midi_input}
end

-- Render

function draw_frame()
  screen.rect(1, 1, viewport.width-1, viewport.height-1)
  screen.stroke()
end

function draw_labels()
  line_height = 8
  -- 
  screen.level(5)
  screen.move(5,viewport.height - (line_height * 1))
  screen.text('Output')
  screen.move(5,viewport.height - (line_height * 2))
  screen.text('Input')
  --
  screen.level(15)
  screen.move(40,viewport.height - (line_height * 1))
  screen.text(devices[params:get("midi_output")])
  screen.move(40,viewport.height - (line_height * 2))
  screen.text(devices[params:get("midi_input")])
  screen.fill()
end

function redraw()
  screen.clear()
  draw_frame()
  draw_labels()
  screen.update()
end

-- 

function set_midi_output(x)
  update_midi()
end

function set_midi_input(x)
  update_midi()
end

local midi_input_event = function(data) 
  print('input',midi.to_msg(data))
end

local midi_output_event = function(data) 
  print('output',midi.to_msg(data))
end

function update_midi()
  if midi_output and midi_output.event then
    midi_output.event = nil
  end
  midi_output = midi.connect(params:get("midi_output"))
  midi_output.event = midi_output_event
  
  if midi_input and midi_input.event then
    midi_input.event = nil
  end
  midi_input = midi.connect(params:get("midi_input"))
  midi_input.event = midi_input_event
end

1 Like

Turned this into a small library


local Midi_Device_Helper = { devices = {}, input = nil, output = nil }

Midi_Device_Helper.init = function(self)
  -- Get a list of midi devices
  for id,device in pairs(midi.vports) do
    print('Midi Device Helper','Found device: '..device.name)
    self.devices[id] = device.name
  end
  -- Create Params
  params:add{type = "option", id = "midi_output", name = "Midi Output", options = self.devices, default = 1, action=self.set_output}
  params:add{type = "option", id = "midi_input", name = "Midi Input", options = self.devices, default = 2, action=self.set_input}
  params:add_separator()
  Midi_Device_Helper.set_output()
  Midi_Device_Helper.set_input()
end

Midi_Device_Helper.get_output_name = function(self)
  return self.devices[params:get("midi_output")]
end

Midi_Device_Helper.get_input_name = function(self)
  return self.devices[params:get("midi_input")]
end

Midi_Device_Helper.set_output = function(x)
  print('Midi Device Helper','Set output device: '..Midi_Device_Helper:get_output_name())
  Midi_Device_Helper.output = midi.connect(params:get("midi_output"))
end

Midi_Device_Helper.set_input = function(x)
  print('Midi Device Helper','Set input device: '..Midi_Device_Helper:get_input_name())
  Midi_Device_Helper.input = midi.connect(params:get("midi_input"))
end

Midi_Device_Helper.on_input = function(self,fn)
  if self.input == nil then print('Midi Device Helper','Missing Input Device') ; return end
  if self.input.event then
    self.input.event = nil
  end
  self.input.event = fn
end

Midi_Device_Helper.on_output = function(self,fn)
if self.output == nil then print('Midi Device Helper','Missing Output Device') ; return end
  if self.output.event then
    self.output.event = nil
  end
  self.output.event = fn
end

return Midi_Device_Helper

How To Use

Include the library

local mdh = include('lib/midi_device_helper')

Start the library

mdh:init()

Get a device name

mdh:get_output_name()

Set a event handler

mdh:on_input(fn)

This is now part of the tutorial.

8 Likes

It’s not very arty I know but… Has anyone used any unit testing approaches to Lua/Norns scripting? I often find myself with moments I could be working on norns scripts but without my norns on hand. I’ve also found some annoying bugs in Meadowphysics which could benefit from a targeted approach to fixing them.

I’ve started trying the Busted testing framework on my laptop locally. Things like gbuf or tabutil have to be included or stubbed in some way though.

1 Like

for norns in particular, there is the problem that many realtime processes are hard to unit-test. by “hard” i mean they require super-super-intensive mocking and results analysis. that said, certainly it’s a good idea for, ehh, “BL” and “UI.”

you can hear me in the hackalong, muttering about unit tests being a good idea as i type ad-hoc tests into the REPL for a very simple Pattern module. certainly for small BL pieces like this its clear that putting test functions in the module code is a good idea.

i guess i think that a test-running framework is the least important part of the process, the most important ones being 1) module/unit/class architecture, 2) the tests themselves.

re: dependencies

things like gbuf or tabutil have to be included

tabutil.lua, like many norns libs, has no dependencies and can be trivially included in any lua environment.

gridbuf.lua, like many other norns libs, uses norns system globals, and talks to hardware and/or external processes. this is where the effort of mocking really blows up and is arguably not worth it (e.g. mocks themselves can have bugs, &c &c), instead break out unit testable components and stick to functional testing for realtime interaction bits.

3 Likes