Nodes¶
SuperCollider’s scsynth server processes audio by traversaing a tree of nodes. Nodes in the tree can be either groups - which group together other nodes - or synths - which perform some kind of audio processing.
During every sample block the server visits each node in its tree in depth-first order, starting from the root node, and performs their audio logic. This traversal order makes the position of those nodes relative to one another significant (was the reverb performed before or after the sample was played?). Nodes can be added or removed from the server at any point while online, allowing you to manipulate the audio processing graph dynamically.
While scsynth’s nodes “live” inside the server, Supriya provides proxy classes for manipulating and inspecting them in Python.
Let’s dig in…
Lifecycle¶
Nodes can only be added to running servers, so let’s create one and boot it:
>>> server = supriya.Server().boot()
Note
Scores
are neither online
nor offline, so you can add nodes to them whenever you like.
Creating groups¶
Create a group, and print its repr to the terminal:
>>> group_one = server.add_group()
>>> group_one
Group(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=1000, parallel=False)
The repr shows the group’s type (Group
), its ID
(1000
), and indicates it has been allocated (+
).
Positioning¶
Let’s add another group. Groups can be added relative to other groups:
>>> group_two = group_one.add_group()
Where was the second group added? Query the server to see:
>>> print(server.query_tree())
NODE TREE 0 group
1 group
1000 group
1001 group
The second group (1001
) was added within the first (1000
), which
seems obvious in retrospect. Let’s add a third group to the first, and query
the server’s node tree again:
>>> group_three = group_one.add_group()
>>> print(server.query_tree())
NODE TREE 0 group
1 group
1000 group
1002 group
1001 group
The third group (1002
) was - again - added within the first (1000
), but
appears before the second (1001
).
By default, adding nodes to a group adds them to the head of the group, rather than the tail (which is identical behavior to sclang). When adding nodes to the node tree, every node must be added relative to another node - before, after, at the beginning (the head) or at the end (the tail) - and we can control that relative position with an add action.
Supriya implements add actions as the enumeration
AddAction
:
>>> for x in supriya.AddAction:
... x
...
AddAction.ADD_TO_HEAD
AddAction.ADD_TO_TAIL
AddAction.ADD_BEFORE
AddAction.ADD_AFTER
AddAction.REPLACE
Use AddAction
to position new groups relative to the
first:
>>> group_four = group_one.add_group(add_action=supriya.AddAction.ADD_AFTER)
>>> print(server.query_tree())
NODE TREE 0 group
1 group
1000 group
1002 group
1001 group
1003 group
>>> group_five = group_one.add_group(add_action=supriya.AddAction.ADD_BEFORE)
>>> print(server.query_tree())
NODE TREE 0 group
1 group
1004 group
1000 group
1002 group
1001 group
1003 group
>>> group_six = group_one.add_group(add_action=supriya.AddAction.ADD_TO_HEAD)
>>> print(server.query_tree())
NODE TREE 0 group
1 group
1004 group
1000 group
1005 group
1002 group
1001 group
1003 group
>>> group_seven = group_one.add_group(add_action=supriya.AddAction.ADD_TO_TAIL)
>>> print(server.query_tree())
NODE TREE 0 group
1 group
1004 group
1000 group
1005 group
1002 group
1001 group
1006 group
1003 group
>>> group_eight = group_one.add_group(add_action=supriya.AddAction.REPLACE)
>>> print(server.query_tree())
NODE TREE 0 group
1 group
1004 group
1007 group
1003 group
Note
Supriya will attempt to coerce a variety of inputs into a valid
AddAction
:
>>> for x in [None, 0, "ADD_TO_HEAD", "add_to_head", "add to head"]:
... supriya.AddAction.from_expr(x)
...
AddAction.ADD_TO_HEAD
AddAction.ADD_TO_HEAD
AddAction.ADD_TO_HEAD
AddAction.ADD_TO_HEAD
AddAction.ADD_TO_HEAD
This allows you to specify the add action via a string, saving a few keystrokes:
>>> server.add_group(add_action="add to head")
Group(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=1008, parallel=False)
Note
When using supernova as your server executable, you can create
_parallel_ groups by specifying parallel=True
in any call you would use
to create a group:
>>> server.add_group(parallel=True)
Group(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=1009, parallel=True)
Parallel groups will process their child nodes via multiple threads. Because the order of node processing within a parallel group is non-deterministic they’re best suited for summing together signals rather than processing in-place.
Creating synths¶
Now, reset the server, then create a synth, and print its repr to the terminal:
>>> server.reset()
<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>
>>> with server.at():
... with server.add_synthdefs(supriya.default):
... synth = server.add_synth(supriya.default)
...
>>> synth
Synth(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=1000, synthdef=<SynthDef: default>)
The repr shows the synths’s type (Synth
), its ID
(1000
), its SynthDef name (default
), and indicates it has been
allocated (+
). We discuss synth definitions in depth later, but suffice it to say a SynthDef represents a
graph of operators that do audio processing. If that sounds fractally like what
we’re already discussing, you’re not wrong. It’s graphs all the way down.
So far we’ve only used the “default” SynthDef, which generates a simple stereo sawtooth wave. Let’s create two more.
This SynthDef generates a continuous train of clicks:
>>> with supriya.SynthDefBuilder(amplitude=0.5, frequency=1.0, out=0) as builder:
... impulse = supriya.ugens.Impulse.ar(
... frequency=builder["frequency"],
... )
... source = impulse * builder["amplitude"]
... out = supriya.ugens.Out.ar(
... bus=builder["out"],
... source=[source, source],
... )
...
>>> ticker_synthdef = builder.build(name="ticker")
This SynthDef reads audio from a bus, reverberates it, then writes back the wet audio mixed with the dry:
>>> with supriya.SynthDefBuilder(
... damping=0.5, mix=0.5, out=0, room_size=0.5
... ) as builder:
... in_ = supriya.ugens.In.ar(
... bus=builder["out"],
... channel_count=2,
... )
... reverb = supriya.ugens.FreeVerb.ar(
... damping=builder["damping"],
... mix=builder["mix"],
... room_size=builder["room_size"],
... source=in_,
... )
... out = supriya.ugens.ReplaceOut.ar(
... bus=builder["out"],
... source=reverb,
... )
...
>>> reverb_synthdef = builder.build(name="reverb")
Create a synth using the “ticker” SynthDef, replacing the “default” synth we just created:
>>> with server.at():
... with server.add_synthdefs(ticker_synthdef):
... synth.add_synth(ticker_synthdef, frequency=4, add_action="replace")
...
Synth(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=1001, synthdef=<SynthDef: ticker>)
Then create a second synth using the “reverb” SynthDef, positioning it after
the previous synth with an ADD_TO_TAIL
add action:
>>> with server.at():
... with server.add_synthdefs(reverb_synthdef):
... server.add_synth(reverb_synthdef, add_action="add_to_tail")
...
Synth(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=1002, synthdef=<SynthDef: reverb>)
Note the order of the two synths (you can tell by their SynthDef names), and how the reverberation kicks in when you instantiate the second synth:
>>> print(server.query_tree())
NODE TREE 0 group
1 group
1000 default
amplitude: 0.1, frequency: 440.0, gate: 1.0, pan: 0.5, out: 0.0
Note
Supriya keeps track of which SynthDefs have already been allocated, and will automatically allocate them for you when you add synths to the server. If you need precise timing, make sure to pre-allocate the SynthDefs.
See SynthDefs and Open Sound Control for more details.
Deleting¶
Reset the server for a clean slate, then add a synth:
>>> server.reset()
<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>
>>> with server.at():
... with server.add_synthdefs(supriya.default):
... synth = server.add_synth(supriya.default)
...
You can remove a node from the server by freeing it:
>>> synth.free()
Note how the audio cuts off abruptly. Freeing nodes terminates them immediately without any fade-out.
Now add another synth and release it:
>>> synth = server.add_synth(supriya.default)
>>> synth.free()
Some synths can be released, depending on their
SynthDef, and will fade out before freeing themselves automatically
from the server. By convention with sclang, synths with a gate
control can be released, although it’s up to the author of the SynthDef
to guarantee they behave as expected.
Groups can also be freed:
>>> group = server.add_group()
>>> group.free()
Inspection¶
Reset the server for a clean slate:
>>> server.reset()
<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>
… then create a group and add three synths to it:
>>> with server.at():
... group = server.add_group()
... with server.add_synthdefs(supriya.default):
... synth_a = group.add_synth(supriya.default, frequency=333)
... synth_b = group.add_synth(supriya.default, frequency=444)
... synth_c = group.add_synth(supriya.default, frequency=555)
...
Every node has a id_
and a reference to its context:
>>> group.id_, group.context
(1000, <Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>)
>>> synth_a.id_, synth_a.context
(1001, <Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>)
>>> synth_b.id_, synth_b.context
(1002, <Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>)
>>> synth_c.id_, synth_c.context
(1003, <Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>)
Position¶
Nodes know about their position in the node tree.
Caution
This position information is cached on our realtime context based off of
the various /n_go
and /n_end
messages sent back by scsynth,
so must be taken with a grain of salt - it may be stale by the time you act
upon it.
The synths we created know that the group we (also) created is their parent:
>>> synth_a.parent
Group(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=1000, parallel=False)
And they know the entire parentage between themself and their root:
>>> for node in synth_a.parentage:
... node
...
Synth(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=1001, synthdef=<SynthDef: default>)
Group(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=1000, parallel=False)
Group(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=1, parallel=False)
RootNode(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=0, parallel=False)
Likewise, the group we created knows about its children:
>>> for node in group.children:
... node
...
Synth(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=1003, synthdef=<SynthDef: default>)
Synth(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=1002, synthdef=<SynthDef: default>)
Synth(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=1001, synthdef=<SynthDef: default>)
Querying controls¶
We can query each synth’s frequency
and amplitude
:
>>> synth_a.get("frequency", "amplitude")
{'frequency': 333.0, 'amplitude': 0.10000000149011612}
>>> synth_b.get("frequency", "amplitude")
{'frequency': 444.0, 'amplitude': 0.10000000149011612}
>>> synth_c.get("frequency", "amplitude")
{'frequency': 555.0, 'amplitude': 0.10000000149011612}
Interaction¶
Reset the server for a clean slate:
>>> server.reset()
<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>
… then add a group, a ticker synth and a reverb synth using the two SynthDefs we defined earlier:
>>> with server.at():
... group = server.add_group()
... with server.add_synthdefs(ticker_synthdef, reverb_synthdef):
... ticker_synth = group.add_synth(ticker_synthdef)
... reverb_synth = group.add_synth(
... reverb_synthdef, add_action="add_to_tail"
... )
...
Note the click train emitted by the ticker synth and the reverberation added by the reverb synth.
Now we’ll interact with these three nodes to modify their sound …
Moving¶
Nodes can be moved relative to other nodes, using the same add actions used when allocating nodes.
Move the reverb synth to the head of its parent group - before the ticker synth - and notice how the click train’s reverberation dies out:
>>> reverb_synth.move(group, "add_to_head")
>>> print(server.query_tree())
NODE TREE 0 group
1 group
1000 group
1002 reverb
damping: 0.5, mix: 0.5, out: 0.0, room_size: 0.5
1001 ticker
amplitude: 0.5, frequency: 1.0, out: 0.0
Now move the reverb synth after the ticker synth, and listen to the reverberation return:
>>> reverb_synth.move(ticker_synth, "add_after")
>>> print(server.query_tree())
NODE TREE 0 group
1 group
1000 group
1001 ticker
amplitude: 0.5, frequency: 1.0, out: 0.0
1002 reverb
damping: 0.5, mix: 0.5, out: 0.0, room_size: 0.5
Setting controls¶
Setting controls on nodes via set()
.
Change the ticker synth’s frequency control to 1
Hertz, clicking
once every second:
>>> ticker_synth.set(frequency=1)
Change the reverb synth’s “room-size” control to 0.1
to reduce the size
of the simulated reverb space:
>>> reverb_synth.set(room_size=0.1)
Multiple controls can be set on a synth simultaneously. Let’s create a long bright reverb by changing both the reverb’s room size and its damping:
>>> reverb_synth.set(damping=0.1, room_size=0.95)
Because groups are aware of their child synths’ controls, we can set the control of any of their children by setting it on the group.
Let’s set the frequency of the ticker synth’s click train by setting
frequency
on the parent group:
>>> group.set(frequency=10.0)
The group does not actually have a frequency
control - it just propagates
the control setting to any synth in its subtree.
Query the node tree to see the control settings:
>>> print(server.query_tree())
NODE TREE 0 group
1 group
1000 group
1001 ticker
amplitude: 0.5, frequency: 10.0, out: 0.0
1002 reverb
damping: 0.1, mix: 0.5, out: 0.0, room_size: 0.95
Pausing¶
Nodes can be paused and unpaused. Paused synths perform no audio processing, and all children of paused groups are considered paused.
Let’s pause the ticker synth, and notice how the click train stops:
>>> ticker_synth.pause()
You can still hear the reverberation from the reverb synth since it wasn’t paused.
Unpause the ticker synth to resume the click train:
>>> ticker_synth.unpause()
Now let’s pause the ticker and reverb synths’ parent group:
>>> group.pause()
Notice how audio is completely silenced, both the ticker’s click train and the reverb’s reverberation.
Unpause the parent group to resume audio processing:
>>> group.unpause()
Configuration¶
The maximum number of nodes available in a context is controlled by its options.
Options.maximum_node_count
Options.initial_node_id