SuperCollider TUTORIALS/GETTING-STARTED

12. Groups

Getting Started With SuperCollider

Our discussion about the order of synths on the server brings us to the topic of groups. Synths on the server are a type of what are called nodes. There's one other type of node: groups. Groups are simply collections of nodes, and can contain synths, other groups, or combinations of both. They are mostly useful in two ways: First they are very helpful in controlling order, second, they allow you to easily group together nodes and send them messages all at once. As you've probably guessed, there's a handy Server abstraction object to represent group nodes in the client app: Group.

Groups as Ordering Tools

Groups can be quite helpful in terms of controlling order. Like synths they take targets and addActions as arguments, which makes it easy to put them in position.

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. Let's reconsider our reverb example from the previous section.

(
// 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, b], ~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;

Note that we probably don't care what order the sources and effects are within the groups, all that matters is that all effects synths come after the source synths that they process.

If you're wondering about the names '~sources' and '~effects', placing a tilde (~) in front of a word is a way of creating an environment variable. For the moment, all you need to know about them is that they can be used in the same way as interpreter variables (you don't need to declare them, and they are persistent), and they allow for more descriptive names. You should consider using variable definitions and Functions wherever no later direct access is needed - a large number of environment variables may cause bugs that are hard to find. Remember to clear the currentEnvironment (see above) to avoid interference.

// to be sure, create a new Environment:
Environment.new.push;

// some code..

// restore old environment
currentEnvironment.pop;

All the addActions

At this point it's probably good to cover the remaining add actions. In addition to \addBefore and \addAfter, there is also the (rarely) used \addReplace, and two add actions which apply to Groups: \addToHead and \addToTail. The former adds the receiver to the beginning of the group, so that it will execute first, the latter to the end of the group, so that it will execute last. Like the other addActions, \addToHead and \addToTail have convenience methods called 'head' and 'tail'.

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;

'queryAllNodes' and node IDs

Server has a method called 'queryAllNodes' which will post a representation of the server's node tree. You should have seen something like the following in the post window when executing the example above:

nodes on localhost:
a Server
Group(0)
        Group(1)
                Group(1000)
                        Group(1001)
                                Synth 1002

When you see a Group printed here, anything below it and indented to the right is contained within it. The order of nodes is from top to bottom. The numbers you see are what are called node IDs, which are how the server keeps track of nodes. Normally when working with Server abstraction objects you won't need to deal with node IDs as the objects keep track of them, assigning and freeing them when appropriate.

You may have been wondering why there were four groups posted above when we only created two. The first two, with the IDs 0 and 1, are special groups, called the RootNode and the 'default group'.

The Root Node and the Default Group

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.

default group [
    source group [
        source synth1
        source synth2
    ]
    effects synth
]
recording synth

Groups as, well, groups...

The other major use of groups is to allow you to easily treat a number of synths as a whole. If you send a 'set' message to a group, it will apply that message to all nodes contained within it.

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;

Groups, their Inheritance, and More on Tracking Down Help

Now for a little more OOP theory. Both Group and Synth are examples of what are called subclasses. You can think of subclasses as being children of a parent class, called their superclass. All subclasses inherit the methods of their superclass. They may override some methods with their own implementation (taking advantage of polymorphism), but in general subclasses respond to all the methods of their superclass, and some other ones of their own. Some classes are abstract classes, which means that you don't actually make instances of them, they just exist to provide a common set of methods and variables to their subclasses.

We might for instance imagine an abstract class called Dog, which has a number of subclasses, such as Terrier, BassetHound, etc. These might all have a 'run' method, but not all would need a 'herdSheep' method.

This way of working has certain advantages: If you need to change an inherited method, you can do so in one place, and all the subclasses which inherit it will be changed too. As well, if you want to extend a class to make your own personal variant or enhanced version, you can automatically get all the functionality of the superclass.

Inheritance can go back through many levels, which is to say that a class' superclass may also have a superclass. (A class cannot, however have more than one immediate superclass.) All objects in SC in fact inherit from a class called Object, which defines a certain set of methods which all its subclasses either inherit or override.

Group and Synth are subclasses of the abstract class Node. Because of this, some of their methods are defined in Node, and (perhaps more practically important) are documented in Node's helpfile.

So if you're looking at a helpfile and can't find a particular method that a class responds to, you may need to go to the helpfile for that class' superclass, or farther up the chain. Most classes have their superclass listed at the top of their helpfile. You can also use the following methods for getting this kind of info and tracking down documentation (watch the post window):

Group.superclass;                 // this will return 'Node'
Group.superclass.help;
Group.findRespondingMethodFor('set');        // Node-set
Group.findRespondingMethodFor('postln');    // Object-postln;
Group.helpFileForMethod('postln');         // opens class Object help file

For more information see:

Group, Node, default_group, RootNode, Introduction to Objects, Order of execution, Synth, More on Getting Help, Internal Snooping

____________________

This document is part of the tutorial Getting Started With SuperCollider.

Click here to go on to the next section: 13. Buffers

Click here to return to the table of Contents: 00. Getting Started With SC