Norns — receive OSC notes (from Ableton)

I’d love to wirelessly send notes to norns from Ableton. My impression is that wireless MIDI is not an option, so I figured I’d try to do it through OSC.

After some digging I found a device in Connection Kit called OSC MIDI Send, which simply converts MIDI notes to OSC messages like so —

image

I figured this would work out of the box, but it doesn’t seem like norns is configured to recognize these messages as notes — if it isn’t already clear, I’m not too familiar with the technical details of MIDI or OSC.

So now I’m wondering — if I want this functionality, would it be as simple as writing a script to translate the OSC messages back into MIDI?

If so, what would be the most elegant way to integrate this functionality into pre-existing scripts? For instance, if I wanted to trigger molly_the_poly, would I simply insert my translation script at the top of its main file or something?

And is there a reason this functionality isn’t already supported? Lag perhaps? It seems like a desirable feature to me.

hey! great q’s.

OSC is supported, it’s just up to the script to state how it’d like to handle OSC messages (since each script defines its interactions with controllers – grids, MIDI, OSC, arc, HID, etc).

this raises a different question, though – why don’t more scripts support OSC control out the box? from my understanding, OSC messages aren’t always standardized the way MIDI messages are, since they can be customized and totally arbitrary. it seems that Connection Kit has decided on a particular style, but perhaps the wide variance of approaches contributes to a lack of built-in support in scripts? there’s probably some work to do on the norns documentation, as well – clearer API would help make OSC easier to add.

for now, the OSC-centric norns study is a good place to start unpacking how to work with OSC + norns: https://monome.org/docs/norns/study-5/#numbers-through-air

pretty much, yeah.

using molly as an example, MIDI input is defined starting on line 153.

molly's MIDI input code
-- MIDI input
local function midi_event(data)
  
  local msg = midi.to_msg(data)
  local channel_param = params:get("midi_channel")
  
  if channel_param == 1 or (channel_param > 1 and msg.ch == channel_param - 1) then
    
    -- Note off
    if msg.type == "note_off" then
      note_off(msg.note)
    
    -- Note on
    elseif msg.type == "note_on" then
      note_on(msg.note, msg.vel / 127)
      
    -- Key pressure
    elseif msg.type == "key_pressure" then
      set_pressure(msg.note, msg.val / 127)
      
    -- Channel pressure
    elseif msg.type == "channel_pressure" then
      set_pressure_all(msg.val / 127)
      
    -- Pitch bend
    elseif msg.type == "pitchbend" then
      local bend_st = (util.round(msg.val / 2)) / 8192 * 2 -1 -- Convert to -1 to 1
      local bend_range = params:get("bend_range")
      set_pitch_bend_all(bend_st * bend_range)
      
    -- CC
    elseif msg.type == "cc" then
      -- Mod wheel
      if msg.cc == 1 then
        set_timbre_all(msg.val / 127)
      end
      
    end
  
  end
  
end

using the OSC study + your sample OSC messages as an example, you could add a parallel osc_event function under that midi_event function which connects received OSC messages with the script’s defined note_on and note_off functions.

example code (not tested or totally comprehensive, but should get you started):

function osc_in(path, args, from)

     if path == "/Note1" then
          osc_note = args[1]
     elseif path == "/Velocity1" then
          osc_vel = args[1]
     end

     if osc_vel > 0 then
          note_on(osc_note, osc_vel)
     else
          note_off(osc_note)
     end

end

osc.event = osc_in

hopefully this helps get you going? lmk!

3 Likes

Ooh yes, very cool! Thanks for the info.

I think you’re right — the OSC specification doesn’t at all define a “note” in the way the MIDI spec does.

That’s exactly what I’m thinking… I’d love to see a norns “OSC note” spec that individual scripts could accept (or flip on its head) — but perhaps I should get this working in molly first :slight_smile:


Speaking of which, thanks for the code! I’ll certainly dig into this later and report back.

One thing I’ll say though — perhaps for someone who is reading this later — is that Connection Kit appears to encode polyphony in its OSC Container (as I believe it is called). So Note1 / Velocity1 are the messages for the 1st key held down, Note2 / Velocity2 the messages for the 2nd key held down, and so on.

