Overview

These are my notes on Busses, Grous, the NodeTree and signal flow in SuperCollider.

Content

Busses

Busses are used to route signals from one place to another — either between Synths or to/from the audio hardware. There are two types of busses: audio busses and control busses.

Control busses

Control busses carry control-rate signals. By default, their indices start at 0 and go up to 16383, but this number can be changed in the server options before booting.

Audio busses

Audio busses carry audio-rate signals and are divided into three categories:

  • Audio outputs
  • Audio inputs
  • Private (internal) audio busses

The indices of audio busses depend on the server’s configuration variables:

  • `numOutputBusChannels` → number of audio output channels
  • `numInputBusChannels` → number of audio input channels

For example, if your server has 4 output channels and 2 input channels:

  • Audio output bus indices = 0 to `numOutputBusChannels - 1` (here 0–3) are reserved for the outputs
  • Audio input bus indices = `numOutputBusChannels` to `numOutputBusChannels + numInputBusChannels - 1` (here 4–5) are reserved for the inputs
  • Remaining indices (from `numOutputBusChannels + numInputBusChannels` up to the max audio bus count, usually 1023) are available as private audio busses, used for internal routing between Synths These are not connected to physical I/O by default.

Standard bus sizes

  • Audio buses: 1024 by default
  • Control buses: 16384 by default
  • These numbers are fixed after the server is booted.

Setting custom bus sizes (before boot)

You can change the bus sizes by setting server options before booting:

s.options.numOutputBusChannels = 8;   // number of audio outputs
s.options.numInputBusChannels  = 4;   // number of audio inputs
s.options.numControlBusChannels = 2048; // number of control busses (optional)
 
s.boot;  // boot the server with these settings
 
