OSC messaging, Processing and SuperCollider

posted by on 2016.10.15, under Supercollider
15:

OSC stands for Open Sound Control, and consists in a protocol for networking between computers, synths and various multimedia devices. For instance, it allows a software, like Ableton Live, say, to communicate with a hardware synth, whenever the latter supports OSC. You might think that you already now how to do this via MIDI, and you’d be partially right. The differences between OSC and MIDI are many: accuracy, robustness, etc. One of the most important, or rather most useful difference, though, is that OSC allows to send *any* type of messages at high resolution to any address. Differently, the MIDI protocol has its own specific messages, like note On, not Off, pitch, etc., with low resolution (0-127). This means that if you use MIDI to communicate between devices, you’ll be required to translate your original message, say for instance the position of a particle or the color of a pixel at mouse point, via the standard MIDI messages. And this is often not enough. An example is usually better than many principled objections, so here comes a little Processing sketch communicating to SuperCollider. The idea is quite simple: in the Processing sketch, systems of particles are spawned randomly, with the particles having a color, a halflife, and also a tag. The user can click on the screen and generate a circle. If a particle traverses one of the circles, it will tell SuperCollider to generate a synth, and it will pass by various information, like its position, velocity, color, etc. This data will affect the synth created in SuperCollider. Here’s the Processing code, which uses the library oscP5

//// Setup for OscP5;

import oscP5.*;
import netP5.*;
OscP5 oscP5;
NetAddress Supercollider;

//// Setting up the particle systems and the main counter;


ArrayList<Parsys> systems;
ArrayList<Part> circles;
int count = 0;

void setup(){
  size(800, 800);
  background(255);
  oscP5 = new OscP5(this,12000);
  Supercollider = new NetAddress("127.0.0.1", 57120);
 
  systems = new ArrayList<Parsys>();
  circles = new ArrayList<Part>();
 
}

void draw(){
  background(255);
  for (int i = systems.size() - 1; i>=0; i--){
    Parsys sys = systems.get(i);
    sys.update();
    sys.show();
    if (sys.isDead()){
      systems.remove(i);
    }
    for (int j = 0; j < circles.size(); j++){
      Part circ = circles.get(j);
      sys.interact(circ);
    }
  }
 
  for (int i = 0; i < circles.size(); i++){
    Part circ = circles.get(i);
    circ.show();
  }
  if (random(0, 1) < 0.04){
    Parsys p = new Parsys(random(0, width), random(0, height));
    systems.add(p);
  }
}

void mousePressed(){
  Part circ = new Part(mouseX, mouseY, 0, 0, 0, 30);
  circ.stuck = true;
  circ.halflife = 80;
  circles.add(circ);
}

/////Define the class Parsys

class Parsys {
  ArrayList<Part> particles;
 
  Parsys(float x, float y){
    particles = new ArrayList<Part>();
   
    int n = int(random(20, 80));
    for (int i = 0; i < n; i++){
      float theta = random(0, 2 * PI);
      float r = random(0.1, 1.2);
      Part p = new Part(x, y, r * cos(theta), r * sin(theta), int(random(0, 3)));
      p.tag = count;
      count++;
      particles.add(p);
    }
  }
 
  void update(){
    for (int i =  particles.size() - 1; i>=0; i--){
      Part p = particles.get(i);
      p.move();
      if (p.isDead()){
        particles.remove(i);
      }
    }
  }
 
  void show(){
    for (int i =  particles.size() - 1; i>=0; i--){
      Part p = particles.get(i);
      p.show();
    }
  }
 
  boolean isDead(){
    if (particles.size() <=0){
      return true;
    }
    else return false;
  }
 
  void interact(Part other){
    for (int i = 0; i < particles.size(); i++){
      Part p = particles.get(i);
      if (p.interacting(other)){
        float dist = (p.pos.x - other.pos.x)* (p.pos.x - other.pos.x) + (p.pos.y - other.pos.y)*(p.pos.y - other.pos.y);
        if (!p.active){
          float start = other.pos.x/width;
        p.makeSynth(start, dist/(other.rad * other.rad));
        p.active = true;
        }
        else {
          p.sendMessage(dist/(other.rad * other.rad));
        }
      }
      else
      {
        p.active = false;
      }
    }
  }
}

/////Define the class Part

class Part {
  PVector pos;
  PVector vel;
  int halflife = 200;
  int col;
  float rad = 2;
  boolean stuck = false;
  boolean active = false;
  int tag;

  Part(float x, float y, int _col) {
    pos = new PVector(x, y);
    col = _col;
  }

  Part(float x, float y, float vx, float vy, int _col) {
    pos = new PVector(x, y);
    vel = new PVector(vx, vy);
    col = _col;
  }

  Part(float x, float y, float vx, float vy, int _col, float _rad) {
    pos = new PVector(x, y);
    vel = new PVector(vx, vy);
    rad = _rad;
  }

  void move() {
    if (!stuck) {
      pos.add(vel);
      halflife--;
    }
  }

  void show() {
    noStroke();
    fill(255 / 2 * col, 255, 100, halflife);
    ellipse(pos.x, pos.y, rad * 2, rad * 2);
  }

  boolean isDead() {
    if (halflife < 0) {
      active = false;
      return true;
    } else return false;
  }

  boolean interacting(Part other) {
    if (dist(pos.x, pos.y, other.pos.x, other.pos.y) < rad + other.rad) {
      return true;
    } else return false;
  }

  void makeSynth(float start, float dist) {
      OscMessage message = new OscMessage("/makesynth");
      message.add(col);
      message.add(vel.x);
      message.add(vel.y);
      message.add(start);
      message.add(dist);
      message.add(tag);
      oscP5.send(message, Supercollider);
    }
   
    void sendMessage(float dist){
    OscMessage control = new OscMessage("/control");
    control.add(tag);
    control.add(dist);
    oscP5.send(control, Supercollider);
  }
}

Notice that the messaging happens in the functions makeSynth() and sendMessage() in the class Part. The OSC message is formed in the following way: first there is its symbolic name, like “/makesynth”, and then we add the various information we want to send. These can be integers, floats, strings, etc. The symbolic name allows the “listening” media device, in this case SuperCollider, to perform an action whenever the message with the specific symbolic name arrives. You need to specify an address where to send the message: in my case, I used the default incoming SuperCollider address. Here is the code on the SuperCollider side

s.boot;

(

fork{
SynthDef(\buff, {|freq = 10, dur = 10, gate = 1, buff, rate = 1, pos = 0, pan = 0, out = 0, spr|
    var trig = Impulse.ar(freq);
    var spread = TRand.ar((-1) * spr * 0.5, spr * 0.5, trig);
    var sig = GrainBuf.ar(2, trig, dur, buff, rate, pos + spread + spr, 0, pan);
        var env = EnvGen.kr(Env([0.0, 1, 1, 0], [Rand(0.05, 0.8), Rand(0.1, 0.4), Rand(0.01, 0.5)]), doneAction: 2);
    Out.ar(out, sig * env * 0.02 );
}).add;

SynthDef(\rev, {|out, in|
    var sig = In.ar(in, 2);
    sig = FreeVerb.ar(Limiter.ar(sig), 0.1, 0.5, 0.1);
    Out.ar(0, sig);
    }
).add;

~rev = Bus.audio(s, 2);
s.sync;
Synth(\rev, [\in: ~rev]);

~synths = Dictionary.new;


~buffers =  "pathFolder/*".pathMatch.collect({|file| Buffer.readChannel(s, file, channels:0)});

~total = 0;

OSCdef(\makesynth, {|msg|
    //msg.postln;
    if(~total < 60, {
        x = Synth(\buff, [\freq: rrand(0.1, 40), \dur: rrand(0.01, 0.5), \buff: ~buffers[msg[1]], \rate: msg[3] * rrand(-3, 3), \pan: msg[2], \pos: msg[4], \spr: msg[5], \out: ~rev]);
        ~synths.put(msg[6], x);
        x.onFree({~total = ~total - 1; ~synths.removeAt(msg[6]);});
        ~total = ~total + 1;
        //~total.postln;
    }, {});
}, "/makesynth");

 OSCdef(\control, {|msg|
    ~synths[msg[1]].set(\freq2, msg[2]);
 }, "/control");

}
)

The listening is performed by OSCdef(name, {function}, symbolic name), which passes as argument to the function the message we have sent via Processing. Notice that the first entry of the array msg will always be the symbolic name of the message. Pretty simple, uh? Neverthless, the SuperCollider code has some nuances that should be explained. First, the synth you create should better be erased when it finishes its job, otherwise you’ll have an accumulation of them which will eventually freeze your computer. Also, I’ve put a cap on the total number of synth which I allow at a given time, to avoid performances issues. We also want to be able to control various parameters of a given synth while the particle that generated it is still inside one of the circles. To do this, we have to keep track of the various synths which are active at a given moment. I’ve done this by “tagging” each new created particle with an integer, and by using the Dictionary variable ~synths. Recall that a Dictionary is a collection of data of the form [key, value, key, value,…]: you can retrieve the given value, in this case a synth node, via the associated key, in this case the particle tag. When the given synth node is freed, via the method onFree() we decrease the total number of active synths, and remove the key corresponding to the particle tag.
I hope the example above shows how powerful OSC communication is, and how nuanced one can be in the resulting actions performed.
Here it is an audio snippet.

Audio clip: Adobe Flash Player (version 9 or above) is required to play this audio clip. Download the latest version here. You also need to have JavaScript enabled in your browser.

pagetop