Extensive OSC tutorial

Hi, I’ve seen that a lot of in_thread members are using OSC controls extensively in their everyday practice with Sonic Pi. However, the in-built tutorial is only covering the basic aspect of OSC messaging, both for input and output.

I’m starting a project which requires to make an intensive usage of OSC messaging: using Sonic Pi as the language for my live-coding performance, and Max/Msp as the support for audio-synthesis. I’m running into various problems when trying to send multiple messages in a very short amount of time (enveloppe trigger, filter cutoff, resonance, various controls, etc…)

I would like to start this topic to gather everything you’ve learned so far about OSC messaging in Sonic Pi, and what you will recommand for others to do in order to use this functionality with efficiency. For instance, how to properly send melodic or harmonic information, how to create good osc controls, etc…

This topic could be used in the future as a basis for a larger article or something else, covering this aspect of working with Sonic Pi.

Thanks, have a good day.

5 Likes

Hi there, I’d love to hear more about the specific issues you’re facing using OSC.

I have used OSC messaging extensively with Sonic Pi in a wide range of projects involving messages both to and from SP and to a range of interfaces from python scripts processing sketches TouchOSC and a brief play with Ableton live connection kit.
I think it best if you kick off with details of the problems you are finding.

I will post a more detailled example tomorrow, but here is a little snippet:

live_loop :sine do
  note =(ring 50, 54, 57, 59).tick(:note)
  sine(note, 2)
  sine_adsr(2000, 100, 1, 10)
end

Sine and sine_adsr are two functions defined in another buffer. The first one is sending three OSC messages : 1) note 2) note-on 3) note-off. The second one is sending four messages : 1) attack 2) decay 3) sustain 4) release.

The functions are :

define :sine do |note, len|
  use_osc "localhost", 5000
  osc "/trig 1"
  osc "/note " + note.to_s
  sleep len
  osc "/trig 0"
end

define :sine_adsr do |a, d, s, r|
  osc "/env_a " + a.to_s
  osc "/env_d " + d.to_s
  osc "/env_s " + s.to_s
  osc "/env_r " + r.to_s
end

These messages are received by the Max/Msp patch, but some packages are missing. A few of them are not received at all, some are late or desynchronized to the beat.

I think it’s mostly my fault, which is the reason why I was asking for advices to send OSC properly and efficiently. Here is a snippet of the journal when running the code:

{run: 79, time: 350.0, thread: :live_loop_sine}

├─ OSC -> localhost, 5000, /trig 0, []

├─ OSC -> localhost, 5000, /env_a 2000, []

├─ OSC -> localhost, 5000, /env_d 100, []

├─ OSC -> localhost, 5000, /env_s 1, []

├─ OSC -> localhost, 5000, /env_r 10, []

├─ OSC -> localhost, 5000, /trig 1, []

└─ OSC -> localhost, 5000, /note 59, []

Maybe a good workaround would be simply to rewrite everything using MIDI messages. :thinking:

Do you really mean to send what is essentially data as part of the OSC address?

eg osc "/env_a " + a.to_s
when a is 2000 is sending no data to address
"/env_a 2000"

or are you meaning to send
osc "/env_a",a.to_s
and then extract the data value sent (2000)?

It depends on how your max/msp patch is set up of course.
It seems a bit inefficient to have a separate osc address for every single data value.

Also, can your max/msp patch be set up to receive ALL the adsr data in one call. eg
osc "/adsr",a,d,s,r

In my patch, I’m using an external object from the CNMAT library that allows to route OSC messages using this particular format: /something datafollowing.

I was thinking that sending a bunch of them was okay. I will try to group all OSC informations concerning one synthesizer or sound device in one message, and format it later in Max.

My initial idea for separating the different messages was to keep the syntax as light as possible on the Sonic Pi side.

I’ve been working a bit on this project and now everything is fine. You were right @robin.newman. I’ve reworked my OSC messages to send every information in one message per synth:

define :simpleosc do |note, vel, a, d, s, r, cutoff, resonance, q, mix|
  use_osc "localhost", 5000
  osc note.to_s + " " + vel.to_s + " " +  a.to_s + " " + d.to_s + " " + s.to_s + " " + r.to_s + " " + cutoff.to_s + " " + resonance.to_s + " " + q.to_s + " " + mix.to_s
end

I’ve been able to synchronize my Max synthesizers with Sonic_Pi, and I’ve been playing for a while routing the audio back to Sonic Pi to mess the sound with the default effects.

Thanks for the suggestion.

Glad you got it going. I’d be interest in full details of the project on the max side once you have it finished.

Hi,

I’m going to hitchhike on this thread. @robin.newman You say that it’s better to put multiple data in to one message.

But in my case I have 3 devices sending minimum of 4 osc messages (prefereably more but I’m content with 4 if possible) .
With sync I can only sync multiple messages from one device.

How do I sync multiple devices?

What would be the code you wish you could write and what would be its behaviour?

Well every code on every device pulls sensor data and sends it repeatedly with a few seconds of sleep (this can be change to minutes) the data would trigger parameters in the sonic pi code. It would change delay times, sustain time also trigger certain cases of sample or small live_loops.

I would like for example to use

a, b = sync(osc)
c = sync(osc1)
d = sync(osc2)

And it would read al the sync or 1 sync that can handle multiple osc

a, b, c, d = sync(osc1, osc2, osc3)

Does this make sense?

Almost, but not quite yet.

Did you want the following to match all or just one of the incoming OSC messages:

a, b = sync(osc)
c = sync(osc1)
d = sync(osc2)