Other than, that the code looks great :slight_smile: I’m looking forward to getting this working.

1 Like

Aaand I’m done lol.

This goes anywhere:

-- OSC input
local osc_vel  = nil
local osc_note = nil 
function osc_in(path, args, from)
  
  if string.find(path, 'Velocity') ~= nil then
    osc_vel  = args[1]    -- Ableton Connection Kit sends velocity before note, so we
    osc_note = nil        -- 1. Erase previous note upon receiving new velocity.
  
  elseif string.find(path, 'Note') ~= nil then
    osc_note = args[1]    -- 2. Update note value upon receiving it.
  
  end

  if osc_note ~= nil then -- 3. Trigger a complete note.
    
    -- Note on
    if osc_vel > 0 then
      note_on(osc_vel, osc_note / 127)
    
    -- Note off
    else                    
      note_off(osc_note) -- Note off

    end

  end

end

And

  osc.event = osc_in

goes in the init function. And it works!!

Quite magical I must say… I can now effortlessly play or sequence norns from Ableton (or, anything else for that matter — thinking specifically of desktop Orca).

Amazing stuff! Can you imagine desktop Orca and norns Orca sequencing one another? Lol

15 Likes

Hi @tejomay your code worked instantly with molly_the_polly and i was also able to adapt it for Mx.Synths, thank you! and also @dan_derks . I also tried to figure out how to make it work with Mx.Samples but that was quite a bit over my head…

I’m not using Ableton so I couldn’t use the Max for Live device you mentioned in the first post but I made a Camomile-Plugin using Pure Data that does the same thing (I think…)
I only tried it using Bitwig Studio on Windows but it should work also work in any DAW on Mac and Linux, or at least it should be quite easy to make it work by pushing some files around.

CamomileOscOut.zip (7.6 MB)

4 Likes

@tejomay how adaptable is the code above for any script? (just looking for pointers on things to keep an eye out for!)

and i’m guessing the note stuff would be pretty straight forward when being sent from max?

This could also be a great Mod. System wide OSC to notes and velocity.

2 Likes

Yes I thought so too! There seems to be no standard way of handling note messages internally but I just had a quick look at gridkeys mod and its using a virtual midi port. Maybe thats a good starting point for am OSC midi mod :slight_smile:
One could probably even make it work with pitchbend aftertouch, a few ccs and all!

1 Like

untested, but wrapping @tejomay code into a mod should look something like:

local mod = require 'core/mods'


-- -------------------------------------------------------------------------
-- UTILS: MIDI IN

local function send_midi_msg(msg)
  local data = midi.to_data(msg)
  local is_affecting = false

  -- midi in
  for _, dev in pairs(midi.devices) do
    if dev.port ~= nil and dev.name == 'virtual' then
      if midi.vports[dev.port].event ~= nil then
        midi.vports[dev.port].event(data)
        break
      end
    end
  end
end

local function note_on(note_num, vel)
  local msg = {
    type = 'note_on',
    note = note_num,
    vel = vel,
    ch = 1,
  }
  return send_midi_msg(msg)
end

local function note_off(note_num)
  local msg = {
    type = 'note_off',
    note = note_num,
    vel = 100,
    ch = 1,
  }
  return send_midi_msg(msg)
end


-- -------------------------------------------------------------------------
-- UTILS: OSC IN

-- OSC input
local osc_vel  = nil
local osc_note = nil
local function script_osc_in(path, args, from)

  if string.find(path, 'Velocity') ~= nil then
    osc_vel  = args[1]    -- Ableton Connection Kit sends velocity before note, so we
    osc_note = nil        -- 1. Erase previous note upon receiving new velocity.

  elseif string.find(path, 'Note') ~= nil then
    osc_note = args[1]    -- 2. Update note value upon receiving it.

  end

  if osc_note ~= nil then -- 3. Trigger a complete note.

    -- Note on
    if osc_vel > 0 then
      note_on(osc_note / 127, osc_vel)

      -- Note off
    else
      note_off(osc_note) -- Note off

    end
  end
