Blog

Using Harry Partch’s One-Footed Bride on Bach’s Wedding Chorales

I’ve been trying new tuning algorithms to find the optimum low number ratios to tune Bach chorales. James Kukula on the Facebook group Microtonal Music and Theory suggested I explore Simulated Annealing to find the optimum tuning. I asked my friendly local LMM running granite-code:20B for some advice, and it helpfully coded up the algorithm for me.

The basic idea is that you start with a cent value for each of the four notes in each chord, then change it until you reach a chord that scores well on an analysis function. The key trick of Simulated Annealing is that you start by probabilistically accepting non-optimum values for an interval instead of the very lowest number ratio. As you repeat the process, you gradually lower the temperature controlling how likely you are to reject the optimum. Eventually, after enough repetitions, you are only accepting the optimum choices. This avoids the situation where the first choice you make causes the options for subsequent choices to be bad. You want to avoid what are called local minima, suboptimal arrangements that you arrive at by blindly only accepting the optimum at the start. Sometimes if you start out sub-optimally for the first few intervals, the sum of all the ratios in an interval is lower.

In my case, I start with a tonality diamond to a variable limit. I’ve found through repeated grid searches that either the 31-limit or 47-limit produce the best scores. I split the 4-note chord into 6 separate intervals, then run a function that returns all the valid intervals that could be used for each of the six intervals. Valid means:

It is an interval in the tonality diamond, made up of all the intervals with numerators and denominators from 2 through the specified limit. If the limit is 5, for example, the diamond includes the ratios 3:2, 4:3, 5:4. As we increase the limit to 31, you are offered ratios like 31:30, 25:37, and other decidedly not low number ratios. I tried some low limits, like 15, and the results were poor.

Ensure that none of the selected intervals will change the underlying midi note value, the 12-tone equal temperament value of each note in the chord. If the ratio is too large, such that adding a cent value to the interval would round to a D sharp instead of a D natural, it is excluded from the returned value. I generally find 10-15 valid ratios to choose from.

I return a list of indices into the tonality diamond structure, each of which includes the ratio value in floating point, the cent value of the interval, and sum of the numerator and denominator of the ratio.

In this way the algorithm can tune a chord to cent values that represent low number ratios.

I then have the option of choosing from the list of valid ratios the one that has the lowest number ratio. Or not. Simulated Annealing starts out with a high temperature, then with each iteration, it lowers the temperature by a ratio. In my case, I did many grid searches and determined that starting with a temperature of 64.0, then multiplying it by a cooling rate of 0.998 with each pass, results in the lowest number ratios. I started out with temperatures of tens of thousands, and cooling rates closer to 1.0, but found that they did not improve the results.

I achieved better results by systematically rearranging the order of the notes in the chord, then ran the simulated annealing multiple times. This lowered the scores significantly. I use a python library called numpy, which includes a function called roll, which takes a 4-note chord and rearranges it. If I start with C E G A#, a roll of one returns E G A# C, two returns G A# C E, and so forth. I found a rolls of 0,1,2,3,4,5 for every chord produces the lowest scores. Even the simulated annealing gets stuck at local minima and simply running the same chord through the algorithm more than once, it picks a better arrangement.

I score each chord by simply adding the sum of the numerator and denominator for each of the six intervals. This produces the score for the chord. I’ve been using this for several months now, running several different Bach chorales through the algorithm. I start with a set of midi values for each note in each chord, and convert it to a number from 0-11, plus a separate value for the octave. Then I run it through the annealing process which produces a 4-note chord in cents. What I noticed recently is that this is precisely what Harry Partch was getting at in his famous (to me) “One Footed Bride” chart.

Partch used this chart in his great book “Genesis of a Music”, page 135. He identifies ratios by several adjectives: Power, Emotion, Approach, and Suspense. But the important value is how far the ratio sticks out: low number rations like 3:2 and 4:3 stick way out and indicate strength; higher number ratios like 5:4 and 8:5 stick out less; still higher number ratios like 7:6 and 8:7 stick out even less. In other words, there is greater strength the lower number the ratio reduces to. That’s just what my scoring algorithm is doing: tuning each chord to the lowest number ratio possible for each chord. In honor of the Partch One-footed bride, I tuned Bach’s Wedding Chorales BWV154-164. Audio files are at the bottom of this post.