i.e. should all of osc, osc1 and osc2 come in before waiting for the next batch, or just any of osc, osc1 or osc2 come in before waiting for any of osc osc1 or osc2 again.

Same question for this line:

a, b, c, d = sync(osc1, osc2, osc3)

Here’s an example which uses wild cards to allow one loop to respond to three different addresses
and to extract the data for each one. They can be received in any combination.

#example using one sync loop to determine which of three osc commands is received

use_osc "localhost",4560
use_debug false
use_osc_logging false

#use function based on unpublished get_event routine to resolve wild cards
define :parse_sync_address do |address|
  v= get_event(address).to_s.split(",")[6]
  if v != nil
    return v[3..-2].split("/")
  else
    return ["error"]
  end
end

live_loop :test3 do
  #this routine sends 0 to 3 osc commands each pass
  #commands addresses are com1 com2 and com3
  #each sends different data
  puts "pass",tick
  osc "/com1",1 if one_in 3
  osc "/com2",2,20 if one_in 3
  osc "/com3",3,30,300 if one_in 3
  sleep 2
end

#this loop receives and decode ALL the osc commands sent
live_loop :decode do
  #use a wild card * to match all of com1,com2 and com3
  a= sync "/osc*/com*" #a is a list of the data items received
  res = parse_sync_address("/osc*/com*")
  #puts res #uncommentfor testing
  #res[1] holds com1, com2 or com3 as strings. extract last character and convert to integer
  code =res[1][-1].to_i
  case code #use a case statement to carry out differnt commands for each osc received
  when 1
    puts a[0] #print single data item for com1
    play :c3
  when 2
    puts a[0],a[1] #print the two data items for com2
    play :c4
  when 3
    puts a[0],a[1],a[2] #print all three data tiems for com3
    play :c5
  end
  
end

EDIT sounds quite nice if you reduce the sleep time in teh first loop from 2 to 0.2 Not really the point of the example but quite cool. I had it at 2 to make it easier to fathom what is going on.

@samaaron In my case the second statement would be true. Any osc coming in before waiting any following osc message per osc “channel”.
The timing between them doesn’t matter. In case there is a limit of total incoming messages. I can alter the delay in my Arduino code or use timers to make sure that they don’t all come in at the same moment.

But the main thing is that I need to have alle of the osc messages coming in as for now I only get the first sync data.

@robin.newman

#use function based on unpublished get_event routine to resolve wild cards
define :parse_sync_address do |address|
  v= get_event(address).to_s.split(",")[6]
  if v != nil
return v[3..-2].split("/")
  else
return ["error"]
  end
end

#this loop receives and decode ALL the osc commands sent
live_loop :decode do
  #use a wild card * to match all of com1,com2 and com3
  a= sync "/osc*/com*" #a is a list of the data items received
  res = parse_sync_address("/osc*/com*")
  #puts res #uncommentfor testing
  #res[1] holds com1, com2 or com3 as strings. extract last character and convert to integer
  code =res[1][-1].to_i
  case code #use a case statement to carry out differnt commands for each osc received
  when 1
    puts a[0] #print single data item for com1
    play :c3
  when 2
    puts a[0],a[1] #print the two data items for com2
    play :c4
  when 3
    puts a[0],a[1],a[2] #print all three data tiems for com3
    play :c5
  end

This would be all I need right to get several devices their osc messages into 1 single SonicPi code.

Yes this would work with several devices,as they all send to the same port, and you amks out the first bit with differnt ip addresses etc. For convenience I used very similar osc addresses /com1 /com2 /com3, but you could use more complex ones although it would make the extrication of the different addresses a little more involved. For convenince in the example I just sent osc calls from the local machine.

Is there any reason why you can’t use multiple live loops? One live loop per osc message you want to match and sync on?

Actually not, I’m trying it out like you said. I think I’m getting there with that setup.

@samaaron Thanks it works!

1 Like

@samaaron I cheered to soon.

uncomment do
live_loop :test do
delayosc = sync “/osc*/MHZ19/”
with_fx :gverb do
with_fx :octaver do
live_loop :input0 do
synth :dark_ambience, sustain: 0.70, attack: 0.50, release: delayosc
sleep 1
end
end
end
end
end

I want the loop to run continously and the data should change whenever an osc message comes in.
And I want multiple of these live_loops.

But I’m not getting the continously sound. It’s late so maybe I’m overlooking something?? :sweat_smile:

Hi @dewildequinten,

it’s pretty unusual to see nested live loops. However, from what you’ve written, what I would imagine woudl happen is that the :test live loop will wait for an incoming OSC message, and when one occurs, it will set the delayosc variable before creating two new FX and then an internal live loop that uses the delayosc variable for the release duration of the :dark_ambience synth.

On the next incoming OSC message, it will create a new local delayosc variable, create two new FX synths (which will immediately be discarded because the internal live loop already exists and therefore will not generate a new thread for them to wrap). The input0 live loop will be updated to use the new delayosc value next time it spins round.

Is this not what you’re observing? Could you explain what you’re getting and what you expected it to do?

This is indeed what I’m observing.

But I thought and need the loops to start playing from the start continuously.
And only when an OSC message comes in it should change parameters of the variables. The issue is with multiple different OSC messages that if they are declared “globally” it only picks the first.

I need to put the OSC message in a separate live loop as you said above. So I thought that was away.

5 different OSC messages from 3 OSC devices should come in and alter already playing music. I’m not sure if I’m making it clear to you :sweat_smile:

I know what I want but I’m not sure how it would work.