// check the actual bus sizes
s.options.numOutputBusChannels.postln;   // prints 8
s.options.numInputBusChannels.postln;    // prints 4
s.options.numControlBusChannels.postln;  // prints 2048
  • Writing to or Reading from Busses

    Via the Out UGen, you can write signals to a bus. The rate method (ar or kr) determines whether the signal is written to an audio or a control bus.

    The first argument specifies the bus index. The second argument must be a UGen or an Array of UGens. If you pass an array, SuperCollider will perform multichannel expansion: the first element will be written to the specified bus, the second to the next bus, and so on. Each array element is mapped to a contiguous channel starting from the given bus index.

    // write a control signal to the control signal bus '0'
    { Out.kr(0, SinOsc.kr) }.play
     
    // write an audio signal to the audio signal bus '3'
    { Out.ar(1, SinOsc.ar) }.play
     
    // write an array of audio signals to the audio signal busses '4' to '6'
    { Out.ar(4, SinOsc.ar([220, 225, 227])) }.play

    An audio-rate signal can be downsampled to control rate by writing it to a Out.kr UGen. This effectively converts the signal to control rate.

    In some cases, it is also possible to upsample a control-rate signal to audio rate via interpolation. However, not all UGens support this kind of automatic rate conversion.

    // This throws an error. Can't write a control rate signal to an audio rate bus
    { |out| Out.ar(out, SinOsc.kr) }.play;
     
    // This will work as the audio rate signal is downsampled to control rate
    { |out| Out.kr(out, SinOsc.ar) }.scope;

    When multiple synths write their signals to the same bus, the signals will be summed and downmixed.

    (
    SynthDef("tutorial-args", { arg freq = 440, out = 0;
        Out.ar(out, SinOsc.ar(freq, 0, 0.2));
    }).add;
    )
    // both write to bus 1, and their output is mixed
    x = Synth("tutorial-args", ["out", 1, "freq", 660]);
    y = Synth("tutorial-args", ["out", 1, "freq", 770]);

    To read from a bus, you can use the In UGen. Its first argument is the bus index, and the second argument specifies the number of channels to read. If the second argument is greater than one, the result will be an array of audio signals.

    In returns an OutputProxy, which acts as a placeholder for future audio inputs and supports multichannel expansion.

    In.ar(0, 1); // this will return 'an OutputProxy'
    In.ar(0, 4); // this will return an Array of 4 OutputProxies
    • Example with a Control Bus

      You can always scale one control signal to several ranges. Here is an example how to use a Control Bus to control different paramters.

      ~myControl = Bus.control(s, 1);
       
      c = { Out.kr(~myControl, Pulse.kr(freq: MouseX.kr(1, 10), mul: MouseY.kr(0, 1)))}.play;
       
      (
      {
        Blip.ar(
          freq: LFNoise0.kr([1/2, 1/3]).range(50, 60),
          numharm: In.kr(~myControl).range(10, 100),
          mul: LFTri.kr([1/4, 1/6]).range(0, 0.1)
        )
      }.play;
       
      {
        Splay.ar(
          Pulse.ar(
            freq: LFNoise0.kr([1.4, 1, 1/2, 1/3]).range(100, 1000) * In.kr(~myControl).range(0.9, 1.1),
            mul: SinOsc.ar([1/3, 1/2, 1/4, 1/8]).range(0, 0.03)
          )
        )
      }.play;
      )
       
      c.free;
    • asMap

      You can also assign a busses output a a synth node, even when synth does not contain an In arg.

      // create a SynthDef
      SynthDef(\simple, {arg freq = 440; Out.ar(0, SinOsc.ar(freq, mul: 0.2))}).add;
       
      // create control buses
      ~oneBus = Bus.control(s, 1);
      ~anotherBus = Bus.control(s, 1);
       
      // start controls
      { Out.kr(~oneBus, LFSaw.kr(1).range(100, 1000)) }.play;
      { Out.kr(~anotherBus, LFSaw.kr(2, mul: -1).range(500, 2000)) }.play;
       
      // start a note
      x = Synth(\simple, [\freq, 800]);
      x.set(\freq, ~oneBus.asMap);
      x.set(\freq, ~anotherBus.asMap);
      x.free;
  • Microphone Input

    Microphone Input (or any input channel of your audio interface) can be received via the SoundIn UGen. This is only different from the In.ar in the way, it is counting channels. SoundIn starts indexing the input channels with 0 - In.ar() is counting the overall channels.

    // Warning: use headphones to avoid feedback
    {SoundIn.ar(0)}.play; // same as In.ar(8): takes sound from the first input bus
     
    // Stereo version
    {SoundIn.ar([0, 1])}.play; // first and second inputs
     
    // Some reverb just for fun?
    {FreeVerb.ar(SoundIn.ar([0, 1]), mix: 0.5, room: 0.9)}.play;
  • Private Busses

    Private busses are used for internal communication between Synths within the SuperCollider server. They allow you to send audio or control signals from one Synth to another without interfering with hardware input or output busses.

    You can create private busses and reserve bus channels for them using the Bus class. This class handles dynamic allocation of available bus indices.

    For example, suppose you are working on a system with 2 output channels and 1 input channel. In this case, the first two audio busses (indices 0 and 1) are reserved for audio output, and the next one (index 2) for audio input. When you create a private audio bus using b = Bus.audio(s);, SuperCollider will automatically allocate the next free bus index (e.g., index 3).

    If you later change your hardware configuration (e.g., adding more input or output channels), the Bus class ensures that your private busses still refer to valid, non-overlapping indices. This makes your code more portable and robust.

    The Bus class has also some more convenient features.

    s.reboot; // this will restart the server and thus reset the bus allocators
     
    b = Bus.control(s, 2);    // a 2 channel control Bus
    b.index;         // this should be zero
    b.numChannels         // Bus also has a numChannels method
    c = Bus.control(s);
    c.numChannels;        // the default number of channels is 1
    c.index;        // note that this is 2; b uses 0 and 1
    • Some Examples with Private Busses

      Check carefully to understand these.

      (
      SynthDef("tutorial-Infreq", { arg bus, freqOffset = 0, out;
          // this will add freqOffset to whatever is read in from the bus
          Out.ar(out, SinOsc.ar(In.kr(bus) + freqOffset, 0, 0.5));
      }).add;
       
      SynthDef("tutorial-Outfreq", { arg freq = 400, bus;
          Out.kr(bus, SinOsc.kr(1, 0, freq/40, freq));
      }).add;
       
      b = Bus.control(s,1);
      )
       
      (
      x = Synth.new("tutorial-Outfreq", [\bus, b]);
      y = Synth.after(x, "tutorial-Infreq", [\bus, b]);
      z = Synth.after(x, "tutorial-Infreq", [\bus, b, \freqOffset, 1000]);
      )
      x.free; y.free; z.free; b.free;

      This examples shows how to work with effect busses.

      (
      // the arg direct will control the proportion of direct to processed signal
      SynthDef("tutorial-DecayPink", { arg outBus = 0, effectBus, direct = 0.5;
          var source;
          // Decaying pulses of PinkNoise. We'll add reverb later.
          source = Decay2.ar(Impulse.ar(1, 0.25), 0.01, 0.2, PinkNoise.ar);
          // this will be our main output
          Out.ar(outBus, source * direct);
          // this will be our effects output
          Out.ar(effectBus, source * (1 - direct));
      }).add;
       
      SynthDef("tutorial-DecaySin", { arg outBus = 0, effectBus, direct = 0.5;
          var source;
          // Decaying pulses of a modulating sine wave. We'll add reverb later.
          source = Decay2.ar(Impulse.ar(0.3, 0.25), 0.3, 1, SinOsc.ar(SinOsc.kr(0.2, 0, 110, 440)));
          // this will be our main output
          Out.ar(outBus, source * direct);
          // this will be our effects output
          Out.ar(effectBus, source * (1 - direct));
      }).add;
       
      SynthDef("tutorial-Reverb", { arg outBus = 0, inBus;
          var input;
          input = In.ar(inBus, 1);
       
          // a low-rent reverb
          // aNumber.do will evaluate its function argument a corresponding number of times
          // {}.dup(n) will evaluate the function n times, and return an Array of the results
          // The default for n is 2, so this makes a stereo reverb
          16.do({ input = AllpassC.ar(input, 0.04, { Rand(0.001,0.04) }.dup, 3)});
       
          Out.ar(outBus, input);
      }).add;
       
      b = Bus.audio(s,1); // this will be our effects bus
      )
       
      (
      x = Synth.new("tutorial-Reverb", [\inBus, b]);
      y = Synth.before(x, "tutorial-DecayPink", [\effectBus, b]);
      z = Synth.before(x, "tutorial-DecaySin", [\effectBus, b, \outBus, 1]);
      )
       
      // Change the balance of wet to dry
      y.set(\direct, 1); // only direct PinkNoise
      z.set(\direct, 1); // only direct Sine wave
      y.set(\direct, 0); // only reverberated PinkNoise
      z.set(\direct, 0); // only reverberated Sine wave
      x.free; y.free; z.free; b.free;

      Here is another example how to set and unset control busses. Control busses hold their last value until something new is written.

      (
      // make two control rate busses and set their values to 880 and 884.
      b = Bus.control(s, 1); b.set(880);
      c = Bus.control(s, 1); c.set(884);
      // and make a synth with two frequency arguments
      x = SynthDef("tutorial-map", { arg freq1 = 440, freq2 = 440, out;
          Out.ar(out, SinOsc.ar([freq1, freq2], 0, 0.1));
      }).play(s);
      )
      // Now map freq1 and freq2 to read from the two busses
      x.map(\freq1, b, \freq2, c);
       
      // Now make a Synth to write to the one of the busses
      y = {Out.kr(b, SinOsc.kr(1, 0, 50, 880))}.play(addAction: \addToHead);
       
      // free y, and b holds its last value
      y.free;
       
      // use Bus-get to see what the value is. Watch the post window
      b.get({ arg val; val.postln; f = val; });
       
      // set the freq2, this 'unmaps' it from c
      x.set(\freq2, f / 2);
       
      // freq2 is no longer mapped, so setting c to a different value has no effect
      c.set(200);
       
      x.free; b.free; c.free;

      Look carefully at this line: b.get({ arg val; val.postln; f = val; }); This line has a callback function, which is executed, when the serve has delievered the value, that is called by b.get(). Since the server has some latency, it is important to understand, that this

      (
      f = nil; // just to be sure
      b.get({ arg val; f = val; });
      f.postln;
      )

      would not work.