I started out in my algorithm search achieving average scores for the six intervals in a 4-note chord of around 60. This implies that on average, each chord used six intervals where the sum of the numerator and denominator for the interval was around 10. Imagine a chord consisting of [‘G♮’, ‘D♮’, ‘B♮’, ‘G♮’], which in midi is represented by [7, 2, 11, 7], once you remove the octave. If I choose the ratios [ 700 202 1086 700] for each of the four notes, I end up with the six intervals tuned to the following ratios. The first two numbers are the relative notes in the chord, 0 being the first, 1 the second, 2 the third and so forth. Python always starts with zero instead of 1. The first interval (0,1) is 4/3 with sums to 7. The second (0,2) is 5/4 which sums to 9. the third (0,3) is the same note in both cases, G♮’, the fourth (1,2) is 5/4 sums to 8. Do that for all six and you end up with a score of 40. (7+9+0+8+7+9=40). The score for this chord is therefore 40. Pretty good. Tuned to just, with the lowest number ratios:

[(0, 1, ‘ 498’, ‘ 4/3 ‘), (0, 2, ‘ 386’, ‘ 5/4 ‘), (0, 3, ‘ 0’, ‘ 1 ‘), (1, 2, ‘ 884’, ‘ 5/3 ‘), (1, 3, ‘ 498’, ‘ 4/3 ‘), (2, 3, ‘ 386’, ‘ 5/4 ‘)]

Do this for all the chords in a chorale, sum the numerators and denominators of all the six intervals in each chord and evaluate it based on the lowest possible score. When I started using the simulated annealing, with all 24 permutations of each chord, I began getting average scores around 55. Grid searches for the optimum hyperparameters of the algorithm reduced the averages to below 50 for most chorales. But the problem now is that it would take an hour or more to calculate the optimum.

I implemented several caching schemes, to speed up the interval ratio lookups, and scoring. I also use the numpy function called unique, which examines all the chords and eliminates any that are duplicates, returning just the unique ones, and a list of those removed so they can be restored. This eliminated 40% of the chords that had to be tuned.

By default, python only runs on a single CPU regardless of how many I have in my servers. I solved that by asking a new granite model, granite3.1-moe:3B, to suggest ways to parallelize the python. It came up with a brilliant solution which allowed me to exploit multiple cores in my servers.

I quickly discovered that most of my servers advertised multiple threads on Intel and AMD chips. But just because each core provided two virtual cores, it turned out that during CPU intensive processing only one was active at a time. My 12-core server that offered 24 threads took just as long when I parallelized it 12 ways as 24 ways.

The latest Intel chips have abandoned multi-threading, because the majority of workloads get very little benefit from it. I’m in the process of getting one built now, with 24 real cores. I was able to test it and found that it performed well at 3.7 gHz, tuning 10 chorales in 15 minutes across all 20 cores. I had to switch off turbo to 5.7 gHz because of heat problems. I’m working on solving that now.

The final result is a set of tunings for chorales that have very low number ratios. That means that I have lots of chords with tunings that are not in normal 12-tone equal temperament. That includes some with ratios like the third beat of measure 7 of bwv263, chord number 104, with a score of 107. This high score caused by tuning the notes [‘D♮’, ‘G♯’, ‘F♮’, ‘B♮’]. Here is measure 6 and 7:

104: [2, 8, 5, 11] [‘D♮’, ‘G♯’, ‘F♮’, ‘B♮’] [ 200, 782, 466, 1084] 107

104: [(0, 1, ‘ 582’, ‘ 7/5 ‘), (0, 2, ‘ 266’, ‘ 7/6 ‘), (0, 3, ‘ 884’, ‘ 5/3 ‘), (1, 2, ‘ 316’, ‘ 6/5 ‘), (1, 3, ‘ 302′, ’25/21’), (2, 3, ‘ 618’, ‘ 10/7’)]

It includes several relatively high number ratios: 7:5, 7:6, 25:21, and 10:7. But it’s just a passing tone that quickly resolves away from that diminished chord. Bach loves the tension that diminished chords produce. Think stacked minor thirds.

