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