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: