Monday, March 1, 2010

W.A.Mozart KV331: Rondo a la Turca, played by Arc

This midi file was written by arc:

"too many notes, Mozart!"
Emperor Joseph II, on hearing Marriage of Figaro

Musical notation is outrageously repetitive; it seems a ripe candidate for refactoring into a more concise language. Bach had twenty children, and they all had to earn their keep, so he had no shortage of labour for copying out manuscripts, despite which he nonetheless went blind before his death in 1750. So I want to help you make some great music in arc without having to write out zillions of notes all over the place, risking your eyesight in the process. I'm sure you'd like to

  (play:with-feeling moonlight-sonata)
(play:vigorously mozart-rondo)
(play:quite-seriously art-of-fugue)

as much as I would. Traditional notation is the assembly language of music, it has been waiting centuries for arc! Step aside, Beethoven; out of my way, Chopin ... s-exps FTW!!1!

Here's what I have so far:

  1. A mini-language for expressing music. ("Language" is a bit of a grandiose term for a ragtag collection of functions and macros)

  2. A converter to convert expressions in this language into midi events.

  3. A writer to write midi events to a midi-format file.

  4. A midi player (rainbow-dependent) that calls a java api to play music.

Only the player depends on rainbow; the rest is vanilla arc.

Rondo a la Turca is low-hanging fruit for this kind of exercise; it's a short piano piece, there is a lot of duplication, which would make for easy compression were it not for the slight variations in each repetition. For example, the second passage consists of

- these are identical except for the hanging half-note at the end of the first version. More insidiously, here are the two variations on the third motif:

It's the same tune, but the first variation specifies a series of notes to be played as octave chords; the second variation specifies the same series of notes, played alternately with their octaves. But I can't express this as a simple transformation; the differences between the passages are not completely consistent.

I'm going to describe how it all works; I'm assuming Dear Reader knows as much about midi as I did when I started, which was approximately nothing, so forgive me if I am mistaken and/or you spot boring bits.

Simple representation

Write arc-music by creating lists of notes. This is what a note looks like:

  (note volume duration . options) ; options specify grace notes, staccato, and other decorations

A chord is a list of notes to be played simultaneously:

  ( (note1 volume duration) (note2 volume duration) )

Music is a sequence of chords.

  ( ((note1 volume1 duration1) (note2 volume2 duration2)) ((note3 vol3 dur3) (note4 vol4 dur4)) etc... )

Single notes are expressed as a chord of one note. Hence, the C Major scale

  (assign c-major-scale
(mono (c4 mp crotchet) (d4 mp crotchet) (e4 mp crotchet) (f4 mp crotchet)
(g4 mp crotchet) (a4 mp crotchet) (b4 mp crotchet) (c5 mp crotchet)))

or alternatively,

  (assign c-major-scale
(apply mono (map [_ mp crotchet] (list c4 d4 e4 f4 g4 a4 b4 c5))))

mono converts a sequence of single notes into a sequence of single-note chords, returning this for c-major-scale:

  (((60 55 16)) ((62 55 16)) ((64 55 16)) ((65 55 16)) ((67 55 16)) ((69 55 16)) ((71 55 16)) ((72 55 16)))

Midi representation

With the above simple representation, the start-time for each note is the sum of all preceding note-durations. Midi doesn't work like this though; it requires note-on and note-off events, each with an explicit time offset, measured in "ticks". To convert from arc-notation to something more closely resembling midi, use make-music:

  (assign music (make-music 0 c-major-scale))

Most music makes ample use of simultaneous notes belonging to different voices; it would be a pain to attempt to express these as chords. Separate voices out this way:

  (assign moonlight (make-music 0 moonlight-sonata-right-hand
0 moonlight-sonata-left-hand))

The 0 specifies the midi channel to use, and make-music generates something like

  ( (0 note-on 0 60 64)  (4 note-off 0 60)  (4 note-on 0 62 64)  (8 note-off 0 62)
(8 note-on 0 64 64) (12 note-off 0 64) (12 note-on 0 65 64) (16 note-off 0 65)
(16 note-on 0 67 64) (20 note-off 0 67) (20 note-on 0 69 64) (24 note-off 0 69)
(24 note-on 0 71 64) (28 note-off 0 71) (28 note-on 0 72 64) (32 note-off 0 72))

This list is very close to the structure of an actual midi file, except here I use absolute tick offsets, whereas midi stores delta tick offsets from each event to the next. Items in this list have the following structure:

  (tick-offset event-type channel key velocity)

tick is the offset in ticks from the start of the music when this event should occur; you control the tempo of your music by varying ticks-per-second. Event-type is note-on or note-off (for now; more types coming later); channel specifies the midi channel, so you can play several instruments simultaneously; key corresponds to pitch for most instruments, and velocity to volume (the loudness of a piano keystroke depends on the velocity with which it is struck).

