1 title:: Synchronous and Asynchronous Execution
2 summary:: The problem of simultaneous synchronous and asynchronous execution
3 categories:: SC3 vs SC2
5 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.
7 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.
9 In SC2 this was relatively simple to handle. One scheduled synchronous tasks during synthesis, i.e. within the scope of a code::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.
11 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.
13 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.
16 // Execute these one at a time
18 // Boot the local Server
24 // Compile a SynthDef and write it to disk
26 SynthDef("Help-SynthDef", { arg out = 0;
27 Out.ar(out, PinkNoise.ar(0.1))
31 // Load it into the server
32 s.loadSynthDef("Help-SynthDef");
34 // Create a Synth with it
35 x = Synth.new("Help-SynthDef", s);
37 // Free the node on the server
40 // Allow the client-side Synth object to be garbage collected
44 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. code::var mySynth;::) than it would have only persisted within that scope. (See the helpfile link::Reference/Scope:: for more detail.)
46 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 code::~mysynth;:: see the link::Classes/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.
50 SynthDef("Help-SynthDef", { arg out = 0;
51 Out.ar(out, PinkNoise.ar(0.1))
55 // make a Synth and assign it to an environment variable
56 ~mysynth = Synth.new("Help-SynthDef", s);
61 // but you've still got a Synth object
64 // so remove it from the Environment so that the Synth will be garbage collected
65 currentEnvironment.removeAt(\mysynth);
68 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 link::Classes/SynthDef:: that the server needs has not yet been received.
71 // Doing this all at once produces the error "FAILURE /s_new SynthDef not found"
74 name = "Rand-SynthDef" ++ 400.0.rand; // use a random name to ensure it's not already loaded
76 SynthDef(name, { arg out=0;
77 Out.ar(out, PinkNoise.ar(0.1))
84 A crude solution would be to schedule the dependant code for execution after a seemingly sufficient delay using a clock.
87 // This one works since the def gets to the server app first
90 name = "Rand-SynthDef" ++ 400.0.rand;
92 SynthDef(name, { arg out = 0;
93 Out.ar(out, PinkNoise.ar(0.1))
96 SystemClock.sched(0.05, {Synth.new(name, s);}); // create a Synth after 0.05 seconds
100 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.
102 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.
107 SynthDef("Help-SynthDef", { arg out = 0;
108 Out.ar(out, PinkNoise.ar(0.1))
113 Let's take a look at the method definition for SynthDef-play and see what it does.
116 play { arg target,args,addAction=\addToTail;
118 target = target.asTarget;
120 synth = Synth.basicNew(name,target.server); // create a Synth, but not a synth node
121 msg = synth.newMsg(target, addAction, args);// make a message that will add a synth node
122 this.send(target.server, msg); // ** send the def, and the message as a completion message
123 ^synth // return the Synth object
127 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 code::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 link::Classes/Synth:: object I've already created, when and only when the def has been sent to the server app. (See the helpfile link::Reference/Server-Command-Reference:: for details on messaging.)
129 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:
132 // Compile, send, and start playing
134 SynthDef("Help-SynthDef", { arg out=0;
135 Out.ar(out, PinkNoise.ar(0.1))
136 }).send(s, ["s_new", "Help-SynthDef", x = s.nextNodeID]);
137 // this is 'messaging' style, see below
139 s.sendMsg("n_free", x);
142 The completion message needs to be an OSC message, but it can also be some code which when evaluated returns one:
145 // Interpret some code to return a completion message. The .value is needed.
146 // This and the preceding example are essentially the same as SynthDef.play
148 SynthDef("Help-SynthDef", { arg out=0;
149 Out.ar(out, PinkNoise.ar(0.1))
150 }).send(s, {x = Synth.basicNew("Help-SynthDef"); x.newMsg; }.value); // 'object' style
155 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 code::newMsg::, code::setMsg::, etc. with objects to create appropriate server messages. The two proceeding examples show the difference. See the link::Guides/NodeMessaging:: helpfile for more detail.
157 In the case of link::Classes/Buffer:: objects a function can be used as a completion message. It will be evaluated and passed the link::Classes/Buffer:: object as an argument. This will happen after the link::Classes/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.
161 SynthDef("help-Buffer",{ arg out=0, bufnum;
164 PlayBuf.ar(1,bufnum,BufRateScale.kr(bufnum))
168 y = Synth.basicNew("help-Buffer"); // not sent yet
170 b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav", action: { arg buffer;
171 // synth add its s_new msg to follow
172 // after the buffer read completes
173 y.newMsg(s,\addToTail,[\bufnum,buffer])
174 } // .value NOT needed, unlike in the previous example
183 The main purpose of completion messages is to provide OSC messages for the server to execute immediately upon completion. In the case of link::Classes/Buffer:: there is essentially no difference between the following:
187 b = Buffer.alloc(s, 44100,
188 completionMessage: { arg buffer; ("bufnum:" + buffer).postln; }
192 // this is equivalent to the above
195 ("bufnum:" + b).postln;
199 One can also evaluate a function in response to a 'done' message, or indeed any other one, using an link::Classes/OSCFunc::. See its help file for details.
203 SynthDef("help-SendTrig",{
204 SendTrig.kr(Dust.kr(1.0), 0, 0.9);
207 // register to receive this message
208 a = OSCFunc({ arg msg, time;
209 ("This is the done message for the SynthDef.send:" + [time, msg]).postln;
210 }, '/done', a.addr).oneShot; // remove me automatically when done
211 b = OSCFunc({ arg msg, time;
214 c = OSCFunc({ arg msg;
215 "this is another call".postln;
219 x = Synth.new("help-SendTrig");