Noel recently wrote about Doodle, the compositional drawing library featured in Creative Scala and our new studio-format Essential Scala. Today I want to introduce you to another library called Compose. This new library, which will be featured in future courses, applies the same functional programming principles to music.
I’ll be speaking about Doodle and Compose at Async in Brighton on March 19th. All welcome—come and say hi!
Doodle and Compose are both designed in a classicly functional manner. The user builds a representation of the desired output using a set of primitive objects and combinators. The library then interprets the representation to produce the final result. Although we have used this idea of combinator libraries for media applications, the same ideas apply to business applications.
-
User code is simple and declarative, describing only the expected output and ignoring implementation-specific details.
-
The intermediate representation, if defined correctly, allows the user to easily change aspects of the output: relocating an image to a different part of the screen, or playing a song back at a different tempo.
-
We can provide different interpreters for different situations without changing any user code. For example, Doodle has an interpreter for Swing/Java2D and an interpreter for HTML5/Canvas.
-
Finally, it is possible to clone and re-use parts of images and songs within other images and songs. This is the fundamental nature of a composable representation.
Representing Music as Code
Doodle is based on simple geometric primitives
such as squares, circles, and triangles.
Its composition operations are spatial functions such as
beside
, above
, and below
.
Let’s look at the language Compose uses to represent music
and how it allows us to build complex songs from simple parts.
Primitives: Notes, Rests, Pitch, and Duration
The primitives in a musical score are notes and rests. Notes have a pitch and duration and rests simply have a duration.
Piano keys are a convenient represenation of pitch:
C4
is middle C, D4
the next white note,
Cs4
the C sharp between the two, and so on.
Durations are measured in beats. There are typically four beats a bar but many notes are much shorter than that. Compose provides representations for whole, half, quarter, eighth, sixteenth, and 32nd beats, and combinators to produce “dotted” variants.
The end result is a DSL for producing notes with any cross section of pitch and duration:
import Note._
C4.e // C4, eighth beat duration
Fs4.h // F#4, half beat duration
D5.w.dotted // D5, dotted whole beat duration
Note that we are omitting another important component of music – dynamics. Without changes in volume, our musical compositions will sound artificial. However, we’ve opted to keep things simple for now and save this for a future addition to the library.
Composition
Now we have our primitive building blocks,
let’s think about how we can combine them to create musical scores.
We can combine notes in a sequence (played one after the other) and
parallel (played at the same time).
Compose uses the +
operator for sequential composition and
the |
operator for parallel composition1:
import Note._
// A C major chord:
C4.e | E4.e | G4.e
// A C major scale:
C4.e + D4.e + E4.e + F4.e + G4.e + A4.e + B4.e
And, of course, sequential and parallel forms compose nicely. Here’s a simple chord progression:
val Cmaj = C3.q | E3.q | G3.q
val Fmaj = C3.q | F3.q | A3.q
val Gmaj = D3.q | G3.q | B3.q
val chords = Cmaj + Fmaj + Gmaj + Fmaj + Cmaj
Behind the scenes Compose builds up a representation of the
music using Score
objects. There are Score
wrappers
for notes, rests, sequences, and parallel combinations:
sealed trait Score {
def +(that: Score) = SeqScore(this, that)
def |(that: Score) = ParScore(this, that)
}
case class NoteScore(note: Note, duration: Duration) extends Score
case class RestScore(duration: Duration) extends Score
case class SeqScore(a: Score, b: Score) extends Score
case class ParScore(a: Score, b: Score) extends Score
Ignoring dynamics and volume,
we can represent any composition using Scores
.
Playback
To play songs back, we have to think of them as
programs to be compiled and interpreted.
You can review the code Compose uses for this on Github.
The essential idea is to compile the score into a
sequence of Commands
that can be interpreted directly:
sealed trait Command
case class NoteOn(channel: Int, freq: Double) extends Command
case class NoteOff(channel: Int) extends Command
case class Wait(millis: Long) extends Command
The player, which uses the ScalaCollider library
to communicate with SuperCollider,
allocates a fixed number of channels to play back sounds.
If we have 4 channels it means we can play 4 samples at the same time.
The NoteOn
and NoteOff
commands affect a specified channel
and the Wait
command delays for a specified time period.
NoteOn
and NoteOff
commands with too high a channel
are ignored during playback, allowing for graceful degradation
if the song we’re playing contains
more simultaneous notes than we have channels.
The compiler also has a notion of tempo,
allowing us to play the same score quickly or slowly
depending on user preferences.
Summary
Compose demonstrates how classical functional programming principles can be applied to music. We model a problem domain using primitives such as notes and rests, and combine them using parallel and sequential operators. Finally, we interpret the resulting score to produce a useful output.
Functional library design allows library users to concentrate on the components and relationships in their compositions without concerning themselves with implementation details such as then number of channels available for playback. Songs can be combined and transformed to create new songs with different patterns or in different keys.
You might validly argue that, while the DSL used in Compose is expressive enough to store music, it isn’t particuarly readable as regular sheet music. One final feature of a functional library design is the ability to create new DSLs on top of the existing representations and interpreters. More on this in a future post!