NOTE: Alda 2.0.0 was released in Spring 2021. It was a total rewrite from the ground up in Go and Kotlin. As a consequence, a lot of the information below about the Clojure internals of Alda (v1) is no longer relevant!
In 2018, Dave released the alda-clj library, which is the new recommended way to write/generate Alda scores in Clojure.
A more FP-friendly Alda
Just recently, I rewrote a big chunk of the Alda codebase; the result is something I’m a lot happier with, as a Clojure programmer. Clojure is a language that encourages programming in a functional style, minimizing the need to keep track of the state of variables and reliance upon unpredictable side effects.
My first pass at writing the core library of Alda was admittedly not very faithful to the tenets of functional programming. This wasn’t a conscious decision; it just ended up being the quickest way to write the code and have it work reasonably well.
This first cut of Alda used to work like this:
- There were a bunch of dynamic vars defined in the
alda.lisp
namespace, each of which represented the current state of some aspect of the Alda score being evaluated. - As an Alda score file was parsed and evaluated, each “event” would modify one or more of the top-level vars. For example, a
note
event would add some note data to*events*
and make a note of the updated position in the score of the instrument that played the note by updating*instruments*
. To keep track of which instrument(s) were currently active, the score evaluation process would access and modify the*current-instruments*
var. - Each time a new score was evaluated, the
score
event would re-initialize all of the dynamic vars, losing any state that had been accumulated by the previous score.
Here is a Clojure REPL session demonstrating how this worked before:
This was our immediate problem: an Alda process could only handle one score at a time. This worked OK for experimenting in a Clojure REPL, but in practice, it became evident that we needed an Alda process to be able to manage multiple scores. For example, a user might want to play one score, and then parse or play another score while the first score is still playing. The top-level var-based system was simply not able to accommodate this use case; this was my catalyst for rewriting Alda in a more functional style.
FP in a nutshell
When working with a functional programming language like Clojure, the programmer avoids redefining variables that have already been defined.
For example, consider this imperative code written in JavaScript:
In Clojure, we wouldn’t define something and then redefine it. Instead, we would express the sum as a single expression, like this:
In the JavaScript example, we constructed an imperative for
loop where we change the value of sum
on each iteration, then we returned the final value.
In the Clojure example, we represented this more concisely as a mathematical calculation:
- Take the range of numbers from 0 until 10. (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
- Perform a reducing operation over this range:
- adding the next number to the accumulated result
- starting with the number 5 as an initial value
If you’re new to functional programming, you may not be familiar with reduce
. reduce
works sort of like a constrained for
loop, but we usually don’t perform any “side effects” like changing the values of variables. Instead, we define a sort of “formula” for what the result should be in each iteration of the loop, then each result is fed into the next iteration of the loop until we’re out of things to loop over and we have our result.
The “formula” is a function that takes two arguments: the “accumulator” (the result that gets fed back into the loop each time) and the next value.
In the case of the example above, the formula is accumulator + next-value
, so we can conveniently just use +
as our reducing function.
So, given the initial accumulator value of 5, and the range 0-9 to reduce over, the reducing process looks something like this:
The key thing about representing this calculation as (reduce + 5 (range 10))
is that it is anonymous and completely self-contained. Notice that we did not have to define any variables to calculate this sum. That means we don’t have to worry about accidentally forgetting to set the initial value of a variable, and we don’t have to worry about some other process altering the state of the variable before we get the value we want. This is the power and simplicity of functional programming, in a nutshell.
A formula for calculating a musical score
To reiterate the problem we were having with Alda: we were storing all of our
“working state” in top-level variables like *events*
and
*current-instruments*
, and those variables could be modified by any process
that was trying to create a score. The scores were not anonymous and
self-contained, so if you had two or more processes that were both trying to
create or modify a score using the same Alda server, then they could potentially
conflict with one another.
The solution I came up with was to make creating or updating a score a reducing operation. The reducing function was basically this:
An “event” could be any number of things: a note, a rest, a chord, a change in the value of an instrument’s “attributes” like octave or volume, etc. I implemented update-score
as a Clojure multimethod, a special kind of function that has different behavior depending on arbitrary properties of its arguments. The update-score
multimethod examines the type of event and updates the score accordingly. For example, when it encounters a “new part” event, it finds or creates the appropriate instrument and stores that context on the anonymous “score” that is being accumulated.
At no point during this score-updating process does the original score get modified. Each iteration of the score-updating reducing function is returning a slightly different copy of the score, rather than modifying the score and returning it. This is an essential thing to understand about functional programming. In our case, it is beneficial because it means we can safely process multiple scores at the same time without having to worry about one score clobbering the state of another.
Using Alda in a Clojure REPL
To better illustrate how this works, I can show you a few examples of different events and what the update-score
multimethod does when it encounters them.
Events: what do they look like?
Alda has a convenient Clojure DSL that allows you to express a musical score in the form of a Lisp S-expression.
For example, consider the following sheet music:
This is a simple musical score containing four notes: C, D, E, F.
Assuming you wanted these notes played by a French horn, the corresponding Alda score would look like this:
When the Alda compiler parses this code, the result is a single S-expression of Clojure code:
The resulting Clojure code makes use of several functions defined in the alda.lisp
namespace provided by Alda. Each of these functions has different semantics, but all of the functions that are considered “events” work in the same basic way: they return a Clojure map representing an event.
Take alda.lisp/part
, for example:
The result of evaluating a part
form is a Clojure map containing an :event-type
, which tells the update-score
multimethod what kind of event this is, and any number of other fields used by Alda to update the score appropriately.
In the case of the “part” event, Alda adds an instance of the appropriate type of instrument to the score (declared via the :instrument-call
) and then reduces through all of the part’s :events
to add them to the score. A part’s “events” are things like attribute changes, notes, and chords.
A “note” event looks like this:
Just like we saw before with part
, the note
event returns a map containing one required field :event-type
, which tells the score evaluation process what type of event it is so it knows what to do with the other information in the map.
The REPL representation of :pitch-fn
looks kind of funky, but all it is is a function that is applied to the current instruments’ octave and key signature in order to get the actual pitch of the note. For example, if an instrument is in octave 4 and has no key signature, then the note “C” corresponds to MIDI note number 60, and has a frequency of about 262 Hz:
The remaining fields have values when a duration is assigned to the note. In the example above, the note has the duration of a whole (1) note, which means it lasts for 4 beats. The :ms
field has a non-zero value if the note’s duration is expressed in milliseconds instead of as a note length.
Events: what do they do?
To see the effect of updating a score with an event, we can define a score using alda.lisp/score
, update the score using alda.lisp/continue
, and then use clojure.data/diff to show what’s different about the updated score:
Things worth noting:
-
Continuing a score with
alda.lisp/continue
does not modify the original score; it produces a new one. In the REPL session above, we defined the original (empty) score ass1
, then continued it and defined the resulting score (with bassoon part added) ass2
.s1
was not modified in the process of creatings2
. -
Adding a “part” event to an Alda score changes a handful of things in the score map, namely
:instruments
,:current-instruments
,:voice-instruments
, and:current-voice
. You may not need to understand the subtle differences between these fields; sufficeth to say that usingalda.lisp/part
in a score has an impact on the instruments in the score and which ones are active at that moment in the score.
Let’s continue, and see what the note
event does:
As you can see, the note event changed a couple things:
-
The bassoon instrument’s “last offset” and “current offset” changed to reflect how far into the score (in milliseconds) that instrument is after having played the note. These new values will be used to determine where in the score the next note the bassoon plays will be placed.
-
:events
, which was an empty set#{}
before, now contains a single note event, which is represented as a map containing information like the volume, panning, pitch, and duration of the note.
Putting it all together
At this point, you may be wondering: How is it practical to write a score this way? Do I have to define a new var like s1
, s2
and s3
each time I add something to the score?
A more practical way to use alda.lisp
is to define a score as an atom:
Now your score can be continued in-place using swap!
with alda.lisp/continue
:
To make this slightly more convenient, you can use alda.lisp/continue!
which is a shortcut for the above:
Playing your score
alda.now
provides a quick and easy way to create and play Alda scores in a Clojure application or REPL. You can read the documentation for more information on the kinds of things it allows you to do, but for a quick demo, we can use alda.now/play-score!
to play the score we created above.
There will be a delay* as the MIDI synthesizer is initialized, and then you should hear a marimba playing three notes: C, D, E.
*This delay only happens the first time you play something in a session; playback will be immediate each time after that.
For playing one-off snippets of music instead of pre-defined scores, you can use alda.now/play!
:
That’s it
If you’re a Clojure programmer, hopefully this gives you enough background on how Alda works as a Clojure library that you can use it as a tool to create music or sound effects in your Clojure programs.
Each time we release a new version of Alda, in addition to releasing the
command-line executable on GitHub, I also upload the package to
Clojars. So, if any of this stuff interests you, I
encourage you to add alda
as a dependency with your favorite Clojure
build tool and play around with it. Have fun!