Groups and the Node Tree

The Node Tree

Synth instances on the SuperCollider server are executed in a specific order, determined by their position in the node tree. Every Synth themself is a node. This order affects how and when audio or control signals are written to and read from buses. s.plotTree; creates a graphical representation of all you synth nodes.

For example: if one Synth reads from a bus, and another Synth writes to it, the writer must be placed before the reader in the execution order. Otherwise, the reader might access stale or uninitialized data. The most recent synths get added to the top by default.

To control the execution order, you can use methods like Synth.new with the optional addAction argument, or you can explicitly place a Synth after another with Synth.after.

These methods allow precise control over when each Synth is executed relative to others, which is essential when working with buses.

Synth-new has two arguments which allow you to specify where in the order a synth is added. The first is a target, and the second is an addAction. The latter specifies the new synth’s position in relation to the target. There is \addAfter and \addBefore, and the (rarely) used \addReplace.

x = Synth("default", [\freq, 300]);
// add a second synth immediately after x
y = Synth("default", [\freq, 450], x, \addAfter);
x.free; y.free;

Methods like Synth-after are simply convenient ways of doing the same thing, the difference being that they take a target as their first argument.

// These two lines of code are equivalent
y = Synth.new("default", [\freq, 450], x, \addAfter);
y = Synth.after(x, "default", [\freq, 450]);

