Generally useful effects

This tutorial lists out the effects I use almost all the time when composing. Many of them are very simple, but I write them in a way that I can call on them very easily by name, without having to think a lot about the specific implementation.

Preliminary: Instr/Patch/FxPatch

All of these effects are written using the Instr class, which is found in the crucial library (part of the main distribution). This class has some specific advantages that are ideal for effect writing:

This tutorial is not intended to be a complete lesson on Instr and Patch, but I should point out a few things at the outset:

Spec.specs.asSortedArray.do(_.postcs) 

When patching an Instr, the arguments are supplied in an array. Every time you supply nil for an argument, Patch looks to the spec defined in the instrument and constructs the default control. For ControlSpec, the default is a synth control that can be modulated while the synth is playing.

For a brief example,

Instr(#[\busfx, \dist], { arg bus, numChan, preGain, postGain;
   (In.ar(bus, numChan) * preGain).distort * postGain;
}, [\audiobus, ObjectSpec(1), #[0.05, 20, \exponential, 0, 1], 
   #[0.05, 20, \exponential, 0, 1], #[0,1]]);

FxPatch([\busfx, \dist], [0, 2]);  // FxPatch #1
FxPatch([\busfx, \dist], [0, 2, 5, 0.15]);  // FxPatch #2

This distortion effect has four arguments: the first two defining the audio bus and the number of channels, and the last two being audio parameters. Creating FxPatch #1 as above, bus receives the hard-coded value 0 and numChan the hard-coded value 2. In the resulting synthdef, these parameters will not be visible as controls that can be modified using /n_set messages. preGain and postGain, however, will be created as modulatable controls because they are associated with ControlSpecs.

In FxPatch #2, all four arguments have hard-coded values. The synthdef will not have any modulatable inputs, which is not good for tuning, but it's ideal for "set-and-forget" effects that are already tuned. Because the "hard-coded" values are patched in at run-time, you don't have to write a whole new synthdef for each variant. The Instr is the template, out of which you can produce many synthdefs for the server.

Note: Hard-coded values are slightly more efficient to process in the synthesis server... not by much, but every little bit counts.

Note: FxPatch is not part of the main distribution. It is available in my library. It's very simple, though, since it only overrides one method, so you can install it yourself to use these effects.

FxPatch : Patch {

   asSynthDef {
      ^synthDef ?? {
         synthDef = InstrSynthDef.build(this.instr,this.args,ReplaceOut);
         defName = synthDef.name;
         numChannels = synthDef.numChannels;
         rate = synthDef.rate;
         synthDef
      }
   }
}

Usage example

Next, let's look at how one of these effects can be used in the composition process. This example will set up a basic kick drum and use a dynamics processor to fatten the sound without incurring distortion. A GUI for the effect Patch will be used to tune the parameters.

First, the kick sound. We'll also set up a PeakMonitor (another class in my library) to make sure the dynamic processing doesn't take the level far out of range.

////////// usage example: kick drum

s.boot;

// simple electro kick drum
SynthDef(\kik, { |basefreq = 50, envratio = 3, freqdecay = 0.02, ampdecay = 0.5, outbus = 0|
   var   fenv = EnvGen.kr(Env([envratio, 1], [freqdecay], \exp), 1) * basefreq,
      aenv = EnvGen.kr(Env.perc(0.005, ampdecay), 1, doneAction:2);
   Out.ar(outbus, SinOsc.ar(fenv, 0.5pi, aenv));
}).send(s);

m = MixerChannel(\kik);
PeakMonitor(m, 5);

r = fork {
   loop {   // the args here are to make sure the sound goes through the mixer
      s.sendBundle(0.1, [\s_new, \kik, -1, 0, m.synthgroup.nodeID, \outbus, m.inbus.index]);
      0.5.wait;
   }
};

Before making the patch, let's check the arguments so we have some idea what we're getting into before starting anything on the server. The definition for the companderd effect is lower on this page.

// a method implemented in dewdrop_lib, to list each arg in the Instr with its spec
Instr.at([\busfx, \companderd]).listArgs;

// with the output:
bus -> ControlSpec(0, 128, 'linear', 1, 0, "")
numChannels -> StaticSpec(1, 8, 'linear', 1, 2, "")
thresh -> ControlSpec(0, 1, 'linear', 0, 0.5, "")
slopeBelow -> ControlSpec(0.1, 10, 'exp', 0, 1, "")
slopeAbove -> ControlSpec(0.1, 10, 'exp', 0, 1, "")
clampTime -> ControlSpec(0.001, 5, 'exp', 0, 0.001, "")
relaxTime -> ControlSpec(0.001, 5, 'exp', 0, 0.001, "")
postGain -> ControlSpec(0.1, 10, 'exp', 0, 1, "")

The defaults are OK to open up this effect without further initialization, so let's play it as an effect on the MixerChannel.

// MixerChannel-playfx makes the FxPatch and automatically substitutes the correct bus number
// you could also do
// p = FxPatch([\busfx, \companderd], [m.inbus.index, 1]).play(m.synthgroup, nil, m.inbus.index);

p = m.playfx(Instr.at([\busfx, \companderd]), [0, 1]);
p.gui;   // make a control panel for it

You won't hear any change, because the slope above and below the threshold is 1. you can now use the sliders to tweak the threshold, slopes and times. I found the following values work well, but feel free to experiment.

thresh -> 0.337
slopeBelow -> 1
slopeAbove -> 0.307   // roughly 3:1 compression
clampTime -> 0.00269
relaxTime -> 0.0475
postGain -> 1.887

The "run" method is an easy way to do an A/B comparison (effect on vs. effect off).

// to prove it's doing something, turn the compander on and off
p.run(false);      // off
p.run(true);      // on -- sounds fatter, but the peak level is about the same

When you have the settings you like, you can use the # button in the patch GUI to print out all the numeric values you chose. The button's output can be copied and pasted right into your piece.

// output, slightly reformatted, from the # button
FxPatch(#[\busfx, \companderd], #[16, 1, 0.33720930232558, 1, 0.30787334695492,
   0.0026922339168174, 0.047581692465085, 1.887182729615]);

