KeyKit :: Language Reference Manual

Introduction

KeyKit is a programming language for manipulating and generating music. It was written to support algorithmic and interactive experimentation with music composition and MIDI-compatible equipment. The data types are character strings, integers, floating-point numbers, musical phrases, arrays, functions, and objects. The language was originally inspired by and has many similarities to awk - variables need not be declared, user-defined functions can have arguments and return values of any type, and arrays are associative. Objects can be used to encapsulate data and methods, and object methods can be inherited and delegated. KeyKit is multi-tasking, a feature of critical importance for a responsive and flexible realtime system. Any number of tasks can run simultaneously, interleaved at a low level and sharing a common global variable space. MIDI input is continuously recorded, and MIDI output can be scheduled in realtime while other tasks are running. Fifos are used for all I/O and inter-task communication. A multi-window graphical interface provides a piano-roll representation of music with pop-up menus for invoking actions. KeyKit programs can freely intermix graphics, mouse actions, MIDI I/O, and console I/O. This is a reference manual for the KeyKit language, necessarily terse in an attempt to be complete.

A primary goal of KeyKit has been to build a highly extensible system, therefore the built-in capabilities of the KeyKit language are intentionally minimal. The default user-defined function library contains KeyKit code for an extensive multi-window sequencer-like environment with sliders, buttons, and other tools for creating and editing MIDI music. A separate reference manual and tutorial describe this user interface. This document describes only those capabilities that are actually built into the KeyKit language.

If you're just getting started with KeyKit, this is not the document you want to read first.

Statements

Each KeyKit statement takes one of the following forms:

variable  assignment-operator  expr
if ( condition ) statement [ else statement ]
while ( condition ) statement
for ( statement ; condition ; statement ) statement
for ( variable in array-name-or-phrase-expr ) statement
function { name | ? } [ ( arguments ) ] { statement(s) }
task expr ( arguments )
class name { method name (arguments ) {...} ... }
new name ( arguments )
return ( [ expr ] )
break
continue
#define name (args ) value
#include "filename"
#library "filename" function-name
eval string-expr
delete array-element-or-object
undefine variable
readonly variable
global variable
Semicolons and newlines are statement separators unless escaped with a backslash. Statements can be grouped with braces or a comma. Conditions are expected to be numeric expressions; 0 is false, non-0 is true. Any word beginning with # (except #define and #include) is the start of a comment; all input until the end of that line is ignored. Most binary operators have operator-assignment versions (+=, -=, etc.). Increment (++) and decrement (--) operators are available in both pre- and post- forms. The #define and #include statements operate as in the C preprocessor, including the ability to have macros with arguments. The substitution of arguments into a macro's value occurs even inside quoted strings.

Variables and Data Types

Variable names must begin with an alphabetic character, and if it is an upper-case character, the variable is global. The type of a variable is determined by whatever value is currently assigned to it, and can be changed at any time. There are 7 data types: strings, integers, floating-point numbers, musical phrases, arrays, functions, and objects.

Any variable that is not global (either implicitly, because it starts with an upper-case character, or explicitly, because of a global statement or function) automatically becomes a local variable in the function in which it is encountered. Previous versions of KeyKit required that you "declare" local variables by including them as extra arguments in the parameter list of a function (as in awk). This is no longer required. So, local variables are created merely by using them.

Phrases are ordered collections of notes. Note durations and times are expressed in units of clicks;> there are normally 96 clicks in each beat, although this can be changed. Associated with each note is a pitch (0 to 127), duration (in clicks), volume (0 to 127), MIDI channel (1 to 16), and a time (in clicks relative to the beginning of the phrase). Notes in a phrase are sorted by time, type, pitch, channel, and volume.

Arrays are associative - array indicies can be strings as well as numbers. A musical phrase can be used as an array index, but it is converted to a string in the process. The expression [] creates an empty array. The contents of an array can be initialized with a list of index=value expressions inside the square brackets. For example, the expression [0='c',4='e',7='g'] produces an array with 3 elements whose index values are 0, 4, and 7.

Array values can be manipulated much like any other data value, but it is important to understand that array values have pointer semantics - i.e. an array value does not contain the contents of an array, it points to the contents of an array. For example, arguments to functions are passed "by value", but since an array value is really a pointer, passing an array to a function is essentially passing the contents of the array by reference - any changes to the array elements will be reflected outside the function. Copying an array value to another variable merely makes a copy of the pointer - both values refer to the same array.

Any expression whose type is an array value can precede the square-bracket notation used to refer to individual array elements. For example, if a function foo() returns an array value, the expression foo()[0] could be used to refer to one of its elements. Multi-dimensional arrays are merely arrays whose elements are arrays:


a = []
a["foo"] = [0="hello",1="world"]
print(a["foo"][1])                 # Prints "world".
b = [[0=0,1=1],[0=1,1=0]]          # Creates 2-d array.

Constants

String constants are enclosed in double-quotes, e.g. "hello", and the usual escape sequences (e.g. \n, \t, \b, \r) can be used within them. As a convenience, a b (beat) suffix on an integer number automatically multiplies by the number of clicks in a beat, which is 96 by default. For example, 2b is equivalent to 192.

Phrase constants are enclosed in single-quotes, and consist of note expressions separated by commas and/or white space (these two types of separators are not equivalent, see below). A note expression must start with one of the following:

