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

1

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:

supercollider

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.

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 frontend
  • sclang ─ the language interpreter
  • scsynth ─ 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.

client f.e. scide scsynth commands soundcard

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...

1

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.

Tonnetz (image source: wikipedia)

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:

Pseudo-classical chord progression

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:

FromTo
Iany
iiV, vii°
iiivi, IV
IVI, ii, V, vii°
VI, vi, vii°
viii, 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 and ctrl on Windows and Linux.

ShortcutAction
cmd+bstart server
cmd+.stop all sound
cmd+enterexecute/interpret selection or current line
cmd+shift+pclear post window
cmd+dopen 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