2021-04-13 music math composition
Every so often I come back to the idea of generating self-similar or "fractal" chord progressions by recursively applying grammar rules, as in my 2015 composition Dharmapala. I like the general method described in that article, but one thing I don't like so much about the finished piece is that it sounds the same all the way through. There's a little bit of "development" or shift in texture over the course of the piece, largely driven just by my performance-time changes to the synthesizer settings and the fact that I allow the notes to be chosen from a wider range toward the end.
I think part of the reason for that sameness throughout the piece is that I'm using basically the same rules for the whole thing. There are basically just two rules that generate the chord progression in Dharmapala:
major(x) → minor(vi(x)) major(iv(x)) major(i(x)) major(v(x))
minor(x) → major(vi(x)) minor(iv(x)) minor(i(x)) minor(v(x))
Those are applied recursively to a seed chord or short sequence of chords, so that for instance starting from C we would get Am-F-C-G, then applying the rules again Am changes to F-Dm-Am-Em, and so on. With each layer of recursive substitution the number of chords in the sequence is multiplied by four. As described in the article, I add some additional logic forcing the chord voicings to go in different directions, and that adds a bit of variety, but it is still fundamentally only two simple expansion rules, or even just one rule with the major/minor qualities flipped. Once the listener gets accustomed to the simple rule, there's not much more to attract interest.
One way to create more variation would be to add more "qualities" of chords. Instead of only major and minor, maybe also have sevenths, suspended chords, added-ninth chords, whatever seems interesting. Then there could be a rule for each one, and the rules could be more varied, something like this:
major(x) → major(v(x)) major(ii(x))
minor(vi(x)) minor(iii(x)) minor(vi(x))
sus2(i(x)) add9(iv(x)) major(i(x))
seventh(x) → seventh(v(x)) major(i(x))
minor(x) → minor(i(x)) minor(v(x)) minor(i(x))
sus2(x) → sus2(i(x)) major(i(x))
add9(x) → major(i(x)) add9(iv(x)) major(i(x))
I call that the "sailor" rule set because its main, long rule, the one for major chords, is basically the same chord progression I used in A sailor from the sea.
A sailor from the sea [MP3] [FLAC]
But outside the context of live improv, this set of rules might not work very well. The major chord rule is quite long - each major chord turns into eight new chords every time it's applied, and some of them will also be major chords, triggering the same expansion in the next round. So we can't afford to apply too many rounds of expansion that will include that rule without making the piece too long. The rule for seventh chords is not mentioned in any of the other rules, and only once in its recursive invocation of itself. So if we want to have seventh chords in the finished piece then we need to have them in the initial seed, and we will never have any more seventh chords in the finished piece than the number that were in the initial seed. The minor chord rule only invokes itself, so on repeated substitutions there will come to be longer and longer stretches of only minor chords.
Except at the very lowest level, where the chord qualities actually determine which notes to play in combination with each other, these chord qualities are really just a way for each step in a rule to tell the expansion algorithm "Go use this other rule when you are expanding at this point." So why not make that more explicit - and take it a step further to describe entire sets of rules The use of different chord qualities in the "sailor" rule set will drive different rules to be used in different parts of the piece, according to the pattern set by the very first few rules applied at the top level, but it's still basically the same set of rules that will be applied everywhere. Swapping out entire sets of rules might give more variety.
I defined three sets of rules, each with an expansion for every quality of chord on my list, and added annotations to the rules describing how to switch among rule-sets when recursively expanding the rules. A rule with these annotations might look something like this:
Sailor rule set, major chord rule:
major(v(x)) and continue with the current rules
major(ii(x)) and continue with the current rules
minor(vi(x)) and continue with the current rules
minor(iii(x)) and skip ahead two steps in the list of rule sets
minor(vi(x)) and continue with the current rules
sus2(i(x)) and go back to the previous rule set
add9(iv(x)) and go to the next rule set
major(i(x)) and continue with the current rules
Those descriptions of the rule-set-skipping are simplified a little. My software actually keeps a finite list of rule sets currently available and applies different permutations to the list as it descends. The point is that switching among rule sets may level out variations in the rule lengths. Different entire sets of rules, which may each create a perceptible mood, will apply in different parts of the piece. The switching between them will feel pseudo-random, but because each rule set often recurses into itself, there will tend to be stretches of music mostly controlled by one set, containing smaller stretches of other sets, each possibly containing even smaller breaks into other rule sets, in a fractal way.
Something else I wasn't satisfied with in Dharmapala was that it has very little in the way of rhythm or melody: it's just a sequence of chords played one after another, spiced up only a little with a complicated envelope that switches between modular filters mid-note. I actually lost my notes about the patch I used in that recording when I took it down from Soundcloud, but I think it's a Tiptop Z2040 and an Erica Polivoks filter; the track was made before I launched the company, and I hadn't started building my own filters yet.
In the new project I wanted to have more variation in the rhythm - longer and shorter notes, interacting in different ways; and it made sense to me to implement that as additional annotations on the grammar rules. I recently bought a Trigger Riot and I've found that its model of having a single knob specify something like "beat division such-and-such, offset by so-and-so beats, with this probability" makes a lot of sense. I extended my chord-substitution engine to allow adding and removing instructions like that in the middle of a rule.
Here's the Prolog code for one of my substitution rules, with that enhancement and a few others. I don't plan to go through all the details of how this rule works and interfaces to the rest of my code; the point is just to show the way a substitution rule turns a single chord entry into a sequence of instructions that can be further expanded.
expand_rules([[chord(Level,Rules,Root,add9),LB,UB,_,_]|B],C):- Level>0,Rules=[sailor|_], LI is Level-1, RootIV is (Root+5) mod 12, choose_bounds(LB,UB,LBA,LBB,LBC,UBA,UBB,UBC), rotate_rules(Rules,RR), swap_rules(Rules,SR), !,expand_rules([ [push(chord_time,16),_,_,_,_], [chord(LI,RR,Root,major),LBC,UBA,_,_], [place(14,[rhythm,2,0,15,HX]),_,_,_,_], [place(15,[block,2,1,85,HY]),_,_,_,_], [chord(LI,SR,RootIV,add9),LBB,UBB,_,_], [remove(14,HX),_,_,_,_], [chord(LI,Rules,Root,major),LBA,UBC,_,_], [remove(15,HY),_,_,_,_], [pop(chord_time),_,_,_,_] |B],C).
The software maintains a list of "instructions" which starts out as basically just a few hand-chosen chords, as the seed. When one of the chords in the list happens to be an add-9 chord, and the "sailor" rule set is the prevailing one at the head of the queue of rule sets, then this code should runs. The first few lines of code are just pattern-matching to detect that situation.
Then there's a line that calculates the variable RootIV, which is the fourth of the current scale, five semitones up from the current root that was passed in as the variable Root. The call to choose_bounds/8 places constraints on the highest and lowest notes that can eventually be used in voicing the chords under this rule, because I'm doing the same kind of gradually-expanding range thing that seemed to work well in Dharmapala. Then the "rotate" and "swap" lines compute two rearranged versions of the list of rule sets, that will be used in the two recursive calls.
Finally in the rule code, there's a list of nine new instructions that will be spliced into the global instruction list, replacing the one chord instruction that this rule matched. Three of those are new chord instructions (encoding the chord progression I, IV+9, I), which may be subject to further recursion with the new lists of rule sets. When those get substituted they will use other rule sets instead of "sailor," because the rearranged lists will put other sets at the head. The rest of the newly added instructions are commands to the chord voicer, telling it how to add additional onsets to the rhythm - much like twiddling the knobs on a Trigger Riot to add additional beats - and how long chords, in general, in this segment of music should last. Those side instructions are not further expanded.
After a few rounds of substitution like this, the seed sequence grows to a list of a few hundred to a few thousand instructions. After that point it's much like Dharmapala. The software goes through the list and finds the times for all the note onsets, but those times are more complicated than just a regular stream of equal-length notes because they're determined by the groove instructions embedded in the list. There a variable created for every note's pitch subject to all the constraints of being harmonious with the current chord, with the next and previous notes in the same voice, being within the upper and lower limits of the pitch range, and so on. Once all the constraints are in place, my software runs an off-the-shelf constraint solver to find actual values for all the note pitches to satisfy the constraints.
Constraint solving with these tools is a semi-interactive process. In practice, I had to go back and forth between making small modifications to the rules and trying the solver again to find a set of constraints that could really be solved in a reasonable amount of time. After some experiment with different seeds and using different depths of recursion and so on, I arrived at a score I was happy with. My solver software produced Lilypond output from which I generated PDF sheet music and some MIDI files for driving the synthesizers.
Actually recording it was another adventure. Although I could have recorded each voice separately and then mixed them down, I really wanted to do all four main voices on the modular at once, and I wanted to use my own North Coast modules as much as possible. That meant building a few more, because I didn't have enough filters and ADSR envelopes in my own rack to build four full voices with North Coast filters and envelopes at the time. I was able to overlap that effort with a couple of prototypes I wanted to build anyway to test out some new transistors that will be substitutes for the discontinued PN200A type in modules that used them. Most of these builds were broadcast in my Twitch stream. In the end I did also use a couple of my old Pittsburgh ADSR envelopes too, and on a couple of voices I multed the envelope outputs to drive both VCA and VCF, because I didn't really want to build four more Transistor ADSRs just to have two for each of four voices.
Here's the rack I used for recording the main voices. I kept the Z3000 in the rack because I had space, it was in my previous configuration, and it made tuning-up easier; but after using its frequency counter to tune the Middle Paths I disconnected it and it didn't enter into the final patch. The final patch used all the other modules shown. The cables were too dense for a complete diagram of the connections to be readable, but it was basically a straightforward patch of four subtractive voices, with the MIDI sequencer driving the four oscillators of the Middle Paths, then those patched into the two Leapfrog and Coiler filters. I used the built-in VCAs on the Leapfrogs, and passed the Coiler outputs through a dual Pittsburgh VCA. One of my own Transistor Mixers and one Pittsburgh four-knob mixer created "mid" and "side" signals which I used the Octave Switch to decode into "left" and "right." Those went into the Clouds for a little bit of polishing. Getting a stereo effect that way makes more sense to me than trying to have a "stereo filter"; stereo is best done as far downstream in the patch as possible. I also added a few side connections, such as from Fixed Sine Bank to oscillator modulation inputs and from the shaper outputs of the Middle Paths to the otherwise-unused inputs of the mixers, so that I could add some additional effects while playing the sequence. The whole patch uses nearly all of my three-row case, and nearly all the patch cables I own.
The modular is really the star of the show. I played the sequence through my four subtractive voices and adjusted the patch in real time to bring out different parts of the music over the course of the track. But I also wanted to make a very dense multi-layered recording, so I generated some MIDI of just the background chords (shown as chord names in the score, not individual noets) and recorded tracks of those chords using different "pad" presets on a Roland D-05 (digital hardware synthesizer emulating the classic D-50) and in a software synth with Precisionsound vocal samples. I loaded up all the background tracks and the modular-synth recording, in Audacity and mixed down the modular with different combinations of pads along the timeline to try to capture moods that followed what I'd been feeling while recording the modular.
Here's the result. I call it "Aconcagua" after the mountain in Argentina. Pay attention to the way the mood shifts - especially with the changes in rhythm - as the substitution process moves into different rule sets.
◀ PREV Panel painting photo gallery || Marbled and hydrographic panel gallery NEXT ▶