To remove a synth from the server you have a few options:

  • synth.free - like in the examples before
  • Cmd+. - to kill all synths on the server
  • doneAction: 2 - like with UGens as Line. This frees the synth after Line done it’s job

Here is an example with Line and it’s doneAction: 2. Watch the node tree with these two.

// without doneAction: 2
{WhiteNoise.ar(Line.kr(0.2, 0, 2))}.play;
 
// with doneAction: 2
{WhiteNoise.ar(Line.kr(0.2, 0, 2, doneAction: 2))}.play;

Groups as Collection of Nodes

Groups are, alongside Synths, the other type of Nodes in SuperCollider. A Group is a collection of Nodes and can contain Synths, other Groups, or both.

Groups are useful in two main ways:

  • They help to control the execution order of nodes on the server.
  • They allow you to group nodes together and send them messages collectively (e.g. free, set, or move).

Groups are represented by the class Group, which acts as a server-side abstraction for managing node hierarchies.

Creating a Group is done with these commands.

g = Group.new;
h = Group.before(g);
g.free; h.free;

This can be very helpful for things like keeping effects or processing separate from sound sources, and in the right order.

Here is an example.

(
// a stereo version
SynthDef(\tutorial_DecaySin2, { arg outBus = 0, effectBus, direct = 0.5, freq = 440;
    var source;
    // 1.0.rand2 returns a random number from -1 to 1, used here for a random pan
    source = Pan2.ar(Decay2.ar(Impulse.ar(Rand(0.3, 1), 0, 0.125), 0.3, 1,
        SinOsc.ar(SinOsc.kr(0.2, 0, 110, freq))), Rand(-1.0, 1.0));
    Out.ar(outBus, source * direct);
    Out.ar(effectBus, source * (1 - direct));
}).add;
 
SynthDef(\tutorial_Reverb2, { arg outBus = 0, inBus;
    var input;
    input = In.ar(inBus, 2);
    16.do({ input = AllpassC.ar(input, 0.04, Rand(0.001,0.04), 3)});
    Out.ar(outBus, input);
}).add;
)
 
