Routines vs. Tasks? Event vs. Synth.new?

Hi list,

I am trying to understand the respective advantages of Routines over
Tasks and would like to ask for help here.

From some texts about these two classes I understand that Tasks prevent
double execution and may be paused[1].

Routines may be executed multiple times and may be placed in their own thread, means that multiple
routines may run in parallel when .play’ed, or in the parent in series when called via .embedInStream.
Routines may also be quantized using .play(quant:4).[2]

Are there other differences between the two? Currently Routines appear
more useful to me for music sequencing applications, no?

Oh, and one more question: Is there other advantages of using Events
to play synthdefs rather than just calling Synth.new() besides the pitch
specification conversions?

Thanks for all ideas!
Peter

[1] Scott Wilson and Julio d’Escriván “Composition with Super Collider”
[2] Eli Fieldsteel “Super Collider for the Creative Musician”

You would think so, but that’s not actually the case.

(
r = Routine {
	inf.do { |count|
		[thisThread.beats, count.asInteger].postln;
		1.0.wait;
	}
};
)

// play the routine multiple times "in parallel"
(
var nextBeat = TempoClock.beats.roundUp;

4.do { |i|
	r.play(quant: [nextBeat, 0.25 * i]);
};
)

[ 186.0, 0 ]   -- first time scheduled
[ 186.25, 1 ]  -- second time scheduled
[ 186.5, 2 ]   -- third time
[ 186.75, 3 ]  -- fourth time
[ 187.0, 4 ]   -- first time scheduled, 1 beat later
[ 187.25, 5 ]  -- etc
[ 187.5, 6 ]
[ 187.75, 7 ]

r.stop;

That’s not parallel exactly. Based on the way the Routine is written, you would expect the counter to increment by one for each beat. But when used this way, it increments four times per beat.

True parallel streams have to be separate streams.

(
// a function to make a new Routine
r = {
	Routine {
		inf.do { |count|
			[thisThread.beats, count.asInteger].postln;
			1.0.wait;
		}
	}
};
)

// play 4 routines "in parallel"
(
var nextBeat = TempoClock.beats.roundUp;

t = Array.fill(4, { |i|
	r.value.play(quant: [nextBeat, 0.25 * i]);
});
)

[ 468.0, 0 ]
[ 468.25, 0 ]
[ 468.5, 0 ]
[ 468.75, 0 ]
[ 469.0, 1 ]
[ 469.25, 1 ]
[ 469.5, 1 ]
[ 469.75, 1 ]

t.do(_.stop);

This idea has come up a couple of times lately – that routines “can be played multiple times” while tasks cannot, concluding that routines are more flexible – but the actual behavior of playing the same routine multiple times is probably not what you want.

Both Routines and Tasks take a quant argument when played.

For the most common cases, there’s no significant difference (except that Tasks protect you from making the multiple-scheduling mistake).

When you get up to more exotic cases, such as rescheduling a thread for a different time without interrupting it – you can reschedule a routine for a later time but not an earlier time. Task/PauseStream can reschedule in either direction.

Latency: Scheduling and Server timing | SuperCollider 3.12.2 Help

// runs on the next hardware buffer boundary
// timing in a sequence may be unreliable
a = Synth(\default);

// runs at a scheduled time -- better timing in sequences
s.bind { a = Synth(\default) };

// runs at a scheduled time -- better timing in sequences
(instrument: \default, freq: 440).play;

Also, for me, the fact that events automatically release the note is a big plus.

hjh

James, thanks a lot for your replies!

[…]

That’s not parallel exactly. Based on the way the Routine is written, you would expect the counter to increment by one for each beat. But when used this way, it increments four times per beat.

True parallel streams have to be separate streams.
[…]

This idea has come up a couple of times lately – that routines “can be played multiple times” while tasks cannot, concluding that routines are more flexible – but the actual behavior of playing the same routine multiple times is probably not what you want.
Thanks for the clarification. I just got the ‘parallel’ idea from page
151 in Eli Fieldsteel’s book, meaning to play different routines in
parallel. I didn’t state the fact that these are different routines
explicitely in my initial post.

Latency: Scheduling and Server timing | SuperCollider 3.12.2 Help

// runs on the next hardware buffer boundary
// timing in a sequence may be unreliable
a = Synth(\default);

// runs at a scheduled time -- better timing in sequences
s.bind { a = Synth(\default) };

// runs at a scheduled time -- better timing in sequences
(instrument: \default, freq: 440).play;

Great information, thanks again James! My s.latency is preset to 0.2
seconds, and this is the time delay I feel when executing events in
comparison to Synth.new().

