Skip to content

Instantly share code, notes, and snippets.

@turbo
Last active July 13, 2022 09:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save turbo/3a55cd31103722b4c03087ac02679e6d to your computer and use it in GitHub Desktop.
Save turbo/3a55cd31103722b4c03087ac02679e6d to your computer and use it in GitHub Desktop.
Send coarse NRPN messages from TouchOSC

General Approach

For synths that use CC-over-NRPN (like the Circuit Tracks), assembling NRPN messages is pretty simple in a script:

function sendNRPN(ch, MSB, LSB, val)
  -- nrpn address msb
  sendMIDI({ 176 + ch - 1, 99, MSB })
  -- nrpn address lsb 
  sendMIDI({ 176 + ch - 1, 98, LSB }) 
  -- coarse entry (CC-over-NRPN)
  sendMIDI({ 176 + ch - 1,  6, val }) 
  -- null nrpn address msb
  sendMIDI({ 176 + ch - 1, 99, 127 }) 
  -- null nrpn address lsb
  sendMIDI({ 176 + ch - 1, 98, 127 }) 
end

You need to copy this function into each component's script and then call it from there, e.g. from an onValueChanged handler. For example, if we want to change the source for compressor 1, we look into the reference manual for the Circuit:

image

And then send the appropriate message, for example (for a radio control):

sendNRPN(16, 2, 55, self.values.x)

Here's a small version that is easier to copy around:

function sendNRPN(ch,MSB,LSB,val)
 local f=sendMIDI;ad=176+ch-1
 f({ad,99,MSB})
 f({ad,98,LSB}) 
 f({ad,6,val}) 
 f({ad,99,127}) 
 f({ad,98,127}) 
end

Global Dispatch

To prevent us from having to copy the function into each component, we can create a global dispatcher in the root element. Click on the backround to access the root control and add this script:

function onReceiveNotify(cmd, vt)
  if cmd == 'nrpn' then
    sendNRPN(unpack(vt))
  end
end

function sendNRPN(ch,MSB,LSB,val)
 local f=sendMIDI;ad=176+ch-1
 f({ad,99,MSB})
 f({ad,98,LSB}) 
 f({ad,6,val}) 
 f({ad,99,127}) 
 f({ad,98,127}) 
end

Now in any control, we can directly send NRPN messages like this:

root:notify('nrpn', {15, 2, 55, self.values.x})

Receiving NRPN

We can build a reverse dispatch to route incoming NRPN back to the controls. First, we need code to parse incoming NRPN and find applicable controls to send the NRPN to. We introduce a tag nrpn_enabled, so that any control with this tag will receive any incoming NRPN. In the root control code:

local buf_nrpn = nil

function onReceiveMIDI(message, connections)
  if message[1] == 248 then
    return
  end
  
  if message[2] == 99 and message[3] ~= 127 then
    buf_nrpn = { message[1] - 176 + 1, message[3] }
  elseif message[2] == 98 and message[3] ~= 127 then
    if not buf_nrpn or #buf_nrpn ~= 2 then
      print('error: LSB did not follow MSB')
      buf_nrpn = nil
    end
    table.insert(buf_nrpn, message[3])
  elseif buf_nrpn and message[2] == 6 then
    table.insert(buf_nrpn, message[3])
    print('IN/NRPN', unpack(buf_nrpn))
    distributeNRPN(buf_nrpn)
    buf_nrpn = nil
  elseif buf_nrpn then
    print('error: unknown NRPN structure (abandon)')
    buf_nrpn = nil
  else
    print('IN/MIDI', unpack(message))
  end
end

function distributeNRPN(vt)
  -- send to all tag:nrpn_enabled
  local recvs = root:findAllByProperty('tag', 'nrpn_enabled', true)
  print('dispatching NRPN to ' .. tostring(#recvs) .. ' controls')
  for _, ctrl in ipairs(recvs) do
    ctrl:notify('nrpn', vt)
  end
end

and inside of a control, we can respond like this:

function onReceiveNotify(cmd, vt)
  if cmd == 'nrpn' then
    if vt[1] == 16 and vt[2] == 2 and vt[3] == 55 then
      self.values.x = vt[4]
    end
  end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment