| « Another tool to fall in love with | First go with Lilypond » |
Neo-complexity rhythm generator
Last week, in my SuperCollider lesson, my student came in with an excellent question: how to generate rhythms similar to "neo-complexity" composers such as Brian Ferneyhough. (During my grad school days, we used to mock this loose-knit band of composers for their excessive academicism. Since then, I've heard others say that this music suffers in recording, as a good deal of the energy of the music is the performers' effort in grappling with impossible notation and you don't see it on a CD. I've yet to hear any of this music in concert, so I'll have to keep my mind open in case I ever do.)
A common rhythmic "tic" shared by many of these composers is a fondness for odd subdivisions nested two or three levels deep: 7 notes in the space of 3/5 of a 4/4 measure. They arrive at such rhythms by dividing a duration of time by some factor, and then dividing these new, shorter durations by a different factor. In computer programming, this is a recursive process: do it once, and then do it again, but now to the results of the first "do it." That process can be extended as many levels down as needed.
Simply, then, in "eager" style. (This is using a method defined in the ddwRational quark, to print the results as fractions.)
~subdivide = { |durs, level = 0|
var n;
if(level > 0) {
durs.asRational.postln;
durs = durs.collect { |dur|
n = rrand(2, 5);
Array.fill(n, dur/n)
}.flat;
~subdivide.value(durs, level - 1);
} { durs };
};
~subdivide.value([4], 3).asRational;
[ 4 ] [ 4/5, 4/5, 4/5, 4/5, 4/5 ]
(With the final result split into separate lines, to show the relationship to the preceding 4/5 values.)
[ 2/5, 2/5, 4/25, 4/25, 4/25, 4/25, 4/25, 2/5, 2/5, 4/25, 4/25, 4/25, 4/25, 4/25, 1/5, 1/5, 1/5, 1/5 ]
The problem with this style is that it has to calculate all the values at once. That might not be practical in a long-running algorithmic piece. Is there a way to do this "lazily"? Sure -- you could evaluate the above function in a routine, but instead of returning durs when level == 0, stream them out using yield. That's still a bit clunky for my taste. I'm really fond of patterns, for their ability to express complex behaviors as objects rather than lists of commands (declaratively vs. imperatively).
Enter another idea: string rewriting, derived from Lindenmayer systems. Start with an original series of values. For each one, find the "rule" that matches the item and use the rule to transform it. Guess what? You can then apply the same rules to the transformed values -- recursion.
SuperCollider already has a handful of rewriting systems: Prewrite in the main library, rewriteString in the MathLib quark and LSys / PLSys in the NatureToolkit quark. Prewrite was the most attractive to me since it's closest to the pattern idiom, but it's deterministic: a given item will be transformed by replacing it with one or more other items. Introducing randomness into the transformation appears to be quite difficult. I wanted a nondeterministic version -- so I wrote one for Affectations, and then tidied it up for inclusion in my ddwPatterns quark.
With this, generating two-part counterpoint is remarkably compact and elegant.
p = Ppar(
{ |i|
Pbind(
\pan, i*2 - 1,
[\dur, \level], PpatRewrite(
Pn([4, 0], 1), // input pattern
Pseries(1+i, 1, inf).fold(1, 4),
[
nil -> { |item, level|
var subdiv = rrand(2, 5);
if(0.75.coin) {
Ptuple([
Pseq(item[0] / subdiv *
subdiv.partition((subdiv * 0.7)
.roundUp.asInteger, 1), 1),
Pseq([item[1], Pn(level, inf)])
])
} { Pn(item, 1) }
}
]
),
\freq, Pexprand(100, 220, inf) * (2 ** (Pkey(\level) - 1)),
\sustain, 2 ** (Pkey(\level).neg)
)
} ! 2
).play(quant: 1);
p.stop;
Within PpatRewrite, we have the source values -- an infinite stream of arrays [4, 0]. (Why arrays? Below...) Then, a pattern returning the number of levels of recursion, increasing and decreasing between 1 and 4. Last, an array of rules:
matching condition -> transformation
nil is defined (in matchItem) to match everything, so this sets up a global rule that will apply to every output value. Then the transformation decides what to do: 3/4 of the time, it will do the subdivision and return a Ptuple to spit out the subdivided rhythm. Equal divisions turned out to be boring; partition-ing the number of subdivisions allows, for instance, 5:4 to turn into (2+1+2):4. That's it, nothing more to write.
Except... with purely random frequencies, we hear the rhythm lurch forward and stop on a dime, but the recursive structure isn't apparent. That's the reason for returning arrays from PpatRewrite: each result is the duration and the recursion level that produced this moment in time. Then we can use the level to push the more intricate subdivisions into a higher register, and also sustain lower levels -- the "structural" notes that are ornamented by the further subdivisions -- for a longer time.
Is it especially musical? I don't know that I would go so far; certainly, more attention to pitch would help. But the two parts played together sound eerily coordinated while spending most of the time not tightly synchronized. It's oddly compelling. I listened for 15-20 minutes after writing this little snippet.
- PpatRewrite class (bottom of file)
- A short help file
No feedback yet
Comments are not allowed from anonymous visitors. Please use the Contact link at the top or bottom of this page to email me for a user account. This is just an antispam measure.