Facing the music

If you're using rainbow, you can pass this list of note-events to play-sequence and actually hear something come out of your speakers.

  (play-sequence (make-music 0 c-major-scale))

You could play more interesting stuff by combining voices:

  (play-sequence (make-music 0 violins-1-part
0 violins-2-part
1 viola-part
2 cello-part
3 trombone-part
4 trumpet-part
5 oboe-part
6 flute-1-part
6 flute-2-part
7 percussion-part))

I haven't tried anything this complex yet. The play-sequence function supports tempo control - I haven't worked that into the midi writer yet.

Depending on your OS and version of java, you may hear music, or something resembling a flatulent wasp. The wasp finally quit my machine after the java update for MacOS in December last year.

The music notation "language"

Having transcribed only two small pieces of keyboard music, I haven't made much language - so far there are only a few simple elements.

Pitch definitions

c1 d1 e1 f1 ... all the way up to f8 g8 a8 b8. Each of these symbols defines a function that creates the corresponding arc-music note.

  (c4 quarter quiet) 

returns (60 4 48). Sometimes you need to transform a given starting note, so

  (c4 'transform 2)

returns a function that is identical to d4

The "four-note sequence"

It quickly becomes tedious to write out individual notes, even if they have lovely names. It is the nature of music that small sequences are repeated, perhaps transposed, inverted, or extended. In this example,

four of the six groups of notes are painfully close to being the same thing. Wouldn't it be nice to use a function to create that shape,
taking a pitch and duration parameter? So instead of repeating (mono (c4 mf quaver) (d4 mf quaver) (e4 mf quaver) (f4 mf quaver)), you can write (s2/4/5 c4 mf mf quaver). The 2/4/5 represent the semitone interval count between the first note of the sequence and each subsequent note.

Here are a couple of examples:

(s-2/-3/-2 b5 f)
(s1/3/0 f5 f)

The sX/Y/Z functions are generated from a simple macro invocation. This

2 3 5
2 4 -3
2 4 0)

generates the functions s/2/3/5, s/2/4/-3, s/2/4/0. So it is trivial to extend the vocabulary of this language for other sequences. There is no three-note-sequence macro yet, but Rondo didn't need it. (BWV 147 will, so it's coming)

Volume control

amp transforms the loudness of notes in a sequence. For example ((amp 20) c-major-scale) makes our C Major scale 20 midi-velocity-units louder. loud and quiet are shortcuts for (amp 16) and (amp -16), respectively.

((crescendo 20 8) c-major-scale) transforms the scale by applying a smooth crescendo over the sequence of 8 notes.

An example

The result is pretty ugly. I'm not sure how to improve it; music is at least two-dimensional, code is mostly unidimensional. Compare the musical notation for the opening motif

with the arc version:

  (def rondo-theme (cresc) (+
(s-2/-3/-2 b4 80)
(mono (c5 100 2 'staccato) '(pause 2))
(s-2/-3/-2 d5 80)
(mono (e5 100 2 'staccato) '(pause 2))
(s-2/-3/-2 f5 80)
((if cresc (crescendo 20 8) idfn) (repeat-list 2 (s-2/-3/-2 b5 80)))
(mono (c6 (if cresc 120 100) 4))))


In terms of compression, here are the stats:

  • rondo.arc: 2586 tokens, 346 lines
  • rondo-left-hand: 848 items
  • rondo-right-hand: 1129 items
  • total: 1977 items, where an item corresponds roughly to a note in a traditional printed score.
  • rondo-music: 5699 midi events (half of them being 'note-on events, the other half note-off)
  • resulting midi file, not using any size optimisations: 22864 bytes.

So there are somewhat fewer arc tokens than there are note-on events. It doesn't feel like victory yet though, because s/2/4/5 isn't jumping off the page and ringing bells for me. It's not exactly Emily Howell yet (having a day job and a family is likely to delay progress on that goal). You have ideas for making this better; I'm all ears.

Get the code

I don't know why you'd want to do this: the api is still unstable, and its use is error-prone and frustrating.

arc/lib/midi/midi.arc Collection of functions and macros for declaring music
arc/lib/midi/rondo.arc Defines rondo-music, which can subsequently be passed to a midi writer or player
arc/lib/midi/midi-writer.arc Defines write-midi-file which outputs your music in midi format (format type 0, the simplest). Supports the bare minimum
for playing music - note-on and note-off events. No support for any other event type yet.
arc/rainbow/midi/midi.arc (rainbow-dependent) Defines play-sequence, which uses a java api to play midi music, and
rondo, a convenience function to load dependent libraries and invoke (play-sequence rondo-music)

The fastest way to get this running on your machine is to clone rainbow from github, and run either rainbow or scheme from your working copy.

Playing Rondo from rainbow is as easy as


Write it to a midi file like this

  (write-midi-to "rondo.midi" rondo-music)