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: