Synchronous and Asynchronous Execution

Using a program such as SuperCollider introduces a number of issues regarding timing and order of execution. Realtime audio synthesis requires that samples are calculated and played back at a certain rate and on a certain schedule, in order to avoid dropouts, glitches, etc. Other tasks, such as loading a sample into memory, might take arbitrary amounts of time, and may not be needed within a definite timeframe. This is the difference between synchronous and asynchronous tasks.

Problems can arise when synchronous tasks are dependent upon the completion of asynchronous ones. For instance trying to play a sample that may or may not have been completely loaded yet.

In SC2 this was relatively simple to handle. One scheduled synchronous tasks during synthesis, i.e. within the scope of a Synth.play. Asynchronous tasks were executed in order, outside of synthesis. Thus one would first create buffers, load samples into them, and then start synthesis and play them back. The interpreter made sure that each step was only done when the necessary previous step had been completed.

In SC3 the separation of language and synth apps creates a problem: How does one side know that the other has completed necessary tasks, or in other words, how does the left hand know if the right is finished? The flexibility gained by the new architecture introduces another layer of complexity, and an additional demand on the user.

A simple way to deal with this is to execute code in blocks. In the following code, for instance, each block or line of code is dependent upon the previous one being completed.

// Execute these one at a time

// Boot the local Server

(

s = Server.local;

s.boot;

)

// Compile a SynthDef and write it to disk

(

SynthDef("Help-SynthDef",

{ arg out=0;

Out.ar(out, PinkNoise.ar(0.1))

}).writeDefFile;

)

// Load it into the server

s.loadSynthDef("Help-SynthDef");

// Create a Synth with it

x = Synth.new("Help-SynthDef", s);

// Free the node on the server

x.free;

// Allow the client-side Synth object to be garbage collected

x = nil;

In the previous example it was necessary to use interpreter variables (the variables a-z, which are declared at compile time) in order to refer to previously created objects in later blocks or lines of code. If one had declared a variable within a block of code (i.e. va r mySynth;) than it would have only persisted within that scope. (See the helpfile Scope for more detail.)

This style of working, executing lines or blocks of code one at a time, can be very dynamic and flexible, and can be quite useful in a performance situation, especially when improvising. But it does raise the issues of scope and persistence. Another way around this that allows for more descriptive variable names is to use environment variables (i.e. names that begin with ~, so ~mysynth; see the Environment helpfile for details). However, in both methods you become responsible for making sure that objects and nodes do not persist when you no longer need them.

(

SynthDef("Help-SynthDef",

{ arg out=0;

Out.ar(out, PinkNoise.ar(0.1))

}).send(s);

)

// make a Synth and assign it to an environment variable

~mysynth = Synth.new("Help-SynthDef", s);

// free the synth

~mysynth.free;

// but you've still got a Synth object

~mysynth.postln;

// so remove it from the Environment so that the Synth will be garbage collected

currentEnvironment.removeAt(\mysynth);

But what if you want to have one block of code which contains a number of synchronous and asynchronous tasks. The following will cause an error, as the SynthDef that the server needs has not yet been received.

// Doing this all at once produces the error "FAILURE /s_new SynthDef not found"

(

var name;

name = "Rand-SynthDef" ++ 400.0.rand; // use a random name to ensure it's not already loaded

SynthDef(name,

{ arg out=0;

Out.ar(out, PinkNoise.ar(0.1))

}).send(s);

Synth.new(name, s);

)

A crude solution would be to schedule the dependant code for execution after a seemingly sufficient delay using a clock.

// This one works since the def gets to the server app first

(

var name;

name = "Rand-SynthDef" ++ 400.0.rand;

SynthDef(name,

{ arg out=0;

Out.ar(out, PinkNoise.ar(0.1))

}).send(s);

SystemClock.sched(0.05, {Synth.new(name, s);}); // create a Synth after 0.05 seconds

)

Although this works, it's not very elegant or efficient. What would be better would be to have the next thing execute immediately upon the previous thing's completion. To explore this, we'll look at an example which is already implemented.

You may have realized that first example above was needlessly complex. SynthDef-play will do all of this compilation, sending, and Synth creation in one stroke of the enter key.

// All at once

