SuperCollider is a platform for audio synthesis and algorithmic composition. It has three major components:
The client/server architecture is a main feature of SuperCollider. Among other advantages, it allows us to control the server using a client and programming language other than sclang. This is what we'll explore in this tutorial.
Of the three major components provided by SuperCollider, we'll keep the audio server (scsynth), but we'll replace the other two with our own tools. Instead of sclang, we'll use Common Lisp together with cl-collider, a Lisp library that helps us communicate with the audio server, using the Open Sound Control (OSC) protocol. The development environment is more of a personal preference, but see below for recommendations.
It is assumed that you know at least some basic Common Lisp. Fortunately, there's a range of resources available to learn Lisp if you're just starting out. Musicans experienced with Lisp-based musical systems enjoy a headstart.
It is also assumed that you have some notions of how digital audio works.
You don't need to know anything about SuperCollider. Later on you may find it's useful to grasp some sclang basics. This will allow you to better understand the example code that accompanies the documentation, follow tutorials, check out other people's projects, etc.
I find it very instructive to take SuperCollider resources, like books and online tutorials, and translate the demonstration code into Common Lisp. I've been keeping these undertakings in a github repository of cl-collider examples.
Download and install the latest SuperCollider release for your system.
In the Lisp side of things, you'll need an implementation (I recommend SBCL), an editor (a classic choice is Emacs with either Slime or Sly, along with other packages to help write Lisp code), and Quicklisp. If you don't have all this up and running already, just install Portacle and be done with it—Portacle has everything configured and ready to go.
Now load the cl-collider library:
(ql:quickload "cl-collider") (in-package :sc-user)
Check if the variables *sc-synth-program*
, *sc-plugin-paths*
, and *sc-synthdefs-path*
are set to the correct paths for your SuperCollider installation, and setf
them if they aren't.
According to a SuperCollider naming tradition, we'll use *s*
to store an object representing the audio server, which we want to run on the same machine as the client (localhost
), on the arbitrarily selected port number 4444:
(setf *s* (make-external-server "localhost" :port 4444))
Finally, let's boot the server:
(server-boot *s*)
This runs scsynth and keeps it in the background, ready to accept OSC messages. It should print some information about your audio interface and drivers, along with other configuration options, and if all goes well we'll be ready to start.
Try:
(play (white-noise.ar 0.1))
The way SuperCollider works is by connecting unit generators, also known as UGens. In a simplified description, UGens produce signals, which can be distributed to other UGens, and finally sent over to the audio interface's outputs, so we can hear the result. WhiteNoise is one such generator. It produces white noise: a random signal having equal intensity at all frequencies. In our code, its name appears translated to the naming conventions in Common Lisp: white-noise
.
What about the .ar
part? Each UGen can operate at different rates. .ar
stands for audio rate, which means that values are calculated often enough for the generation and processing of an audio signal. Most UGens have two versions: the audio rate one, that ends with .ar
, and the control rate one, that ends with .kr
. Control rate is useful for signals that can be updated less frequently, thus needing less computer processing power. Some UGens also provide a .ir
version, which stands for initialization rate, meaning that the output value is calculated only once, when the UGen is created.
On the SuperCollider documentation pages we can browse the available UGens and read about their description, inputs, examples, etc. Also on the Lisp side there are docstrings available for each UGen with a simple description.
white-noise.ar
has two optional arguments: mul
and add
. They multiply and add to the output signal, respectively. Many UGens have these two arguments, which are used for scaling the output. In the example, we multiply the signal by 0.1 to reduce its amplitude and avoid blasting noise through the loudspeakers. The same thing could be achieved, in an arguably clearer way (especially when dealing with UGens with many arguments), by:
(play (* 0.1 (white-noise.ar)))
Finally, we use play
to prepare and send the appropriate messages to the audio server. play
returns a node object:
#<CL-COLLIDER::NODE :server #<CL-COLLIDER::EXTERNAL-SERVER localhost-127.0.0.1:4444> :id 1000 :name "temp-synth">
SuperCollider structures synthesis processes in a directed graph, in which we just created a new node. Note that an unique number id
was automatically created—1000—as well as the name "temp-synth"
.
We can call (server-query-all-nodes)
to get a tree representation of all the active nodes.
To remove the synthesis process from the server and stop the sound we call free
with either the node object or its id
. It's often practical to store the node in a variable when using play
, which makes it easier to free it later. Like this:
(defparameter *white-noise* (play (white-noise.ar 0.1))) ;;; Later: (free *white-noise*)
Another way of stopping things is with the appropriately named (stop)
. This immediately frees all the nodes in the default group (nodes can be organized into groups, but this is a more advanced topic).
Let's use a sinusoidal oscillator to make a pitched sound:
(play (sin-osc.ar 220 0.0 0.1))
The arguments to sin-osc.ar
are the frequency (220), the phase (0.0), and like before mul
(0.1) and add
(not used, the default is 0). They are all optional arguments.
An UGen can be routed to modulate the input of another UGen:
(play (sin-osc.ar (sin-osc.kr 1 0.0 110 330) 0.0 0.1))
The output of sin-osc
oscillates between -1.0 and +1.0. We take the control rate version of it (ends in .kr
), and multiply the output by 110, so the limits become -110 and +110. Finally, we add 330 to get +220 and +440. This is fed into another sin-osc
, now in audio rate (ends in .ar
), to produce an audible signal that oscillates between 220 Hz and 440 Hz.
If we already know what the minimum and maximum values will be, we can avoid the arithmetic by using range
:
(play (sin-osc.ar (range (sin-osc.kr 1) 220 440) 0.0 0.1))
It's possible to create complex textures just by nesting modulators. The following example only uses instances of sin-osc
and lf-pulse
, a pulse oscillator.
(play (sin-osc.ar (+ (lf-pulse.kr (lf-pulse.kr 0.5 0.5 0.8 1.5 0.5)
0.0
(range (sin-osc.kr 0.15) 0.10 0.45)
(range (sin-osc.kr 0.2) 330 660))
(lf-pulse.kr (lf-pulse.kr 0.25 0.0 0.2 2.5 1)
0.0
(range (sin-osc.kr 0.25) 0.15 0.35)
(range (sin-osc.kr 0.1) 110 770))))
:gain 0.2)
This also shows how we can add two control signals. Try playing with the values—little changes yield a lot of variation. Also note that we use yet another way of adjusting the output volume, with play
's keyword gain
.
So far everything we've done is in mono and comes out on the left speaker only (assuming a stereo monitoring system). SuperCollider offers a number of UGens to deal with multichannel audio. One of the simplest ones is pan2
, a two channel equal power panner.
(play (pan2.ar (ringz.ar (white-noise.ar 0.1) (range (lf-pulse.kr 2 0.0 0.1) 300 1300) 0.5) (lf-tri.kr 0.05 3.0) 0.1))
In this example, white noise is passed through a resonant filter (ringz
), whose center frequency is modulated with a low frequency pulse oscillator, alternating between 300 Hz and 1300 Hz. The resulting signal is fed into pan2
, as its first argument.
The second argument is the pan position, which ranges between -1 (left) and +1 (right). A triangular oscillator (lf-tri
) with a low frequency of 0.05 Hz provides the constantly moving positions—its output also ranges between -1 and +1. A phase offset is also provided, to make the oscillator start at the lowest value. Phase offset is defined as a value ranging from 0 to 4. The correct value for the intended effect is 3.0. This way, we hear the sound coming from the left speaker first, then slowly moving to the right, and back again indefinitely.
Finally, pan2
's third argument (0.1) is the final output level—yet another way of setting the volume.
An important, powerful and sometimes confusing concept is multichannel expansion. Simply put, when a list is given as an input to a UGen, it causes multiple copies of that UGen to be created, each using a different value from the input list. Let's take the first example in this section, but with two frequencies for the oscillator, 660 and 770, instead of a single one:
(play (sin-osc.ar '(660 770) 0.0 0.1))
Two tones come out, one on the left and the other one on the right channel.
With a list of more than two values, the resulting UGens will be sequentially assigned to the next output channels on the audio device. If there are only two available, the expansion won't be heard beyond this limit. We can, however, reduce the expansion back to a single channel using mix
, like in the following eight-note chord:
(play (mix (sin-osc.ar (mapcar #'midicps '(60 64 67 71 78 81 85 87.5)) 0.0 0.05)))
Here, instead of entering the frequency values directly, we use midicps
to convert midi note values (60 is C4) to frequencies.
A practical way of spreading a multichannel signal across the stereo field is with splay
:
(play (splay.ar (loop :for i :from 1 :upto 3 :collect (* (sin-osc.ar (* 220 i 1.6) 0) (lf-pulse.kr (+ (/ i 3) (sin i)) 3 0.1 0.2)))))
The first tone, with the lowest frequency, plays on the left side, the second on the center, and the third and highest-pitched one on the right side.
So far we've been using play
to make sounds. Another more involved and flexible way of working is by first defining audio processes with defsynth
. Then they'll silently live on the server until instantiated by synth
.
(defsynth tone ((freq 440) (amp 0.2)) (out.ar 0 (saw.ar freq amp)))
In the example above, we define a synth named tone
, and give it two parameters, amp
and freq
, each with a default value (respectively 1 and 440).
Next comes a body of expressions. If we want to hear the sounds that our synth will produce, and unlike previously, now we have to use a new UGen, out
, to send the signal to an output bus. Buses will dealt with later, but for now it suffices to note that SuperCollider reserves the bus indexes starting with 0 to the hardware output channels. Our left channel is 0, and the right channel is 1. The first argument to out.ar
is the index of the bus that we want to write to—in this case it's 0, the left channel.
Finally the signal chain has only one UGen, saw.ar
, a sawtooth wave generator, called with the parameters that we defined before.
The above code, however, won't play any sound. Upon evaluating the synth definition, it will be constructed on the server, and silently live there until instantiated. To do so, we need:
(synth 'tone)
When we have enough:
(stop)
We can also store the resulting node on a variable, *tone*
, and free
it later. Also, we can provide values for the synth's parameters, like so:
(defparameter *tone* (synth 'tone :freq (midicps 42) :amp 0.3)) ;;; When we have enough: (free *tone*)
Beyond freeing, this also allows us to change the synth's parameters while it's playing, through ctrl
:
(ctrl *tone* :freq (midicps 48))
Note that what play
was doing behind the scenes was essentially defining a temporary synth and running it.
If the signal chain consists of multiple channels, the bus index provided to out.ar
is the first one, and the others are mapped to successive indexes. The next version of our synth plays slightly detuned sawtooth waves in the left and right channels.
(defsynth tone ((freq 440) (amp 0.2)) (out.ar 0 (saw.ar (let ((detune (* freq 0.01))) (list (- freq detune) (+ freq detune))) (/ amp 2)))) (let ((node (synth 'tone))) (sleep 2) (free node))
In the above code, we used sleep
and then free
to stop the synth automatically after two seconds. But there must be a better way, and of course there is one—/envelopes/.
(defsynth tone ((freq 440) (amp 0.2)) (out.ar 0 (* (saw.ar (let ((detune (* freq 0.01))) (list (- freq detune) (+ freq detune)))) (env-gen.kr (perc 0.1 1.8) :level-scale amp :act :free)))) (synth 'tone)
In the example above, a sawtooth generator (saw
) produces the audible sound, which is multiplied by the envelope generator env-gen
, in order to dynamically control its amplitude. The first argument of env-gen
defines the envelope itself. For convenience, some frequently used envelope shapes are already predefined, among them perc
(for "percussive"), here called with an attack time of 0.1 seconds and a release time of 1.8 seconds.
Since we have an amplitude envelope, it's seems appropriate that amp
is moved to its keyword argument level-scale
.
The final keyword argument, act
, controls what happens when the envelope is finished playing. In this case, it will free
the synth automatically. If we hadn't done this, and even though the synth is now silent, because the envelope ran its course completely, the corresponding node would still be kept on the server. But we don't want to waste the server's resources, and therefore it's a good practice to free what we don't plan to keep using.
Now let's look at a more involved example:
(defsynth tone ((freq 440) (amp 0.2) (gate 1)) (out.ar 0 (* (rlpf.ar (+ (saw.ar (let ((detune (* freq 0.01))) (list (- freq detune) (+ freq detune)))) (* (brown-noise.ar) (env-gen.kr (asr 1.5 0.175 0) :gate gate))) (env-gen.kr (env '(80 2400 200 2400) '(0.05 0.2 5)))) (env-gen.kr (adsr 0.05 0.4 0.4 1.2) :gate gate :level-scale amp :act :free)))) (defparameter *tone* (synth 'tone :freq (midicps 42)))
This shows other envelope recipes. Instead of perc
, the volume envelope is now defined with adsr
, which follows the standard shape of attack (0.05), decay (0.4), sustain level (0.4), and release (1.2). Since we now have a sustain segment, we also need to tell the envelope when to leave the sustain segment and proceed to the release one. We use gate
for that. When gate
is 1 (or any value above 0) the envelope starts, and it is held open until gate
is set to 0, moving the envelope to the release portion. Also, gate
is now a also a synth parameter, with 1 as a default value, so the envelope triggers as soon as the synth is instantiated, but it's possible to later instruct the running synth to change this value to 0.
Our synth now also has a resonant low pass filter (rlpf
), whose frequency is modulated by an envelope. We use env
to define the envelope. env
is the basic specification for envelopes. First, we give it a list of levels, then a list of times (optionally also a list of curves, not used in the example). So the envelope starts at level 80, takes 0.05 seconds to increase to level 2400, then takes 0.2 seconds to move down to level 200, and finally takes 5 seconds to move back up to 2400, holding the last value indefinitely.
There's also a new sound source: a brown noise generator. The amount of noise that gets mixed in with the sawtooth oscillator is controlled with an asr
envelope, that has three segments: attack, sustain, and release.
To trigger the release portion of the envelopes, and ultimately stop the sound, we change the gate to 0:
(ctrl *tone* :gate 0)
The next example uses our synth and its gated envelopes together with sleep
as a crude way of sequencing notes, in this case a kind of spectral arpeggio. To avoid blocking the main thread while this is playing, we move the synth logic to a function and create a new thread to run it.
(defun rising-arp (&optional (start-freq 80) (inharmonicity 0.425)) (loop :for i :from 1 :upto 50 :for mult := (if (zerop (mod i 3)) i (/ i (- 1 inharmonicity))) :collect (synth 'tone :freq (* mult start-freq inharmonicity) :amp (* 0.007 i)) :into nodes :do (sleep (alexandria:random-elt '(1/6 1/7 1/8 1/9 1/10))) :finally (mapc (lambda (node) (ctrl node :gate 0)) nodes))) (bt:make-thread #'rising-arp :name "rising-arp")
Yet another way of creating, starting and controlling audio processes is with proxy
. Unlike play
, where if we evaluate the same code again we create a new node on the server, redefining a proxy
replaces (crossfades) the currently running node with a new one. This makes it especially useful for experimenting and building up our sounds, and also for live coding.
(proxy :fm-hits (let ((trig (impulse.kr .5))) (apply #'freeverb2.ar (splay.ar (loop :repeat 10 :collect (* (pm-osc.ar (+ 100 (random 1600)) (+ 100 (random 700)) (random 10) 0 0.3) (env-gen.kr (perc 0.01 2) :gate trig)))))) :fade 0.1)
When we reevaluate the proxy, new random values are calculated for the synth's parameters and the timbre changes in the time given in :fade
(that's what I'm doing in the audio recording).
We've already covered a good deal of introductory information to get going with cl-collider. But many topics are left. On the next installments we'll see: buses and routing, sampling and audio manipulation, scheduling and more sophisticated ways of sequencing events, setting up OSC responders for bidirectional communication between client and server and for interfacing with external devices, some useful Emacs configuration options, and more.
Enter a short musical phrase in the input box above, choose the complexity level, push the button and wait (it might take a few seconds). You'll get an automatically generated pdf with a bunch of melodic variations on the original phrase. This can be useful to discover new ideas for composing and improvising, to overcome the blank page syndrome, to practice reading, etc.
To input the theme, use the following syntax:
a
-g
), or r
for a rest.s
for sharp, b
for flat (eg, cs
is C-sharp, bb
is B-flat).c'
is the C above middle C (C5, as it is also usually called); ab,,
is the A-flat two octaves below middle C (A2).c1
is a quarter-note C; d.5
is an eighth-note D (.5=half a beat); e1.75
is a double-dotted quarter-note E, or a quarter-note tied to a dotted eight-note… (the exact notation is set by the computer); fs4
is a whole-note F-sharp.g,,1 a1 b1 c'1
, the first three notes are two octaves below middle-C, and the last one one octave below (G2 A2 B2 C3).g,,1 a b c'
.Some more examples of valid phrases:
f1 r.5 f'1 d'.5 e'1
(Moose The Mooche)r.5 g g g eb2 r.5 f f f d2
(Beethoven's fifth)d2 a f d cs d1 e f2.5 g.5 f e d1
(Bach's Art of the Fugue)cs'.5 e gs b as1.5 gs.5 fs ds b,1
(Coltrane's solo on Giant Steps)d.5 e f g e1 c.5 d1.5
(The Lick)After dealing with all above: more algorithms, fine-tuning parameters, midi file download.
Contact me or head to the issue tracker. Also, I'd love to know how you use this and whether it finds a place in your next magnus opus.
Continuing with the exploration of three-pitch sets, it's now time for the very special (012) trichord.
Let's take a C, a C# and a D. We can put the three notes in the same octave, a cluster of pitches as compact as possible. Or we can move each of the notes to another octave, up or down. Now, how many ways are there to voice a chord consisting of just those three notes? Let's use an additional constraint: no intervals larger than an octave between consecutive notes. Starting from a middle C, we can write the six possible combinations like this:
These are all instances of the (012) trichord class.
This expresses in a very particular way the sonority of the twelve-tone equal temperament, focusing on the smallest available intervals and its inversional equivalents.
The (012) trichord is also found in a simple ornamental technique: surrounding a target note with its chromatic neighbors above and below. Some people call this an enclosure, in the context of the analysis of bebop melodies and solos. The idea then is to perceive the target note and its neighbors as a single entity, and play all three pitches simultaneously. This is a form of integrating ornaments into harmony, a transformation that - we can speculate - is prevalent in the historical development of western music, e.g. leading tones being incorporated into chords. Why not take this idea further and promote the chromatic neighbors to the same harmonic hierarchy of chord tones?
For a Cmaj7 chord with the 9th, #11th and 13th1, this yields:
This is what I'm practicing in the video.
While I go through the harmonization of the target notes - C E G B D F# A and back - I also cycle through the all possible voicing types seen before, changing the octave of individual pitches.
That's it. In a way, for me these are all possible voicings for a C major chord, although offering a particularly striking sonic effect.
Instead of a traditional harmonic entity like a major 7th chord, the sequence can also be seen, more abstractly, as intertwined cycles of alternating major and minor thirds, (012) trichords, and a series of all possible distribution of pitches along neighboring octaves.
A variety of perspectives about noise serves as a framework for the development of structures and techniques in the composition of 2458208, for ensemble and electronics (2018). Thinking about noise promptly reveals its paradoxes. Noise may present itself opposed to concepts like signal or pitch. Noise can be seen as the genesis and backdrop of all there is. But how do these concepts reflect on the practical problem of creating a musical work? This text offers a personal perspective, along with the discussion of software tools and techniques, illustrated with code examples.
Read the article in pdf.
Here's what's going on:
Append four combinatorial trichords1: such that all twelve (4x3=12) notes together constitute an aggregate (the twelve notes of the chromatic scale).
The resulting row is not used exactly in dodecaphonic fashion, because we don't have to progress ineluctably from tone to tone; rather, we define four different places (the four trichords), and we can linger on any of them for as long as we like, or go back and forth between them, or use some as consonances and the others as dissonances. The integrity of the trichord, however, should be preserved—in principle all three notes are sounded, simultaneously or in succession, before we progress to the next trichord. This is similar to the technique of triad pairs, but dealing with other three-note groupings beyond the major and minor triads, and with four groupings in total instead of only two.
This idea is thoroughly explored in John O'Gallagher's book Twelve-Tone Improvisation—A Method for Using Tone Rows in Jazz.
Of the existing trichord classes, one that I've been focusing on is (014)2. We get a nice intervallic combination of thirds and sixths together with minor seconds (or ninths, etc.) and major sevenths. It also captures the augmented second sound associated, e.g., with the harmonic minor scale, while being a building block for other frequent musical structures. For what it's worth, in a 2009 study, participants classified it as "non-familiar dissonant", associated with words like "repulsive", "hard", "cold", "cruel", "excited", "unstable", "dangerous", "oppressive", "unfamiliar" and "odd". Given that, what's not to like?
Building a row from juxtaposing (014) trichords is something that has interested many composers. It offers a high degree of symmetry and combinatorial possibilities. Examples of this specific type of tone rows are common, for example, in Schoenberg (op. 29; op. 41; op. 50c "Modern Psalm"; "Die Jakobsleiter") and Webern (op. 18, no. 2; op. 24; op. 29). One important feature is that two (014) trichords can be put together to generate a hexachord of class (014589)—e.g., {C, C#, E, F, Ab, A}—otherwise known as the augmented scale, or Babbitt's third order set. This hexachord is also related to Messiaen's third mode of limited transposition.
There's more than one way of combining the trichords to form the row. I like to use a structure with the following sets:
In the video, I'm practicing the trichords as simultaneities and in open voicing. I take each one of them and go through all the rotations3, ascending and descending using the entire range of the instrument. Here's the score:
This is just for illustration. Playing off the written notation is a very inefficient way of learning this or other concepts.
This text will use just a tiny bit of pitch class set theory nomenclature. I've met people really hostile to it, for some reason, but for me it's useful for labeling and articulating many musical concepts. If some terms are unfamiliar, a good textbook is Introduction to Post-Tonal Theory by Joseph N. Strauss, and of course there are many instructive materials on-line. Just don't be put off right away.
Simply put, this is a set of pitches comprising a given starting note (the "0" part), another one up or down a semitone ("1"), and another one in the same direction four semitones from the starting point ("4"). For example, {C, C#, E} or {C, B, Ab}. Each note can be in any octave and all orderings are equivalent, e.g, {C, C#, E} = {C#, C, E}.
Take the lowest note and make it the highest, keeping the same pitch class—what's commonly known as an inversion in tonal theory.
My master's thesis.
With the issues of human-machine interaction in the compositional process as a background, the present dissertation follows three simultaneous vectors: (a) a portfolio of some of the music that I wrote in the past two years, in which I applied (b) a set of computer-assisted algorithmic composition tools that I developed synchronously in that period of time, following (c) a reflection on the underlying concepts, presented here monographically. A library in Common Lisp stands out among the tools, including functions to perform simple combinatorics with large pitch collections and to the exploration of geometric properties observed in rhythmic and tonal cycles, as well as a program to manipulate audio using Markov chains. The written music—which is an integral part of this work—is simultaneously the point of departure and the point of arrival for the research path that I followed.