FORMULA lets you define Forth words that play music. For example, you might define a word row-tune that plays "Row, Row, Row Your Boat":
:ap row-tune c c c d e 5$ e d e f g r 6$ +c g e c 4$ g f e d c r 6$ ;ap
You can type this directly to the Forth interpreter, or store it in a file and use eload or fload to compile it (see Appendix A). In either case, typing row-tune plays the tune.
The words c, d, etc. push numbers representing the corresponding pitches. r pushes a zero, representing "no pitch" (rest). 4$, 5$ and 6$ take pitch numbers from the stack and play the notes in sequence. Each note is played by sending a MIDI key-down command, pausing for a certain amount of time, then sending a MIDI key-up command. :ap and ;ap are like Forth's : and ; except they add a vocabulary containing c, 4$, etc. to the search list.
Notes are .5 seconds long by default. The following plays the tune with the correct rhythm:
:ap row ::tsg 2/4 /8.16 /4 /8.16 /8.16 2/4 4/4 /8.16 /8.16 2/4 ;;tsg row-tune ;ap
The code between ::tsg and ;;tsg generates a sequence of rhythmic durations (2/4 is two quarter notes, /8.16 is a dotted eighth and sixteenth, etc.). The ::tsg ... ;;tsg construct creates a new "timing sequence generator" process that executes this code. The main process continues and executes row-tune.
Now suppose we want to play the tune as a four-voice round, using a different MIDI channel and patch for each voice:
:ap row-round /1 ::ap 0 to $channel bass row-tune ;;ap rest ::ap 1 to $channel violin row-tune ;;ap rest ::ap 2 to $channel trumpet row-tune ;;ap rest ::ap 3 to $channel flute row-tune ;;ap ;ap
The ::ap ... ;;ap construct creates a new process executing the enclosed code. The main process creates four of these, pausing for one measure between them. The vocabulary standard-MIDI-defs contains definitions for bass, violin, etc. based on the standard MIDI patch numbers. If your synthesizer uses different patch numbers, you can create a directory containing your own versions.
In the above examples, response to keyboard input is delayed while music is playing, because the main process (the Forth interpreter) is tied up. This can be changed by creating a separate process to play the music:
:ap async-row ::ap" Row Your Boat" ::gp row-round ;;gp ;;ap ;ap
After you type async-row, the keyboard immediately responds to further input. If you type async-row several times quickly, you can get multiple copies of the round going at once. You can do anything you want while processes are playing music: edit files, or (on the Macintosh) run other applications. Type
to get a list of processes, showing their names and ID numbers. ::ap" is a variant of ::ap that assigns a name to the process it creates. Type
to get rid of all processes.
The ::gp construct creates a "group" to contain processes and/or other groups, allowing them to be manipulated as a unit. Suppose, for example, that the ID number of a group (as shown by .all) is 5. You can type
5 .gp 5 suspend 5 resume 5 kill
to examine, stop, start and get rid of the processes in the group.
Finally, here's a program that, when you hit a key on your MIDI keyboard, plays "Row, Row, Row Your Boat" transposed to that key. This lets you start rounds by hitting the same key every 4 beats.
:ap shifted-row ( key-number velocity --- ) if \ ignore key-ups c - \ get offset to middle C ::ap [ 1 args ] 8<< to $transpose \ and transpose by that amount row ;;ap else drop then ;ap
' shifted-row 1 MIDI-action !
The last line arranges for shifted-row to be called every time a MIDI key is pressed or released; the key number and velocity are passed on the stack. shifted-row ignores key-ups. For key-downs, it creates a process that plays the tune transposed to that key. The line
[ 1 args ]
causes 1 item (the offset, in this case) to be copied to the stack of the new process. The process stores this number in per-process variable called $transpose (1/256-semitone units, hence the 8<<) which is added to the pitches of all notes played by the process.