Overview
Welcome to the seminar
Experimental coding with supercollider - paramettric abstract composing1
by Mathias Hinke and Özgür Kesim
This book is accompanying the seminar and provides additional material and helpful links. It will be updated frequently during the course.
last update: Do 4. Feb 12:12:43 CET 2021
See the VDL for details.
Introduction
What is programming? And how can we use it to create music? In particular, how can we use programming to
- compose music (in an abstract sense)
- produce and manipulate sound (in real time)
This book tries to give answers to these questions. It's goal is to enable you −the composer− to use programming as a method and programming languages as tools to create music.
If we would have asked "What is woodworking?", we would end up talking about fundamental properties of wood, such as the direction of grain, hardness, moisture absorption and wood movement. We would also talk about joinery −the various tested and proper ways to join pieces of wood− and how your particular idea of a piece of furniture can be broken down into to a combination of joints. And when we learn particular techniques to work with wood, we would necessarily introduce specific tools (like saws, planes and chisels).
In the following chapters, you are like the woodworker, digital signal processing and the computer's audio capabilities is the wood, the furniture is the music you want to compose and the joinery is what we want to present here, using supercollider as the tools chest.
Installing supercollider
You can download the latest version of supercollider from supercollider.github.io, which should be embedded in the frame here:
Select and install the version according to your platform. If all went well and you start supercollider, the UI should look something like this:
A first glimpse
Start the IDE of supercollider and open a new file (from the menu: File→New). Follow along with the code snippets below which you can copy and paste into the editor.
Simple Commands
Let's start with a simple statement:
5.squared;
Copy and paste that code into the editor. Now that we have a line written down, we want to execute it.
To execute code in the IDE, place the cursor into the line (or region) and enter
cmd+⏎
1.
We write cmd
and mean ⌘
on Mac and ctrl
on Windows and Linux. A list of all
available keyboard shortcuts can be found in the Appendix.
You should see the output -> 25
appear in the Post Window. The input
5.squared;
gives already a taste of the flavour of syntax of this programming
language. Read it like take the value 5 and square it. We will go in much
deeper detail about the syntax of the programming language sclang
in later
chapters.
Playing sounds
Starting the server scsynth
Before we can create sound, we need to start the actual server which will be doing the sound production:
s.boot;
Executing this line (again with cmd+⏎
) will generate some output in the Post
window, similar to this:
Booting server 'localhost' on address 127.0.0.1:57110.
Found 0 LADSPA plugins
...
SuperCollider 3 server ready.
JackDriver: max output latency 5.3 ms
Requested notification messages from server 'localhost'
localhost: server process's maxLogins (1) matches with my options.
localhost: keeping clientID (0) as confirmed by server process.
Shared memory server interface initialized
The server scsynth
is now read to serve.
Hint: You can clear the post window by entering
cmd+shift+p
.
to be continued...
The server: scsynth
An installation of Supercollider comes with three main components:
scide
─ the user frontendsclang
─ the language interpreterscsynth
─ the server that acts as synthesizer, i.e. is responsible to produce sound.
In this section we focus on the role of scsynth
.1
scsynth
is a standalone program and is responsible for signal processing and
sound production via a soundcard. It is usually launched from scide
─ but
can be launched manually, too ─ and listens for incoming commands over the
network from clients, such as scide
. Because it serves the clients,
scsynth
is considered to be a server in computer jargon.
The commands allow a client to
- instantiate particular generators and processors of signals (called UGens)
- route the in- and output signals of those Ugens through busses
All that happens inside the process scsynth
.
to be continued...
We loosely follow the description in http://doc.sccode.org/Guides/ClientVsServer.html
Functions
A function in sclang is an expression which defines operations to be
performed when it is sent the value
message. It has no name or identifier
attached to it, which is known as lambda expression or anonymous
function in other
programming languages. Function definitions in sclang are enclosed in curly
brackets {}
, f.e.:
{ 3 + 4 }
Functions are not evaluated immediately when they occur in code, but their definitions are passed as values just like integers or strings.
f = { 3 + 4 }; // Let f be this function - no evaluation yet.
f.value; // Perform the operation, see Post window for output.
Arguments
Argument declarations, if any, follow the open bracket.
f = { arg a, b; a*a + b} // Let f be a function that takes two arguments
f.value(2, 3) // Pass the arguments in order and evaluate it
f.value(b: 3, a: 2) // Arguments can be passed by name, in any order
f.value(2) // Oops, second argument is nil.
Default values for the arguments can be provided in the following form:
{ arg a = 1, b = 2; a*a + b}
Controlling arguments
Arguments in sclang are more than they seem to be. You have control over the arguments outside the function. This is very useful in the context of sound production.
f = { arg f = 440, m = 0.1; SinOsc.ar(freq: f, mul: m)!2 }
p = f.play;
// Change the value of the arguments while the Synth is playing
p.set(\f, 350);
p.set(\m, 0.3);
First class citizens
Functions are themselves ─ so called ─ first class citizens in the language and can are treated like other simple types. For example, you can return a function from a function.
~makeF = { arg v; { arg a; v*a } }
t = ~makeF.value(3)
t.value(2)
They can also be passed as arguments. For example, arrays provide the method
collect
that expects a function as argument.
[0, 1, 2, 3, 4].collect({ arg i; i*i + 1 })
To be continued.
- Scope
- Variables
- Syntax variations
- Methods
See http://doc.sccode.org/Reference/Functions.html for more information.
Examples
The following sections contain examples that do not explore sound production very much. Instead, we choose more abstract tasks such as generating chord progressions of specific flavor or finding inversions for optimal voice leading.
The examples are not intended to suggest any sort of aesthetic framework or guidance. They are rather pure test objects on which we try to exercise our ability to transform an abstract idea into code in sclang.
Example: Voice Leading
The complete example can be downloaded here: voice-leading.scd.
Suppose we have two chords as part of a progression, given as triads in root position, and we want to choose an appropriate inversion of the second chord for good voice leading. For example, given the triads C-major and e-minor, we want to choose instead of or .
How can we programmatically differentiate these cases? One way would be to define and calculate a distance between two triads and compare the distances in all cases. If we consider a triad as a 3-dimensional vector with integer entries, we can simply use the euclidean distance. For two vectors \((a, b, c)\) and \((x, y, z)\) their euclidean distance is defined as \(\sqrt{(a-x)^2 + (b-y)^2 + (c-z)^2}\). Let's write this as a function in sclang:
// ~distance is a function that expects two triads as arguments and calculates
// the euclidian distance between them. In other words, it considers both
// triads as points in a three-dimensional space.
~distance = {
arg tr1, tr2;
// Given two triads tr1=[a, b, c] and tr2=[x, y, z], the euclidian distance is
// defined as
// dist(tr1, tr2) = sqrt((a-x)² + (b-y)² + (c-z)²)
// In sclang, we can write this from the inside out:
(tr2-tr1).sum({|i| i**2}).sqrt();
};
We can now write the function that, given two triads, will return the "best" inversion for the second triad in the sense that it chooses an inversion of the second triad that has the minimal "distance" to the first:
// ~findBestInversion is a function that expects two triads as arguments and
// returns an inversion of the second triad that has the minimal distance to
// the first in terms of steps in the voices.
~findBestInversion = {
arg tr1, tr2; // triads given.
// Let's generate an array of all inversions of the second triad. We
// will later find and return the one with the minimal distance.
var inversions = [
tr2, // root position
(tr2 + [0 , -12, -12]).sort, // first inversion, down an octave
(tr2 + [0 , 0, -12]).sort, // second inversion, down an octave
tr2 - 12, // root position, down an octave
];
// For each chord in the array `inversions` we calculate its ~distance
// to the first triad and collect the results into an array.
var distances = inversions.collect({|chord| ~distance.value(tr1, chord)});
// Find the index that contains the minimal distance.
var minIdx = distances.minIndex;
// Return the inversion at that index as our choice for the progression.
inversions[minIdx];
};
After putting all the code above within the same scope (…)
, let's try the
function ~fundBestInversion
on
C-major ([0, 4, 7]
) and e-minor ([4, 7, 11]
):
~findBestInversion.value([0, 4, 7], [4, 7, 11]);
In the Post window we see the result [ -1, 4, 7]
which corresponds to the
first inversion, as expected.
We will soon use the function ~findBestInversion
within the other examples of
chord progressions.
Example: Neo-Riemannian Orbit
The complete example can be downloaded here: neo-riemannian.scd.
Suppose we want to generate chord progressions based on Neo-Riemannian Theory, which gives a theoretical framework to navigate through all possible major- and minor triads by applying certain transformations. To quote from Wikipedia about Neo-Riemannian Theory:
The principal transformations of neo-Riemannian triadic theory connect triads of different species (major and minor), and are their own inverses (a second application undoes the first). These transformations are purely harmonic, and do not need any particular voice leading between chords: all instances of motion from a C major to a C minor triad represent the same neo-Riemannian transformation, no matter how the voices are distributed in register.
The three transformations move one of the three notes of the triad to produce a different triad:
- The P transformation exchanges a triad for its Parallel. In a Major Triad move the third down a semitone (C major to C minor), in a Minor Triad move the third up a semitone (C minor to C major)
- The R transformation exchanges a triad for its Relative. In a Major Triad move the fifth up a tone (C major to A minor), in a Minor Triad move the root down a tone (A minor to C major)
- The L transformation exchanges a triad for its Leading-Tone Exchange. In a Major Triad the root moves down by a semitone (C major to E minor), in a Minor Triad the fifth moves up by a semitone (E minor to C major)
The transformations P, R and L
We will have to implement the transformations P, R and L. Let's keep them in a
dictionary called riemann
, where we put each transformation associated with
it's symbol, like \L
.
var riemann = (); // Dictionary <symbol> -> <neo-riemannian-transformation>
The P transformation exchanges a triad for its Parallel. In a Major Triad move the third down a semitone (C major to C minor), in a Minor Triad move the third up a semitone (C minor to C major).
Even if we don't have yet the means to decide programmatically if a given triad
is a major or minor one, or how to raise or lower particular notes in it, we
can start expressing our intend right away by writing code that calls
not-yet-available functions, but with names that we like to use. For example,
the description for the transformation \P
above could be expressed like this:
riemann[\P] = { |triad| // [a, b, c]
case
// In a Major Triad move the third down a semitone (C major to C minor),
{ isMajor.(triad) } { moveThird.(triad, -1); }
// in a Minor Triad move the third up a semitone (C minor to C major)
{ isMinor.(triad) } { moveThird.(triad, 1); }
{true}{triad.debug("[P] unknown modus");};
};
We use the case {}{}…{}{}
construct to differentiate between different cases
(major, minor and catch-all case (for detecting errors)) and call functions
isMajor
, isMinor
and moveThird
, which we will need to implement
seperately. Nevertheless, our intend has been clearly stated and readers of our
code will understand.
Before implementing the missing functions, let's write the other two transformations in a similar manner.
The R transformation exchanges a triad for its Relative. In a Major Triad move the fifth up a tone (C major to A minor), in a Minor Triad move the root down a tone (A minor to C major)
riemann[\R] = { |triad| // [a, b, c]
case
// In a Major Triad move the fifth up a tone (C major to A minor),
{ isMajor.(triad) } { moveFifth.(triad, 2) }
// in a Minor Triad move the root down a tone (A minor to C major)
{ isMinor.(triad) } { moveRoot.(triad, -2) }
{true}{triad.debug("[R] unknown modus");};
};
The L transformation exchanges a triad for its Leading-Tone Exchange. In a Major Triad the root moves down by a semitone (C major to E minor), in a Minor Triad the fifth moves up by a semitone (E minor to C major)
riemann[\L] = { |triad| // [a, b, c]
case
// In a Major Triad the root moves down by a semitone (C major to E minor),
{ isMajor.(triad) } { moveRoot.(triad, -1) }
// in a Minor Triad the fifth moves up by a semitone (E minor to C major)
{ isMinor.(triad) } { moveFifth.(triad, 1) }
{true}{triad.debug("[L] unknown modus")};
};
Helper functions
Finally, we will now implement the missing functions and put them before what we have written so far. First, we need function to distinguish Major and Minor chords:
var isMajor = { |triad|
var f = triad[1]-triad[0]; // first interval
var s = triad[2]-triad[1]; // second interval
// Ex.: triad == [0, 4, 7] (C Major)
// ⇒ triad[0] == 0, triad[1] == 4, triad[2] == 7
// ⇒ f == 4 and s == 3
// Check all possible inversions of a major chord by looking at the
// intervals:
case
{(f==4 && s==3) || (f==3 && s==5) || (f==5 && s==4)} { true } // major!
{(f==3 && s==4) || (f==5 && s==3) || (f==4 && s==5)} { false } // minor!
// Other interval combinations mean invalid triad, help debugging it:
{ true }{ triad.debug("[isMajor] unknown modus"); false } // error!
};
var isMinor = { |triad|
var f = triad[1]-triad[0]; // first interval
var s = triad[2]-triad[1]; // second interval
case
{(f==3 && s==4) || (f==5 && s==3) || (f==4 && s==5)} { true } // minor!
{(f==4 && s==3) || (f==3 && s==5) || (f==5 && s==4)} { false } // major!
{ true }{ triad.debug("[isMinor] unknown modus"); false } // error!
};
... and functions manipulating chords in specific ways:
var moveRoot = { |triad, d|
// triad = [a, b, c]
var a = triad[0];
var b = triad[1];
var c = triad[2];
case
// root position: fifth between a and c, root == a
{ c-a == 7 }{ triad + [ d, 0, 0 ] }
// first inversion: forth between b and c, root == c
{ c-b == 5 }{ triad + [ 0, 0, d ] }
// second inversion: forth between a and b, root == b
{ b-a == 5 }{ triad + [ 0, d, 0 ] }
// Couldn't identify the root! Help debugging it.
{ true }{ triad.debug("unknown root") }
};
var moveThird = { |triad, d|
var a = triad[0];
var b = triad[1];
var c = triad[2];
case
// root position: fifth between a and c, third == b
{ c-a == 7 }{ triad + [ 0, d, 0] }
// first inversion: forth between b and c, third == a
{ c-b == 5 }{ triad + [ d, 0, 0] }
// second inversion: forth between a and b, third == c
{ b-a == 5 }{ triad + [ 0, 0, d] }
{ true }{ triad.debug("unknown third") }
};
var moveFifth = { |triad, d|
var a = triad[0];
var b = triad[1];
var c = triad[2];
case
// root position: fifth between a and c, fifth == c
{ c-a == 7 }{ triad + [ 0, 0, d] }
// first inversion: forth between b and c, fifth == b
{ c-b == 5 }{ triad + [ 0, d, 0] }
// second inversion: forth between a and b, fifth == a
{ b-a == 5 }{ triad + [ d, 0, 0] }
{ true }{ triad.debug("unknown fifth") }
};
For printing chord names:
var noteNames = ["C", "C♯", "D", "D♯", "E", "F", "F♯", "G", "G♯", "A", "A♯", "B"];
var getRoot = { |triad|
var a = triad[0];
var b = triad[1];
var c = triad[2];
case
{ c-a == 7 }{ a }
{ b-a == 5 }{ b }
{ c-b == 5 }{ c }
{ true }{ triad.debug("[getRoot] unknown root") }
};
var chordName = { |triad|
var root = getRoot.(triad);
var name = noteNames[root];
case
{ isMajor.(triad) } { name ++ " Major" }
{ isMinor.(triad) } { name.toLower ++ " Minor" }
{ true }{ triad.debug("[chordName] unknown modus") }
};
Secondary transformations
The N (or Nebenverwandt) relation exchanges a major triad for its minor subdominant, and a minor triad for its major dominant (C major and F minor). The "N" transformation can be obtained by applying R, L, and P successively.
riemann[\N] = riemann[\R]<>riemann[\L]<>riemann[\P];
S (or Slide) relation exchanges two triads that share a third (C major and C♯ minor); it can be obtained by applying L, P, and R successively in that order:
riemann[\S] = riemann[\L]<>riemann[\P]<>riemann[\R];
The H relation (LPL) exchanges a triad for its hexatonic pole (C major and A♭ minor):
riemann[\H] = riemann[\L]<>riemann[\P]<>riemann[\L];
The combination PRL has no name on wikipedia, transforming C major to D major. Let's call it U.
riemann[\U] = riemann[\P]<>riemann[\R]<>riemann[\L];
An orbit routine
neoOrbit
is a function that expects a chord and a stream of neo-riemannian
transformations (as symbols) and returns a routine that yields the progression
of chords by applying the transformations consecutively.
~neoOrbit = {
arg chord = [0, 4, 7], stream = Pseq([\P, \L, \R], inf).asStream;
Routine {
var tr;
loop{
chordName.(chord).debug("chord");
// Eject the current chord to the consumer of this
// routine, when called .next() on us
chord.yield;
// Get the next transformation (symbol) from the stream
tr = stream.next().debug("trans");
// Apply the transformation
chord = riemann[tr].(chord);
// Stay within the 0-11 range
chord = chord%12;
// After applying `%12` the array might be out of order
chord.sort;
};
};
};
Following the orbit
Let's put everything together and generate a chord progression, starting at D-Major and alternating between the transformations R and L:
(
Pbind(
\scale, Scale.chromatic,
\degree, ~neoOrbit.value(chord: [0, 4, 7]+2, stream: Pseq([\R, \L], inf).asStream),
\dur, 0.5,
).play;
)
If everything worked well, you should hear a chord progression, starting at D-Major and moving through the whole Tonnetz, i.e. orbiting through all 24 possible chords. The output in the Post window should look like this:
chord: D Major
trans: R
chord: b minor
trans: L
chord: G Major
trans: R
chord: e minor
trans: L
chord: C Major
trans: R
chord: a minor
trans: L
chord: F Major
trans: R
chord: d minor
trans: L
chord: A♯ Major
trans: R
chord: g minor
trans: L
chord: D♯ Major
⋮
Have a listen to the first few seconds of the progression:
Pseudo-classical chord progression
The complete example can be downloaded here: pseudo-classical-progression.scd.
Consider the following graph:
Each arrow represents an "allowed" progression from one functional harmony to another in a western major scale. So I, ii, iii, IV, V, vi and vii° represent the triad or chord over the corresponding note in the scale (with lower-case roman numerals for minor chords and a dimished chord for the seventh). From I we are allowed to progress to any other chord, represented by "*".
Let's write a random chord progression in sclang that follows the arrows in this graph!
To approach this task, we can represent the graph in form of a table:
From | To |
---|---|
I | any |
ii | V, vii° |
iii | vi, IV |
IV | I, ii, V, vii° |
V | I, vi, vii° |
vi | ii, iv |
vii° | I |
And in sclang we can represent this table as an array of arrays:
var majorTrans = [
[1, 2, 3, 4, 5, 6, 7]-1, // any
[5, 7]-1, // V, vii°
[4, 6]-1, // IV, vi
[1, 2, 5, 7]-1, // I, ii, V, vii°
[1, 6, 7]-1, // I, vi, vii°
[2, 4]-1, // ii, IV
[1]-1, // I
];
Each position in the array majorTrans
represents the corresponding From
entry in the table above, except that the first position I is represented as
index 0 in the table. The corresponding entry in the array is an array
itself, representing the list of allowed positions to progress to. These are
as in the graph/table (starting with 1 for the first position), so we have to
substract 1 from each element to get valid indices into our own table (again,
because the first index being 0).
If we continue to think abstractly only in "positions", we will need functions to build major, minor and dimished chords based on the position in the scale:
var major3 = { arg root = 0; root + [0, 4, 7] };
var minor3 = { arg root = 0; root + [0, 3, 7] };
var dimis3 = { arg root = 0; root + [0, 3, 6] };
var triads = [major3, minor3, minor3, major3, major3, minor3, dimis3];
Note that triads
is an array of functions: At each position of a major
scale we store the corresponding function to build the corresponding triad.
Now we have everything to generate a routine that randomly walks through the graph:
~classicOrbit = {
arg center = 0, // The tonal center in semitones from c
position = 0; // The current position in the scale
var semitone = center + Scale.major.degrees[position]; // The current root in semitones from c
var major3 = { arg root = 0; root + [0, 4, 7] };
var minor3 = { arg root = 0; root + [0, 3, 7] };
var dimis3 = { arg root = 0; root + [0, 3, 6] };
var triads = [major3, minor3, minor3, major3, major3, minor3, dimis3];
var majorTrans = [
[1, 2, 3, 4, 5, 6, 7]-1, // any
[5, 7]-1, // V, vii°
[4, 6]-1, // IV, vi
[1, 2, 5, 7]-1, // I, ii, V, vii°
[1, 6, 7]-1, // I, vi, vii°
[2, 4]-1, // ii, IV
[1]-1, // I
];
Routine {
// For the given tonal center and position, yield the
// corresponding triad, in degrees of semitones.
triads[position].value(semitone).debug("start").yield;
// For the current position, choose from the array of allowed
// progressions.
while {position = majorTrans[position].choose; position != nil}
{
// Calculate actual degree in semitones for the given
// tonal center and current position.
semitone = center + Scale.major.degrees[position];
// Yield the correspoding triad
triads[position].value(semitone).debug("next "++(position +1)).yield;
}
}
}
Let's play the progression:
(
Pbind(
\scale, Scale.chromatic,
\degree, ~classicOrbit.value(center: -12),
\dur, 0.5,
).play;
)
Adding modulation
We can spice it up by adding modulation by, say, 75% of the times whenever we reach the IV or V position, which then become the new tonal center and progression continues:
if(
// the predicate:
((position == 3) || (position == 4)) && (1.0.rand < 0.75),
// holds, do:
{
(position+1).debug("modulate to");
center = (semitone % 12).debug("new center");
position = 0;
},
// else, do nothing:
{}
);
Incorporating this into ~classicOrbit
and making the rate of modulation an
argument modulationRate
, as well as adding an argument transpose
for
transposing all chords we end up with:
~classicOrbit = {
arg center = 0, // The tonal center in semitones from c
position = 0, // The current position in the scale
modulationRate = 0, // Rate of modulation
transpose = 0; // Transpose all chords by this
var semitone = center + Scale.major.degrees[position]; // The current root in semitones from c
var major3 = { arg root = 0; root + [0, 4, 7] };
var minor3 = { arg root = 0; root + [0, 3, 7] };
var dimis3 = { arg root = 0; root + [0, 3, 6] };
var triads = [major3, minor3, minor3, major3, major3, minor3, dimis3];
var majorTrans = [
[1, 2, 3, 4, 5, 6, 7]-1, // any
[5, 7]-1, // V, vii°
[4, 6]-1, // IV, vi
[1, 2, 5, 7]-1, // I, ii, V, vii°
[1, 6, 7]-1, // I, vi, vii°
[2, 4]-1, // ii, IV
[1]-1, // I
];
postln("starting classicOrbit with tonal center "++ center ++
" at position "++ (position +1) ++
", transposed by "++ transpose ++
" and modulation rate "++ rate);
Routine {
// For the given tonal center and position, yield the
// corresponding triad, in degrees of semitones.
(triads[position].value(semitone) + transpose).debug("start").yield;
// For the current position, choose from the array of allowed
// progressions.
while {position = majorTrans[position].choose; position != nil}
{
// Calculate actual degree in semitones for the given
// tonal center and current position.
semitone = center + Scale.major.degrees[position];
// Yield the correspoding triad
(triads[position].value(semitone) + transpose).
debug("next "++(position +1)).
yield;
// Spicing up the progression with sporadic modulation
if(
// the predicate:
((position == 3) || (position == 4)) && (1.0.rand < modulationRate),
// holds, do:
{
(position+1).debug("modulate to");
center = (semitone % 12).debug("new center");
position = 0;
},
// else, do nothing:
{}
);
}
}
}
Let's start the orbit with a rate of modulation of 0.75 and transposing all chords by -12 semitones:
(
Pbind(
\scale, Scale.chromatic,
\degree, ~classicOrbit.value(transpose: -12, modulationRate: 0.75),
\dur, 0.5,
).play;
)
In the post-window we see an output like this:
starting classicOrbit with tonal center 0 at position 1, transposed by -12 and modulation rate 0.75
start: [ -12, -8, -5 ]
next 6: [ -3, 0, 4 ]
next 2: [ -10, -7, -3 ]
next 5: [ -5, -1, 2 ]
modulate to: 5
new center: 7
next 1: [ -5, -1, 2 ]
next 5: [ 2, 6, 9 ]
next 6: [ 4, 7, 11 ]
next 2: [ -3, 0, 4 ]
next 5: [ 2, 6, 9 ]
modulate to: 5
new center: 2
next 6: [ -1, 2, 6 ]
next 4: [ -5, -1, 2 ]
modulate to: 4
new center: 7
next 7: [ 6, 9, 12 ]
next 1: [ -5, -1, 2 ]
next 5: [ 2, 6, 9 ]
modulate to: 5
new center: 2
next 1: [ -10, -6, -3 ]
next 4: [ -5, -1, 2 ]
next 2: [ -8, -5, -1 ]
next 5: [ -3, 1, 4 ]
next 1: [ -10, -6, -3 ]
next 5: [ -3, 1, 4 ]
...
This is how it sounds:
Better voice leading
All the chords in the progression so far are in first position. For better
voice leading, we can incorporate our functions dist
and findBestInversion
from the Voice Leading example. The final version of
~classicOrbit
than looks like this:
(
~classicOrbit = {
arg center = 0, // The tonal center in semitones from c
position = 0, // The current position in the scale
modulationRate = 0.0, // Rate of modulation
transpose = 0; // Transpose all chords by this
var semitone = center + Scale.major.degrees[position]; // The current root in semitones from c
/*
Allowed transitions in a major scale:
From | To
-----+-----------
I | anywhere
II | V, VII°⁶
III | VI, IV
IV | II, V, VII°⁶, I
V | I, VII°⁶, VI
VI | II, IV
VII°⁶| I
*/
var majorTrans = [
[2, 3, 4, 5, 6, 7]-1, // any
[5, 7]-1, // V, vii°
[4, 6]-1, // IV, vi
[1, 2, 5, 7]-1, // I, ii, V, vii°
[1, 6, 7]-1, // I, vi, vii°
[2, 4]-1, // ii, IV
[1]-1, // I
];
var major3 = { arg root = 0; root + [0, 4, 7] };
var minor3 = { arg root = 0; root + [0, 3, 7] };
var dimis3 = { arg root = 0; root + [0, 3, 6] };
// triads is an array containing functions, namely the corresponding
var triads = [major3, minor3, minor3, major3, major3, minor3, dimis3];
// dist and findBestInversion are helper functions to find the
// inversions with the best voice leading. See example "Voice Leading".
var dist = {arg tr1, tr2; (tr2-tr1).sum({|i| i**2}).sqrt(); };
var findBestInversion = {
arg tr1, tr2;
var inversions = [
tr2,
(tr2 + [0 , -12, -12]).sort,
(tr2 + [0 , 0, -12]).sort,
tr2 - 12,
];
var distances = inversions.collect({|chord| dist.(tr1, chord)});
var minIdx = distances.minIndex;
inversions[minIdx];
};
postln("starting classicOrbit with tonal center "++ center ++
" at position "++ (position +1) ++
", transposed by "++ transpose ++
" and modulation rate "++ modulationRate);
Routine {
// For the given tonal center and position, yield the
// corresponding triad, in degrees of semitones.
var chord = ((triads[position].value(semitone) + transpose)%12).sort;
var prev = chord; // Remember _this_ chord for later
chord.debug("start");
chord.yield;
// For the current position, choose from the array of allowed
// progressions.
while {position = majorTrans[position].choose; position != nil}
{
// Calculate actual degree in semitones for the given
// tonal center and current position.
semitone = center + Scale.major.degrees[position];
chord = (triads[position].value(semitone) + transpose);
// For better voice leading, find the best inversion
// for the chord, based on the previous chord in the progression.
chord = findBestInversion.value(prev, chord);
// Now, yield the correspoding triad
chord.debug("chord at next position "++(position +1));
chord.yield;
prev = chord; // Remember this chord for the next round.
// Spicing up the progression with sporadic modulation
if(// the predicate:
((position == 3) || (position == 4)) && (1.0.rand < modulationRate),
// holds, do:
{
(position+1).debug("modulate to");
center = (semitone % 12).debug("new center");
position = 0; // We are now back to root position
},
// else do nothing:
{}
);
}
}
}
)
Let's put the orbit into a Pbind:
(
x = Pbind(
\scale, Scale.chromatic,
\degree, ~classicOrbit.value(center: -6, modulationRate: 0.75),
\dur, 0.5,
).play;
)
This time, it sounds like this:
Example: Switching Orbits
Now that we have two generators of harmonic progressions, neo-riemannian and pseudo-classical, let's build a generator of harmonic progressions that switches between the two.
A simple solution - pausing and jumping
You find this solution in pausingOrbitJumper.scd.
In a first attempt, we could simply launch each of the other orbits and draw
chords from one for a while and then switch to the other. Here is a function
that internally launches the neo-riemannian and pseudo-classical orbits and
returns a Routine that switches between the two, continuing where it had
stopped in that orbit before. The argument switchEvery
controls after how
many chords of the same orbit the switch to the other orbit should occur.
(
~pausingOrbitJumper = {
arg switchEvery = 16;
var orbits = [~classicOrbit.value(), ~neoOrbit.value()];
var idx = 0; // idx maintains the current index into the array orbits.
var curTriad = orbits[idx].next();
var counter = 1;
Routine {
curTriad.debug("starting pausingOrbitJumper with");
while {curTriad != nil} {
curTriad.yield;
counter = counter + 1;
if (counter == switchEvery) {
idx = (idx + 1) % 2;
if (idx == 0)
{ postln("pausing neoOrbit, switching back to classicOrbit") }
{ postln("pausing classicOrbit, switching back to neoOrbit") };
counter = 1;
};
curTriad = orbits[idx].next;
}
}
};
)
Note that we assume that the environment symbols
~classicOrbit
and~neoOrbit
refer to the functions that we implemented in the previous examples Neo-Riemannian Orbit and Pseudo-Classical Orbit. They do not need to be part of the same code here, but must have been evaluated once during this session (and prior to the code below), even if in another tab.
Let's give it a try:
(
Pbind(
\scale, Scale.chromatic,
\degree, ~pausingOrbitJumper.(switchEvery: 8),
\dur, 0.5,
).play;
)
We get an output in the post-window like this:
starting classicOrbit with tonal center -6 at position 1, transposed by 0 and modulation rate 0.5
start: [ 0, 4, 7 ]
starting pausingOrbitJumper with: [ 0, 4, 7 ]
chord at next position 5: [ -1, 2, 7 ]
chord at next position 1: [ 0, 4, 7 ]
chord at next position 4: [ 0, 5, 9 ]
chord at next position 5: [ 2, 7, 11 ]
chord at next position 1: [ 0, 4, 7 ]
chord at next position 6: [ 0, 4, 9 ]
pausing classicOrbit, switching back to neoOrbit
chord: C-Major
trans: R
chord: a-minor
trans: L
chord: F-Major
trans: R
chord: d-minor
trans: L
chord: A♯-Major
trans: R
chord: g-minor
trans: L
chord: D♯-Major
pausing neoOrbit, switching back to classicOrbit
chord at next position 2: [ 2, 5, 9 ]
…
Another solution - changing styles
The first solution above has the property that it ignores the current chord when switching between orbits. But what if we want to maintain the current chord and only switch the type of progression, continuing from that chord?
Given our current implementations of ~neoOrbit
and ~classicalOrbit
we know
that we can not change the style of progression from an arbitrary chord. For
example, there are no dimished chords in the Neu-Riemannian Tonnetz. Also, our
current implementation of ~classicalOrbit
can only transition within a major
scale.
But we can deal with these restrictions and simply detect if a given chord during progression is a major chord and only allow those to be the pivot chords for the next style. We have implemented such a function before in our pseudo-classical orbit example:
var isMajor = { |triad|
var f = triad[1]-triad[0]; // first interval
var s = triad[2]-triad[1]; // second interval
case
{(f==4 && s==3) || (f==3 && s==5) || (f==5 && s==4)} {true}
{(f==3 && s==4) || (f==5 && s==3) || (f==4 && s==5)} {false}
{true}{false}
};
We can now adjust our first solution by changing the orbit on the first major
chord after switchEvery
many chords have been played:
…
Routine {
…
while {curTriad != nil} {
…
if ((counter >= switchEvery) && isMajor.(curTriad)) {
…
To pick up from the same chord in the new type of progression, we will simply start a new orbit of the new type and pass it the current chord as starting point. For the neo-riemannian orbit this is simply the major chord at hand. For the pseudo-classical orbit we will consider the current chord as the first position in the new tonal center. Our if-statement will now look like this:
if ((counter >= switchEvery) && isMajor.(curTriad)) {
idx = (idx + 1) % 2;
// stop the current orbit
orbits[idx].stop;
if (idx == 0)
{ postln("stopping neoOrbit, starting new classicOrbit at "+curTriad);
orbits[idx] = ~classicOrbit.(center: curTriad[0], position: 0);
curTriad.next;
}
{ postln("stopping classicOrbit, starting new neoOrbit at "+curTriad);
orbits[idx] = ~neoOrbit.(chord: curTriad);
curTriad.next;
};
counter = 1;
};
In total, we have now this solution:
(
~orbitStyleChanger = {
arg switchEvery = 16;
var orbits = [~classicOrbit.(), ~neoOrbit.()];
var idx = 0;
var counter = 1;
var curTriad = orbits[idx].next();
var isMajor = { |triad|
var f = triad[1]-triad[0]; // first interval
var s = triad[2]-triad[1]; // second interval
case
{(f==4 && s==3) || (f==3 && s==5) || (f==5 && s==4)} {true}
{(f==3 && s==4) || (f==5 && s==3) || (f==4 && s==5)} {false}
{true}{false}
};
Routine {
while {curTriad != nil} {
curTriad.yield;
counter = counter + 1;
if ((counter >= switchEvery) && isMajor.(curTriad)) {
idx = (idx + 1) % 2;
orbits[idx].stop;
if (idx == 0)
{ postln("stopping neoOrbit, starting new classicOrbit at "+curTriad);
orbits[idx] = ~classicOrbit.(center: curTriad[0], position: 0);
}
{ postln("stopping classicOrbit, starting new neoOrbit at "+curTriad);
orbits[idx] = ~neoOrbit.(chord: curTriad);
};
counter = 1;
};
curTriad = orbits[idx].next;
}
}
};
)
Again, we assume that the environment symbols
~classicOrbit
and~neoOrbit
refer to the functions that we implemented in the previous examples Neo-Riemannian Orbit and Pseudo-Classical Orbit and had been been evaluated once prior to this code.
The final version - adding inversions and more choices
You find this solution in orbitStyleChanger.scd.
Let's round up our solution by
- better voice leading by incorporating our functions from the voice leading example,
- adding an argument
modulationRate
that is being used whenever we start a pseudo-classical orbit - adding an argument
neoStream
which defines a stream of symbols for the transformations R, L, P, N, U, H and S from the neo-riemannian example.
The final solution looks like this:
(
~orbitStyleChanger = {
arg switchEvery = 16,
modulationRate = 0.50,
neoStream = Prand([\P, \R, \L], inf).asStream;
var orbits = [~classicOrbit.(modulationRate: modulationRate),
~neoOrbit.(stream: neoStream) ];
var idx = 0;
var n = 1;
var curTriad = orbits[idx].next();
var prev = curTriad;
var isMajor = { |triad|
var f = triad[1]-triad[0]; // first interval
var s = triad[2]-triad[1]; // second interval
case
{(f==4 && s==3) || (f==3 && s==5) || (f==5 && s==4)} {true}
{(f==3 && s==4) || (f==5 && s==3) || (f==4 && s==5)} {false}
{true}{false}
};
// getRoot will find the root of a major or minor triad which might be in
// any inversion.
var getRoot = { |triad|
var a = triad[0];
var b = triad[1];
var c = triad[2];
case
{c-a == 7}{ a }
{b-a == 5}{ b }
{c-b == 5}{ c }
{true}{triad.debug("[getRoot] unknown root"); a}
};
// dist and findBestInversion are helper functions find good voice leading.
var dist = {arg tr1, tr2; (tr2-tr1).sum({|i| i**2}).sqrt(); };
var findBestInversion = {
arg tr1, tr2;
var inversions = [
tr2,
(tr2 + [0 , -12, -12]).sort,
(tr2 + [0 , 0, -12]).sort,
tr2 - 12,
];
var distances = inversions.collect({|chord| dist.(tr1, chord)});
var minIdx = distances.minIndex;
inversions[minIdx];
};
Routine {
postln("starting orbitStyleChanger with triad "++curTriad++
", switching orbits on the first major chord "++
"after "++switchEvery++" chords ");
while {curTriad != nil} {
curTriad = findBestInversion.(prev, curTriad);
curTriad.yield;
prev = curTriad;
n = n + 1;
if ((n >= switchEvery) && isMajor.(curTriad)) {
idx = (idx + 1) % 2;
orbits[idx].stop;
if (idx == 0)
{ postln("stopping neoOrbit, starting new classicOrbit at "+curTriad);
orbits[idx] = ~classicOrbit.(center: getRoot.(curTriad), position: 0, modulationRate: modulationRate);
}
{ postln("stopping classicOrbit, starting new neoOrbit at "+curTriad);
orbits[idx] = ~neoOrbit.(chord: curTriad, stream: neoStream);
};
n = 1;
// The new orbits start with the curTriad, which we already
// yielded, so skip to the next
orbits[idx].next;
};
curTriad = orbits[idx].next;
}
}
};
)
And, remember, we assume that the environment symbols
~classicOrbit
and~neoOrbit
refer to the functions that we implemented in the previous examples Neo-Riemannian Orbit and Pseudo-Classical Orbit and had been been evaluated once prior to this code.
Let's run it this time:
(
Pbind(\scale, Scale.chromatic,
\degree, ~orbitStyleChanger.(switchEvery: 8, neoStream: Pseq([\R, \L], inf).asStream),
\dur, 0.5,
).play;
)
This is the output in the post-window:
starting classicOrbit with tonal center 0 at position 1, transposed by 0 and modulation rate 0.5
start: [ 0, 4, 7 ]
Preparing recording on 'localhost'
starting orbitStyleChanger with triad [ 0, 4, 7 ], switching orbits on the first major chord after 8 chords
chord at next position 7: [ -1, 2, 5 ]
chord at next position 1: [ 0, 4, 7 ]
chord at next position 6: [ 0, 4, 9 ]
chord at next position 4: [ 0, 5, 9 ]
chord at next position 2: [ 2, 5, 9 ]
chord at next position 5: [ 2, 7, 11 ]
stopping classicOrbit, starting new neoOrbit at [ 2, 7, 11 ]
chord: G Major
trans: R
chord: e Minor
trans: L
chord: C Major
trans: R
chord: a Minor
trans: L
chord: F Major
trans: R
chord: d Minor
trans: L
chord: A♯ Major
trans: R
chord: g Minor
trans: L
chord: D♯ Major
stopping neoOrbit, starting new classicOrbit at [ 3, 7, 10 ]
starting classicOrbit with tonal center 3 at position 1, transposed by 0 and modulation rate 0.5
start: [ 3, 7, 10 ]
chord at next position 3: [ 2, 7, 10 ]
chord at next position 6: [ 3, 7, 12 ]
chord at next position 4: [ 3, 8, 12 ]
modulate to: 4
new center: 8
chord at next position 6: [ 5, 8, 12 ]
chord at next position 2: [ 5, 10, 13 ]
chord at next position 7: [ 7, 10, 13 ]
chord at next position 1: [ 8, 12, 15 ]
stopping classicOrbit, starting new neoOrbit at [ 8, 12, 15 ]
chord: G♯ Major
trans: R
chord: f Minor
…
And this is how it sounds:
Useful Keyboard Shortcuts for scide
Note: In the following table, we write
cmd
and mean⌘
on Mac andctrl
on Windows and Linux.
Shortcut | Action |
---|---|
cmd+b | start server |
cmd+. | stop all sound |
cmd+enter | execute/interpret selection or current line |
cmd+shift+p | clear post window |
cmd+d | open help file of word at cursor position |
The complete list of keyboard shortcuts can be found in the supercollider documentation, also available online.
Glossary
- IDE
- Integrated Development Environment, in the case of supercollider the program scide. It combines an editor, the execution engine, the debugging output and the help system into one user interface.
- Server (program)
- A server
program is a program that runs standalone, usually without an user
interface, and provides services for client programs via a communication
channel.
In supercollider, the program scsynth is a server: it runs standalone and listens for commands comming from the IDE or other programs via network.
- Syntax
- In programming, the
syntax
of a programming language is the set of rules that specifies the allowed
combinations of symbols to create correctly structured expressions and
statements. It does not, however, say anything about the meaning (or
semantics) of those statements.
To compare it with the "language" of mathematical formulas:
- =2 %3 is syntactically incorrect: it simply doesn't makes sense.
- 0 = 1 is syntactically correct, but semantically wrong.
- 5=4+1 is syntactically and semantically correct.
- UGen (or Unit Generator)
-
A unit generator in SuperCollider is an object that processes or generates
sound. There are many types of unit generators, like, for various forms of
noise, periodic signals, but also filtering etc.
For more information about UGens see http://doc.sccode.org/Guides/UGens-and-Synths.html