One of the problems with this method of tuning chords individually is that it picks the best cent value for each chord regardless of the surrounding chords. Notes can change cent values from one chord to the next, even though they are the same midi value. For example, in the two measure excerpt above, measures six and seven of bwv163, the first D eighth note in the soprano part of measure 6 has been chosen to have a cent value of 202, while the one that follows is at 200 cents. To resolve that change, I slide from 202 down to 200 over the duration of those 9 eighth notes in the chords. This is too small to notice. But the slides in measure three are more prominent, where I have to move a G♮ in the alto part from 700 cents all the way up to 749 cents. The measure starts with a G major chord, then quickly gets weird. But over the five separate chords from the beginning of the measure to the halfway point, the alto voice has to cover half a semi-tone. It’s subtle. I’ve slowed the performance down to make the movements less obvious.

Over the course of this chorale there are 183 separate slides, but with all the repetitions, they are all using these 9 glissandi:

glide# decimal cents ratio

1500 0.9988 -2 1

1501 1.0012 2 1

1502 1.0116 20 51/50

1503 0.9977 -4 1

1504 1.0287 49 36/35

1505 0.9857 -25 49/50

1506 0.9885 -20 49/50

1507 0.9834 -29 49/50

1508 0.98 -35 49/50

Csound has no trouble with slides.

To summarize, the key technologies brought to bear on this musical challenge were:

Code up a way to determine the valid intervals that could be used for each interval in the chord

Find the optimum tonality diamond limit value to reduce the average score: 31 and 43

Code up a scoring algorithm to evaluate chords for their use of low number ratios

Cache the results of calls to determine the valid intervals and chord scores. I achieved 98% cache hits on the ratio selection, and 88% on the scoring. This reduced the workload to accomplish the tuning.

Find a way to slide from one cent value of a note to another for notes that had the same midi value but different cent values. This took some special Csound coding.

Build a simulated annealing algorithm that could find the optimal chord tuning in cents based on low number ratios

Find a way to parallelize the algorithm so each core in the CPU could work on a different chord.

Determine the optimum rearrangement scheme to send chords with different arrangements through the simulated annealing function.

Compress the chorale so each unique chord would only have to be tuned once, no matter how many times it appeared in the chorale.

Send the chord through a mix of sampled brass and woodwind instruments from the McGill University Master Samples using Csound.

Here is BWV263 from the Wedding Chorales by J.S.Bach:

BWV257:

And BWV258:

Re-tuning and Orchestration of J.S.Bach Chorales from the Passion of St. John

These pieces can be described as a re-tuning and orchestrating the chorales from the St. John Passion into fantasias. All the orchestrations are done by aleatoric algorithms I write in python. I create several variations on each chorale, then listen to them and make modifications until I find one of each that sounds interesting. The process uses subtractive composition, which is where I create a vast array of notes, then judiciously remove over 99% of them, leaving varying degrees of density and sparseness.

The retuning is also done by algorithm. It seeks to chose pitches in each chord that minimize the numerator and denominator of the ratios between the notes. This treats all ratios with low numbers as preferable to those with higher numbers. Just for comparison purposes, each interval in the 12 tone equal temperament scale has the ratio to the next semi-tone in the scale of 2385698798484657/2251799813685248, although it is sometimes approximated as 196/185. Lower numbers make for a stronger sound. The highest ratios in these pieces is often 8/7 or 10/9, with an occasional 27/20, or 25/18 when the algorithm couldn’t find any alternative that preserved the sense of the note as the composer intended. For this I would have had to read the mind of J.S. Bach. Or give it my best guess. I chose the latter.

Here is a zip file of my current favorites:

Zip file of Fantasias on Bach Chorales from the St. John Passion for Large Ensembles

Here are the first two you can listen to here:

bwv245.15 :

# this one has some nice transitions between the instruments of the large ensemble of strings, woodwinds, brass, percussion, and electric guitars.
bwv245.3 :
# this one starts off with a nasty bee buzzing on Ernie Ball Super Slinky Guitar Strings.

All the code is available here: https://github.com/prentrodgers/Auto_Just_Intonation
I can’t post the samples, since I don’t own the rights to them all.

Adaptive Tuning of Herzliebster #27

