Effects

1. Embedding Effects in Synths

To make our music more interesting, we may want to use effects to our sounds. For example, we could add a delay to make our sounds echo, or a reverb to make it sound like the music is being performed in a cave. Because SuperCollider is so open, we can simply add effects into the synth definition if we so choose. This is the most obvious and simple way of doing things:

(defsynth echokick ((freq 440) (time 0.25) (delay 0.2) (amp 0.5) (out 0))
  (let* ((env (env-gen.kr (env (list 0 1 1 0) (list 0.001 time 0.3))))
         (fenv (env-gen.kr (env (list 1 0) (list time)) :level-scale freq))
         (sig (sin-osc.ar fenv 0 (* env 0.2)))
         (sig (+ sig (comb-c.ar sig delay delay (* delay 25))))
         (sig (leak-dc.ar sig)))
    (detect-silence.ar sig 1.0e-4 :time delay :act :free)
    (out.ar out (pan2.ar sig 0 amp))))

In the above synth, our source sound is simply a sine wave sweep from the freq to 0–the env-gen's level-scale argument allows us to easily multiply the envelope by the frequency to get the sweep. Our sin-osc uses the frequency envelope as its frequency input and 0 as its phase. Its mul is 0.2 multiplied by our amplitude envelope env.

Below that, we apply the delay effect. An easy way to make a delay in SuperCollider is to use a comb filter–in this case, comb-c–which is basically the same thing as a normal delay. comb-c's arguments are:

  • The input. In our case, sig, which is the sine sweep.
  • The maximum delay time. This is used by SuperCollider to allocate memory for the delay when the synth is created. For a simple use-case like this, we can just provide the delay time itself. If you were going to modulate the delay time, this would need to be set to the highest time you expect to use, because this argument cannot be modulated after the synth has been created.
  • The delay time. The actual amount of time between each repeat of the delay. This can be modulated.
  • The decay time. This is basically the amount of time that it takes for the delays to fade out to silence.
  • The standard mul and add arguments.

Our last step of processing for the signal is to send it through the leak-dc UGen, which removes what is known as a "DC offset" that some of SuperCollider's comb filters can occasionally produce. A DC offset is basically a very low frequency that causes the audio waveform to not be centered on zero, potentially resulting in clipping or audio filters misbehaving later in the signal path.

One such UGen that can misbehave with a DC offset is detect-silence, which, as the name suggests, is used to detect silence, but may not detect silence if its input has a DC offset. When silence is detected by detect-silence, it outputs a 1, and when the input is not silence it outputs 0. However, it can also be used to automatically free the synth when silence is detected, which is what we're using it for here–hence the :act :free keyword argument. We also set its time argument to our delay, to make sure if our delay time is too long, detect-silence won't free the synth before the first delay actually occurs.

Finally, we use pan2 to turn our mono signal sig into a stereo signal, and send it to the synth's output with out.ar.

2. Effects as Their Own Nodes

Most of the time, it's preferred to run just one instance of an effect which multiple notes or synths would all be routed through. For example, having a separate reverb for each instance of a synth would not only cause unnecessarily high CPU usage, but it would also be more likely to result in audio clipping or other unwanted sound artifacts.

To convert the previous example to such a configuration, we can use the proxy macro as introduced in chapter 3:

  (defsynth echokick ((freq 440) (time 0.25) (amp 0.5) (out 0))
    (let* ((env (env-gen.kr (env (list 0 1 1 0) (list 0.001 time 0.3)) :act :free))
           (fenv (env-gen.kr (env (list 1 0) (list time)) :level-scale freq))
           (sig (sin-osc.ar fenv 0 (* env 0.2))))
      (out.ar out (pan2.ar sig 0 amp))))

(defparameter *echo-bus* (bus-audio :chanls 2))

(proxy :echo
       (let* ((sig (in.ar *echo-bus* 2))
              (delay 0.2)
              (sig (+ sig (comb-c.ar sig delay delay (* delay 25))))
              (sig (leak-dc.ar sig)))
         (out.ar 0 sig)))

Though buses have been mentioned before in this tutorial, they haven't been fully explained. In SuperCollider, and thus cl-collider, there is the concept of a "bus" which is basically a location in memory that can be written to or read to, similar to a buffer, but meant to be done continuously. A bus lets us do things like sending audio from one synth (or multiple) to another. Thus buses allow us to do things like implement effects that multiple synths can have their output processed by.

Here we define the *echo-bus* variable, which is a two-channel audio bus. Aside from audio buses, SuperCollider also supports control buses which are generally used for lower-frequency signals like LFOs that don't need as much processing power, similar to the .ar~/.kr~ UGen dichotomy.

Aside from the bus, it should also be noted that we don't use detect-silence anymore. We don't use it in the synth since there is no built-in delay and our volume envelope lasts the full length of the sound. And we don't use it in our proxy either since we want it to continue running even if it's silent.

With those three expressions evaluated, the synth is defined, the bus created, and the echo effect is running. To play the echokick through the effect, just make sure the synth's out parameter is set to the bus that the effect is reading from:

(synth :echokick :out *echo-bus*)

Last updated: 2022-04-16 Sat 02:15