Algorithmic music: grammars

Evoking Elliott Carter

Here's a piece built from a very simple recursive grammar which sound remarkably like Elliott Carter's music---in this case, the piece is for piano and harpsichord, meaning it resembles Carter's Double Concerto for Piano and Harpsichord. Here's the piece.

Simple melodic line

Here's a short composition built from applying a recursive grammar. I love how simple rules can make something that sounds almost like human expression.. it has phrases, themes, a sense of flow.

Here's part of the code that makes this composition. It declares rules that indicate how to expand symbols. An "A" expands to "ABA". A "B" expands to "BAB". Also, each symbol carries attributes: duration, pitch, and amplitude. The formulas indicate how to compute the attributes for new symbols.

recurgram.declare( "AB", "pitch", "dur", "amp" )

Rule( "A", "ABA",
      "A1.dur = A1.dur",
      "A1.pitch = A1.pitch - 1",
      "A1.amp = A1.amp ",

      "B1.dur = A1.dur * 2.0",
      "B1.pitch = A1.pitch + 1",
      "B1.amp = A1.amp -6",

      "A2.dur = A1.dur",
      "A2.pitch = A1.pitch + 5",
      "A2.amp = A1.amp ",
      )

Rule( "B", "BAB",
      "B1.dur = B1.dur",
      "B1.pitch = B1.pitch + 3",
      "B1.amp = B1.amp",

      "A1.dur = B1.dur * 0.9",
      "A1.pitch = B1.pitch -2",
      "A1.amp = B1.amp",

      "B2.dur = B1.dur",
      "B2.pitch = B1.pitch - 5",
      "B2.amp = B1.amp + 3",
      )

An example of how the composition is initialized:

  n1 = acomp.Node( symbol="A",
                    pitch = 60,
                    dur = 1.0,
                    amp = 65,
                    )

  initial = [ n1 ]

Here's the piece.

Here's some other information about how the symbols are converted to notes. In this case, after 3 to 5 applications of the grammar, you end up with a string of anywhere from 27 to several hundred symbols. Each symbol becomes a note of the specified pitch, duration, and amplitude. I regard these notes as a single melodic line of non-overlapping notes. Thus the duration determines the rhythmic patterns.

First attempt at counterpoint

Then what interested me was creating a sense of overlapping, simultaneous or near-simultaneous events. How do you take a grammar which is inherently sequential and create from it counterpoint?

My first attempt to address this problem was to assign extra attributes, representing channel (notes are divided into right and left channels to give the sense of two instruments), and two extra attributes to affect rhythm. First I conceptually separated the rhythm from the duration in this way: Notes still have duration, but what determines when the next note arrives is something called the "span". If the spans are short, for example, the rhythm will be fast. But at the same time the durations could be long, leading to overlapping notes. There is one other attribute to control rhythm, which is "time offset." To determine the actual start time of a note, first all the notes are taken as though a single melody with the 'span' attribute indicating how much time passes between attacks, and then each note is offset in time according to the 'time offset' attribute.

Thus some notes move close together in time and feel more like a single event, a clump separated from other events in the composition.

An example rule set is here. This indicates that every occurance of 'A' in the string of symbols turns into 'ABA', and every occurance of B turns into 'BAB'.

def ruleSet2() :
   r1 = Rule( "A", "ABA",
              "A1.dur = A1.dur * 1.333",
              "A1.pitch = A1.pitch - 1",
              "A1.amp = A1.amp -4 ",
              "A1.span = A1.span * 0.75",
              "A1.offset = - 1.333 * A1.offset ",
              "A1.channel = (A1.channel + 1) % 2",
           
              "B1.dur = A1.dur * 0.75",
              "B1.pitch = A1.pitch",
              "B1.amp = A1.amp+1",
              "B1.span = A1.span * 1.1",
              "B1.offset = 0.8 * A1.offset",
              "B1.channel = A1.channel",
           
              "A2.dur = A1.dur",
              "A2.pitch = A1.pitch + 7",
              "A2.amp = A1.amp-2 ",
              "A2.span = A1.span * 0.8",
              "A2.offset = 0.5 * A1.offset",
              "A2.channel = A1.channel",
           )

   r2 = Rule( "B", "BAB",
              "B1.dur = B1.dur * 1.333",
              "B1.pitch = B1.pitch - 1",
              "B1.amp = B1.amp",
              "B1.span = B1.span * 0.666",
              "B1.offset = B1.offset",
              "B1.channel = (B1.channel + 1) % 2",
           
              "A1.dur = B1.dur",
              "A1.pitch = B1.pitch +1",
              "A1.amp = B1.amp  + 3",
              "A1.span = B1.span * 1.2",
              "A1.offset = B1.offset - 0.1666 * B1.span",
              "A1.channel = B1.channel",
           
              "B2.dur = B1.dur * .5",
              "B2.pitch = B1.pitch + 6",
              "B2.amp = B1.amp-1",
              "B2.span = B1.span * 0.5",
              "B2.offset = B1.offset + 0.3333*B1.span",
              "B2.channel = (B1.channel + 1) % 2",
           )

   ruleList = [ r1, r2 ]
   return ruleList

