Synchronous and Asynchronous Execution:
Guides | Language > SC3 vs SC2

Synchronous and Asynchronous Execution

The problem of simultaneous 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 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.

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. var mySynth;) than it would have only persisted within that scope. (See the helpfile Scoping and Closure 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.

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.

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

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.

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

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:

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

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 Node Messaging 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.

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:

One can also evaluate a function in response to a 'done' message, or indeed any other one, using an OSCFunc. See its help file for details.