// edited further to fit into a playfx command
m.playfx(FxPatch(#[\busfx, \companderd], #[16, .....]));
// or:
m.playfx(Instr.at(#[\busfx, \companderd]), #[16, .....]);

After testing, it's a good idea to clean up any objects you don't need any more.

// clean up
r.stop;
p.free;
m.free;

The effects

Here follows an annotated list of my busfx library so far. You can download the code, without the external comments, from the link at the top of the page.

The first few examples are light on the processing, but they illustrate the form an effect Instr should take, at least in my usage. The first two arguments specify the index of the input audio bus, and the number of channels desired. The input signal should be read in using In.ar(). Then the Instr simply outputs the processed signal.

Note that the spec for the numChannels argument is ObjectSpec(1). This is necessary because you can never modulate the number of channels while a patch is playing. ObjectSpec guarantees that, if you don't supply a number of channels, the object specified will be the default as a hard-coded value. StaticSpec is also OK for a numChannels or similar argument.

Other parameters should generally get ControlSpecs with sensible ranges and default values.

Note: You should never use a ControlSpec or a KrNumberEditor for an argument that determines the number of channels or number of UGens in any Patch. Patch building will fail.

// General effects examples
// dewdrop_world, http://www.dewdrop-world.net
// code is released under the LGPL, http://creativecommons.org/licenses/LGPL/2.1/

// dynamics processing

Instr(#[\busfx, \limiter], { arg bus, numChannels, level, lookAhead, gain;
   Limiter.ar(In.ar(bus, numChannels), level, lookAhead) * gain;
}, [\mybuf, ObjectSpec(1), #[0, 1, \linear, 0, 1], #[0.001, 0.1],
   #[0.1, 10, \exponential, 0, 1]]);

Instr(#[\busfx, \compander], { arg bus, numChannels, thresh, slopeBelow, slopeAbove,
      clampTime, relaxTime, postGain;
   var sig;
   sig = In.ar(bus, numChannels);
   Compander.ar(sig, sig, thresh, slopeBelow, slopeAbove, clampTime, relaxTime, postGain);
}, [\audiobus, ObjectSpec(1), #[0, 1, \linear, 0, 0.5], #[0.1, 10, \exponential, 0, 1],
   #[0.1, 10, \exponential, 0, 1], #[0.001, 5, \exponential], #[0.001, 5, \exponential],
   #[0.1, 10, \exponential, 0, 1]]);

Instr(#[\busfx, \companderd], { arg bus, numChannels, thresh, slopeBelow, slopeAbove,
      clampTime, relaxTime, postGain;
   var sig;
   sig = In.ar(bus, numChannels);
   CompanderD.ar(sig, thresh, slopeBelow, slopeAbove, clampTime, relaxTime, postGain);
}, [\audiobus, ObjectSpec(1), #[0, 1, \linear, 0, 0.5], #[0.1, 10, \exponential, 0, 1],
   #[0.1, 10, \exponential, 0, 1], #[0.001, 5, \exponential], #[0.001, 5, \exponential],
   #[0.1, 10, \exponential, 0, 1]]);


// simple distortion (variants may be built easily on this template)

Instr(#[\busfx, \dist], { arg bus, numChan, preGain, postGain;
   (In.ar(bus, numChan) * preGain).distort * postGain;
}, [\audiobus, ObjectSpec(1), #[0.05, 20, \exponential, 0, 1],
   #[0.05, 20, \exponential, 0, 1], #[0,1]]);

These two effects introduce a new element: a wet/dry control. XFade2 does an equal power crossfade between two signals, so it's your best bet. XFade2 expects the crossfade argument to be -1..1, while I prefer to specify in terms of 0..1, so I use multiplication and subtraction to map the range.

I don't use these effects commonly, but they work for adding a simple filter to a signal chain.

// filters

Instr(#[\busfx, \rlpf], { |bus, numChan, freq, rq, xfade|
   var sig, new;
   sig = In.ar(bus, numChan);
   new = RLPF.ar(sig, freq, rq);
   XFade2.ar(sig, new, xfade * 2 - 1)
}, [\audiobus, ObjectSpec(1), \freq, \myrq, \amp]);

Instr(#[\busfx, \lpf], { |bus, numChan, freq, rq, xfade|
   var sig, new;
   sig = In.ar(bus, numChan);
   new = LPF.ar(sig, freq, rq);
   XFade2.ar(sig, new, xfade * 2 - 1)
}, [\audiobus, ObjectSpec(1), \freq, \myrq, \amp]);

These delays don't use wet/dry, on the assumption that you will run them on a separate mixer channel and use an auxiliary send to feed the effect. See the tutorial on MixerChannel signal routing for more details.

Note: ObjectSpec can be used to pass a UGen class into the Instr! If you want to create a \singleDelay with a DelayC (cubic-interpolation delay) instead of DelayL, you would write the arguments as [0, 1, DelayC, ...].

The maxTime argument should generally be hard coded, since it can't be modulated while the synth is playing.

// some simple delays

Instr(#[\busfx, \singleDelay], { arg bus, numChan, delayClass, maxTime, time, mul, add;
   delayClass.ar(In.ar(bus, numChan), maxTime, time, mul, add)
}, [\audiobus, ObjectSpec(1), ObjectSpec(DelayL), #[0.25, 20], #[0.0001, 20]]);

Instr(#[\busfx, \combDelay], { arg bus, numChan, delayClass, maxTime, time, decay, mul, add;
   delayClass.ar(In.ar(bus, numChan), maxTime, time, decay, mul, add)
}, [\audiobus, ObjectSpec(1), ObjectSpec(DelayL), #[0.25, 20], #[0.0001, 20],
   #[0.0001, 20]]);

Instr(#[\busfx, \pingpong], { arg bus, numChan, bufnum, time, feedback, rotate;
   PingPong.ar(bufnum, In.ar(bus, numChan), time, feedback, rotate);
}, [\audiobus, ObjectSpec(1), #[0, 128, \lin, 1, 0], #[0, 20], #[0, 1],
   #[0, 20, \linear, 1, 1]]);

Nothing surprising here, just a cross-fadable ring modulator.

// simple ringmod

Instr(#[\busfx, \ring1], { arg bus, numChan, freq, mod_amp, xfade;
   var sig, new;
   sig = In.ar(bus, numChan);
   new = sig * SinOsc.ar(freq, 0, mod_amp);
   XFade2.ar(sig, new, xfade * 2 - 1)
}, [\audiobus, ObjectSpec(1), \freq, \amp, \amp]);

OK, now let's add some juice. Chorus effects usually depend on a number of parallel delays added together. SuperCollider excels at building these structures because you can use loops to create them dynamically. \chorus2 and \chorus2band2 use this technique to create a very rich effect. Because the numDelays argument helps determine how many UGens are created, it must also be hard-coded.

The simpler effects, \chorus and \chorus2band, create only one delay per channel. The effect is much subtler.

The two-band choruses are useful for bass sounds, where you should not introduce phase differences between the channels for low frequencies. The chorus effect applies to frequencies above the cut-off, while lower frequencies are passed through without processing.

////////// more complex delay effects

// chorus

Instr(#[\busfx, \chorus], { arg bus, numChan, predelay, speed, depth, ph_diff, xfade;
   var in, sig;
   in = In.ar(bus, numChan);
   in.isKindOf(Collection).if({
      sig = in.collect({ arg ch, i;   // ch is one channel
         DelayL.ar(ch, 0.5, SinOsc.kr(speed, ph_diff * i, depth, predelay));
      });
   }, {
      sig = DelayL.ar(in, 0.5, SinOsc.kr(speed, ph_diff, depth, predelay));
   });
   XFade2.ar(in, sig, xfade * 2 - 1);
//   xf.value(in, sig, xfade)      // when felix has XOut working, this can be better
}, [\audiobus, ObjectSpec(1), #[0.0001, 0.2, \exponential, 0, 0.001],
   #[0.001, 10, \exponential], #[0.0001, 0.25, \exponential], #[0, 2pi], #[0, 1]]);


// based on Sound-on-Sound Synth Secrets: 
// http://www.soundonsound.com/sos/jun04/articles/synthsecrets.htm
// allows mono-to-stereo

Instr(#[\busfx, \chorus2], { arg bus, numInChan, numOutChan, numDelays, predelay, speed,
      depth, ph_diff;
   var in, sig, mods;
   in = In.ar(bus, numInChan);
   mods = { |i|
      SinOsc.kr(speed * rrand(0.9, 1.1), ph_diff * i, depth, predelay);
   } ! (numDelays * numOutChan);
   sig = DelayL.ar(in, 0.5, mods);
   Mix(sig.clump(numOutChan))
}, [\audiobus, ObjectSpec(1), ObjectSpec(1), ObjectSpec(1),
   #[0.0001, 0.2, \exponential, 0, 0.001], #[0.001, 10, \exponential],
   #[0.0001, 0.25, \exponential], #[0, 2pi], #[0, 1]]);


Instr(#[\busfx, \chorus2band], { arg bus, numChan, predelay, speed, depth,
      ph_diff, crossover, xfade;
   var in, lo, hi;
   in = In.ar(bus, 1);
   lo = LPF.ar(in, crossover);
   hi = HPF.ar(in, crossover);
   lo = DelayL.ar(lo, 0.1, SinOsc.kr(speed, ph_diff, depth, predelay));
   hi = Array.fill(numChan, { |i|
      predelay = predelay + depth;
      DelayL.ar(hi, 0.5, SinOsc.kr(speed, ph_diff * i, depth, predelay));
   }).scramble;
   lo = lo + hi;
   XFade2.ar(in, lo, xfade * 2 - 1)
}, [\audiobus, ObjectSpec(1), #[0.0001, 0.2, \exponential, 0, 0.001],
   #[0.001, 10, \exponential], #[0.0001, 0.25, \exponential], #[0, 2pi], \freq, #[0, 1]]);


Instr(#[\busfx, \chorus2band2], { arg bus, numChan, numDelays, predelay, speed, depth,
      ph_diff, crossover, xfade;
   var in, lo, hi, sig, mods, indexBase;
   in = In.ar(bus, 1);
   lo = LPF.ar(in, crossover);
   hi = HPF.ar(in, crossover);
   mods = { |i|
      SinOsc.kr(speed * rrand(0.9, 1.1), ph_diff * i, depth, predelay);
   } ! (numDelays * numChan);
   sig = DelayL.ar(hi, 0.5, mods);
   indexBase = (0, numChan .. mods.size-1);
   hi = { |i| Mix(sig[indexBase + i]) } ! numChan;
   lo = lo + hi;
   XFade2.ar(in, lo, xfade * 2 - 1)
}, [\audiobus, ObjectSpec(1), ObjectSpec(1), #[0.0001, 0.2, \exponential, 0, 0.001],
   #[0.001, 10, \exponential], #[0.0001, 0.25, \exponential], #[0, 2pi], \freq, #[0, 1]]);


// a feedback chorus -- this one is prone to strong comb effects

Instr(#[\busfx, \fbchorus], { arg bus, numChan, predelay, speed, depth, ph_diff, decaytime,
      xfade;
   var in, sig;
   in = In.ar(bus, numChan);
   in.isKindOf(Collection).if({
      sig = in.collect({ arg ch, i;   // ch is one channel
         CombL.ar(ch, 0.5, SinOsc.kr(speed, ph_diff * i, depth, predelay), decaytime);
      });
   }, {
      sig = CombL.ar(in, 0.5, SinOsc.kr(speed, ph_diff, depth, predelay), decaytime);
   });
   XFade2.ar(in, sig, xfade * 2 - 1);
}, [\audiobus, ObjectSpec(1), #[0.002, 0.2, \exponential, 0, 0.01],
   #[0.001, 10, \exponential], #[0.0001, 0.25, \exponential], #[0, 2pi],
   #[0.001, 10, \exponential]]);

Reverbs are the Holy Grail, of course. These are very unsophisticated implementations, but with careful tuning, I've gotten good results from them. See also Josh Parmenter's SuperCollider port of the FreeVerb plug-in, located on the SuperCollider forum. That could be used easily in an effect Instr as well.

////////// reverbs

// better for long reverbs

Instr(#[\busfx, \rvb_allpass], { arg bus, numChan, maxDelay, preDelay, decay, 
      numRefl, random;
   var sig, new;
   sig = In.ar(bus, numChan);
   new = sig;
   numRefl.do({
      new = AllpassN.ar(new, maxDelay, Array.fill(numChan, { random.rand }) + preDelay, decay);
   });
   new
}, [\audiobus, ObjectSpec(1), #[0.25, 2], #[0.001, 1.5, \exponential, 0, 0.05],
   #[0.01, 10, \exponential, 0, 0.25], ObjectSpec(4), #[0.001, 1, \exponential, 0, 0.03]]);

Instr(#[\busfx, \rvb_allpass2], { arg bus, numChan, maxDelay, preDelay, decay,
      numRefl, random;
   var sig, new, dlys;
   sig = In.ar(bus, numChan);
   new = sig;
   dlys = Array.fill(numRefl, {
      new = AllpassN.ar(new, maxDelay, Array.fill(numChan, { random.rand }) + preDelay, decay);   });
   Mix.ar(dlys * Array.series(numRefl, 1, (numRefl+1).reciprocal.neg))
}, [\audiobus, ObjectSpec(1), #[0.25, 2], #[0.001, 1.5, \exponential, 0, 0.05],
   #[0.01, 10, \exponential, 0, 0.25], ObjectSpec(4), #[0.001, 1, \exponential, 0, 0.03]]);


// better for tight reverbs

Instr(#[\busfx, \rvb_comb], { arg bus, numChan, maxDelay, preDelay, decay, numRefl, random;
   var sig, new;
   sig = In.ar(bus, numChan);
   new = Mix.arFill(numRefl, {
      CombC.ar(sig, maxDelay, Array.fill(numChan, { random.rand }) + preDelay, decay);
   });
   new
}, [\audiobus, ObjectSpec(1), #[0.25, 2], #[0.001, 1.5, \exponential, 0, 0.05],
   #[0.01, 10, \exponential, 0, 0.25], ObjectSpec(4), #[0.001, 1, \exponential, 0, 0.03]]);

Feel free to download all the effects in an sc-ready rtf file, using the link at the top of the page.