Composing Music in Scheme
(library 'music) provides an interface to compose and play MIDI-sequenced
play a demo song using
(demo 'songs). Once the demo is loaded, you can load
one of the demo songs using
(demo-song <song-name> [times] [tempo]), where
times defaults to 1 and the default tempo depends on the song. You can also
play a song in the same format as one of the demos from GitHub Gist with
(gist-song <gist-id> <song-name> ...). To play a song from your local files,
(local-song <song-name> ...), where
song-name is both the name of the
file and the name of the song.
Song Source Code:
Note: My music theory knowledge is extremely limited. Some of these terms may not mean the same thing in actual music theory. I'm just using them here to refer to concepts in writing music with Scheme.
A beat is one musical quarter-note. The Scheme music library has no concept of time signatures to change this. The tempo of playback is defined in terms of beats per minute.
An instrument is a data abstraction that can be one of several types. The only one you should create directly is a playable instrument, which can be press or release notes immediately, or schedule them for later playback. The library uses other types of instruments to record a sequence of notes for later playback or to count the total number of beats in a song.
A note is a single musical note that can be played. It can be given in either MIDI numerical notation or scientific pitch notation as a string or symbol (e.g.
A song is defined as a Scheme procedure that takes in one or more instruments as arguments and plays notes on only those instruments (i.e. it does not play any notes on instruments other than its arguments). A song may pass its instruments to other songs to compose them together. Songs should generally not be passed instruments directly, as the timing for this is not precise (it may work for simple songs, but multiple instruments will quickly get out of sync).
A track is a song that takes a single instrument as its argument. Songs with more than one instrument should generally pass each instrument to a track in parallel.
A composition is a Scheme source file in the format given below to allow
(demo 'songs)to load and play a song efficiently.
This is not comprehensive of the functions available in the music library, though they should be enough to write most songs.
(instrument <name> [gain] [release])
(instrument <name> [gain] [attack] [decay] [sustain] [release])
Creates a new playable instrument from the given name's soundfont with the given options. Gain is the volume, which defaults to 1. Attack, decay, sustain, and release are better defined elsewhere. They default to 0.01, 0.1, 0.9, and 0.3 respectively. In general, release should be higher when you want notes to blend together an lower when you don't.
Returns a playable instrument for percussion. Unlike other instruments, each "note" for this instrument plays a different percussive instrument. See here for a list.
(shift <instrument/song> <shift>)
Takes an instrument or song and returns one that shifts all played notes up by the given number of MIDI notes. There are 12 MIDI notes in an octave.
(shift <instrument> 12)
(shift <instrument> -12)
Takes multiple instruments and combines them into a single instrument. Any note played on this new instrument will be played on every instrument passed in.
(inst-press <instrument> <note>...)
Begins playing one or more notes on an instrument. It's usually easier to use
inst-play unless there are overlapping staggered notes on the track.
(inst-release <instrument> <note>...)
Stops playing one or more notes on an instrument. It's usually easier to use
inst-play unless there are overlapping staggered notes on the track.
(inst-rest <instrument> <beats>)
For playable instruments, pauses execution for a number of beats in the instrument's tempo. For recording instruments, this advances their timer by a number of beats.
(inst-play <instrument> <beats> <note>...)
Begins playing one of more notes on an instrument, rests for a number of beats and then stops playing the previously started notes on the instrument. Most songs will call this (possibly through a helper function) frequently.
(inst-seq <instrument> <beats> <note>...)
Plays multiple notes on the same instrument for the same number of beats in
sequence. Helper function that calls
(inst-play <instrument> <beats> <note>)
for each note.
(rhythm <instrument> <beats> <note or list of notes>...)
Designed for use with the percussion instruments. Each list of notes (or single note) will be played evenly spaced over the given number of beats. An empty list will play nothing at that time.
(repeat <times> <code>)
Macro that runs the given code a given number of times.
Macro that wraps each
expr in a procedure and then calls each one on a
separate thread of execution. Note that only one thread will run at any given
time, but if any threads are paused (perhaps by
inst-rest), others can run.
Blocks until all threads complete.
Useful to make multi-instrument songs technically work by running each track
in parallel, though it is more accurate to play songs using
(play-sequence <songs> <sequence> [tempo=120] <inst>...)
songs should be a JS object mapping keys of the form
song-<id> to song
sequence should be a list of song ids (from the keys) to be played
play-sequence will first record each song, storing the recording
songs. It will then replay the songs in
ensures that each song only needs to be recorded once if there are songs in a
sequence that repeat. If you pass the same
songs object to
again, it won't need to do any recording.
play-sequence will pause the
audio context, stop any scheduled notes, schedule its own notes, and then
resume the audio context.
(play-sequence-dry-run <songs> <sequence> <num-insts>)
play-sequence, except that it only records the songs; it doesn't
actually play them on any instruments.
(repeat-list <times> <item>)
Returns a list with
(repeat-song <times> <song> [tempo=120] <inst>...)
Helper function to call
(play <song> [tempo=120] <inst>...)
(repeat-song 1 <song> [tempo=120] <inst>...)
Pauses the audio context so that no music plays until resumed.
Resumes the audio context after being paused.
(count-beats <song> <num-instruments>)
Runs a song with special instruments that count how many beats have been played on them. Returns a list of numbers representing the number of beats played on each instrument after the song is run. Songs should generally play the same number of beats on each instrument, so this is useful for debugging songs.
Every song file should start with the line:
(if (not (bound? 'soundfont)) (library 'music #f))
This loads the music library only if it hasn't already been loaded, since doing so requires fetching the soundfont-player library from the network.
The song file should then define names for its preferred instruments to be
nil if they are not already defined.
sarias-song defines the following:
(if (not (bound? 'ocarina)) (define ocarina nil)) (if (not (bound? 'english-horn)) (define english-horn nil)) (if (not (bound? 'pizzicato-strings)) (define pizzicato-strings nil))
The song file should include a procedure called
play-<song-name> with the
(define (play-<song-name> . args) (define times (if (null? args) 1 (car args))) (define tempo (if (= (length args) 2) (car (cdr args)) 140)) (parallel (if (null? <inst>) (begin (display "Loading <inst>...\n") (set! <inst> (instrument "instrument MIDI name" options...)))) ... repeat for each instrument declared above) (if (not (bound? 'songs)) (define songs (js-object))) (js-set! songs "song_<id>" <song>) ... repeat for any additional song parts (play-sequence songs <list of song parts to play in order> tempo <instruments>))
This format is not the most efficient, as it always loads all notes for an instrument, even if you're only playing a few. See the demo songs for examples of a structure that loads instruments efficiently.