Common Design Patterns in SuperCollider

Hello!
Do we have a compendium of commonly used design patterns when coding in SuperCollider? What are your favorites? How do you organize your code?

I feel that this topic can be very individual… but at the same time, it would be great to have references about how different people (especially super-experienced ones) are dealing with it.

1 Like

Having something like this would be really awesome!

Maybe a good starting point would be to gather the patterns used on sccode and write some descriptions about it.

I’ve began to do a project like this, collecting commonly used SynthDefs and the Patterns that would make “more sense” to use with each SynthDef. It’s called SynthDEFaults . IMO it would be great if SC could provide the user a large default library of SynthDefs and Patterns examples, it would smooth the learning curve.
Here is the code:
http://sccode.org/1-5aD
http://sccode.org/1-5aE

If you want some help in building and/or contributing to these project, let me know!
[]'s

2 Likes

This is great, I’ll dive into it for sure, but just to make it clear: I’m looking more for design patterns from a programming point of view, it’s more about how code is organized into (perhaps) projects. I’ll try to give some examples:

  • it looks like a common live coding pattern is indeed to write synthdefs and then improvise sequences that play them (synthdef + “Pbinds”). A sequence of sequences can be written to create the form of a piece. The rest might be fine tuning of parameters or durations.
  • a pattern I use is to create a class (or prototype) to manage a process. Instances will create and store sets of synths or nodeproxies, which accept controls through method calls, guis and midi.
    Furthermore, sometimes a class is a wrapper for a single node, adding features such as osc responders, or parameter coordination (reflecting changes of a parameter to other parameters), or (analysis) data caching. Other times, a class is a wrapper for managing multiple copies of a single synthdef (such as filter banks, or multiple coordinated sample players).
  • a process is carried on entirely within an Ndef.

I know this is very sketchy, and I would like to see clearly more details. The topic is huge, but I feel it would be an important step for communicating, sharing and reusing SC code.
I agree we could start by scanning through sccode, but I hope some of the hardcore long-time users could have some answers ready :slight_smile:

1 Like

I think it’s hard to make a list of “things I’m doing right.” It may be more useful to approach the question from the other direction – things that are commonly done in a way that isn’t ideal, and suggest alternate approaches.

Off the top of my head:

Antipattern 1: The base SC class should do everything

This is probably a result of the fact that it’s hard enough to learn what’s in the class library, so the natural tendency is to learn that and then struggle through or hack around the limitations. This shows up in a lot of ways:

  • SynthDef should handle dynamic structures
  • SynthDef or Synth should handle controller mapping
  • Patterns should manage resources

What’s often forgotten in this anti-pattern is that, even if SC had a more powerful class to do the more complex job, it would still be necessary to have the base class supporting it.

The solution is some kind of super structure that uses the base classes.

  • Dynamic SynthDefs: Use a helper function to generate variants with different names. (A far extreme of this is crucial library Instr/Patch.)
  • Controller mapping: In my own libraries, I’ve got GenericGlobalControl which wraps a language-inside value and a server-side control bus into one object.
  • Patterns: My own solution is a “process” object that encapsulates a pattern with all of the resources it needs. If the pattern plays onto an audio bus, the process creates the bus (and releases the bus when freed). If the pattern needs a buffer, the process creates and destroys it. Then the pattern is free to do what it does well (put information into events), while there is still a structure binding that pattern to other resources.

Antipattern 2: GUIs should be interchangeable with values

Nope. A GUI is responsible for interaction. It isn’t data storage and it shouldn’t be treated as such.

// naive design: GUI is free-standing
z = Slider(nil, Rect(800, 200, 200, 50)).front;

p = Pbind(
	// and the pattern queries the GUI object directly
	\freq, Pfunc { z.value.linexp(0, 1, 200, 800) },
	\dur, 0.1
).play;

ERROR: Qt: You can not use this Qt functionality in the current thread. Try scheduling on AppClock instead.