end


-- -------------------------------------------------------------------------
-- MAIN

mod.hook.register("script_pre_init", "osc-in-midi", function()
                    local script_init = init
                    init = function ()
                      script_init()

                      print("mod - osc-in-midi - init")
                      osc.event = script_osc_in
                    end
end)

now please someone make a cheap midi 2 osc over wifi adapter using an ESP32, Raspi Pico or a Teensy LC+ESP8266 combo :grin:

3 Likes

Will check this when I get home tonight!

1 Like

hi @eigen in theory would i just need to put this in any script and it work? (sorry i’m very new to norns and theres a lot to get my head round!)

not exactly, you’d need to create a directory /home/we/dust/code/osc-in-midi/lib/ and put the above code in a file named mod.lua under it.

alright, screw that, i just made a repo (@tejomay i can transfer its ownership if you want)

just do in maiden:

;install https://github.com/p3r7/osc-in-midi

after that, the mod should be visible and would have to be enabled in the dedicated menu (see doc).

also, i had a typo in the above code that i just fixed. it is still untested.

5 Likes

I tried to to a straight copy/paste, and after i changed the osc.event call in the bottom, it seemed to work. I don’t have Ablteon though, so started to change it to to be usersettable adresses for the incoming notes. But it gave me enough interest that I’ve started to do a new mod from the bottom up, as a LUA excersize.

TLDR: Code works!

4 Likes

@eigen thank you! i tried to make your mod work before but probably did something wrong… with the version in your repo it works, theres still quite a bad bug though: in line 64 you have note_on(osc_note / 127, osc_vel) which generates incredibly deep notes :smiley: without /127 everything seems to work great so far!

@Jensu if you are making a new mod, i think it would be great to have a bit more midi functionality in the osc midi translator mod like maybe pitchbend, aftertouch and a few ccs to map into scripts! I’d be happy to adapt my OscOut plugin i shared above for that :slight_smile:

1 Like

yeah, there was a mistake that i later fixed, as spotted by:

i’ve just removed the / 127 scaling factor.

adding all the other standard midi features (pitch bend, aftertouch…) should just be pretty straightforward.

1 Like

oh okay, i thought jensu was referencing something else, sorry!

Edit:
I had a look at the osc and midi sections in the norns docs and realized that i probably can do it! :slight_smile: I modified the osc-in-midi mod and to receive a new osc messge for midi ccs.
I tested it with MIDI Monitor and there everything looks right, but somehow i can’t map any parameters to these midi ccs coming from the virtual port, either it doesn’t ger treated the same way as usb midi devices or there’s something wrong with my messages.
Maybe someone else has an Idea whats going on?

my changes to the mod

i added this bit after the osc note message handling:

  elseif string.find(path, 'controlchange') ~= nil then
    osc_ccval = args[1]    -- 3. Update cc value upon receiving it
    osc_ccnum = args[2]
    osc_ccch = args[3]
type or paste code here

and this here after the midi note gets send:

   if osc_ccval ~= nil then -- 5. Trigger a complete cc.
     cc(util.round(osc_ccnum), util.round(osc_ccval), util.round(osc_ccch))
     osc_ccval = nil        -- 6. Erase previous cc upon sending cc 
   end

visting this code today and I reworked it for OSC coming from a different source (this osmid code on another raspberry pi).

@rgkies where were you sending CCs from? from the Ableton OSC Send device? Wondering because your arguments seem to be in a different order from what I get.

I can also confirm that the virtual device does not seem to work for MIDI Mapping in Params. (although, if you want to map standard params, that’s easy to do directly with OSC instead)

I’m sending from a pure data patch I made and used the order of the arguments I got from the midi objects there. I could still switch them around if thats not the standard way to do it.

Thanks for finding that @okyero I didn’t know how to figure that out. I think it would be great if mapping parameters from the virtual midi would be possible. Just using midi learn to map script parameters is much easier and more flexible than changing the osc parameters on the senders side…