Evanstein MAT Thesis SCAMP
Evanstein MAT Thesis SCAMP
Evanstein MAT Thesis SCAMP
Santa Barbara
SCAMP:
Suite for Computer-Assisted Music in Python
Master of Science
in
Media Arts and Technology
by
Committee in charge:
July 2019
SCAMP:
Suite for Computer-Assisted Music in Python
Copyright
c 2019
by
ii
To the Making and Breaking of Rules
iii
Acknowledgements
The work presented here owes a great deal to my colleagues and friends at the UC
Santa Barbara Departments of Music and of Media Arts and Technology. In particular,
I owe a special thanks to the members of my committee: Clarence Barlow, Curtis Roads,
and Karl Yerkes. The many unique perspectives on music and music-making that I
encountered at UC Santa Barbara were crucial to identifying and understanding the
musical problems addressed in this work.
On a personal level, the support of my family and friends, and above all of my
wife Emily, lies behind every significant undertaking I accomplish. Our cat Minuet also
deserves mention as the inspiration for the cute, yet roguish acronym ”scamp”.
iv
Abstract
SCAMP:
Suite for Computer-Assisted Music in Python
by
This document consists of two papers describing the SCAMP (Suite for Computer-
Assisted Music in Python) framework for music composition. The first — and more
substantial — of these papers outlines the framework as a whole, discussing its motivating
principles and design goals, stepping through the key features of its API, and situating
it within the context of other tools for computer-assisted composition. The second paper
goes into further detail about the sub-package of SCAMP for managing the flow of musical
time, entitled clockblocks.
The Code
Both SCAMP and clockblocks are hosted on PyPI (Python Package Index), where
instructions for installation and links to the source code can be found:
https://pypi.org/project/scamp/
https://pypi.org/project/clockblocks/
v
Contents
Abstract v
1 SCAMP:
A Suite for Computer-Assisted Music in Python 1
1.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.1.1 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.1.2 Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.1.3 Designed for Flexibility . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2 Introductory Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.2.1 ”Hello World” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.2.2 Duration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.2.3 Generating Notation . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.2.4 Quantization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.3 Details and Further Possibilities . . . . . . . . . . . . . . . . . . . . . . . 14
1.3.1 Multi-Part Music . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.3.2 Multi-Tempo Music . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.3.3 Skipping Forward in Time . . . . . . . . . . . . . . . . . . . . . . 19
1.3.4 Envelopes and Continuous Parameters . . . . . . . . . . . . . . . 19
1.3.5 Playback Implementations . . . . . . . . . . . . . . . . . . . . . . 24
1.3.6 Additional Note Properties . . . . . . . . . . . . . . . . . . . . . . 26
1.4 Conclusions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
1.4.1 Directions for Further Development . . . . . . . . . . . . . . . . . 28
1.4.2 Evaluation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
vi
2.3 Parallelism . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
2.3.1 Parallel Clocks Example . . . . . . . . . . . . . . . . . . . . . . . 37
2.3.2 Parallel Clocks Implementation . . . . . . . . . . . . . . . . . . . 38
2.4 Compensating for Calculation Time . . . . . . . . . . . . . . . . . . . . . 40
2.5 Nested Clocks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
2.5.1 Tempo Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . 41
2.5.2 A Nested Tempo Example . . . . . . . . . . . . . . . . . . . . . . 43
2.6 Comparison with other approaches . . . . . . . . . . . . . . . . . . . . . 46
2.7 Directions for Further Development . . . . . . . . . . . . . . . . . . . . . 47
2.8 Conclusions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Bibliography 50
vii
Paper 1
SCAMP:
A Suite for Computer-Assisted
Music in Python
1
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
1.1 Introduction
1.1.1 Motivation
Consider a composer who wishes compose a piece for string quartet based on climate
data. Having downloaded the data in CSV format, they wish to process it and experiment
with different mappings by ear. Finally, having crafted their preferred mappings into an
overarching musical form, they wish to output some preliminary notation, and then
reshape the result by hand in their preferred score-writing software.
Or consider a composer who wants to write a piece for instrument and electronics
where a simple mass-spring simulation generates a stream of glissandi, which simultane-
ously drives a modular synthesizer and results in written notation for the instrumentalist.
Or consider a composer who wishes to write an algorithmically generated piano con-
certo in which the piano and orchestra follow separate accelerating and decelerating
tempo curves. The pianist requires a score notated from the point of view of the piano’s
tempo curve, while the conductor requires a score notated from the point of view of the
orchestra’s tempo curve, perhaps with a renotated piano part for reference.
What these scenarios have in common is the translation of musical data between
different domains. In particular, all three contend with the transition between the con-
tinuous domain of sounding music and the discrete (and idiosyncratic) domain of notated
music.
These considerations were the driving forces behind the creation of SCAMP (Suite
for Computer-Assisted Music in Python), a GPL3.0-licensed pure-Python framework for
music composition, which is the subject of this paper.
2
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
1.1.2 Overview
3
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
• SoundFont-based [8] playback via pyFluidSynth [9]. (SCAMP handles the pitch-
bends and channel management required for microtonality and glissandi, hiding
such complications from the end user.)
The goal of the SCAMP framework is to act as a hub that connects the composer
to other resources, while offering as open-ended a mental model as possible. As such, it
does not aim to offer utilities, such as generative toolkits or scalar / harmonic models,
which align with particular aesthetic approaches. Such utilities may be imported from
third-party libraries, or may be constructed by the composer. (The scamp extensions
package also exists as a repository for such functionality.)
The basic features and limitations of SCAMP are summarized in Table 1.1.
Fig. 1.1 illustrates both the internal and external dependency structure of SCAMP.
The packages in yellow represent external dependencies. These dependencies connect
SCAMP to various forms of musical input and output: LilyPond notation (via abjad),
SoundFont interpretation and playback (via sf2utils [11] and pyFluidSynth [9]), MIDI
input/output (via python-rtmidi [12]), and OSC input/output (via pythonosc [13]).
The packages in blue together comprise the SCAMP suite. The scamp package itself
contains the main functionality of the suite, with time management (clockblocks), param-
4
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
Features Limitations
• Compose in continuous time, quan- • Not designed for direct sound syn-
tize to notation in discrete time thesis
eter shaping and control (expenvelope), and MusicXML export (pymusicxml) separated
out into self-contained packages. Note that clockblocks relies on expenvelope, since the
tempo curve of a clock derives from the Envelope class.
This modular structure, inspired by the Unix Philosophy, was chosen for two key
reasons:
• Discardability and reusability: not all composers will need all of the functionality
that SCAMP has to offer. For instance, some may be interested only in polyphonic
5
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
pyfluidsynth rtmidi
midi input and
SoundFont playback
output streams
sf2utils
interpreting
abjad SoundFont files
P
M
CA scamp
S
Main Functionality
clockblocks pymusicxml
Time Management MusicXML Export
expenvelope
Parameter Management
tempo control or direct MusicXML export. By isolating these components from the
rest of SCAMP, composers with different goals can potentially incorporate them
into other frameworks.
This second point is an important one: every composer develops a unique workflow,
reflecting their unique set of aesthetic concerns. For most, this ultimately means patching
together a variety of tools. When tools are bundled together, this patching process can
become cumbersome. SCAMP’s modular structure allows composers to use (and perhaps
repurpose) only that which is relevant to them.
One final aspect of SCAMP that provides enormous flexibility for the composer is
that it is situated within the broader Python ecosystem. This affords:
• Easy access to a wide variety of libraries that can be adapted for musical purposes,
such as data processing (e.g. numpy, sci-py, matplotlib) and machine-learning (e.g.
tensorflow, scikit-learn) toolkits.
6
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
• In particular, access to the many forms of data input and output available through
both standard Python and third-party libraries.
Taken together, the goal of all of these design choices to create a framework that is
as adaptable as possible to the needs of different composers and compositions.
As an introduction to the SCAMP API, we begin with a program that plays a short
arpeggio:
The result of this program will be the sound of a violin playing a C major arpeggio, at
max volume, over the course of two seconds. The first step, after importing the SCAMP
namespace, is to create a Session object. Most SCAMP programs will start in this
7
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
way, as the Session object is the central hub through which most of the functionality of
SCAMP flows. In the same way that a session in a DAW encompasses tracks, transport,
and recording functionality, the Session object in SCAMP inherits from — and thereby
combines the functionality of — an Ensemble, a master Clock, and a Transcriber (see
Figure 1.2).
Ensemble
Hosts and manages shared
settings and resources
for ScampInstruments
Clock
Session
Manages and coordinates
multiple streams of time
at different tempi
Transcriber
Records notes played
to a Performance object
for later reuse or notation
The Ensemble functionality of the Session object — i.e. its role as a host of the
instruments being used — is employed in the third line, where a new violin part is
created and stored in the violin variable. By default, new parts created in this way will
play back via pyfluidsynth using a General MIDI Soundfont, with fuzzy string matching
being used to find an appropriate preset based on the name of the part. Finally, the call
to play note takes three arguments: MIDI pitch (where 60 = Middle C), volume (on a
scale from 0 to 1), and length in beats.
1.2.2 Duration
One might reasonably ask at this point how long a beat is. As mentioned above,
Session inherits from the Clock class, which is defined in clockblocks, a subpackage of
8
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
SCAMP detailed in a separate paper [14]. When a Session object (or Clock object
more generally) is constructed, it becomes the default clock used on the current thread,
and is also assigned a default tempo of 60 BPM, or one beat per second. By way of
these defaults, when play note is called in above example, it looks to see which clock is
operating on the current thread, finds the session (s) we created, and plays the note for
0.5 beats on that clock (which corresponds to 0.5 seconds at the default tempo of 60).
In this way, the whole four-note arpeggio lasts two seconds.
We can play back the arpeggio faster or slower simply by setting the session’s tempo
attribute. For instance, if we add the following line directly after the second line in the
above example, the arpeggio will last twice as long:
s.tempo = 30
Calls to play note are blocking by default, not moving on to the next line until the
note has finished. This conforms to musical expectations (playing a note takes time), and
is a model of musical time similar to that used by the Euterpe language [15]. However,
SCAMP also allows the user to start and end notes manually. In fact, internally, a call
to violin.play note(60, 0.7, 1.5) is essentially equivalent to:
(Note that, like the play note function, the wait function captures the current clock
from context and takes a number of beats as an argument.)
Finally, in addition to the options above, it is also possible to call play note in a non-
blocking manner by setting the ”blocking” keyword argument to False. For instance,
9
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
the following would play two notes, each lasting two seconds, that overlap by one second:
violin.play_note(60, 1, 2, blocking=False)
wait(1)
violin.play_note(64, 1, 2)
The default tempo of 60 BPM was chosen because it allows the composer to think
in terms of durations in seconds if so desired. This exemplifies a guiding philosophy
of the SCAMP framework: musical parameters are treated as continuous during the
compositional process unless explicitly quantized. It is only later on, when converting
the music to a score, that quantization becomes a necessity.
In order to generate notation from the above examples, we use the third function of
the Session object: its role as a Transcriber. The purpose of a Transcriber is to keep
10
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
track of any notes played by the instruments that have registered with it, and to save
the result as a Performance, which is essentially a note-event list. This performance
can then be quantized and converted into a Score, which is capable of saving either to
LilyPond (via the abjad library) or to MusicXML (via pymusicxml, a part of SCAMP).
The advantage of first transcribing the music as a Performance and then converting it to
a Score is that the performance exists in continuous time and parameter space, outside
of notational constraints. In fact, Performances can be replayed, either in part or in
whole, with the same instruments or with different instruments, at the same tempo or
at a different or changing tempo, and can even be re-transcribed after such alterations.
To transcribe the first example above and convert it to music notation, we would do
the following:
s = Session()
violin = s.new_part("Violin")
11
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
a) b)
c)
d)
This results in the notation shown in Fig. 1.3a. When show is called, the score
representation within SCAMP is converted to an abjad score, which then outputs and
compiles LilyPond code and displays the result as a PDF. It is also possible to instead
call show xml, which uses pymusicxml to export a MusicXML document and opens the
result in a score editor (e.g. MuseScore, Sibelius, Finale).
By default, the to score function quantizes the music to 4/4 time. If a different time
signature is desired, one merely has to provide it as an argument:
performance.to_score("3/8").show()
This results in Fig. 1.3b. A looping or non-looping list of time signatures can also be
provided, as can a list of beats on which to place bar lines.
1.2.4 Quantization
Things get more interesting when we use random floating-point durations, since some
quantization is required:
12
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
performance = s.stop_transcribing()
performance.to_score("3/4").show()
This results in Fig. 1.3c. By default, divisors of the beat up to 8 are allowed, which
may be more complex than desired. Simpler results can be achieved by using a custom
QuantizationScheme that limits the max divisor to 4:
performance.to_score(
QuantizationScheme.from_time_signature(
"3/4", max_divisor=4
)
).show()
13
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
in [16]). The user can also specify a relative weighting of onset vs. termination error.
Total Total
NOTE NOTE NOTE
DIV. Onset Term.
OFF ON OFF
Error Error
2
3
4
5
6
One weakness of this approach is that it does not allow nested tuplet structures.
A possible future development, therefore, would be the incorporation of the Q-Grid
approach proposed by Nauert [17], which allows for such structures, taking into account
their degree of complexity.
We now consider in more depth the range of possibilities for playback and notation
that SCAMP offers.
The above examples, for simplicity, were all single-part; however, writing multi-part
music is straightforward:
14
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
oboe = s.new_part("oboe")
bassoon = s.new_part("bassoon")
s.start_transcribing()
# start the oboe and bassoon parts as
# two parallel child processes
s.fork(oboe_part)
s.fork(bassoon_part)
15
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
This results in the notation seen in Fig. 1.5. The fork method of the Session
object, inherited from the clockblocks Clock class, takes a function as a parameter and
runs it as a parallel child process. There is considerable flexibility here: child processes
can spawn their own child processes, and so on. Unlike a naive approach to parallelism
using Python’s built-in threading module, all processes forked in this way will remain
tightly coordinated, with the clock also compensating for calculation time.
Figure 1.5: Example of notated multi-part music in SCAMP, having been exported
as MusicXML and imported into MuseScore.
16
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
s = Session()
trumpet = s.new_part("trumpet")
trombone = s.new_part("trombone")
17
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
This results in the notation shown in Fig. 1.6a. Within the context of a session-wide
acceleration from 60 BPM to 100 BPM, the clock for the trumpet part slows down to
half speed. (Note that, in SCAMP, “rate” and “tempo” are alternate names for the same
underlying property, just with different units: a tempo of 60 BPM corresponds to a rate
of 1, and a tempo of 120 BPM corresponds to a rate of 2, etc. Tempo units are valuable
for their musical connotations, while rate units are generally more comprehensible in the
context of nested clocks.)
43
= 60.0 ( = 65.1) ( = 70.6) ( = 76.6) ( = 83.1) ( = 90.0) = 100.0
accel. ( = 97.3)
7
trumpet
43
trombone
b.)
Figure 1.6: The same music quantized to two different clocks, having been exported
as MusicXML and imported into MuseScore.
s.fast_forward_to_time(900)
Similar to the process of “nesting” described by Smoliar [15], this causes the program
to execute as rapidly as possible up to the appointed time, while skipping note playback
calls. In this way, the program is in exactly the same state as it would have been had it
run normally.
The accelerandi and decelerandi in section 1.3.2 are an example of the continuous
shaping of a parameter over time, in this case tempo. As touched on in section 1.1.3, each
Clock manages it tempo using a TempoEnvelope, a subclass of Envelope, the piecewise-
exponential envelope class defined in the separate package expenvelope.
Instances of Envelope are used broadly in SCAMP when any musical parameter is
19
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
changing over time, such as in glissandi or dynamic playback. They can also be employed
by the composer for the larger-scale shaping of algorithmic processes.
A simple, evenly-spaced Envelope can be constructed and plotted as follows:
This results in the plot shown in Fig. 1.7 (plotting is done using matplotlib).
Evenly Spaced
72
70
68
66
64
62
60
Each segment of the envelope can furthermore be assigned a duration and a shaping
attribute:
e = Envelope.from_levels_and_durations(
[60, 72, 66, 70], [3, 1, 1],
curve_shapes=[2, -2, -2])
e.show_plot("Uneven with Shaping")
This results in the plot shown in Fig. 1.8. The values in the curve shapes attribute
range from −∞ to ∞, with 0 being linear, negative values corresponding to early change,
20
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
70
68
66
64
62
60
0 1 2 3 4 5
The Envelope class is equipped with a range of utilities, including integration (needed
by TempoEnvelope to determine the length in time corresponding to a certain number
of beats at a changing tempo), approximation of arbitrary functions, and mathematical
operations, such as addition, multiplication, division, and concatenation.
Glissandi
instrument.play_note(e, 1.0, 4)
This results in the notation shown in Fig. 1.9. Note that the glissando is scaled to
the length of the note, and that, by default, the pitch of the glissando is renotated on
every beat and at every local maximum or minimum of the curve. In this case, since the
segment durations are in the proportions 3:1:1, this results in quintuplets.
21
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
Figure 1.9: Glissando notation resulting from passing an Envelope as the pitch argu-
ment to play note, as compiled by LilyPond, via abjad.
Since directly creating an Envelope object each time a glissando is desired is would
be cumbersome, any list given as the pitch argument to play note is interpreted as an
envelope:
If segment durations and curve shapes are desired, a list consisting of [values, dura-
tions, curve shapes] may be used:
Microtonality
The reader may have noticed quarter-tone accidentals in Fig. 1.9. Microtonality in
SCAMP is as simple as using floating-point values for pitch. When using SoundFont-
based playback via pyFluidSynth (which utilizes the MIDI protocol) or a MIDI stream to
an external synthesizer, SCAMP internally handles all pitchbend messages. Since these
messages are channel-wide, notes that change pitch (or might change pitch) are placed
on separate channels, with channels being recycled automatically.
If more specificity of pitch is desired in the notation, the user simply has to turn on
22
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
engraving_settings.show_microtonal_annotations = True
piano.play_chord([62.7, 71.3], 1.0, 1)
piano.play_chord([65.2, 70.9], 1.0, 1)
piano.play_chord([71.5, 74.3], 1.0, 1)
Dynamics
An Envelope object (or its corresponding list shorthand) may also be used to apply a
continuous volume curve to a note. For instance, a forte-piano-crescendo can be achieved
in the following way:
fp_cresc = Envelope.from_levels_and_durations(
[1.0, 0.3, 1.0], [0.1, 0.9]
)
piano.play_note(60, fp_cresc, 4)
Since dynamic notation is more subjective than pitch notation, arbitrary parameter
curves like this are not currently notated. This is an area for future development.
23
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
As mentioned in Section 1.2.2, play note internally breaks down into separate calls
to start note, wait, and end. With continuous changes of parameter, further calls are
involved. For instance, consider the following:
While a call to play note is clearly more succinct, there are situations where the
length and course of the note are not known in advance, and which are therefore better
suited to the second approach. This is especially true with the incorporation of live input.
In the preceding examples, all playback is done via pyfluidsynth using a default Gen-
eral MIDI soundfont. However, one of the major flexibilities built into SCAMP is its
variety of available playback implementations, as well as the ability to define custom
implementations.
24
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
s = Session()
25
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
SoundfontPlaybackImplementation
piano
(ScampInstrument) MIDIStreamPlaybackImplementation
s synth OSCPlaybackImplementation
(Session) (ScampInstrument)
and ”synth/change pitch”) will be sent to port 57120. Finally, when a note is played
using “silent”, nothing will happen, since it has no PlaybackImplementations. (Silent
instruments can nevertheless be useful: for instance, as a reference, one might want
to notate the pitch collection being used by an algorithmic process without generating
sound. Or one could use a silent instrument to aggregate the notations of several sounding
instruments using different SoundFont presets, which might come in handy for, say, a
violin part using multiple bow techniques as well as pizzicato.)
The play note function in SCAMP can take a fourth, optional properties argument.
This argument acts as a wildcard, accepting a properly formatted string or a dictionary
specifying a variety of playback and notation details:
This produces the notation shown in Fig. 1.12. In addition to modifying the notation,
articulations like staccato and tenuto will affect playback duration, and accents will affect
volume. Exactly how these notations affect playback is an adjustable (and disableable)
setting with sensible defaults.
The properties argument may also be used to determine note spelling, either directly
or by suggesting a key:
Figure 1.13: Notes spelled both directly and by specifying a key context
The properties argument can also be used to define aspects of playback other than
27
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
Any keys in the properties argument starting with the prefix "param " or
ending with the suffix " param" are interpreted as additional playback parame-
ters. In the case of an OSCPlaybackImplementation this results in outgoing OSC
messages with address patterns “vibrato inst/change parameter/vib depth” and “vi-
brato inst/change parameter/vib freq”. As with dynamics playback, translating this to
some form of notational output is an area for future development.
1.4 Conclusions
As mentioned above, some form of notation for dynamics and other user-defined
playback parameters is needed. For dynamics, a distinction will need to be made between
28
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
note-level dynamics (such as sfz or fp) and phrase-level dynamic spans (such as cresc.
or dim.). The easiest approach will likely be to allow specific dynamic notations such
as these to be defined and applied to notes or phrases, influencing both notation and
playback, as opposed to trying to infer notation from playback (which is the general
approach taken in SCAMP). For other parameters, some form of graphical notation
incorporating the shape of the envelope curve may be appropriate. In general, since the
Performance class retains continuous-time musical data, alternate mappings to different
kinds of score are possible.
This points to the value of expanding the options for translating musical data. Cur-
rently, a Performance can be converted to a Score, but not vice-versa. Likewise, a Score
can be translated to an abjad/LilyPond or MusicXML representation, but not vice-versa.
One development goal is to enable two-way translation between all of these representa-
tions. Also, although SoundFont playback and streaming MIDI output are possible, it
is not currently possible to convert a Performance to a MIDI file, or to directly save
playback to a sound file. These are planned improvements.
In terms of playback, a medium-term goal is to offer SFZ-file-based playback using
LinuxSampler [18]. As a modern, open standard for sampled sound playback, SFZ files
offer exactly the kind of flexibility that SCAMP is designed to afford [19]. Another
playback goal is to enable certain note properties, such as “pizzicato”, to trigger a preset
switch, much as they do in professional score-writing software, such as Sibelius. Finally,
MAX/MSP and SuperCollider abstractions are currently being developed for receiving
OSC output from SCAMP.
One last area of development is in the domain of live input. Although SCAMP is
not designed for live coding, the ability to receive real-time MIDI and OSC input, and
to thereby shape the result of an algorithmic process, would be of great value to many
composers’ workflow.
29
SCAMP:
A Suite for Computer-Assisted Music in Python Paper 1
1.4.2 Evaluation
Although all of the planned developments above are worthy goals, the most important
next step is to create music with SCAMP and to cultivate a broad and varied user-base.
This will allow any future development to be guided by the true obstacles that composers
face while using this framework.
To this end, I plan to lead workshops and classes devoted to exploring computer-
assisted composition using SCAMP, as well as to develop detailed online documentation
and tutorials. SCAMP was developed and re-designed over a period of many years,
based on my own compositional practice. Its true potential, however, can only be discov-
ered through interaction with other composers, approaching it with potentially radically
different compositional aims.
30
Paper 2
2.1 Introduction
2.1.1 Context
In recent years there has been a proliferation of interest in, and tools designed for,
computer-assisted music composition. Among the options available, one might broadly
distinguish between domain-specific languages, such as SuperCollider or Max/MSP, and
frameworks that operate within general-purpose programming languages, such as abjad
[3] or jMusic [20]. While both approaches have advantages, one major advantage of
31
Clockblocks: A Pure-Python Library for Controlling Musical Time Paper 2
• To provide facilities for flexible and extensible note playback, e.g. via FluidSynth
or over OSC. (Effortless microtonality and glissandi have been built in.)
32
Clockblocks: A Pure-Python Library for Controlling Musical Time Paper 2
A key value underlying the development of SCAMP is that of modularity and adher-
ence as much as possible to the Unix Philosophy. For instance, the MusicXML export
capability is available separately as pymusicxml, the flexible musical Envelope class is
available separately as expenvelope, and the system for managing musical time is avail-
able separately as clockblocks. It is this last library that is the subject of this paper.
2.1.2 Goals
Clockblocks arose to address several recurring problems with the scheduling of note
playback events and the recording of note event data in Python:
1. The time.sleep function from the Python Standard Library has limited accuracy,
especially for longer wait times.
4. A system for controlling and modulating tempo is needed, ideally one allowing for
multiple independent streams operating simultaneously.
33
Clockblocks: A Pure-Python Library for Controlling Musical Time Paper 2
clock = Clock(initial_tempo=60)
def log_timing():
print(
"Beat:", clock.beats(),
"Time:", round(clock.time(), 2),
"Tempo:", round(clock.tempo, 2)
)
34
Clockblocks: A Pure-Python Library for Controlling Musical Time Paper 2
Note that the faster the tempo, the less time advances for a given beat, and the
slower the tempo, the more time advances. The speed of a clock can be set using any of
three interrelated properties: its rate, its beat length, and its tempo. These are defined
as follows:
R = 1/Lb (2.1)
T = 60 · R = 60/Lb (2.2)
Where Lb represents beat length and is measured in seconds (at least on a top level
35
Clockblocks: A Pure-Python Library for Controlling Musical Time Paper 2
clock), R represents rate and is measured in in beats per second, and T represents tempo
and is measured in beats per minute. Setting any one of these properties for a clock
automatically sets the other two. In some ways, rate and beat length are the most
natural descriptors, especially when clocks are nested inside of each other. However,
tempo is retained as a property because of its associated musical intuition.
2.2.2 Implementation
Example TempoEnvelope
2.5
2.0
1.5
Beat Length
1.0
0.5
0.0
0.0 2.5 5.0 7.5 10.0 12.5 15.0 17.5 20.0
Beat
Figure 2.1: Graph of the clock’s TempoEnvelope from the example above
Although the user is likely thinking in terms of rate or tempo, internally the tempo
36
Clockblocks: A Pure-Python Library for Controlling Musical Time Paper 2
envelope is based on beat length for ease of calculations. Figure 2.1 shows this beat
length curve for the above example; when the tempo jumps to 120 bpm on beat 4, the
beat length cuts to 0.5, and then from beat 8 to beat 16 it slowly increases to 2 during
the ritardando. The TempoEnvelope class keeps track internally of the current beat, and
whenever the user calls clock.wait(beats), the area under the curve is integrated from
the current beat forward to the destination beat to determine the associated wait time
in seconds.
2.3 Parallelism
The above example featured a single stream of musical time. However, the true
strength of clockblocks lies in its ability to coordinate multiple parallel streams of time,
as in the following example:
master = Clock()
37
Clockblocks: A Pure-Python Library for Controlling Musical Time Paper 2
master.fork(child1)
master.fork(child2)
master.wait_for_children_to_finish()
In this example we create a master clock and define two parallel processes, child1
and child2. The first function prints every beat until beat 6, while the second prints
every third of a beat until beat 3. We then fork these functions on the master clock. The
results are as follows:
Note that the child functions take a single argument which gets passed a handle to
the clock being forked. It is also possible to call get_current_clock() to capture the
clock running at any given moment.
Whenever a child clock calls wait, it registers a wake up time with its parent and then
pauses execution until the parent rouses it. The parent clock maintains a cue of wake up
38
Clockblocks: A Pure-Python Library for Controlling Musical Time Paper 2
calls from its children, and whenever it calls wait, it looks to see if there is a child clock
with a wake up time in the near future so that it can rouse it at the appropriate time.
An example sequence of events with both parent and child starting at t = 0 might be
as follows:
t = 0:
- Child calls wait(0.5), registers wake up time of
0.5 with parent.
- Parent calls wait(1), sees that a child is set to
be woken up at t = 0.5, and so waits instead for
0.5 beats.
t = 0.5:
- Parent wakes up and rouses child
- Child wakes and calls wait(1.0), registering a
wake up time of 1.5 with parent.
- Parent sees no other child wake events during the
rest of its wait of 1, and so sleeps for the
remaining 0.5.
t = 1.0:
- Parent wakes from its sleep, calls wait(2) this
time, and sees that a child has registered a wake
up time of 1.5. As a result, it waits 0.5 second.
t = 1.5:
- Parent wakes up and rouses child.
- Child wakes up and chooses to terminate process.
- Parent sees no other child wake events during the
rest of its wait of 2, and so sleeps for the
remaining 1.5.
t = 3.0:
- Parent wakes, sees it has no children, suffers
from empty nest syndrome.
39
Clockblocks: A Pure-Python Library for Controlling Musical Time Paper 2
When a forked function reaches the end of its execution, the child clock associated
with it is terminated. In the example in Sec. 2.3.1, the master clock, acting as the
parent to both child clocks, calls wait_for_children_to_finish, which causes it to
wait indefinitely, rousing its child clocks at the appropriate times until all children have
finished execution.
Note that child clocks can fork their own child clocks, and so on. In this case, a clock
may find itself in the role of both parent and child, waking its children at the appointed
times, and registering a wake up call with its parent whenever it wishes to wait itself.
Only a master clock, a clock with no parent, actually calls time.sleep (or rather, the
more precise version explained in Sec. 2.1.2). All other clocks simply register a wake up
call with their parent when they wish to sleep.
One of the initial problems that clockblocks was designed to solve was the
fact that, unless compensated for in some way, any calculation time on a thread
will cause that thread to slow down relative to the sum of all of its calls to
time.sleep. It should be clear from the above that this problem is already solved
for all but the master clock, since wake up times are absolute and will not drift. It only
remains to ensure that the master clock itself takes calculation time into account.
When a master clock wakes up, it immediately notes down the current time. Then,
after all relevant calculations have taken place and a new wait call is made, it refers back
to the time that it originally woke up in determining how long to sleep.
In some cases, when calculations are intensive and the wait time is short, it may
already be past the the desired wake up time, and the clock finds itself running behind.
At this point there are two main options:
40
Clockblocks: A Pure-Python Library for Controlling Musical Time Paper 2
1. Allow the clock to stay behind and handle subsequent wait times as faithfully as
possible.
2. Try to catch up in future calls to wait by not waiting at all until the clock has
caught up.
Both options are available in clockblocks. The first is termed a “relative” timing
policy (since it emphasizes keeping individual wait times as accurate as possible), while
the latter is termed an “absolute” timing policy (since it emphasizes not drifting from the
absolute time at which events should have occurred). If the playback from clockblocks
is being coordinated in any way with that of an external application, an absolute timing
policy would likely be preferred; if not, a relative policy may be preferred.
The default used by clockblocks is actually a third, hybrid approach. This policy
allows for time to be shaved off of subsequent calls to wait, but only to a certain degree.
Thus, rather than catching up all at once, the clock catches up in small increments. In
many cases this is sufficient to remain faithful to the absolute times at which events
should occur without distorting subsequent wait times noticeably.
The true power of clockblocks as a library lies in the fact that each clock, regardless
of its place in the family hierarchy, is allowed to have and manipulate its own tempo.
However, the actual speed at which a clock runs depends not only on its own tempo, but
also on the tempo of its parent, and its parent’s parent, etc. all the way on up to the
master clock.
41
Clockblocks: A Pure-Python Library for Controlling Musical Time Paper 2
To understand this better, consider that a clock has two different views on the passage
of time: what beat it is on and how much actual time has passed. As we have seen above,
these two properties are related by the clock’s beat length; time passed is the integral of
beat length with respect to beats passed.
In clockblocks, each clock inherits its sense of time from its parent; a beat in the
parent clock constitutes a “second” of time in the child clock. The word “second” here is
in quotes, because unless the clock in question is the master clock, it is not a true second,
but rather a second as filtered through temporal distortions of its parents.
For instance, in Figure 2.2, we consider three generations of nested clocks: a master
clock running at rate 1/2, its child (e.g. the result of a call to fork) running at rate 3,
and its child’s child, running at rate 1/4. Note that the child’s sense of time is inherited
from the master clock’s beat rate, and the grandchild’s sense of time is inherited from
the child’s beat rate. Thus is should be clear from this picture that the tempi of clocks
in a parent / child relationship multiply. The absolute rate of the grandchild clock – its
rate with respect to wall time – is the product of its own rate, its parent’s rate, and its
parent’s parent’s rate.
MASTER time: 0 1 2 3 4 5 6 7 8
rate = 1/2
absolute rate
tempo = 30
beat: 0 1 2 3 4 = 1/2
beat length = 2
CHILD time: 0 1 2 3 4
rate = 3 absolute rate
tempo = 180 = 1/2 * 3
beat: 0 1 2 3 4 5 6 7 8 9 10 11 12 = 3/2
beat length = 1/3
Figure 2.2: Relationship between the rates of three “generations” of clocks running
at different tempi
42
Clockblocks: A Pure-Python Library for Controlling Musical Time Paper 2
What is not depicted in the above example is that each clock can in fact be smoothly
changing rate according to manipulations of its tempo envelope. The actual amount of
wall time corresponding to a wait of, say, two beats in the grandchild clock is calculated
by first integrating for two beats under the grandchild clock’s tempo envelope, then taking
the result and integrating for that many beats under the child clock’s tempo envelope,
and then taking that result and integrating for that many beats under the master clock’s
tempo envelope.
The following example will server to illustrate how nested clocks can follow different,
but interacting, tempo envelopes. It also introduces several new features for shaping a
clock’s tempo over time:
master = Clock()
child_1 = master.fork(child_process_1)
child_2 = master.fork(child_process_2)
master.set_rate_target(3, 15)
master.set_rate_target(1, 40, truncate=False)
master.wait_for_children_to_finish()
Here, we create a master clock and spawn two child processes. One of these pro-
cesses follows a sinusoidal tempo envelope, which we create by calling clock.apply_
tempo_function. (Internally, this tempo function is being approximated by exponential
curve segments, since all tempo envelopes are piece-wise exponential.) The other clock
instead calls apply_tempo_envelope to define this piece-wise exponential curve directly.
Since the loop flag has been set to True, the tempo envelope repeats for as long as the
clock is alive.
The master clock itself changes tempo over the course of the example, going to a rate
of 3 over the course of 15 beats and back to a rate of 1 after 40 beats have passed. Note
that the truncate flag has been set to False in the second call to set_rate_target; by
default when a rate/tempo/beat length target is set, any existing targets are cut off, but
by setting the truncate flag to False, the first target remains in place.
After running the code above, the following lines will generate the plots shown in
Figure 2.3:
44
Clockblocks: A Pure-Python Library for Controlling Musical Time Paper 2
Figure 2.3: Effect of a master tempo envelope of the absolute tempo envelopes of its
children. Note that, unlike in Figure 1, these graphs are of tempo, rather than the
beat length.
As Figure 2.3 illustrates, the tempo envelope of the master clock affects those of the
two child clocks. The plots in the middle column show the tempo envelopes of the child
clocks with respect to their parent (the master clock), and show the sinusoidal variation
and explicitly defined tempo envelope described above. On the other hand, the plots in
the right column show their absolute tempo envelopes, i.e. their tempos with respect to
wall time, having been altered by the acceleration and deceleration of the master clock.
Before we leave this example, it should be pointed out that the child clocks call wait
with the additional keyword argument units="time". What this does is instruct the
clock to wait however many beats will correspond to 40 units of time, (which is the same
as 40 beats in the parent clock). This affords the ability to coordinate with the parent
clock; for instance, in this case the master clock has been instructed to accelerate and
decelerate over the course of 40 beats, which will be the exact same length as 40 units of
time in the two child processes.
45
Clockblocks: A Pure-Python Library for Controlling Musical Time Paper 2
Notice also that time units have been specified for the sinusoidal tempo function
defined on the first child clock. This is why, in the graph of its tempo envelope, the
peaks are wider than the troughs; this is a graph of tempo with respect to beats, and it
will take more beats to cover a given amount of time at a faster tempo than at a slower
tempo. On the other hand, the tempo envelope applied to the second child clock is in
the units of beats (which is the default), so the graph appears undistorted.
In contrast to audio programming environments like Gibber [23] and ChucK [24],
clockblocks does not concern itself with the audio thread directly, or with sample accu-
racy. As the time management engine of SCAMP, clockblocks is designed for scheduling
events at the rate of notes and sound objects. The actual production of sound samples
happens externally, for instance via FluidSynth or OSC messages to some other external
instrument. Temporal precision is of course desired, but sample accuracy is not necessary.
Clockblocks does bear some similarity to ChucK in its syntax, however: the user
performs operations, sets up processes and then then advances time. As in ChucK,
subprocesses can be forked, and these subprocesses can themselves fork subprocesses.
However, clockblocks makes use of nested tempo relationships in a way that ChucK does
not, or at least not natively.
SuperCollider [2] provides the ability to control multiple independent streams of
tempo via the TempoClock object. In some ways, clockblocks also resembles Super-
Collider in its server/client dichotomy; the client language that is in charge of scheduling
is separate from the process of generating audio samples. However, it is not possible in
Supercollider to nest TempoClocks, and any accelerandi and ritardandi must be accom-
plished through rapid incremental changes of tempo.
46
Clockblocks: A Pure-Python Library for Controlling Musical Time Paper 2
Thus, the main contribution of clockblocks is that it combines the nested structure
of an environment like ChucK with the ability to smoothly manipulate tempo at any
layer of this structure. It also provides this functionality in Python, a general purpose
programming language that offers access to a vast array of packages from a wide variety
of disciplines, and one that has at present a dirth of options for managing musical time.
Although in its current implementation clockblocks does not offer sample accuracy (nor
is this necessary for its role within SCAMP), the nested tempo approach presented here
has the potential for broader application, including some contexts (like the scheduling
of microsonic events) where sample-accuracy would be desired. Therefore, one natural
direction for further work would be to translate this system to a language like C++,
using it generating a cue of sample-clock timestamped events. Even within its current
Python implementation, one planned development is to allow time-sensitive playback
events, such as OSC messages or calls to an external synthesizer, to be scheduled on a
queue for later dispatching by an audio callback (for instance, via a PyAudio’s PortAudio
bindings).
Another area of clockblocks currently being developed is the ability to specify and
synchronize rhythmic phase. Here we take inspiration from “Tempocurver” [25], devel-
oped by Matthew Wright in collaboration with composer Edmund Campion, as well as
from CNMAT’s subsequently developed Max / MSP external, “Timewarp” [26].
As we have seen, clockblocks makes it possible to coordinate clocks so that they
reach specific tempi at specifically appointed times. However, it may also be musically
important to coordinate rhythmic phase; for instance, we may want two clocks that are
following different tempo curves to land on the beat at the exact same moment in time.
47
Clockblocks: A Pure-Python Library for Controlling Musical Time Paper 2
This requires fine tuning of the relationship between beats passed and time passed,
Time Passed
Figure 2.4: Illustration of the relationship between curvature and the length (in time)
of an accelerando.
As Figure 2.4 illustrates, the relationship between the number of beats passed and
the amount of time passed during a segment of a TempoEnvelope is mediated not only
by the start and end tempo, but also by the curve shape. In the illustrated accelerando,
by varying the curvature, we can adjust the amount of time passed to anywhere between
1.5 and 4 seconds. Thus, if we wished for this 2 beat accelerando to last precisely 3
seconds, we would need only to find the appropriate curve shape.
By using adjustments of this nature, it should be possible to specify the desired metric
phase (in terms of time, or beats in the parent clock) at the end of an accelerando or
ritardando.
48
Clockblocks: A Pure-Python Library for Controlling Musical Time Paper 2
2.8 Conclusions
Reviewing the initial goals of clockblocks, it is hopefully now clear to the reader that
the system described:
2. Takes efforts to compensate for calculation time, and has an intelligent system for
adjusting when calculation time lasts longer than an intended wait time.
3. Keeps track of multiple interconnected threads of musical time and ensures that
these threads remain in lockstep with one another.
4. Allows for complex control and modulation of tempo, and creates the potential for
nested tempo relationships.
49
Bibliography
[7] H.-W. Nienhuys and J. Nieuwenhuizen, LilyPond, a System for Automated Music
Engraving, Proceedings of the XIV Colloquium on Musical Informatics (2003).
[10] A. Freed and A. Schmeder, Features and Future of Open Sound Control version
1.1, in Proceedings of the 2009 New Interfaces for Musical Expression (NIME)
Conference, 2009.
50
[11] O. Jolly, “sf2utils.” https://gitlab.com/zeograd/sf2utils, 2018.
[12] C. Arndt, “python-rtmidi.” https://github.com/SpotlightKid/python-rtmidi,
2019.
[13] attwad (username), “python-osc.” https://github.com/attwad/python-osc,
2018.
[14] M. Evanstein, Clockblocks: A Pure-Python Library for Controlling Musical Time,
in Proceedings of the 2019 International Computer Music Conference, (New York,
New York, USA), 2019.
[15] S. W. Smoliar, A Parallel Processing Model of Musical Structures, .
[16] C. Barlow, On Musiquantics: Von der Musiquantenlehre translated. Royal
Conservatory The Hague, 2012.
[17] P. Nauert, A Theory of Complexity to Constrain the Approximation of Arbitrary
Sequences of Timepoints, Perspectives of New Music 32 (1994), no. 2 226–263.
[18] “The Linux Sampler Project.”
[19] “The SFZ Format.”
[20] A. Brown, Making Music with Java. Andrew R. Brown, May, 2009.
[21] M. Evanstein, “Scamp: a suite for computer-assisted music in python.”
https://github.com/MarcTheSpark/scamp, 2018.
[22] SuperCollider 3 Documentation Contributors, “Env — supercollider 3.10.0 help.”
http://doc.sccode.org/Classes/Env.html, 2018.
[23] C. Roberts, M. Wright, J. Kuchera-Morin, and T. Höllerer, Gibber: Abstractions
for Creative Multimedia Programming, in Proceedings of the ACM International
Conference on Multimedia - MM ’14, (Orlando, Florida, USA), pp. 67–76, ACM
Press, 2014.
[24] G. Wang, P. R. Cook, and S. Salazar, ChucK: A Strongly Timed Computer Music
Language, Computer Music Journal 39 (Dec., 2015) 10–29.
[25] M. Wright and E. Campion, “Tempocurver.”
https://github.com/CNMAT/CNMAT-Externs/tree/master/java/tempocurver,
2001.
[26] J. MacCallum and A. Schmeder, Timewarp: A Graphical Tool For The Control Of
Polyphonic Smoothly Varying Tempos, in Proceedings of the 2010 International
Computer Music Conference, ICMC 2010, New York, USA, 2010, 2010.
51