One possible way to initialize the composition is here:

  n1 = acomp.Node( symbol="A",
                    pitch = 60,
                    dur = 1.0,
                    amp = 65,
                    span = 0.6,
                    offset = 0.5,
                    channel = 0,
                    )

  n2 = acomp.Node( symbol="B",
                   pitch = 65,
                   dur = 1,
                   amp = 72,
                   span = 0.3,
                   offset = 0.5,
                   channel = 1
                   )

  initial = [ n1 ]

Here's an MP3.

Second attempt at counterpoint.

Here's a second attempt to create multiple instruments and overlapping events. In this case, each symbol of the grammar carries attributes of two notes: two pitches, two durations, two offsets, and so on. (Note, however, there is only one span.) Then upon converting the symbol list into a composition, each symbol becomes two notes, one for each instrument.

An example grammar is here. (Note: apparently this is an earlier version that had a span2 attribute, but currently that is ignored.)

def ruleSet3() :
   """ First attempt to base offset on ratio to span only"""
   r1 = Rule( "A", "ABA",
"A1.offset1 = A1.offset1 + 1.0 * A1.span2",
"A1.offset2 = A1.offset2 + 1.333 * A1.span1",
"A1.dur1 = 2.0 * A1.span2",
"A1.dur2 = 1.25 * A1.span2",
"A1.span1 = 0.666 * A1.span2",
"A1.span2 = 1.0 * A1.dur1",
"A1.pitch1 = -5 + A1.pitch1",
"A1.pitch2 = 5 + A1.pitch1",
"A1.amp1 = -4 + A1.amp1",
"A1.amp2 = -4 + A1.amp1",
"B1.offset1 = A1.offset1 + 1.25 * A1.span2",
"B1.offset2 = A1.offset2 + 0.8333 * A1.span2",
"B1.dur1 = 0.8333 * A1.dur1",
"B1.dur2 = 0.8333 * A1.span2",
"B1.span1 = 1.5 * A1.span1",
"B1.span2 = 1.0 * A1.dur1",
"B1.pitch1 = -1 + A1.pitch2",
"B1.pitch2 = 1 + A1.pitch2",
"B1.amp1 = 3 + A1.amp1",
"B1.amp2 = 0 + A1.amp1",
"A2.offset1 = A1.offset1 + 1.25 * A1.span2",
"A2.offset2 = A1.offset2 + 0.5 * A1.span2",
"A2.dur1 = 1.0 * A1.span2",
"A2.dur2 = 1.0 * A1.span1",
"A2.span1 = 0.75 * A1.dur2",
"A2.span2 = 1.25 * A1.dur1",
"A2.pitch1 = -4 + A1.pitch2",
"A2.pitch2 = 3 + A1.pitch1",
"A2.amp1 = 4 + A1.amp1",
"A2.amp2 = -4 + A1.amp2",


              )
   r2 = Rule( "B", "BAB",
"B1.offset1 = B1.offset1 + 1.5 * B1.span2",
"B1.offset2 = B1.offset2 + 1.25 * B1.span2",
"B1.dur1 = 2.0 * B1.span2",
"B1.dur2 = 0.666 * B1.dur1",
"B1.span1 = 0.75 * B1.span2",
"B1.span2 = 0.8333 * B1.dur1",
"B1.pitch1 = 4 + B1.pitch2",
"B1.pitch2 = -7 + B1.pitch2",
"B1.amp1 = 0 + B1.amp1",
"B1.amp2 = 4 + B1.amp2",
"A1.offset1 = B1.offset1 + 0.666 * B1.span1",
"A1.offset2 = B1.offset2 + 0.5 * B1.span2",
"A1.dur1 = 1.333 * B1.dur2",
"A1.dur2 = 0.666 * B1.dur1",
"A1.span1 = 1.0 * B1.span2",
"A1.span2 = 0.8333 * B1.span2",
"A1.pitch1 = -7 + B1.pitch1",
"A1.pitch2 = 2 + B1.pitch1",
"A1.amp1 = -1 + B1.amp1",
"A1.amp2 = 4 + B1.amp1",
"B2.offset1 = B1.offset1 + 1.25 * B1.span2",
"B2.offset2 = B1.offset2 + 2.0 * B1.span1",
"B2.dur1 = 0.8333 * B1.span1",
"B2.dur2 = 1.0 * B1.span1",
"B2.span1 = 1.333 * B1.dur1",
"B2.span2 = 1.333 * B1.dur1",
"B2.pitch1 = -2 + B1.pitch2",
"B2.pitch2 = -7 + B1.pitch2",
"B2.amp1 = -2 + B1.amp2",
"B2.amp2 = -3 + B1.amp1",
              )
   return [r1, r2]
              
 

And an example of a way to initial the composition is here:

  n1 = acomp.Node( symbol="B",
                   pitch1 = 65,
                   pitch2 = 67,
                   dur1 = 1.0,
                   dur2 = 1.0,
                   amp1 = 70,
                   amp2 = 70,
                   span1 = 0.6,
                   span2 = 0.6,
                   offset1 = 0.0,
                   offset2 = 0.0
                   )

An example of this, using a piano as one instrument, and harpsichord-like synthesis for the other, is here:

MP3 soundfile.