https://doc.sccode.org/Guides/ServerTiming.html
mentions “In general, all synths that are triggered by live input (MIDI,
GUI, HID) should specify no latency so that they execute as soon as
possible. All sequencing routines should use latency to ensure perfect
timing.”
Do I have to take care of this manually? Or will this this latency be
automatically applied whenever I execute something, including
Synth.new(), inside a Routine? When I run a Pattern?

Allow three more questions about Events:

The \sustain argument runs at beats of the default clock. It seems
specifying an Event like this ().play(t), with t referencing a different
TempoClock than the default, will not affect sustain time?

The argument key (instrument: .… ) can not be omitted in Events,
right?

Playing an event will generate a line of quite verbose output in the
post window. Can this be disabled?

Cheersz, Peter

Hi James, list,

some more follow-up questions have arisen:

You would think so, but that’s not actually the case.

(
r = Routine {
	inf.do { |count|
		[thisThread.beats, count.asInteger].postln;
		1.0.wait;
	}
};
)

// play the routine multiple times "in parallel"
(
var nextBeat = TempoClock.beats.roundUp;

4.do { |i|
	r.play(quant: [nextBeat, 0.25 * i]);
};
)

[ 186.0, 0 ]   -- first time scheduled
[ 186.25, 1 ]  -- second time scheduled
[ 186.5, 2 ]   -- third time
[ 186.75, 3 ]  -- fourth time
[ 187.0, 4 ]   -- first time scheduled, 1 beat later
[ 187.25, 5 ]  -- etc
[ 187.5, 6 ]
[ 187.75, 7 ]

r.stop;

That’s not parallel exactly. Based on the way the Routine is written, you would expect the counter to increment by one for each beat. But when used this way, it increments four times per beat.

I see the increment problem, and reproduced it for your Routine with the
following as well:
(
r.play;
r.play;
)

Is there a simple explanation why this happens? Is the argument |count|
not local to each Routine? It seems not. It seems there is not “each
Routine” but still only one single routine. Hence calling r.stop in my
simplified example will stop this one running Routine, right?

Am I correct to assume that a Routine is not, upon being .played,
returning something like a RoutinePlayer? Rather the Routine is the
actual thing that is playing and I can merely control it? Hence starting it a
second time will place a “second play head” inside this single Routine,
eventually also incrementing the above counter, and will not return a
reference to a second instance of the Routine?

True parallel streams have to be separate streams.

(
// a function to make a new Routine
r = {
	Routine {
		inf.do { |count|
			[thisThread.beats, count.asInteger].postln;
			1.0.wait;
		}
	}
};
)

// play 4 routines "in parallel"
(
var nextBeat = TempoClock.beats.roundUp;

t = Array.fill(4, { |i|
	r.value.play(quant: [nextBeat, 0.25 * i]);

Again reproducing the above for myself using
(
m = r.value.play;
n = r.value.play;
)
m and n now hold (different) Routines, and
m.stop
n.stop;
work as expected.

Is writing functions-that-make-a-new-Routine the only way to achieve
truly parallel streams, and streams that can be accessed individually?

Thanks for your patience and explanations, it is much appreciated!
best, Peter

Correct. .play does not duplicate a Routine.

If you want two Routines, then you have to write two Routines… which isn’t a strange requirement, when viewed from this perspective.

Yes. The mechanism to produce the multiple routines may differ, but the principle doesn’t change.

Have another look at the code example:

// runs on the next hardware buffer boundary
// timing in a sequence may be unreliable
a = Synth(\default);

^^ that’s without any latency specified

// runs at a scheduled time -- better timing in sequences
s.bind { a = Synth(\default) };

^^ s.bind attaches the server’s latency

It isn’t playing the event that produces this output.

1+1

This produces a non-verbose line of output, but it doesn’t mean that every addition operation will produce a line of output.

Play the event in a Routine or a scheduled function and the output will not be printed.

hjh

Hi,

I am taking in and enjoying the discussion of the latency and timing,
thanks James!

And I discover that I can write
s.bind {}
or even
s.bind{}
instead of
s.bind({})

Why is that? Can this be generalized to all arguments that contain
(only) functions?

cheersz, Peter

This is why you can write this style if statement

if (0.1.coin) { "yes" } { "no" };

Equivalent to

if(0.1.coin, { "yes" }, { "no" });

Or

(0.1.coin).if({ "yes" }, { "no" });

See the very useful Syntax Shortcuts | SuperCollider 3.12.2 Help