I made this version after I’ve updated the algorithm to prevent moving the B♮ and F♯ from 1100 and 602 cents respectively. This avoids some of the strange jumps between chords. The solution is a bit crude, but I think I can improve on it. I may need to pay attention to some of the other notes, even though those two are the most common ones in the chorale. Next up is the D♮ at 216 cents.

I added some code to list out the cents and ratios of each chord as it passes by. Here are the first few chords in the chorale, and then two from later. The first four numbers are the cent values of the chords. 1100 cents is a B♮, 602 is an F♯, 216 is a D♮, and 1100 is another B♮. The next set are the intervals in the chord stated in ratios and cents. The algorithm favors low integer numbered ratios, and is willing to go pretty far away from 12 TET to find them. For example the second to the last one includes an 8/7, which is far from 12 TET.

([1100, 602, 216, 1100]) [('1/1', 0), ('4/3', 498), ('5/4', 386), ('5/3', 884)]
([1100, 714, 398, 398]) [('3/2', 702), ('5/4', 386), ('6/5', 316), ('1/1', 0)]
([1100, 714, 398, 602]) [('4/3', 498), ('5/4', 386), ('6/5', 316), ('9/8', 204)]
([1100, 216, 1100, 714]) [('5/4', 386), ('5/3', 884), ('5/3', 884), ('5/4', 386)]
...
([699, 930, 348, 117]) [('7/5', 582), ('8/7', 231), ('7/5', 582), ('8/7', 231)]
([602, 918, 420, 216]) [('5/4', 386), ('6/5', 316), ('4/3', 498), ('9/8', 204)]

The goal of this exercise was to create some code that could automatically turn any 12 TET chorale, real or synthetic, into a just intonation chorale, all by itself. The program reads in the MIDI file and searches for a justly tuned chord that will minimize the size of the ratios in the intervals between notes in a chord, while staying reasonably close to the 12 TET note. It tries to minimize those two values, distance and ratio numerator / denominator size, to come up with cent values for the intervals in each chord. I then check to make sure I haven’t moved either of the critical notes, B♮ and F♯.

A lot of the assumptions I made about adaptive tuning were terribly naive. It’s hard. I thought all I had to do was find a chord that contained only low integer ratios by taking each interval of the chord individually. But that ended up with sub-optimal chords, since each interval competed for what the cent value of a given note should be. I ended up re-voicing those that were the most difficult, running some chords through the algorithm to tune the notes in the chord, then re-voicing them back to the way they were. The numpy function roll() provided a means to transform the chords until they found a good tuning. It takes about three and a half minutes to do a 177-chord chorale. I think I can improve that if I remove some of the debugging code.

Vertical Adaptive Tuning of Herzliebster #21

This is another in the series of Vertically Adaptive Tuning of Herzliebster, with some fixes made to the tuning optimization to search harder for just tunings that meet the criteria I set down. The last one, #17, included some wolf chords. These were caused by conflicts between different intervals in a chord. I check all six intervals between the four notes of each chord. I’d start with note 0 to note 1, then 0 to 2, 0-3, then 1-2, 1-3, finally 2-3. Doing the checks in that order guaranteed that notes 2 and 3 were subject to change by a later comparison interval. That wasn’t going to work, so I made some changes to ensure that a change would only take place if it was for the improved the score of the entire chord, not just one interval at the expense of another.

I’ve had much better results if I try different voicing of chords. For example, I was struggling to find code that would produce good results for the MIDI F# major chord: [66, 64, 61, 46], which is note names: [‘F♯’ ‘E♮’ ‘C♯’ ‘A♯’]. It’s just a revoiced F# major scale, but I could not find a set of intervals where all six resulted in consonant chords. I the four notes two at a time, and that means six choices, which sometimes conflict. But when I run four different voicings through, it finds the best ones every time. The Numpy function roll takes a four-note array and moves it over by 0,1,2,3 places, creating four different chords. There must be some bug in my code, because after I do that, it works great.

for inx in np.arange(4):
result = find_intervals(np.roll(chord_in_1200,inx), range = range)
score = score_chord_cents(result)
if score < best_score: best_choice = result best_score = score

Next step is to start the horizonal optimizations.

Vertical Adaptive Tuning of Herzliebster #17

