An OpenFrameworks <-> Supercollider connection solution

Hi -
I have been working on a larger project that uses OpenFrameworks together and I thought I would share my solution to connecting the two environments.
There were a few concerns when developing this workflow.

  1. I wanted to have a way to integrate OF and SC that would maintain interdependency between the environments
  2. With the aim to control SC from OF
  3. I wanted to be able to have read back from SC so that I could have a direct connection with sound a graphics
  4. I wanted to automate starting and quitting SC, and have a script automatically load when sc launched.

Here is the solution I developed:

QSCInterface.h--------------------------------------------------------

  
  
//  
//  QSCInterface.h  
//  
//  
//  Created by MarkDavid Hosale on 13-04-06.  
//  
//  
  
#ifndef __QSCInterface__  
#define __QSCInterface__  
  
#include "ofMain.h"  
#include "ofxOsc.h"  
  
#define HOST "localhost"  
#define PORTS 57110  
#define PORTL 57120  
#define PORTR 57130  
  
// this needs to be threaded otherwise it blocks the main thread  
class StartSC : public ofThread{  
  
public:  
    StartSC(){}  
      
    void openSC(string s){  
        scfile = s;  
        threadStart();  
    }  
      
    void threadStart(){  
        startThread(true, false);  
    }  
      
      
    void threadStop(){  
        ofSystem("kill -s 2 $(ps -A | grep -m1 sclang | awk '{print $1}')");  
        // scsynth is killed via OSC, à la \quit, in the QSCInterface  
        stopThread();  
    }  
      
      
    void threadedFunction(){  
          
#if defined TARGET_OSX  
        // this is for a typical install in OSX, also could be done by setting path variables  
        string path2sc = "/Applications/SuperCollider/SuperCollider.app/Contents/Resources/";  
          
#elif defined TARGET_LINUX  
        string path2sc = ""; //should be included as an environment variable, e.g. no direct path needed, but not tested  
        //  #elif defined TARGET_WIN32 // not sure how to do this on Windows  
#endif  
          
        string path2file = ofToDataPath(scfile, true);  
          
        string termcmd = path2sc + "sclang " + path2file + " -u 57120";  
          
        ofSystem(termcmd);  
    }  
      
      
    ~StartSC(){threadStop();}  
      
      
private:  
    string scfile;  
};  
  
  
  
  
// The SuperCollider Interface --------------------------------------------------------  
class QSCInterface : public ofThread{  
      
public:  
      
    void setup(){  
          
        StartSC startsc.openSC("myscfile.scd");  
          
        // open an outgoing connection to scsynth  
        sender.setup(HOST, PORTS);  
          
        // outgoing connection to sclang is sender.setup(HOST, PORTL);  
        // but this will change as needed, see below  
          
        // listen on the given port  
        receiver.setup(PORTR);  
          
        threadStart();  
    }  
      
    void update();  
    void draw();  
      
    void keyPressed(int key){  
          
        switch (key){  
            case 's':  
                message.clear();  
                message.setAddress("/scresponder");  
                message.addStringArg("startsynth");  
                message.addFloatArg(110.0); // modulation frequency  
                sender.setup(HOST, PORTL); // language port  
                sender.sendMessage(message);  
                break;  
        }  
    }  
      
      
    void keyReleased(int key);  
    void mouseMoved(int x, int y);  
    void mouseDragged(int x, int y, int button);  
    void mousePressed(int x, int y, int button);  
    void mouseReleased(int x, int y, int button);  
    void windowResized(int w, int h);  
    void dragEvent(ofDragInfo dragInfo);  
    void gotMessage(ofMessage msg);  
      
    ~QSCInterface(){  
        threadStop();  
    }  
      
private:  
    ofxOscSender sender;  
    ofxOscReceiver receiver;  
    ofxOscMessage message;  
    StartSC startsc;  
      
    void threadStart(){  
        startThread(true, false);   // blocking, verbose  
    }  
      
    void threadStop(){  
        message.clear();  
        message.setAddress("/quit");  
        sender.setup(HOST, PORTS); // change to server port  
        sender.sendMessage(message);  
        stopThread();  
    }  
      
      
    void threadedFunction(){  
          
        while( isThreadRunning() != 0 ){  
            if( lock() ){  
                  
                // check for waiting messages  
                while(receiver.hasWaitingMessages()){  
                    // get the next message  
                    ofxOscMessage m;  
                    receiver.getNextMessage(&m);  
                      
                      
                    // SuperCollider is ready  
                    if (m.getAddress() == "/scbooted") cout << "scbooted" << endl;  
                      
                    // env end trigger  
                    if (m.getAddress() == "/synthdone") cout << "synthdone" << endl;  
                      
                    // send trigger follower  
                    if (m.getAddress() == "/synth-follow")cout << "synth-follow " << m.getArgAsFloat(0) << endl;  
  
                    // unrecognized messages: display on the bottom of the screen  
                    string msg_string;  
                    msg_string = m.getAddress();  
                    msg_string += ": ";  
                    for(int i = 0; i < m.getNumArgs(); i++){  
                        // get the argument type  
                        msg_string += m.getArgTypeName(i);  
                        msg_string += ":";  
                        // display the argument - make sure we get the right type  
                        if(m.getArgType(i) == OFXOSC_TYPE_INT32){  
                            msg_string += ofToString(m.getArgAsInt32(i));  
                        }  
                        else if(m.getArgType(i) == OFXOSC_TYPE_FLOAT){  
                            msg_string += ofToString(m.getArgAsFloat(i));  
                        }  
                        else if(m.getArgType(i) == OFXOSC_TYPE_STRING){  
                            msg_string += m.getArgAsString(i);  
                        }  
                        else{  
                            msg_string += "unknown";  
                        }  
                    }  
                      
                    cout << msg_string << endl;  
                }  
                  
                unlock();  
                ofSleepMillis(3);  
            }  
        }  
    }  
      
      
};  
#endif /* defined(____QSCInterface__) */  
  
  


myscfile.scd--------------------------------------------------------

  
  
//  for use with QSCInterface.h  
//  Created by MarkDavid Hosale on 13-04-06.  
  
(r= Routine({  
  
	"Booting SUPERCOLLIDER".postln;  
  
	s = Server.local ;  
  
	while { s.serverRunning.not }{  
  
		"Waiting for Server.".postln;  
  
		10.wait; // give server time to boot 1-10 seconds depending on machine  
  
		s.boot;  
  
		s.doWhenBooted({  
  
			//-------------------Initialize Sound----------------------//  
  
			"Initialize Sound...".postln;  
  
			SynthDef(\mysynth, { | modfreq = 20.0, vol = 1.0 |  
				var sampler, bufnum = 2;  
				var env = Env.perc(0.001, 0.75, 1, -1);  
				var gen = EnvGen.kr(env,doneAction: 2);  
  
				// create the OSC sender in the server (SendTrig sends OSC messages when it's triggered)  
				SendTrig.kr(Impulse.kr(10), 0, gen);  
				SendTrig.kr(Done.kr(gen), 1);  
  
					Out.ar(out, SinOsc.ar(220,SinOsc.ar(modfreq, mul:2pi))* gen);  
  
			}).send(s);  
  
			//-------------------OSC RESPONDER:: OF->SC ----------------------//  
  
			"OSC Responder loading...".postln;  
  
			// register comm for listening to openframeworks  
			OSCresponderNode(  nil,'/scresponder',{ arg time,responder,msg;  
  
				// [time, responder, msg].postln;  
  
				switch( msg[1],  
					'startsynth', {  
						var thisStarImplosion = Synth(\implosion);  
						thisStarImplosion.set(\modfreq, msg[2]);  
					}  
				);  
  
			}).add;  
  
  
			//-------------------OSC RESPONDER:: SC->OF ----------------------//  
			~openframeworks = NetAddr("127.0.0.1", 57130); //this is for sending to OpenFrameworks  
  
			OSCresponderNode(nil,'/tr',{ arg time,responder,msg;  
  
				// [time, responder, msg].postln;  
  
				switch( msg[2],  
  
					0, {// env done  
						~openframeworks.sendMsg('/synthdone', 1);  
					},  
  
					//synth follower  
					1, {  
						~openframeworks.sendMsg('/synth-follow', msg[3].asFloat);  
					}  
				);  
  
			}).add;  
  
			~openframeworks.sendMsg('/scbooted', 1);//tell the brain about it  
  
			"Booted!".postln;  
		});  
  
		10.wait; // give server time to boot 1-10 seconds depending on machine  
	};  
}).play;  
  
)  
  



Basically what is happening is that sclang is being launched as a terminal app. This is being done in a seperate thread class because the app tends to block the main thread when sclang is launched. The server (scsynth) is launched from the sc file itself.

For this solution to work it is assumed that the file being launched by SC has a few important elements:

  • a routine that boots the server, then executes the code once it is booted
  • a synthdef :stuck_out_tongue:
  • two OSCresponders:
    – one for receiveing messages from OF
    – and one that sends messages from sclang and relays server messages to OF (this forms a kind of chat app between the two).

I tried to include what I thought were essentials for communication and control in the code I posted.

Note: that it may be possible to collect messages from the server by sending ‘/notify 1’ to the server. However, in ofxOsc there doesn’t seem to be a way to control the ‘send from’ port, which SC server uses to determin where notifications should be sent. In short this requires the ability to create a send and receive on the same port. On a forum it was suggested that instead of using OSCpack a library like liblo could be used instead. Also, there was some indication that OSCpack would support this in the latest version, however this doesn’t seem to be exposed in the ofxOsc implementation. If anyone has any experience with this is would be great to hear.

Quitting sclang happens when the StartSC class is destroyed (on exit), but the same technique would not work for scsynth. Instead the message “/quit” is sent to the server, which seems to be the right solution. You cannot remotely boot an SC server (but you can launch an sclang that executes a file that boots the server as in the example provided), beyond that you can control the server directly from OF (keeping in mind the notification issues I described above). A full reference to those commands is available here: http://danielnouri.org/docs/SuperColliderHelp/ServerArchitecture/Server-Command-Reference.html

I look forward to any comments you may have.

best,

Mark-David Hosale