Instead, data storage should be in dedicated variables or objects. The GUI feeds information into the data storage, and other tasks can query the stored data freely.

f = 440;  // data storage

z = Slider(nil, Rect(800, 200, 200, 50))
.value_(f.explin(200, 800, 0, 1))
// GUI --> data storage
.action_({ |view| f = view.value.linexp(0, 1, 200, 800) })
.front;

p = Pbind(
	// pattern queries data storage
	// but does not touch GUI
	\freq, Pfunc { f },
	\dur, 0.1
).play;

p.stop;

That actually leads to the Model-View-Controller design pattern for interfaces – which is definitely not obvious to wrap your head around at first, but it really is the most flexible design. But this post is already a little long, so I’ll leave it at that for now.

There are probably others, but I’ve written enough for the moment.

hjh

4 Likes

Not sure if that is relevant to your workflow but we have created the CuePlayer Quark which is an encapsulation of a common workflow relevant for compositions that are driven by triggered cues. The structure also includes timeline scheduling functionality for each cue among other things and it is agnostic regarding the content of its cues as it just triggers any SC code.

1 Like

@jamshark70 this idea is super cool, I think it deserves a more complete tutorial or guide!

Could you please show us some examples related to the things you said on antipattern 1 ?

Controller mapping (you need the ddwCommon quark):

a = { |freq = 55, ffreq = 2000, rq = 0.1, amp = 0.1|
	var sig = Saw.ar(freq * [1, 1.005]);
	sig = RLPF.ar(sig, ffreq, rq);
	sig * amp
}.play;

f = GenericGlobalControl(\ffreq, nil, 2000, \freq);
f.gui;

a.set(\ffreq, f.asMap);  // like control bus

f.value = 500;

f * 2  // math usage
f.asPattern  // or in patterns

a.free;

// f.kr --> In.kr(f.bus, 1)
a = { RLPF.ar(Saw.ar(55 * [1, 1.005]), f.kr, 0.1, 0.1) }.play;

a.free;
f.free;

Patterns and resources (you need the ddwChucklib quark and dependencies):

(
PR(\abstractProcess).clone {
    ~event = (eventKey: \singleSynthPlayer, instrument: \bufGrainPan);
    ~prep = {
        ~chan = MixerChannel(~collIndex, s, 2, 2);
        ~buf = Buffer.read(s, ~path);
    };
    ~freeCleanup = {
        [~chan, ~buf].free;
    };
    // subpatterns for BPStream
    // note that this refers to ~buf, but ~buf is not loaded
    // at the time of creating the PR. So we must use Plazy
    // to wait and evaluate ~buf.duration at play time.
    ~start = Plazy {
        Pwhite(0, ~buf.duration - Pkey(\time), inf) * ~buf.sampleRate
    };
    ~time = Pwhite(0.1, 0.3, inf);
    ~pan = Pwhite(0.6, 0.8, inf) * Pseq([-1, 1], inf);
    ~dur = Pwhite(1, 4, inf) * 0.125;
    ~asPattern = {
        Pbind(
            \bufnum, ~buf,
            \dur, BPStream(\dur),
            \time, BPStream(\time),  // must be first; dur depends on it
            \start, BPStream(\start),
            \pan, BPStream(\pan)
        )
    };
} => PR(\bufPlayer);
)

BP(\buf).free;  // just in case, clear out anything already there
(
PR(\bufPlayer).chuck(BP(\buf), nil, (
    path: Platform.resourceDir +/+ "sounds/a11wlk01.wav"
));
)

MixingBoard(mixers: BP(\buf).chan);

BP(\buf).play;

// The Plazy trick works outside the PR definition too
BP(\buf).start = Plazy { Pn(Pseries(~buf.duration - 0.5, -0.1, { rrand(5, 20) }), inf) * ~buf.sampleRate };

BP(\buf).stop;
BP(\buf).free;  // magically destroys the Buffer and MixerChannel

hjh

1 Like