This is another attempt at creating an adaptive tuning that fit’s my preferences as a composer. This version starts by loading a set of acceptable ratios into an array. I chose to load those that I found when studying the Tonality Diamond to the 31 Limit. It’s a set of the 213 ratios that mathematics can come up with using the overtones and undertones of a note. I stick to those ratios, because they include the most consonant of intervals, along with the challenging but interesting ones.

I then wrote some code to transform a Bach chorale known in the Music21 corpus as ‘bwv244.3’, from the St. Matthew Passion. It’s one I’ve used a few times over the years.

Herzliebster Jesu

Using some python code I wrote, it takes each chord in the chorale, and searches for the optimal based on using the lowest possible integer ratios in the tonality diamond that are closest to the 12 tone equal temperament cents of the notes in the chorale. I optimize based on the sum of three values:

  1. lowest integer ratio, expressed as the numerator and denominator of the ratio
  2. closeness to the 12 TET cent value of the note in the chorale as composed by Bach

I add them up and use that to score, with the lowest score winning a slot in the final chord. I do that for all the intervals in each four notes in each chord in the chorale. Given Soprano, Alto, Tenor, Bass, I optimize the intervals from S to A, S to T, S to B, A to T, A to B, and T to B. That’s six compares. The final chord tuning is the result of wining the lowest score for each interval.

At present, I’m not advanced enough in my exploration of adaptive tuning to consider the prior or future notes. I put that in the “horizontally adaptive tuning” category to be dealt with later.

This one sounds pretty neat. It only goes off into crazy land by around 4:30. I think the leading tones throw my code for a loop.

Vertically Adaptive Tuning of Herzliebster #15

I’ve been looking at different adaptive tuning systems, and none do what I want. My preference, which is probably crazy, is a tuning that will find the optimum tuning for a chord, on my terms. In this case, I want the ratios between notes in a chord to use the lowest possible integer ratios. That means I will favor 7/4 of 9/5, even though it’s typical for a just flatted 7th to use a 9/5. I also favor 7/5 over 10/7, even though it might create some awkward moments.

I brought this about with some python code the attempts to find the 72 EDO tuning for each chord that minimizes the size of the ratios between the notes in a chord. The source material is a real Bach chorale used in the St. Matthew Passion, known as Herzliebster. All chorales have four notes. So I wrote code that evaluated a chord by looking at the ratio distance from each note to every other note. That’s six compares: Given soprano, alto, tenor, bass as SATB, then the combinations to evaluate are S to A, S to T, S to B, A to T, A to B, and T to B.

Then I did the same after changing one of the voices by on 72 EDO step, and scored that. I continued that so that I evaluated all six combinations modified by -3 to +3 72 EDO steps, or 50 cents up and 50 cents down, in 16.67 cent steps. The result was a Vertically Adaptive Tuning of Herzliebster. Vertical means I only looked at each chord all by itself. I haven’t written the code that would permit evaluation of one chord to the previous or succeeding step, which is Horizontal Adaptive Tuning. But it’s a start.

There may be some notes that sound strange here:

The wolves of Just minor #9

This version uses a D minor just scale. I transposed a C minor just scale into D and ended up with these ratios.

In numpy for python:

