Composing Music in Scheme

(library 'music) provides an interface to compose and play MIDI-sequenced songs in Scheme using the soundfont-player JavaScript library. You can 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, use (local-song <song-name> ...), where song-name is both the name of the file and the name of the song.

Song Source Code:

Terminology

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.

API

This is not comprehensive of the functions available in the music library, though they should be enough to write most songs.

instrument

(instrument <name> [gain] [release])

or

(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.

percussion

(percussion)

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

(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.

octave-up

(octave-up <instrument>)

Helper for (shift <instrument> 12)

octave-down

(octave-down <instrument>)

Helper for (shift <instrument> -12)

combine

(combine <instrument>...)

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

(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

(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

(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

(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

(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

(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

(repeat <times> <code>)

Macro that runs the given code a given number of times.

parallel

(parallel <expr>...)

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.

play-sequence

(play-sequence <songs> <sequence> [tempo=120] <inst>...)

songs should be a JS object mapping keys of the form song-<id> to song functions. sequence should be a list of song ids (from the keys) to be played in order. play-sequence will first record each song, storing the recording instruments in songs. It will then replay the songs in sequence. This 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 play-sequence 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

(play-sequence-dry-run <songs> <sequence> <num-insts>)

Like play-sequence, except that it only records the songs; it doesn't actually play them on any instruments.

repeat-list

(repeat-list <times> <item>)

Returns a list with times items.

repeat-song

(repeat-song <times> <song> [tempo=120] <inst>...)

Helper function to call play-sequence on song repeated times.

play

(play <song> [tempo=120] <inst>...)

Helper for (repeat-song 1 <song> [tempo=120] <inst>...)

pause-music

(pause-music)

Pauses the audio context so that no music plays until resumed.

resume-music

(resume-music)

Resumes the audio context after being paused.

count-beats

(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.

Song Format

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 following structure:

(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.