In a previous article, we mentioned that mainstream programming languages do not have any concept of time. This article is the second in a series that examines three languages (first, a markup language, then two programing languages) that focus on timing and synchronization. In the first article, we saw how SMIL defines a powerful timing and synchronization model for presenting multimedia content. In this second article, we will see how the ChucK programming language provides a sample-accurate scheduler for the production of computer music, from digital signal processing algorithms to musical composition; and finally, we will look at the synchronous reactive programming model of the Esterel language.
There exist many computer music languages, from the pioneering works of Max Mathews (the so-called Music N languages)and their direct descendants like Csound, SuperCollider, Max and Pure Data, to live coding platforms such as Tidal Cycles or Orca, to mention just a few. But just like most general purpose programming languages, most computer music languages or systems have, at best, a very weak notion of time, which is puzzling considering their target domain; and when they do, that notion may be over abstracted to allow for efficient generation of audio, especially when running in real time. ChucK is another member of the Music N family that describes itself as being strongly-timed. To get an idea of what this means, let’s have a look at the “Harmonic Series Arpeggiator” example program from the Web ChucK IDE.
// Harmonic Series Arpeggiator // Written by Terry Feng // CHANGE ME! ADD MULTIPLE SHREDS! 100 => float baseFrequency; // starting frequency 12 => int numHarmonics; // number of harmonics to play 200::ms => dur noteDur; // note duration // Unit Generator SinOsc osc => dac; osc.gain(0.5); while (true) { // Loop through the number of harmonics for (0 => int i; i < numHarmonics; i++) { // Update the oscillator frequency to the next harmonic baseFrequency + (i * baseFrequency) => osc.freq; // Advance time to play the note noteDur => now; } }
This looks reasonable enough to anyone used to C like languages, except for this =>
operator (called simply chuck) that shows up on almost every line. In the first three lines, it
is used for assignment. We also see that the note duration is not just an int or a float but a dur, and the value is qualified with the ms
(for millisecond) unit. This is the first hint
that ChucK does indeed have a concept of duration and time; there is even a well-defined associated
arithmetic, so that adding e.g., a dur to a time produces a new time.
The next two lines create a graph of audio units, connecting a sine wave oscillator (SinOsc
) to a digital audio converter (dac
), showing a new use of the chuck operator =>
. Connecting audio units is a very common concept in the Music N-derived languages, and is similar
to hardware modular synthesizers where oscillators are connected to filters, amplifiers, and other
effects, and finally to some audio output (here, the DAC.) Some systems like Max and Pure Data allow
describing the graph visually; ChucK is text based, but manages to highlight the signal flow through
the directionality of the =>
.
We then have an infinite while
loop wrapping a for
loop that sets the frequency
of the oscillator (i.e., changing the pitch of the generated audio). The most interesting line is the
last one, which shows one more, and perhaps the most shocking, use of the chuck operator: noteDur => now
. Recall that noteDur
is a duration of 200 milliseconds: this
expression indicates that time moves forward by 200ms, holding the current note for that
duration, until the loop resumes and the pitch rises or resets to its base frequency after 12
iterations.
Going back to the first lines of comment, the author encourages us to modify this program and “add multiple shreds”. A shred is ChucK terminology for a light-weight thread of execution. Here, this means that this program can be run multiple times in parallel; when changing the program parameters (the base frequency, the number of harmonics or the note duration), we can create a polyphonic composition. For instance, changing the note duration to a very similar value (e.g., 202ms) and running a new shred creates a phasing effect recalling the early works of Steve Reich (albeit with much harsher sine tones).
We can read more about the claim of ChucK being “strongly timed” in the 2015 paper ChucK: A Strongly Timed Computer Music Language by Ge Wang, Peter R. Cook and Spencer Salazar, published in the Computer Music Journal. Some of the authors’s preoccupations match ours very closely; they claim early in the paper that control over time in programming languages is underrepresented. Their solution, in addition to adding time and duration types in the language, also brings some ideas from synchronous programming languages, a topic that we will come back to in the next part in this series).
As seen in the example above, now
represents the current time. ChucK programs are synchronous: each instruction runs infinitely fast and time does not move forward until a
new time is assigned to now
. This seemingly fantastical claim can be achieved by introducing
a logical time, separate from actual physical time: logical time is perfectly predictable and
accurate; and the ChucK runtime handles mapping logical time into physical time. Note that while a
live performance would require that advancing logical time by 1 second means actually running for 1
second, logical time can progress at a different rate in offline situations (e.g., bouncing to an
audio file could happen much faster than in real time).
Requiring the programmer to explictly yield (by advancing time with the chuck operator) to allow other shreds to run is known as non-preemptive concurrency, since a running shred cannot be preempted until it yields). This is not a new idea and is usually seen as cumbersome and suboptimal, but it also allows accurate and deterministic timing, which preemptive concurrency does not. In ChucK however, this is considered acceptable since the programmer already needs to think about time and scheduling anyway.
While the comments in the example prompt the user to start multiple shreds through the Web IDE,
more complex programs create new shreds automatically through the spork~
operator. The ChucK
runtime therefore consists of a virtual machine that includes a scheduler (actually called shreduler) that picks one shread at a time and runs it until it yields, which means suspending the
shred until it is time to resume it: scheduling simply consists in keeping a list of shreds sorted by
the time at which they need to be started or resumed. Time actually moves while shreds are suspended
as the VM also constantly computes new samples from the current graph to produce the audio output of
the program.
For musicians, one benefit of the timing model of ChucK is that unlike a lot of its peers it prevents the language from having to distinguish between an audio rate (very fast, as tens of thousands of samples need to be generated every second for every output channel) and a control rate (i.e., the rate at which parameters of the audio graph can change, generally orders of magnitude slower than the audio rate). This allows implementing algorithms that can work one sample at a time, as well as describing musical gestures that can span seconds, minutes, hours or more. But outside of the realm of computer music, ChucK introduces a lot of compelling ideas about how to handle time and concurrency as well as a straightforward runtime model that could be adapted to other domains. ⚅⚂