(

SynthDef("Help-SynthDef",

{ arg out=0;

Out.ar(out, PinkNoise.ar(0.1))

}).play(s);

)

Let's take a look at the method definition for SynthDef-play and see what it does.

play { arg target,args,addAction=\addToTail;

var synth, msg;

target = target.asTarget;

synth = Synth.basicNew(name,target.server); // create a Synth, but not a synth node

msg = synth.newMsg(target, addAction, args);// make a message that will add a synth node

this.send(target.server, msg); // ** send the def, and the message as a completion message

^synth // return the Synth object

}

This might seem a little complicated if you're not used to mucking about in class definitions, but the important part is the second argument to this.send(target.server, msg);. This argument is a completion message, it is a message that the server will execute when the send action is complete. In this case it says create a synth node on the server which corresponds to the Synth object I've already created, when and only when the def has been sent to the server app. (See the helpfile Server-Command-Reference for details on messaging.)

Many methods in SC have the option to include completion messages. Here we can use SynthDef-send to accomplish the same thing as SynthDef-play:

// Compile, send, and start playing

(

SynthDef("Help-SynthDef",

{ arg out=0;

Out.ar(out, PinkNoise.ar(0.1))

}).send(s, ["s_new", "Help-SynthDef", x = s.nextNodeID]);

// this is 'messaging' style, see below

)

s.sendMsg("n_free", x);

The completion message needs to be an OSC message, but it can also be some code which when evaluated returns one:

// Interpret some code to return a completion message. The .value is needed.

// This and the preceding example are essentially the same as SynthDef.play

(

SynthDef("Help-SynthDef",

{ arg out=0;

Out.ar(out, PinkNoise.ar(0.1))

}).send(s, {x = Synth.basicNew("Help-SynthDef"); x.newMsg; }.value); // 'object' style

)

x.free;

If you prefer to work in 'messaging' style, this is pretty simple. If you prefer to work in 'object' style, you can use the commands like newMsg, setMsg, etc. with objects to create appropriate server messages. The two proceeding examples show the difference. See the NodeMessaging helpfile for more detail.

In the case of Buffer objects a function can be used as a completion message. It will be evaluated and passed the Buffer object as an argument. This will happen after the Buffer object is created, but before the message is sent to the server. It can also return a valid OSC message for the server to execute upon completion.

(

SynthDef("help-Buffer",{ arg out=0,bufnum;

Out.ar( out,

PlayBuf.ar(1,bufnum,BufRateScale.kr(bufnum))

)

}).load(s);

y = Synth.basicNew("help-Buffer"); // not sent yet

b = Buffer.read(s,"sounds/a11wlk01.wav",

completionMessage: { arg buffer;

// synth add its s_new msg to follow

// after the buffer read completes

y.newMsg(s,\addToTail,[\bufnum,buffer])

}); // .value NOT needed, unlike in the previous example

)

// when done...

y.free;

b.free;

The main purpose of completion messages is to provide OSC messages for the server to execute immediately upon completion. In the case of Buffer there is essentially no difference between the following:

(

b = Buffer.alloc(s, 44100,

completionMessage: { arg buffer; ("bufnum:" + buffer).postln; });

)

// this is equivalent to the above

(

b = Buffer.alloc;

("bufnum:" + b).postln;

)

One can also evaluate a function in response to a 'done' message, or indeed any other one, using an OSCresponder or an OSCresponderNode. The main difference between the two is that the former allows only a single responder per command, where as the latter allows multiple responders. See their respective helpfiles for details.

(

SynthDef("help-SendTrig",{

SendTrig.kr(Dust.kr(1.0), 0, 0.9);

}).send(s);

// register to receive this message

a = OSCresponderNode(s.addr, '/done', { arg time, responder, msg;

("This is the done message for the SynthDef.send:" + [time, responder, msg]).postln;

}).add.removeWhenDone; // remove me automatically when done

b = OSCresponderNode(s.addr, '/tr', { arg time, responder, msg;

[time, responder, msg].postln;

}).add;

c = OSCresponderNode(s.addr, '/tr', { arg time, responder, msg;

"this is another call".postln;

}).add;

)

x = Synth.new("help-SendTrig");

b.remove;

c.remove;

x.free;