Warning
Under construction.
Routines, streams and logical time¶
Routines in SuperCollider are a special kind of generators that can be scheduled in clocks and keep track of the passing of logical time. They are needed to schedule sequences in time that will generate jitter-free OSC timetags.
Instances of routines are created as shown below, their only argument is a
function or generator function (a function that define yield statements).
When its generator iterator is exhausted routines raise a
StopStream
exception which is a subclass
of StopIteration.
def func():
for i in range(3):
print(i)
yield 1
r = Routine(func)
next(r) # 0
next(r) # 1
next(r) # 2
next(r) # StopStream
Routine objects can be more conveniently created using the decorator function syntax as follow:
@routine
def r():
for i in range(3):
print(i)
yield 1
next(r) # 0
next(r) # 1
next(r) # 2
next(r) # StopStream
Note that a routine object is both a generator function and iterator. To define more than one routine with the same function use the object constructor.
The meaning of routines start to reveal when they are used along clocks. They
respond to the play
, pause
, resume
and stop
methods. The
play
method starts playing the routine in a clock, the default clock which
is SystemClock
.
@routine
def r():
i = 0
while True:
print(i)
i += 1
yield 1
r.play() # Schedule the routine in a clock.
r.pause() # Stop the routine and remove form the clock.
r.resume() # Resume form were it was.
r.stop() # Stop it for good.
r.reset() # Reinitialize it to play again from the beginning.
# r.play() # To start over.
When a routine is stopped it has to be reset in order to be used again. The
method reset
sets it to the initial state, internally it also creates the
generator iterator again.
By the way, don’t play an infinite loop without yield time, it will hang everything.
Timing¶
When routines are scheduled on clocks their yield value is used as wait time
for a next call or cancel its execution. When the return value is a number
(int
or float
) the clock takes this value to re-schedule the routine
after waiting that much seconds (if no tempo is used). When the generator
returns, or yields another type of value, the routine leaves the clock’s queue.
Yielded values, as time, are used to wait in physical time but routines also define logical time which increments only from those values. In other words, the logical time in a routine is the sum of all yielded values so far, relative to the pysical time it started to play and the tempo of the clock.
This way, when a routine is scheduled in Python, its next physical call time may not be precise, it may even have noticeable jitter under load, but when using logical time to generate timetags the wait time sent to the server will be precise according to it.
Physical time can be accessed from main.elapsed_time()
, which is the time
in seconds since the library started. The input value of a routine running in
a clock is a tuple (routine, clock)
, and the logical time can be obtained
from the clock’s seconds
property.
@routine
def r(inval):
_, clock = inval
while True:
print(main.elapsed_time(), clock.seconds)
yield 1
r.play()
Note
For most common cases it’s not necessary to access routine’s clock logical time, the library will manage timing internally.
In the example above we can compare how the decimal part of the logical time,
obtained from the clock that schedules the routine, is always the same while
for elapsed_time()
is constantly changing. Whenever an OSC bundle is sent
from a routine playing on a clock the time used to define its timetag
is the logical time.
This is important to keep in mind because is the only way to have precise timing for rhythmic sequences in real time. And this is one of the two core features of this library (the other being synthdef building capabilities).
Streams¶
Streams are the counter part of Python’s generators iterators but in a SuperCollider way. Routines are the most commonly used stream but not all streams are routines.
Streams support mathematical operations and behave, in concept, in a similar
way to signals represented by ugens. In the next example, the
routine object r
is transposed by 60
by making a sum that results in a
sc3.base.stream.BinopStream
assigned to t
.
@routine
def r():
for i in range(12):
yield i
t = r + 60
next(t) # 60
next(t) # 61
Special builtin methods like sc3.AbstractObject.midicps()
also apply
to streams.
t = t.midicps()
next(t) # MIDI note 63 is ca. 293.6647 Hz.
Random numbers¶
Each sc3.base.stream.Routine
instance has a random number generator,
by default is inherited from its parent routine (or the main time thread) but
random seeds can be changed per routine object. To make use of this
functionality its necessary to use the builtin random functions or methods
which are aware of routines.
@routine
def r():
while True:
yield bi.rrand(48, 60)
next(r) # A random number.
r.rand_seed = 12345
next(r) # First number.
next(r) # Second number.
r.rand_seed = 12345
next(r) # Same first.
next(r) # Same second.
Blocking the main thread¶
Because each clock run in its own thread, for real time scripts, the main thread needs to block until routines’ execution finishes or the script will quit before time.
In the next example the main thread blocks after spawning several routines and
resumes when r
is finished so the script can exit.
#!/usr/bin/env python3
from sc3.all import *
@routine
def r():
for i in range(13):
play(midinote=60 + i, sustain=0.05)
yield 0.25
main.resume() # Resume the main thread.
# Play r after the server has booted.
s.boot(on_complete=lambda: r.play())
# Wait on the main thread and compensate
# latency with end time before exit.
main.wait(tailtime=s.latency)