A Musical Introduction to Generators

A Musical Introduction to Generators

What a generator allows you to do is to take code that is writen in a sequential, looping style, and then treat that code’s output as a collection to be iterated over.

To illustrate what this means, and the the generators package in general, I will use R to perform a suitable piece of music, specifically Steve Reich’s “Clapping Music.”

The piece is starts with a loop counting out groups of 3, 2, 1, 2, 3, 2, 1, 2, 3… with each group seperated by one rest. This adds up to a 12-note loop:

Musical score showing loop

Musical score showing loop

In code, we could implement that idea like the following, printing a 1 for each clap and a 0 for each rest:

print_pattern <- function(counts = c(3, 2, 1, 2), repeats=4) {
  for (i in seq_len(repeats)) {
    for (c in counts) {
        for (j in 1:c)
          cat(1)
        cat(0)
    }
  }
  cat("\n")
}

Testing this, we should see groups of 1, 2, or 3 1s separated by a single 0:

print_pattern(repeats=4);
## 111011010110111011010110111011010110111011010110

“Clapping Music” is based on manipulating this 12-count loop. But it’s hard to manipulate the output of a program that only prints. The calls to cat produce output on the terminal, but they don’t produce data that we can easily manipulate with more programming – we need to make this pattern into data, rather than terminal output.

The generators package allows us to enclose a data-generating process into an object.

To make a generator for this patter, we just enclose the body of the function in a call to gen(), and change each cat() to yield().

library(async) # for gen
gen_pattern <- function(counts = c(3, 2, 1, 2)) { force(counts)
  gen({
    repeat {
      for (n in counts) {
        for (j in 1:n)
          yield(1)
        yield(0)
      }
    }
  })
}

Adding force(counts) is a good idea because of R’s lazy evaluation + mutable bindings. A generator captures its environment like an inner function, so it’s a good idea to fix the value of counts before the outer function returns; this is the same as if you return an inner function that uses arguments to an outer function. Meanwhile, fixing the number of repeats ahead of time is no longer necessary; a generator can be in principle infinite and it will only generate data as long as you keep requesting more.

The code inside gen(...) does not run, yet. The call to gen constructs an [iterator][iterators::iterators-package], which supports the method nextElem. When nextElem() is called on a generator, the generator runs its code only up to the point where yield is called. The generator returns this value, and pauses state until the next call to nextElem().

library(iterators)
p <- gen_pattern()
for (i in 1:23) { cat(nextElem(p)) }; cat("\n")
## 11101101011011101101011

gen(...) constructs and returns an iterator, which means you can apply iterator methods to it. For instance you can collect just the first 24 items with ilimit():

library(magrittr)
show_head <- function(x, n=24) {
  x %>% itertools::ilimit(n) %>% as.list() %>% deparse() %>% cat(sep="\n")
}
show_head(gen_pattern(), 24)
## list(1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 
##     1, 0, 1, 1, 0)

Making noise

We’re a good way into what I advertised as a musical endeavour and haven’t made any sounds yet. First let’s download some handclap samples. I located some on GitHub:

Although R is not known for audio performance, there is an audio package playing sound samples, which we can use like this:

We want to play sounds at a consistent tempo, so here’s a routine that takes in a generator and a sample list, and plays at a given tempo. The profvis package has a pause function that’s more accurate than Sys.sleep().

So we should hear our pattern now:

Some iterator functions

Here’s a couple of utility functions that will come in handy. One is an iterator equivalent of lapply for iterators, which I’ll call iapply. The other one is isink which just consumes all elements from an iterator.

Some iterator functions using generators

Note that in a generator, you can write a for loop with an iterator in the argument. So we can equivalently write iapply and isink like this:

For example, run an iterator through iapply(cat) and isink to print it:

Phasing and combining

“Clapping Music” is a piece for two performers, who both play the same pattern, but after every 12 loops, one of the performers skips forward by one step. Over the course of the piece, the two parts move out and back into in phase with each other. We can write a generator function that does this “skip,” by consuming a value without yielding it:

Here’s a count from one to 12, skipping after three (i.e. skipping every fourth):

The performance directions for “Clapping Music” request that the two performers should make their claps sound similar, so that their lines blend into an overall pattern. We can interpret that as combining the two lines by adding two generators, resulting in 0, 1, or 2 claps at every step, playing the louder sample for a value of 2.

Then, all together:

To narrate this: we are constructing two independent instances of our 12-note generator. One of these patterns is made to skip one beat every N bars. Then we create a third generator that adds together the two.

A performance

Now we should be able to hear our performance

R is definitely not a multimedia environment, plus the audio package is using the OS alert sound facility, which is not really meant for precise timing, so you may hear some glitches and hiccups. Nevertheless, I hope this has illustrated how generators allows control to be interleaved among different sequential processes.