Professional Documents
Culture Documents
Elm and The Algorithm of Music - Leif Battermann PDF
Elm and The Algorithm of Music - Leif Battermann PDF
! Home
" About
# Impressum
# Datenschutzerklärung
0 COMMENTS
It got me really excited about the idea of porting this to Elm and to be able to
use this in web applications.
In the following we will see the core data types and algorithms from Euterpea
ported to Elm. To focus on the core concepts the implementation is stripped
down to the minimum that is required to transcribe and perform an existing
polyphonic piece of music (for a single instrument).
type Primitive
= Note Duration Pitch
| Rest Duration
A Duration is expressed as a Float and represents the note value (the relative
duration of a note). A quarter note e.g. can be defined by the expression 1/4 or
the literal 0.25, an eighth note by 1/8 or 0.125, and so on. I prefer using Float
over a - potentially more accurate - type for rational numbers because Float is
simple and easy to work with in Elm.
Pitch is an integer that represents the number of semitones from C-1 (five
octaves below C4 or Middle C). So C-1 corresponds to pitch 0, and C4 to 60, A4 to
69, and so on. This convention is also used by most MIDI implementations.
With the Primitive type we can now define lots of primitive musical values.
However, we cannot yet express how they should be composed together to
form a larger - more complex - musical value.
There are two fundamentally di"erent ways of combining notes (and rests).
They can be played either in sequence, or simultaneously. Let's implement this
as a custom type:
type Music
= Prim Primitive
| Seq Music Music
| Par Music Music
Seq takes two arguments of type Music that are played in sequence.
Par takes two arguments of type Music that are played in parallel.
Note that the definition of Music is recursive which allows us to easily combine
musical values with each other to form larger structures that again can be
composed together, and so on.
Example
m : Music
m =
Seq
(Par (Prim (Note (1 / 4) 60)) (Prim (Note (1 / 4)
(Par (Prim (Note (1 / 4) 62)) (Prim (Note (1 / 4)
The first thing we need for better syntax is a constructor for a note that returns
a value of type Music:
-- etc. ...
-- ...
Likewise it is useful to have constructors for durations (and rests), that will not
be listed here for the sake of brevity.
Example
With these helper functions the example from above becomes much more
readable:
readable:
qn : Duration
qn =
1 / 4
m : Music
m =
Seq
(Par (c 4 qn) (e 4 qn))
(Par (d 4 qn) (f 4 qn))
{-| Create a rest where duration specifies the length of the pause.
-}
rest : Duration -> Music
rest duration =
Prim (Rest duration)
_ ->
music
Transcribing music
In only a few lines of code we have defined a data structure - and some
convenient functions - that is powerful enough to represent a complex musical
score.
Here is the sheet music for the first 28 bars of Children's Songs No. 6:
Base line
Let's start with the base line (the le! hand voice).
The base line has a lot of repetitions. It consists of only three di"erent phrases
that are each repeated several times which is a perfect fit to apply the times
function.
dqn : Duration
dqn =
3 / 8
bassLine : Music
bassLine =
let
b1 =
line [ b 2 dqn, fs 3 dqn, g 3 dqn, fs 3 dqn ]
b2 =
line [ b 2 dqn, es 3 dqn, fs 3 dqn, es 3 dqn ]
b3 =
line [ as_ 2 dqn, fs 3 dqn, g 3 dqn, fs 3 dqn
in
line [ times 3 b1, times 2 b2, times 4 b3, times 5 b1
Melody
The encoding of the melody follows the same principles:
melody : Music
melody =
let
( en, qn, hn ) =
( 1 / 8, 1 / 4, 1 / 2 )
( dhn, triplet ) =
( 3 / 4, (2 / 3) * (1 / 8) )
in
line
[ line
[ times 3 (line [ a 4 en, e 4 en, d 4 en, fs 4
, cs 4 (dhn + dhn)
]
, line [ d 4 dhn, f 4 hn, gs 4 qn, fs 4 (hn + en),
, line [ as_ 3 en, cs 4 en, fs 4 en, e 4 en, fs 4
, line [ d 5 en, cs 5 en, e 4 en, rest en, as_ 4 en
, line [ d 5 en, cs 5 en, e 4 en, rest en, as_ 4 en
, line [ fs 4 en, cs 4 en, e 4 en, cs 4 en, a 3 en
, line [ graceNote 2 (d 4 qn), cs 4 en, graceNote
, line [ fs 4 en, a 4 en, b 4 (hn + qn), a 4 en, fs
, line [ cs 4 triplet, d 4 triplet, cs 4 triplet,
]
The melody also has some repetitions in the beginning which is a good fit
for times.
The nested line applications give more structure and help readability.
Alteratively all the values could have been in one large single list.
Putting it together
song : Music
song =
Par bassLine melody
Performance
With the types and functions we have created so far we are able to store
musical information in a data structure. But we cannot really do much more
with it. We especially do not have a mechanism, yet, to interpret the music
values as actual audible sound.
time is the point in time (in seconds, relative to the beginning of the
musical piece) when the note should be played
( xs, [] ) ->
xs
( x :: xs, y :: ys ) ->
if compare x < compare y then
x :: merge compare xs (y :: ys)
else
y :: merge compare ys (x :: xs)
Next we define Tempo, a type alias to hold information about tempo which
consists of the number of beats per minute and the beat duration:
( events2, duration2 ) =
musicToMEvents (currentTime + duration1
in
( events1 ++ events2, duration1 + duration2 )
( events1 ++ events2, duration1 + duration2 )
( events2, duration2 ) =
musicToMEvents currentTime tempo music2
in
( merge .time events1 events2, max duration1 duration2
Parameters
currentTime of type Time is the state (or context) that represents the
current time, and that is threaded through the computation. The initial
value is the starting time of the music performance. When musicToMEvents
is called recursively the time is updated if necessary.
tempo of type Tempo is used to calculate the scale factor metro to convert
relative durations (expressed in beats) into absolute durations (expressed in
seconds).
Return value
Function body
The main part of the interpretation of a music value is a pattern match with
four cases:
3. Seq music1 music2: Two music values that are played in sequence are
interpreted recursively one at a time. However, the start time of the second
value is the end time of the first which is calculated by adding the absolute
duration of the first music value to the current time.
In the sequential case the results are combined by appending the list of
events and adding up their durations.
4. Par music1 music2: Two music values that are played in parallel are also
interpreted one at a time. Contrary to the sequential case the start times of
two music values are the same.
In the parallel case the results are combined by merging the lists of events
(with the previously defined merge function) and by determining the
maximum of the two durations.
For convenience we define a function perform that sets the time to begin the
performance to 0 and returns only the music events:
Example
Here is interpretation result of the example from above as a list of music events
(serialized as JSON):
[
{ "time":0, "pitch":64, "duration":0.5 },
{ "time":0, "pitch":60, "duration":0.5 },
{ "time":0.5, "pitch":65, "duration":0.5 },
{ "time":0.5, "pitch":62, "duration":0.5 }
]
To interact with the WebAudioFont JavaScript library we have to use ports and
we have to encode a music event as a JSON value:
Here are the ~30 lines of JavaScript code that uses the WebAudioFont API and
produced real audible sound in the browser.
app.ports.play.subscribe(function (events) {
const fenderRhodes = 45
const volume = 90 / 127
const info = player.loader.instrumentInfo(fenderRhodes)
player.loader.startLoad(audioContext, info.url, info.variable
player.loader.waitLoad(function () {
player.cancelQueue(audioContext)
events.forEach(function (event) {
const time = startTime + event.time
player.queueWaveTable(
audioContext,
audioContext.destination,
window[info.variable],
time,
event.pitch,
event.duration,
volume
)
})
})
return false
})
})
So without further ado, let's listen to the excerpt from Chick Corea's Children's
Songs No. 6. Just hit the the Play button!
PLAY STOP
Note that the output is produced by the Elm application embedded in this
post. This is not prerecorded.
Summary
In roughly 200 lines of pure Elm code (with no other dependencies than
Json.Encode) we defined the types and functions that make it pleasant to
work with quite complex musical values.
With a little bit of JavaScript that music can actually be made audible in the
browser.
All of this is based on very sound concepts and the work that went into the
Haskell Euterpea library. And there are a lot of things that we haven't covered
in this article.
I'd love to turn this into an Elm package at some point. If you have any ideas for
potential use or application, or any other suggestions I'd love to hear them!
0 Comments https://blog.leifbattermann.de/ !
1 Login
LOG IN WITH
OR SIGN UP WITH DISQUS ?
Name
ALSO ON HTTPS://BLOG.LEIFBATTERMANN.DE/