edo_12_ratio_strings = np.array(['1', '25/24', '10/9', '32/27', '5/4', '4/3', '25/18', '40/27', '55/36', '5/3', '16/9', '50/27', '2'], dtype='

Or in scala form:

! d_minor_just
!
Transposition of a c minor just into d
12
!
25/24
10/9
32/27
5/4
4/3
25/18
40/27
55/36
5/3
16/9
50/27
2/1

There are some real wolves in this scale, almost enough to get me to go back to one of the tempered ones I've used lately. Victorian Rational Well Temperament is wonderful in most keys. But it has the nasty effect of having prominent beating in others. This d minor just scale that I used for this piece has the interesting characteristic of really celebrating the wolves. They scream out at the top of their lungs when the hit some of the keys.

This piece is based on a synthetic chorale manufactured by TonicNet. It's number 3,640, one of many in D minor. It has a pitch class entropy score of 3.28, which is fairly high, but not extreme, compared to others. I used music21 to determine the triad chords used in the chorale:


b minor (2)
F# major (2)
b minor
F# major
b minor
d minor (finally!)
g minor
D major
g minor
F major (3)
a minor
C major
d minor (4)
F major (2)
Bb major
F major

So even though it's in d minor, according to music21, it starts in b minor and ends in F major. I don't think this is typical of Bach. But the way TonicNet works is it tries at every moment to choose the next triad that Bach would have chosen at that time-step of the piece. It doesn't look back to consider what it did previously, except in a very limited way. It's kind of guaranteed to sound like it's just wandering around aimlessly imitating Bach without duplicating his technique.

Listen here:

Fantasy on a Highly Entropic Artificial Chorale in Victorian Rational Well Temperament #5

This is early results of exploring the highly entropic chorales created by the TonicNet model.

I built a chorale generating notebook in python that created 4900 examples using the TonicNet model, and then ran those through an evaluation routine (using muspy) to find those that had the highest degree of pitch entropy. There were at least 1000 that included all 12 tones in the tempered scale.

I then chose a few that were in the key of D major. All were strange and wonderful chorales. TonicNet writes them out as MIDI files, with a kind of piano-roll format of four voices and a certain number of notes in each voice, all 1/16th notes. If a note is being played, then a MIDI number appears in the slot for that time-step.

First

You need some logic to turn this piano roll type notation into notes with duration.

I then repeat each note 15 times, turning every 1/16th note into 1/16th less than a whole note. I then apply masks to turn notes off to create arpeggiations. Or for the woodwinds, I just have long held notes.

I transposed the Victorial Rational Well Temperament from the scala scale archive into the key of D. Some of the ratios may seem kind of extreme, but that’s what was required to accurately reflect the ratios in the temperament when transposed. This is the result of that:

! secor_vrwt_D_major.scl
!
George Secor's Victorian rational well-temperament (based on Ellis #2) in D
12
!
4073/3857
3096/2755
654/551
24284/19285
27841/20871
5436/3857
722832/482125
6107/3857
32472/19285
6863/3857
36336/19285
2/1

I then created a finger piano arpeggio vamp with eight voices, and added a double woodwind quartet (oboes, clarinets, french horns, bassoons) playing slow chords. Both voices simply took the notes that the TonicNet model created. I modified some characteristics, including envelopes, volume, timbre, and other factors. The features are changed at the 1/3 and 2/3 points in the piece. The result is a sweet sounding exploration of what the model thought Bach might do.

Exploring Some Highly Entropic Synthetic Chorales Generated by TonicNet

The traditional 12 tone scale can be described in python code as np.array([‘C♮’, ‘D♭’, ‘D♮’, ‘E♭’, ‘E♮’, ‘F♮’, ‘G♭’, ‘G♮’, ‘A♭’, ‘A♮’, ‘B♭’, ‘B♮’]), or the enharmonic equivalent as np.array([‘C♮’, ‘C♮’, ‘D♮’, ‘D♮’, ‘E♮’, ‘F♮’, ‘F♮’, ‘G♮’, ‘G♮’, ‘A♮’, ‘A♮’, ‘B♮’]). Those are basically the notes that Bach used to notate his music (with the exception of B♮ which was called “H” and “B♭” was called “B”). Go figure.

I’m working on a way to improve the sound of some synthetic chorales generated by the Deep Neural Network model known as TonicNet. I’m most interested in the synthetic chorales that have a high degree of pitch entropy. I use the python library known as muspy to evaluate the generated chorales looking for those that have a high pitch class entropy.

The pitch class entropy is defined as the Shannon entropy of the normalized note pitch class histogram.

The formula according the the muspy documentation is:
Pitch Class Entropy
It basically gives a higher score if the pitches used include a lot of notes not in the root scale of the piece. A score over 3 contains a lot of notes outside the root key.

I used the TonicNet neural network to synthesize around 5000 unique chorales in S-A-T-B format, four voices, any number of notes each. I selected the highest scoring chorales, in terms of Pitch Class Entropy, and studied them for some ideas.

I tried retuning them using some standard Well Temperaments, and obtained some nice results. But I thought I might be able to improve on them if I used an adaptive tuning. William A. Sethares has a paper on the subject here: adaptive tuning.

I still need to code it up. But I thought it would be useful to describe what I am trying to accomplish first.