// now we create groups for effects and synths
(
~sources = Group.new;
~effects = Group.after(~sources);     // make sure it's after
~bus = Bus.audio(s, 2);         // this will be our stereo effects bus
)
 
// now synths in the groups. The default addAction is \addToHead
(
x = Synth(\tutorial_Reverb2, [\inBus, ~bus], ~effects);
y = Synth(\tutorial_DecaySin2, [\effectBus, ~bus, \outBus, 0], ~sources);
z = Synth(\tutorial_DecaySin2, [\effectBus, ~bus, \outBus, 0, \freq, 660], ~sources);
)
 
// we could add other source and effects synths here
 
~sources.free; ~effects.free; // this frees their contents (x, y, z) as well
~bus.free;
 
// remove references to ~sources and ~effects environment variables:
currentEnvironment.clear;

There are a few add-actions to order you Groups.

g = Group.new;
h = Group.head(g);        // add h to the head of g
x = Synth.tail(h, \default);    // add x to the tail of h
s.queryAllNodes;        // this will post a representation of the node hierarchy
x.free; h.free; g.free;
  • Send Messages to Groups

    Groups make it convenient to send messages to all synths inside of one group

    g = Group.new;
     
    // make 4 synths in g
    // 1.0.rand2 returns a random number from -1 to 1.
    4.do({ { arg amp = 0.1; Pan2.ar(SinOsc.ar(440 + 110.rand, 0, amp), 1.0.rand2) }.play(g); });
     
    g.set(\amp, 0.005); // turn them all down
     
    g.free;

Node Hierachy

Server has a method called queryAllNodes which will post a representation of the server’s node tree with the corresponding ID’s of the nodes.

When a server app is booted there is a special group created with a node ID of 0. This represents the top of the server’s node tree. There is a special server abstraction object to represent this, called RootNode. In addition there is another group created with an ID of 1, called the default group. This is the default target for all Nodes and is what you will get if you supply a Server as a target. If you don’t specify a target or pass in nil, you will get the default group of the default Server.

s.boot;
a = Synth.new(\default); // creates a synth in the default group of the default Server
a.group; // Returns a Group object. Note the ID of 1 (the default group) in the post window

The default group serves an important purpose: It provides a predictable basic Node tree so that methods such as Server-scope and Server-record (which create nodes which must come after everything else) can function without running into order of execution problems. In the example below the scoping node will come after the default group.

{ SinOsc.ar(mul: 0.2) }.scope(1);
 
// watch the post window;
s.queryAllNodes;
 
// our SinOsc synth is within the default group (ID 1)
// the scope node ('stethoscope') comes after the default group, so no problems

In general you should add nodes to the default group, or groups contained within it, and not before or after it. When adding an ‘effects’ synth, for instance, one should resist the temptation to add it after the default group, and instead create a separate source group within the default group. This will prevent problems with scoping or recording.

Example of organizing Nodes and Groups

This is a example of how you could deal with audio busses and nodes in groups.

// keep watching everything in the NodeTree
s.plotTree;
 
// create some buses
~reverbBus = Bus.audio(s, 2);
~masterBus = Bus.audio(s, 2);
 
// define groups
(
~sources = Group.new;
~effects = Group.new(~sources, \addAfter);
~master = Group.new(~effects, \addAfter);
)
 
// run all synths at once
(
// one source sound
{
  Out.ar(~reverbBus, SinOsc.ar([800, 890]) * LFPulse.ar(2) * 0.1)
}.play(target: ~sources);
// another source sound
{
  Out.ar(~reverbBus, WhiteNoise.ar(LFPulse.ar(2, 1/2, width: 0.05) * 0.1))
}.play(target: ~sources);
 
// some reverb
{
  Out.ar(~masterBus, FreeVerb.ar(In.ar(~reverbBus, 2), mix: 0.5, room: 0.9))
}.play(target: ~effects);
 
// some silly master volume control with mouse
{
  Out.ar(0, In.ar(~masterBus, 2) * MouseY.kr(0, 1))
}.play(target: ~master);
)