a,b,c,d,e,f,g
a normal note
p#
a normal note, where # is the MIDI pitch number
r
a rest
which may be followed in any order by these modifiers:
+
(sharp) or - (flat)
o
and an octave number (-2 to 8)
v
and a volume (0 to 127)
d
and a duration (in clicks)
c
and a channel (1 to 16)
t
and a starting time (in clicks relative to the beginning of phrase)
`
and an attribute value (terminated with another grave quote)
Some of the modifiers don't make sense (but are not disallowed) for some note expressions, for example the rest and pitch expressions will ignore the octave modifier. If any of these modifiers is omitted from a note expression, its value defaults to the value of that modifier for the previous note. For example, all the notes in the phrase 'ao2v90,b,f,d' would be in the 2nd octave and have a volume of 90. At the beginning of each constant phrase, the default values are: octave 3, volume 63, duration 96 (the number of clicks in a beat), channel 1.

The separator between notes in a phrase constant determines the default starting time of the next note. A comma separator (possibly surrounded by white space) sets the default starting time to the end of the previous previous note. Hence the phrase 'e,f,g' is equivalent to 'et0,ft96,gt192'. If there is no comma separator (ie. only white space) between notes, the default starting time will be the starting time of the previous note, so the phrase 'c e g' is a chord, equivalent to 'ct0,et0,gt0'. Naturally, an explicit time modifier on a note will override the default starting time implied by the separator.

As a convenience, the o preceding positive octave numbers can be omitted, e.g. b4 is b in the 4th octave. Negative octave numbers must be specified with the o, to avoid ambiguity with the - used for flats.

Normally, the length of a phrase constant is equal to the ending time of the last note. The length can be explicitly set by using an l (lower-case L) followed by the length in clicks. E.g. 'a,b,c,l96' would have a length of 96 clicks (even though some of the notes extend beyond that).

A phrase constant can include arbitrary MIDI bytes by using x followed by hex characters. For example, the constant 'xb07b00' would be a phrase consisting of a 3-byte MIDI message - an all-notes-off for channel 1. MIDI byte messages can also include a time modifier if timing of the message is important. For example, 'xfe,xfet24' is a phrase containing 2 single-byte messages, the second one occurring at click number 24. MIDI bytes can be combined with normal notes in the same constant phrase, e.g. 'e,f,g,xc005,a,b' is a phrase that contains a program change command in the middle of several normal notes.

As a convention for embedding arbitrary textual information in a MIDI message, a KeyKit phrase constant can contain a string enclosed in double quotes, e.g. 'a,b,"hello world",c,d'. This type of note is called a "text note." It is turned into a system-exclusive message, beginning with the bytes f0, 00, 7f, followed by the ASCII characters of the string, and ending with the byte f7.

A normal note implies two MIDI messages, a note-on and a note-off. In some cases, you may want only the note-on or note-off. These can be specified with an initial + (for note-on) or - (for note-off). For example, '+a,-at96' is equivalent (in terms of MIDI output) to 'a'.

Expressions

Expressions can make use of the following operators, listed in order of increasing priority:

&& ||
and, or
| & ^
bit-wise or, and, xor
== != < > <= >= ~~
equal to, not equal to, etc.
<< >>
left shift, right shift
+ -
addition, subtraction
* / %
multiplication, division, modulo
- ! ~
unary minus, not, one's complement
If two strings are combined with the + operator, the result is the concatenation of the strings. If two strings are compared with a relational operator (e.g. == != <= ), an ASCII string comparison is done. The ~~ relational operator can be thought of as a "contains" operator - the result is true if the first operand contains a substring that matches the second operand, a regular expression.

If two phrases are compared with a relational operator, all of their notes are used in the comparison. Two notes are equal only if all of their attributes (pitch, duration, channel, volume, type) are equal.

Phrase Operators

Phrases can be manipulated with the following operators:
phrase + phrase
The result is the concatenation of the 2 phrases in series, using the length (NOT the ending time of the last note) of the first phrase as the starting time of the second phrase.
phrase | phrase
The result is the merging of the 2 phrases in parallel, and the length is the maximum of the 2 lengths.
phrase - phrase
The result is a copy of the first phrase, after removing all notes that match notes in the second phrase.
phrase & phrase
The result contains all notes in the first phrase that exactly match notes in the second phrase.
phrase % number
The result is a single-note phrase containing the n-th note of the first operand, where n is the value of the second operand. For example, 'a,b,c'%2 would be equal to 'bt96'. Notice that the original time of the note is retained. This operator can also be used on the left-hand side of an assignment (e.g. ph%2='c' ) to replace (or delete, if the right-hand side is the null phrase, '' ) a single note of a phrase.
phrase { condition }
The result of this operation (referred to as a select), is a phrase containing all notes for which the given condition is true. The condition is repeatedly evaluated, with the special token ?? being replaced with each note in the original phrase. For example, 'c,d,e,f,g'{??.pitch>'e'} would be equal to 'ft288,g'.

Note and Phrase Attributes

Attributes of phrases and the notes within them can be manipulated with a syntax reminiscent of C structure elements. For example, 'c'.pitch is equivalent to 60, the MIDI pitch value for that note. The valid attributes are:
pitch
MIDI pitch value (0-127).
vol
MIDI volume value (0-127).
chan
MIDI channel number (1-16).
dur
Note duration, in clicks.
time
The starting time of a note, in clicks, relative to the beginning of the phrase in which it resides.
length
The length of a phrase, in clicks. This attribute is independent of the duration and placement of notes within the phrase. It's primary use is in the semantics of the phrase+phrase operation; the starting time of the second phrase is the length of the first phrase.
type
This attribute of a note indicates what type it is (for example, whether it's a note or a sysex message). The possible values are pre-defined constant values, and a list is given below.
number
Within the conditional expression of a select operation, this attribute can be used to refer to the position (starting at 1) of a note within the selected phrase. For example, the expression 'a,b,c'{??.number>2} is equivalent to 'c'.
attrib
This string-valued attribute of a note can be used to store arbitrary user-defined information. To save memory, this feature may not be enabled on all versions of KeyKit.
flags
This integer-valued attribute of a note can be used to store arbitrary user-defined information. By convention, the lowermost bit of this integer is used to identify picked notes in the graphical interface of KeyKit.
Here is the list of possible values for the .type attribute:
NOTE
a normal note, implying a MIDI note-on and note-off
NOTEON
a note-on only, e.g. '+a'
NOTEOFF
a note-off only, e.g. '-a'
CHANPRESSURE
a channel pressure message
CONTROLLER
a controller message
PROGRAM
a program change message
PRESSURE
a pressure message
PITCHBEND
a pitch bend message
SYSEX
a system exclusive message
SYSEXTEXT
a system exclusive "text note" (see above)
POSITION
a song position pointer message
SONG
a song message
CLOCK
a clock message
STARTSTOPCONT
a start, stop, or continue message
MIDIBYTES
an unrecognized sequence of MIDI bytes
The value of an attribute for a multi-note phrase is the average of the attribute values of the individual notes. When used on the left-hand side of an assignment, an attribute expression changes all notes in the phrase. For example, x='a,b,c' ; x.vol = 60 would set the volume of all 3 notes. Increment, decrement, and operator-assignment statements work on each note independently. For example, x='c,d,e'; x.pitch += 2; print(x) would produce 'd,e,f+'. However, the right-hand side is only evaluated once. For example, x='c,d,e'; x.pitch += rand(4); would add the same random value to the pitch of each note. An attribute of a single note within a phrase can be obtained and set by using the % operator. For example, x='c,ed12'; x%1.pitch=x%2.pitch; print(x) would produce 'e,ed12'.

Type Conversions

When used in numeric expressions, strings are converted to numbers. A musical phrase (constant or variable) can also be used in a numeric expression - its value is the pitch value (0 to 127) of its first note or, if it's a non-NOTE note (e.g. PROGRAM or MIDIBYTES), the value of its first byte.

Explicit conversion to a particular type can be done with a built-in function whose name is the same as the type (similar in style to C++). For example, string(4+5) is equivalent to "9", integer(4.9) is equivalent to 4, float("9"+"."+"9") is equivalent to 9.9, and phrase( "'a" + ",b'" ) is equivalent to 'a,b'. Note that when converting a string to a phrase, the value of the string must contain the surrounding single quotes. And, a phrase converted to a string will contain surrounding single quotes. Strings converted to integers can be interpreted as hexidecimal if they include an initial "0x", for example integer("0x40") is equivalent to 64.

Looping and Conditions

As in awk, the for(var in array) construct iterates over the current set of indicies for an array. Since arrays are associative, the values assigned to var are always strings, although if the original index value was a phrase, it can easily be converted back into a phrase with a type conversion: phrase(var). The similar for(var in phrase) construct can be used to iterate over the notes in a phrase; the value of var becomes a single-note phrase, once for each note in phrase.

As in awk, the if(string-expression in array) construct can be used to test whether a particular array element exists, without having the side effect of creating the array element. A similar construct using phrase expressions, if(phrase-expression in phrase-expression), is true if each note in the first phrase is included anywhere in the second phrase. Only pitch is relevant in this test; time, volume, channel, and duration are ignored.

Functions

User-defined functions can have arguments and return values of any type. The name of a function is actually a normal variable whose value can be considered a pointer to the contents of the function. This function pointer can be manipulated like any other value - for example it can be used as a function argument or return value. Any expression whose type is a function pointer can be used to call the function, by following the expression with parenthesis. This code illustrates:

# These functions expect a single note as an argument,
# and return a chord based on it.
function major(k) { return(k|transpose(k,4)|transpose(k,7)) }
function minor(k) { return(k|transpose(k,3)|transpose(k,7)) }
# The return value of randchord() will be a function pointer.
function randchord() {
     if ( rand(2) == 0 )    # True 500f the time.
          return(major)
     else
          return(minor)
}
f = randchord()
f('c')               # Plays either 'c' major or 'c' minor.
randchord()('c')     # Ditto.
A function definition can actually be used in an expression - its value is the new function's pointer value. So, the return(major) statement above could actually be written as:

return( function major(k){return(k|transpose(k,4)|transpose(k,7))} )
An "in-line" function doesn't need a specific name - a ? can be used instead, and a unique function name will be substituted for it. So, another variation would be:

return( function ? (k) {return(k|transpose(k,4)|transpose(k,7))} )

Variable Arguments

There are several mechanisms for handling a variable number of function arguments. First, the built-in argv() function lets you grab individual arguments by position, and nargs() tells you how many arguments there are:

function add(...) {
        sum = 0
        for ( n=0; n<nargs(); n++ )
                sum += argv(n)
        return(sum)
}
The special token ... can be used in an argument list to represent a variable number of arguments, and can actually be "passed" in the argument list of another function call:

function compute(sign,...) {
        sum = add(...)
        return(sum * sign)
}
Arguments can be packed into an array by giving two parameters to argv(). For example, the array returned by argv(3,10) would contain the values of argv(3) up to (but not including) argv(10). Such an array can then be "unpacked" to create an argument list, by using varg(). This lets you store a list of arguments in a single value (the array), to be used later:

function savecall(f,...) {
        # save a function and argument list
        Callfunc = f
        Callargs = argv(1,nargs())
}
function docall() {
        # call the function we saved, with the saved argument list
        Callfunc(varg(Callargs))
}

Function Loading

The #library statement is used to specify the file that contains the definition of a function. By convention, each directory of the Keypath should contain a file named keylib.k which contains #library statements for all of the functions defined by the files in that directory. When KeyKit encounters a reference to an undefined function, it will automatically read the keylib.k files in order to find the file that defines the function.

Variable Control

The statement "undefine variable" causes the specified variable or function name to become undefined. This is usually used to force the re-loading of a function after its source file has changed. The statement "global variable" forces the specified variable to be considered global. There is also a function global() that can be used to do the same thing in an expression. This is often used when you want to use a function name as a value and the function is not yet defined, since otherwise it would be considered a new local variable. The statement "readonly variable" causes the specified variable to become readonly - any subsequent attempts to change its value will fail. This can be used to protect important functions or variables. The onchange() function can be used to automatically call a function whenever the value of a specified variable is changed. The readonly and onchange features of KeyKit have not been used very much - they may be deleted someday, unless a good use is found for them.

Fifos

Fifos are used for a variety of purposes within KeyKit. A fifo is a first-in-first-out queue of arbitrary KeyKit data values (including array and function pointers). Data values sent to a fifo need not be of the same type. A fifo is created by the open() function, data is inserted with the put() function, and data is retrieved with the get() function. The put() function never blocks - an arbitrary number of data items can be collected in the queue before they are retrieved. The fifosize() function can be used to see how many unread items are in a fifo. The get() function will block if there are no items in the fifo.

Special fifos are used to communicate with the console, mouse, and MIDI. These fifos are automatically opened when KeyKit is booted, and their values are available in the global variables Consolefifo, Mousefifo, and Midiinfifo. For example, this code monitors and prints console input:


for (;;)
        print(get(Consolefifo))
The Consolefifo will return each character typed on the console as a separate string. Another special fifo is the Midiinfifo:

for (;;)
        print(get(Midiinfifo))
Each item read from Midiinfifo will be a single note-on, note-off, or MIDI sysex message. Complete notes will not be seen - if you want to process complete notes, you should make use of the Recorded variable (described later) that collects all MIDI input. In fact, most processing of MIDI input should be done by using the Recorded variable, to avoid the inefficiency of processing each note separately.

The Mousefifo can be read to detect changes in the mouse state:


for (;;) {
        m = get(Mousefifo)
        print("Mouse button state = ", m["button"])
        print("Mouse x,y position = ", m["x"], ",", m["y"])
}
As this example shows, the value received from the Mousefifo is an array - the subscripts of its elements are "button", "x", and "y". Fifos are also the mechanism by which files are read:

f = open("/etc/passwd")
for ( n=1; (v=get(f)) != Eof; n++ )
        print("line ",n," is ",v)
close(f)
Values obtained from a file-reading fifo are normally strings that contain entire lines from the file. The special value Eof is returned when the end of the file is reached. If you want to read individual characters (i.e. bytes) rather than entire lines, you can use the fifoctl() function to declare that the fifo should be handled in "binary" mode:

f = open("/unix","r")
fifoctl(f,"type","b")	# turn on "binary" mode for reading fifo f
for ( nc=0; get(f) != Eof; nc++ ) ;
close(f)
if ( nc > 500000 ) print("Too big.")
File fifos are opened for reading by default, but can also be opened for writing, by using the "w" flag:

f = open("/tmp/debug","w")
put(f,"hello world\n");
close(f)
Fifos reading from pipes can (on those systems where pipes are supported) be created by adding a third argument:

f = open("pwd","r","pipe")
pwd = get(f)
close(f)
print("The current directory is ",pwd)
Writing to a pipe can be done as follows:

f = open("lp","w","pipe")
put(f,"This should appear on the printer\n")
close(f)
Finally, if open() is given no arguments at all, it creates a generic fifo that can be used for inter-task communication.

Tasks

KeyKit is multi-tasking. Any number of tasks can be executed simultaneously, and their execution is interleaved along with realtime I/O at a very fine-grained level. Tasks are relatively cheap (in terms of execution and space overhead) to create and use. It is expected that dozens, if not hundreds, of tasks will be alive at any given time - however, most of them will be blocked on a fifo, and when blocked or sleeping a task imposes no overhead. All tasks have access to the same global variables. A new task is created by invoking a function with the task statement. The following example shows the creation of a task that continuously monitors MIDI input:

# Play a chord whenever a note below a given pitch is seen.
# The 'chordfunc' parameter should be a function value,
# which is called to generate the chord.
function autochord(chordfunc,limit) {
    while ( (n=get(Midiinfifo)) != Eof ) {
        if ( n.pitch < limit )
            realtime(chordfunc(n),0)  # play the chord via MIDI output
    }
}
function major(nt) {
    return ( nt | transpose(nt,4) | transpose(nt,7) )
}
task autochord(major,64)   # a C chord will be played whenever
                           # anything below pitch 64 is seen
print("Play away...")
After the autochord() function was invoked as a task, it would continue on in the background, and the "Play away..." message would be immediately printed. From then on, any time a note below pitch 64 was seen on MIDI input, a major chord corresponding to that note would be generated. The realtime() function used in this example will play a phrase via MIDI output - it will be described in more detail later.

Tasks that are blocked, either waiting for a message on a fifo or waiting for a specific time, impose no overhead. The task statement returns an integer value which is a task id - this value can be given to the kill() function when you want to terminate the task:


tid = task autochord(major,64)
kill(tid)
Communication and synchronization between tasks is typically done through fifos, since a get() on a fifo will block until there is something to read. The wait() function can be used to wait until a particular task is finished, and the sleeptill() function will wait until a particular (absolute) time is reached. These concepts are demonstrated by the example below, which creates an interactive mode in which pressing keys on either the console or MIDI keyboard generates chords.

# A utility function for continuously
# forwarding messages from one fifo to another.
function fifoforward(fromfifo,tofifo) {
    for ( ;; )
        put(tofifo,get(fromfifo))
}
# Generate chords in response to messages received on fifo 'f'
function chordfifo(f) {
    for ( ;; ) {
        m = get(f)
        # The message can be a single note (from the MIDI fifo)
        # or a single-character string (from the Consolefifo).
        if ( typeof(m) == "phrase" )
            realtime( major(m), 0 )
        else if ( m>="a" && m<="g" )
            realtime( major(phrase("'"+m+"'")) )
    }
}
function taskdemo() {
    fmerge = open()
    tid1 = task fifoforward(Midiinfifo,fmerge)
    tid2 = task fifoforward(Consolefifo,fmerge)
    tid3 = task chordfifo(fmerge)
    sleeptill( Now+32b )
    kill(tid1)
    kill(tid2)
    kill(tid3)
}
The fifoforward() function shown above is a simple utility that continuously reads messages from one fifo and forwards them to another fifo. The taskdemo() function spawns two instances of this utility, to forward messages from the Midiinfifo and Consolefifo fifos into a single fmerge fifo. The fmerge fifo is then read by the chordfifo() function, generating a chord in response to each message it receives. After spawning the 3 tasks that will do all the work, taskdemo() uses sleeptill() to wait until 32 beats have elapsed, and then kills the 3 tasks.

Tasks can use the onexit() function to arrange for cleanup operations when they are terminated or killed. A task can also use onexit() to restart itself, resulting in a robust daemon-like task that can recover from run-time errors.

Printing

The built-in printf function is used for formatted printing:

printf("num=0\n",num)
The output of the built-in printf function is always sent to "standard output", which, in a graphics environment, may result in a separate pop-up window. Formatted output to other destinations can be done with sprintf:

f = open("tmpfile","w")
put(f,sprintf("The current tempo is 0\n",tempo()))
close(f)
Note that in the default user interface of KeyKit, the printf function is immediately redefined so that it sends output to the Console window.

The user-defined function library defines a print function that is useful for simple printing - it merely prints its arguments separate by spaces. For clarity, this function (print) is used in most of the examples in this document, rather than printf.

Writing and Reading Phrase Files

Files containing KeyKit phrases are by convention named with a ".k" suffix. Such files are typically created with the following function (found in the standard user-defined library):

function writephr(ph,fname) {
        f = open(fname,"w")
        put(f,string(ph))
        close(f)
}
writephr(ph,"phrasefile.k")     # example usage
Phrases can be read from files with the readphr() function (built-in, not user-defined):

ph = readphr("phrasefile.k")

Realtime

KeyKit can do things in realtime. Time in KeyKit is measured in terms of clicks, and the relationship of clicks to actual time is determined by by the current tempo and the value of the variable Clicks. The currrent tempo is set with the tempo() function and is specified in terms of microseconds per beat. The value of Clicks is the the number of clicks per beat. KeyKit's default settings are:

Clicks = 96        # 96 clicks per beat
tempo(500000)      # 500000 microseconds per beat, i.e. 120 bpm
The variable Now contains the current time, in clicks, and is continuously updated. The sleeptill() function can be used to pause until a specified absolute time:

function reminder(tm,msg) {
        sleeptill(tm)
        print(msg)
}
task reminder(Now+16b,"16 beats are up!")
This example would print the message "16 beats are up!" after 16 beats, which, with the default tempo and Clicks values, would be 8 seconds.

The tempo of realtime playback can be set explicitly with the tempo() function, whose argument is the number of microseconds per beat. The tempo can also be varied during playback with special "text notes" (described previously) of the form "Tempo=###", where ### is the desired speed. For example, the phrase


'"Tempo=500000",c,g,"Tempo=400000",c,g,"Tempo=300000",c,g'
would slowly speed up during its playback. These special text notes also get translated into the tempo messages of a Standard MIDI File.

MIDI Output

Realtime MIDI output is managed by the realtime() function, which creates a new task responsible for playing the output. The following statement:

realtime('c e g, f a c')
would play 2 chords (C and F major) via MIDI output, beginning immediately. A second argument to realtime() can specify the absolute time at which to begin playback:

tid = realtime('c e g, f a c', Now+4b )
This would begin playing the phrase after 4 beats. Because realtime() spawns a new task, it will always return immediately - the playing of the MIDI output is done in the background by the new task. The return value of realtime() is the id of the new task - you can use it to kill the task like any other, thereby terminating the playback of the phrase.

MIDI Input

As shown previously, MIDI input can be read from the special MIDI fifo. Messages read from this fifo will be isolated note-ons and note-offs, suitable for use when producing echoes and other realtime effects. To get and manipulate MIDI input at a higher level, you should make use of the special Recorded variable - a global phrase variable that contains a complete copy of all MIDI input. This example takes whatever MIDI input has occurred during the previous 4 beats, flips it, and plays it:

ph = cut(Recorded,CUT_TIME,Now-4b,Now)
ph = flip(ph)
realtime(ph)

Synchronization

Some of the examples shown previously have made cavalier use of the Now variable. Precise scheduling of MIDI output and other things requires a bit more care, though, since the value of Now is continually changing, and since KeyKit does not execute infinitely fast. This example attempts to play a drum pattern and melody simultaneously:

realtime(drums,Now)     # equivalent to realtime(drums)
realtime(melody,Now)
Since the value of Now might be incremented between the execution of these two function calls, we would not be guaranteed that the drums and melody would be in perfect sync. A slightly better method would be:

start = Now
realtime(drums,start)
realtime(melody,start)
This would synchronize the playback of the two phrases. However, the first note of the drums phrase might still get played before the first note of the melody (though they would be in perfect sync thereafter). This is usually not enough of a problem to worry about, but if you really want to schedule phrases independently and be assured of them starting playback at exactly the same time, you should guarantee that they are all scheduled sometime in the future:

start = Now + 1b/4
realtime(drums,start)
realtime(melody,start)
Of course, for this example you could finesse the whole issue with:

realtime( drums | melody )

Realtime Variables

Several global variables that have special meanings and effects on the realtime operation of KeyKit:
Clicks
The number of clicks in a single beat. The default is 96.
Current
This phrase contains, at any point in time, all notes that are being held down (ie. note-ons without note-offs) at that time.
Merge
If non-zero, all MIDI input is echoed to MIDI output. This is used when your MIDI controller is separate from your MIDI synth.
Now
This is the current time, in clicks.
Record
If the value of this variable is zero, recording of MIDI input is disabled, otherwise recording is enabled. The default value is 1.
Recorded
This phrase records all MIDI input when Record is non-zero.
Recsched
If non-zero, the Recorded phrase also records any MIDI output generated by KeyKit.
A complete list of special variables can be found in the keyvar(5) manual page. There are many things that can be tweaked through those variables, so reading that manual page is important if you want to use KeyKit effectively.

Objects

Objects encapsulate methods and data. The syntax of object references is similar to that of C structures - object.data . However, an object is treated more like a pointer to a structure than a structure. For example, copying an object value does not duplicate the object, it merely duplicates the pointer to the object.

The data elements of an object can take on arbitrary values, but these values can only be accessed from within a method of that object. So, the only way in which objects are manipulated is through invocation of their methods, and the data elements within an object are completely hidden.

Methods are used like functions. This example invokes the method named meth of an object named obj, passing it 3 arguments:


obj.meth(1,2,3)
While executing a method of an object, the special symbol $ is an alias for that object. (See below for an explanation of what the special symbol $$ means.) So, the statement:

$.data = 99
would set the value of the data element in the current object (the object on whose behalf the method is being executed). Since data elements of objects are only accessible within methods, the $ notation is actually the only way that object data can be referenced. The $ notation also becomes a useful visual flag that distinguishes object data from local variables.

To invoke a method whose name is known only at run-time, you can use the following notation:


methname = "meth"
obj.(methname)(1,2,3)
Any expression in parenthesis following an object. will be treated as a string value that will be used as the method name. This lookup is (obviously) done at execution time, and in fact even explicitly-named methods are executed by doing a lookup at execution time.

Object Definition and Creation

Objects are defined with the class statement. For this example, we want to define an object class that acts like a point (i.e. it has an "x" and "y" value). Here is the definition of a class named point:

class point {
        method init {
                $.xvalue = 0
                $.yvalue = 0
        }
        method x {
                return($.xvalue)
        }
        method y {
                return($.yvalue)
        }
        method set (x,y) {
                $.xvalue = x
                $.yvalue = y
        }
}
An object of class point can then be created and manipulated as follows:

o = new point()
o.set(33,44)
print("x is ",o.x()," y is ",o.y())

Objects are (currently) not reference-counted or garbage-collected internally, so they must be explicitly deleted when you want to get rid of them:


delete o
Although it is conventional for objects to have a delete method, this method is not called automatically by the language. The default user-defined library has a deleteobject function that, if used, will call the delete method of an object, allowing it to clean up any tasks and graphics that it owns. The deleteobject function also automatically deletes any children objects.

If you print the value of an object, you will see a result like this:


o = new point()
print(o)
$18448396
The number that gets printed after the $ is the internal id of the object, which attempts to be a unique number (even between invocations of KeyKit). This notation (a $ followed by an integer) can actually be used within KeyKit code - it is a valid constant that will refer to that object. In fact, if you use such a constant, and an object with that id number does not currently exist, a generic object with that id will be created automatically. This becomes the mechanism by which objects can refer to each other, and by which these references can be conveniently maintained between invocations of KeyKit. For example, in the interactive user interface, you can write the current page (i.e. all objects on the current screen) to a file. If you look in this file, you will see lots of such $ values. A button object that refers to another object will contain (as one of the button's data elements) the value of that other object. The button may very well be created and initialized before the other object even exists, but since the button refers to the other object by using a constant such as $12345678, it will create the other object automatically. The other object will eventually get created, and the code that creates it will use the same constant $123454678 to initialize itself, and hence it will become the object that the button is already referring to.

When you want to create an object of a given class, and you want to use an existing object id (as just described), the following syntax should be used:


o = new($123) point()
The value in parenthesis after new is the object that will be initialized with the named class (in this case, point).

Inheritance and Children

All objects have a .inherit method that lets you specify one or more other objects from which methods will be inherited (if not overridden). As an example, the code below defines a polarpoint() class that creates an object that acts like a point object, except that you can also set its value with polar coordinates.

class polarpoint {
        method init {
                $.pt = new point()
                $.inherit($.pt)
        }
        method setpolar (ang,r) {
                x = r*cos(ang)
                y = r*sin(ang)
                $.pt.set(x,y)
        }
}
Note that inheritance requires explicit creation of an object from which methods are inherited. In this example, the setpolar method explicitly calls the set method of $.pt. Because of the inheritance that has been established, this call could actually be written as $.set(x,y). Use of the polarpoint() class is illustrated here:

o = new polarpoint()
o.setpolar(3.14,100)
print("x is ",o.x()," y is ",o.y())
Note that the x and y methods of the polarpoint object will be inherited from the point object.

When executing a method that has been inherited, the special symbol $$ (rather than $) will refer to the higher-level object which has established the inheritance relationship, rather than the inherited object. This can be used with both method invocations and object variable references. This code illustrates:


class A {
        method init {
                $.value = "AVALUE";
        }
        method id() {
                return("A")
        }
        method basefunc(numdollars) {
                if ( numdollars == 1 ) {
                        print($.value)
                        print($.id())
                } else {
                        print($$.value)
                        print($$.id())
                }
        }

}

class B { method init { someA = new A() $.inherit(someA)

$.value = "BVALUE"; } method id() { return("B") } }

b = new B() b.basefunc(1) # will print "AVALUE" and "A" b.basefunc(2) # will print "BVALUE" and "B"

Default Methods

All classes have the following built-in methods:
addchild(child-object)


Each object maintains a list of "children", typically used for forwarding events within the graphical user interface. The addchild method expects an object value to be given as an argument, and adds that object to the list of children for the current object.
removechild(child-object)


Removes an object from the list of children (as created with addchild) for the current object.
children()


Returns an array containing the list of children for the current object. The index values of the array elements are the object values, so you can conveniently loop through them.
childunder(xyarray)


This method is given an xy value representing a point on the screen, and returns the value of the first child object that lies under that point.
inherit(from-object)


Described above.
inherited()


Returns an array containing the list of objects from which the current object inherits methods (as established with the inherit method).

Graphical Features

Graphics in KeyKit is supported by a few built-in object types and a number of special global variables. The built-in support is extremely minimal, designed to support the creation of almost all user-interface semantics (all the way down to the behaviour of pop-up menus) through the use of user-specified KeyKit code. The standard library that comes with KeyKit implements a complete graphical user interface that is described elsewhere; only the very raw built-in graphical capabilities are described here.

Windows

First, an overview of the window features in KeyKit. All KeyKit graphics are done within a single root window (making it portable to environments that have no native window system). Coordinates are expressed in device-dependent pixel units, relative to the upper-left corner (0,0) of the root window. There is only one coordinate space, that of the root window. Coordinates used within sub-windows are expressed in that same coordinate space - they are not relative or scaled (although there is a coordinate space within phrase windows that uses clicks and pitches rather than pixels).

As a convention, many of the graphical methods use arrays with elements whose subscripts are "x0", "y0", "x1", "y1", and whose values are interpreted as coordinates of the origin and corner of a rectangle. This type of array is referred to as an xyarray, and here is a function that creates one:


function xy(x0,y0,x1,y1) {
    return( ["x0"=x0,"y0"=y0,"x1"=x1,"y1"=y1] )
}
In actuality, this function is a built-in function, since it is so heavily used. And, the built-in function is also capable of dealing with only 2 arguments, in which case it creates an array whose subscripts are "x" and "y".

Window Objects

A window object is created with the special built-in class windowobject(). In addition to the standard object methods described above, window objects have the following methods:
style(type)
This sets the drawn style of a window - a type of NOBORDER means no border at all, BORDER means a simple outline border, BUTTON means a 3-d button look, MENUBUTTON means a 3-d button with an extra underline under the text (to distinguish a drop-down menu button), and PRESSSEDBUTTON means a 3-d button that looks like it's pressed. If given no argument, this method returns the current border type.

contains(xyarray)
This method returns 1 (true) if the point specified by xyarray is contained within the window. If xyarray specifies an area, this method returns 1 if the area overlaps (by any amount) the window.

ellipse(xyarray [,mode] )
This draws the outline of an ellipse or circle within the rectangle specified by the coordinates in xyarray. The optional mode can be set to CLEAR, or STORE. The default mode is STORE.

fillellipse(xyarray [,mode] )
This draws and fills an ellipse or circle within the rectangle specified by the coordinates in xyarray. The optional mode can be set to CLEAR or STORE. The default mode is STORE.

fillrectangle(xyarray [,mode] )
This fills a rectangular region using the coordinates in xyarray. The optional mode can be set to XOR, CLEAR, or STORE. The default mode is STORE.

line(xyarray [,mode] )
This draws a line using the coordinates in xyarray. The optional mode can be set to XOR, CLEAR, or STORE. The default mode is STORE.

mousedo(mouse-array)
This processes the data from a mouse event (as received from the Mousefifo) and takes whatever action is appropriate for the current window object. For example, many of the behaviours of a menu object (scrolling, item highlighting) are done in response to handing it mouse events with this method. The return value of mousedo(), when used with a menu object, indicates which item the user has selected. Other valid return values for a menu object are MENU_DELETE (for the X-area in the upper-right corner of a menu), MENU_MOVE (the bar area in the upper-left corner of a menu), and MENU_NOCHOICE (no choice was selected).

rectangle(xyarray [,mode] )
This draws a rectangle using the coordinates in xyarray. The optional mode can be set to XOR, CLEAR, or STORE. The default mode is STORE.

redraw()
This redraws the window. Note that this does not redraw anything inside the window.

resize(xyarray)
This changes the size of the window to the value specified in xyarray. If no argument is given to resize(), it returns the current size of the window (as an xyarray).

restoreunder()
Restores the latest bitmap saved with saveunder().

saveunder()
Saves the screen area covered by the window as a bitmap, which can be later restored with restoreunder(). Intended for use with pop-up menu windows.

setconsole()
Sets the window so that it is considered the "console" - all error messages and the output of print statements are seen in this window.

size(xyarraygp)
This is an alias for the resize method.

textcenter(string,xyarray [,mode] )
Draw the string centered within the area specified byxyarray. The optional mode can be set to XOR, CLEAR, or STORE. The default mode is STORE.

textheight()
Returns the current height, in pixels, of text characters.

textleft(string,xyarray [,mode] )
Draw the string left-justified within the area specified byxyarray. The optional mode can be set to XOR, CLEAR, or STORE. The default mode is STORE.

textright(string,xyarray [,mode] )
Draw the string left-justified within the area specified byxyarray. The optional mode can be set to XOR, CLEAR, or STORE. The default mode is STORE.

textwidth()
Returns the current width, in pixels, of text characters.

windtype()
Returns the window type as a string - "generic", "phrase",

"menu", or "console".
xmax()
Returns the x value at the right side of the window.

xmin()
Returns the x value at the left side of the window.

ymax()
Returns the y value at the bottom of the window.

ymin()
Returns the y value at the top of the window.

Phrase Window Objects

Windows objects are by default "generic" windows, suitable for drawing lines and text. For displaying phrases, you can add a "phrase" argument: o = new windowobject("phrase"). This creates a window object with the following additional methods:
closestnote(xyarray)
Returns the note in the window that is closest to the specified point.

drawphrase(phrase [,mode])
Draws the specified phrase in the window. The optional mode can be set to XOR, CLEAR, or STORE. The default mode is STORE.

scaletogrid(xyarray)
Scales the coordinates in xyarray from raw window values (pixels) to click (time) and pitch coordinates. The scaled coordinates are relative to the window's current view (as set by the view() method).

sweep(fifo,type,xyarray)
Begins a sweep operation.

trackname(string)
Sets the name of the track displayed in the window.

view(xyarray)
This method controls what area of the phrase is seen within the window, i.e. it allows you to zoom and pan around the phrase, using the window as a viewport. The argument to this method is assumed to be an xyarray value that specifies the desired viewing area. The coordinates are specified in terms of click (time) and pitch values. For example, if the phrase in window w were 32 beats in length, this statement would cause it to be dislayed in its entirety: w.view(xy(0,0,32b,127))

Menu Window Objects

For creating window objects that act like menus, use a "menu" argument: o = new windowobject("menu"). This creates a window object with the following additional methods:
menuitem(label)
Adds an item with the specified label to a menu object.

menuitems()
Returns an array containing the current list of menu items in the menu. The index values of the array are the menu item labels.

Built-In Functions

acos ( x )
Returns the arc-cosine of x.

argv( arg-index-start [,arg-index-end] )
Used within a user-defined function to give generalized access to the arguments passed to it. If given one argument, argv returns a single argument from those passed to the current user-defined function. For example, argv(0) will return the first argument. If given two arguments, argv returns an array containing the specified argument range (from the first value up to, but not including, the second value). The index values of the returned array start at 0. For example, argv(0,nargs()) returns an array containing all of a function's arguments.

ascii( integer-or-string )
When given a string argument, this function returns the ascii value of its first character. When given an integer argument, this function returns a string containing a single character whose ascii value is that integer.

asin ( x )
Returns the arc-sine of x.

atan ( x )
Returns the arc-tangent of x.

chdir(dir)
Changes the current directory to dir.

close ( fifo )
Closes the specified fifo.

colorset ( colorindex )
Sets the current color index for drawing things.

colormix ( colorindex, red, green, blue )
Sets the color for a given color index. The values for red, green, and blue can range from 0 to 65535. Color index 0 is main background color, color index 1 is the main foreground color, color index 2 is the "pick" color (used for displaying highlighted notes in phrase windows), color index 3 is the background in buttons, and color index 4 is the shadow in buttons. Other color indicies can be used when drawing lines.

cos ( angle )
Returns the cosine of angle (a value in radians).

currtime()
Returns the current time, in seconds (typically since Jan 1, 1970).

cut( phrase, type, ... )
Returns a phrase containing notes cut from phrase. The type determines what the cut is based on. Possible values for type (as pre-defined macros) are CUT_TIME, CUT_CHANNEL, CUT_TYPE, CUT_NOTTYPE, and CUT_FLAGS.

If type is CUT_TIME, the cut is based on time. The third and fourth arguments specify the starting and ending time. The fifth argument, if present, controls how this cut behaves at the boundaries - possible values are NORMAL (the default), TRUNCATE, and INCLUSIVE. The NORMAL type of time cut is an efficient equivalent to the expression: phrase{??.time>=time1 && ??.time<time2}. A TRUNCATE cut will chop off notes that cross the boundaries, while an INCLUSIVE cut will include those notes unchanged.

If type is CUT_CHANNEL, the cut is based on channel. The third argument is the channel number (as a value from 1 to 16) of the notes that will be in the cut

If type is CUT_TYPE, the cut is based on type. The third argument is the type - any notes that have this value as their .type will be in the cut.

If type is CUT_NOTTYPE, the cut is based on the inverse of a type. The third argument is a type - any notes with this .type value will not be in the cut phrase.

If type is CUT_FLAGS, the cut is based on the flags attribute of the notes. The third argument is a mask that is or'ed with the flags of each note - the cut contains any notes for which this results in a non-zero value.

debug(type)
Used as a debugging hook, whose meaning varies from time to time.

defined(variable-or-function-name)
Returns non-zero if the named variable or function has been defined, and 0 if it is undefined.

error( message )
Generates an error, printing the specified message string and terminating the calling task.

exp ( x )
Returns the exponential function e**x.

exit()
Quits the entire KeyKit program, completely and abruptly.

fifoctl ( fifo, cmd, mode )
Sets the given fifo to a particluar mode. The cmd argument is intended as a hook to machine-dependent fifo commands. The only command universally accepted is "type". If mode is "l", then reads from the fifo are done a line at a time (this is the default mode of fifos). If mode is "b", then reads from the fifo are done a byte at a time rather than a line at a time.

fifosize ( fifo )
Returns the number of unread data values in the specified fifo.

filetime ( filename )
Returns the modification time of the named file, consistent with the values returned by currtime().

finishoff()
Send note-off messages on MIDI output to terminate any currently-held notes.

float ( value )
Converts its argument (typically an integer or string) to a floating point value and returns it.

flush ( fifo )
Flush all unprocessed data values in the specified fifo. If the fifo is attached to a file or pipe, the data is flushed. For other types of fifos, any unprocessed values in the fifo are discarded.

funkey ( num, statement )
Assigns a KeyKit statement (specified as a string beginning with '{' ) to the num-th function key. Whenever that function key is pressed, the statement will be immediately executed.

get ( fifo )
Retrieves a value from the specified fifo. The task blocks if the fifo is empty.

gettid ( )
Returns the task id of the current task.

integer ( value )
Converts its argument (typically a string or float) to an integer value and returns it.

kill ( task-id )
Terminates the specified task, possibly invoking a cleanup function that the task has registered with onexit(). The return value of kill() is normally 0. Killing a non-existant task is okay - no error is produced, and the return value is 1.

log ( x )
Returns the natural logarithm of x.

log10 ( x )
Returns the logarithm of x to base 10.

lsdir ( directory )
Returns an array of the files and directories contained in the specified directory. The index values of the elements in the array are the actual file and directory names. The value of an element is 1 if it is a directory, and 0 if it is a file.

midibytes ( num-or-phrase, num-or-phrase, ... )
Returns a phrase containing a single MIDIBYTES note that is the concatenation of the bytes specified by all the arguments. Each argument can be either a number - specifying a single byte of the result; or a phrase - all of its MIDIBYTES notes are copied to the output phrase.

midifile(filename) or midifile(array,filename)
The first usage (with only a filename as an argument) reads a Standard MIDI File and returns an array containing its tracks, starting at array index 0. The global variable Mfformat is set to the format type (0, 1, or 2). The value of global variable Defrelease specifies the default release velocity. If the value of global variable Onoffmerge is 1 (its default value), noteons and noteoffs are merged.

Used as midifile(array,filename), the elements of the specified array are used as tracks to create a Standard MIDI File in the named file. The array subscripts should be numeric, since they will be sorted to determine the order of tracks in the file. If global variable Tempotrack is 1 (its default value), a tempo track is automatically created as the first track of the file. The value of Clicks is used as the 'divisions' value in the header.

milliclock ( )
Returns the (relative, not absolute) value of a millisecond-resolution clock.

objectinfo(object,info)
Returns an array containing information about the specified object. If the value of info is "methods", the array will contain the names of all of the object's methods. If the value of info is "data", the array will contain the names of all of the object's data - both variables and methods. The array index values will be the method/data names, and the array element values will be the type of each item.

objectlist()
Returns an array containing all objects.

nargs ( )
Returns the number of arguments passed to a user-defined function.

onchange(variable, func)
Arranges for func (a function pointer value) to be called whenever the value of the specified variable is changed.

onexit(func [,arg(s)] )
Arranges for func (a function pointer value) to be called when the current task is finished (either voluntarily or by being killed). If there are additional arguments, they are passed as arguments to func when it is called.

open( [file-or-pipe [,mode] ] )
Allocates a new fifo and returns its id. If given one argument, open interprets it as a filename to be opened for reading. A second argument can modify the interpretation: "w" will open the file for writing rather than reading; "|" will interpret the first argument as a shell command, opening a pipe that can be used to read its output; and "|w" will execute a command, opening a pipe that can be used to write to it.

phrase ( value )
Converts its argument (normally a string which includes the single quotes) to a phrase value and returns it. For example, a=phrase("'a,b,c'").

pow ( x, y )
Returns x**y.

printf(format [,arguments])
Print formatted output. See the section on "Printing" above, and see the description of sprintf below for the type of formatting that can be done.

priority(task [,priority])
With one argument, priority() returns the current priority of the specified task. With two arguments, priority() sets the current priority of the task to the specified priority value. If the value of task-id is -1, priority() returns or sets the global priority limit, which specifies a lower limit for runnable tasks - only tasks with a priority greater than or equal to the current global priority are permitted to run.

put ( fifo, value )
Puts the value on the specified fifo. The return value is normally 0. If fifo does not exist, the return value is -1.

readphr(fname)
Reads the specified file, expecting it to contain a KeyKit phrase whose value is returned. The value of Musicpath is used to search for the file.

realtime(phr [,time])
Spawns a new task for playing the given phrase in realtime via MIDI output, and returns its task id. An optional second argument specifies the starting time; the default value is Now.

reboot()
Forces a reboot, terminating all tasks and calling Rebootfunc(), a function whose initially-null value is typically redefined in keyrc().

rand ( n1 [,n2] )
Returns a random number between n1 and n2, inclusive. If only n1 is given, the random number is between 0 and (n1-1), inclusive. If only n1 is given, and it is negative, then it is used to seed the random number generator.

setmouse(type)
Set the cursor type for the mouse. Values for type are: ARROW, SWEEP, CROSS, LEFTRIGHT, UPDOWN, BUSY, and NOTHING.

sin ( angle )
Returns the sine of angle (a value in radians).

sizeof(arg)
Returns the number of notes in a phrase, or the length of a string, or the number of elements in an array.

sleeptill(time)
Causes the task to go to sleep until the specified time, expressed in absolute clicks.

sqrt ( val )
Returns the square root of val.

split(phrase-or-string)
When given a string as its first argument, this function breaks it into white-space-separated words and inserts them as separate elements into an array. The return value of this function is a pointer to this newly-created array. The subscript of the first array element is 0. When given a phrase, this function breaks it into a array of short phrases, using the starting and ending times of the notes in the original phrase to determine the split points. To visualize this operation, imagine drawing vertical lines through the starting and ending point of every note in the original phrase. The array elements would be the phrases contained between these vertical lines. For example, x = split('a,bt12')would result in x[0]='ad12' ; x[1]='ad84t12 b' ; x[2]='bd12t96'. This is useful for constructing monophonic phrases, and any other operation in which you want to reconsider what notes should be playing whenever any note starts or stops.

sprintf ( format, args )
Formatted printing, with the result returned as a string. The format may contain the following conversion specifications: 0 (decimal), 0 (hex), 0.000000 (float/double), (string), 0 (phrase), and % (literal percent character). Width and precision prefixes (e.g. 0 and 0.00) are recognized.

string ( value )
Converts its argument to a string and returns it.

subbytes(phrase,start,leng)
Works vaguely like substr, but operates on notes whose type is MIDIBYTES, allowing you to pull off individual bytes or ranges of bytes. For example, subbytes('xc005c106c207',3,2) would return 'xc106'. Note that the start value for the first byte is 1, not 0.

substr(string,start,leng)
Returns a substring of a string. Note that the start value for the first character of a string is 1, not 0.

system(string)
The string is executed by the shell (or whatever program is the command interpreter for a given machine). This may not be supported on all machines.

tan ( angle )
Returns the tangent of angle (a value in radians).

taskinfo("list") or taskinfo ( taskid, type )
If given a single argument (whose only valid value is "list"), taskinfo() returns an array with entries for each currently-running task - the array element indicies are the task ids, and the array element values are all zero. If given two arguments, taskinfo() expects the first to be a task id, and the second is a string that indicates what piece of information about the task should be returned by taskinfo(). The valid values of type are: "status" (returns a string describing the running status), "parent" (returns the id of the task's parent), "count" (returns the number of interpreted instructions that the task has executed), "schedtime" (returns the time at which the task is scheduled to awaken, if it is sleeping), "wait" (returns the id of the task whose termination is being awaited), "blocked" (returns the id of the fifo, if any, on which the task is blocked), "fulltrace" (returns a string with a complete function traceback, including parameter values), "trace" (returns a function traceback without parameter values), or "priority" (returns the priority value of the task).

tempo( [newtempo] )
When invoked with no arguments, tempo returns the current tempo. When given an argument, the current playback speed is set to newtempo, whose units are microseconds per beat. The return value is the old tempo.

typeof(arg)
Returns a string describing the type of its argument: "string", "integer", "float", "phrase", "array", "function", or "uninitialized".

undefine(variable-or-function-name)
Causes the definition of the named variable or function to be forgotten. This can be used to force the rereading of a user-defined function from the file that defined it, and is typically useful when a new function is being written and tested.

wait(task-id)
Causes the current task to go to sleep until the specifed task has finished.

xy(x0,y0,x1,y1)
Returns an xyarray (see descrption in the Windows section above) containing the specified values.

Acknowledgments

KeyKit has been a hobby project of mine for many years. In that time, many people have contributed ideas, feedback, assistance, and encouragement. Some of them are: Jon Backstrom, Tom Duff, Geza Feketa, Dick Hamilton, Tony Hansen, John Helton, Tom Killian, Peter Langston, Hector Levesque, Jason Levitt, Howard Moscovitz, Marty Shannon, and Daniel Steinberg. The people who have put significant effort into porting KeyKit to various machines deserve special mention and special thanks - Steve Falco (Mac), Alan Bland (Amiga), Gregg Wonderly (Amiga), Mike Healy (Atari ST), Greg Youngdahl (DOS) Ag Primatic (Mac), and Jack Wright (Mac). Many thanks to all.