Chapter 4: A TUTORIAL TO THE RUBATO LANGUAGE

A brief and casual introduction to the Rubato language is presented in this chapter. The Rubato language is a typographical music input language. This language can be used to represent conventional music notation within a computer and allow for the subsequent performance of the music represented through a music synthesizer connected to a computer system via a MIDI (Musical Instrument Digital Interface) connection. Musical pieces represented in the Rubato language are suitable for processing by the Rubato compiler rc(1) and bear many resemblances to modern, block-structured computer languages.

4.1 INTRODUCTION

The Rubato High Level Language is a music input language that is closely modeled on conventional music notation. However, it is a typographical language rather than a pictorial language, and contains additional features such as variables, 'templates', functions, procedures, dynamic structured block scoping of declarations and concurrency which are analogous to equivalent features in many other 'modern' computer languages.

This tutorial provides a casual 'hands-on' introduction to the Rubato language, with emphasis on introducing the language structure and other features of the language rather than any conscious attempt at generating music of artistic merit. Not all features of the language are covered, some of the less useful features have been deliberately omitted. The next chapter provides a much more complete description of the language.

The tutorial begins with a simple example. This example will be built up as more and more features of the language are progressively introduced. The target audience will be people familiar with at least one other programming language such as Pascal or C, and possessing a minimal background in reading and writing music in conventional music notation.

4.2 STARTING OUT

First of all, access to a machine with the Rubato compiler rc(1) and the Rubato interpreter rx(1) while reading this tutorial is highly recommended. Ideally, the machine should also be capable of playing the music entered through a music synthesizer connected via MIDI. (MIDI stands for Musical Instrument Digital Interface. The appendix entitled Summary and Description of MIDI provides a summary of the technical aspects of MIDI.)

Currently, rc and rx will run on a DEC VAX 11/780 running Unix Version 8 and an IBM PC/XT running MS-DOS Version 3.30. Music performance is only possible on the IBM PC/XT via a player program called adagio(1)[3] and the Roland MPU-401 MIDI Processing Unit.

Musical pieces represented in the Rubato High Level Language are referred to as programs. A program is simply a text file created using a text editor on the host machine. The text file is stored under a filename with an extension of .r or .R. Once a Rubato program has been created, it can be compiled using the command

rc file.r

The compiler will compile the source file and generate an output file with the same file name as the program file but with the extension .r replaced with .m. The output file can be interpreted using the command

rx file.m

which will generate a player file, usually called rubato.gio. On a machine equipped with adagio, the music may be performed on a synthesizer via the player program using the command

adagio rubato.gio

4.3 A SIMPLE EXAMPLE

Below is a simple example, somewhat akin to the 'hello world' examples typical of most computer language tutorials, which we shall call example1.r. This example, when performed on a synthesizer, will cause an ascending C major scale starting from middle C to be played on the synthesizer. The individual notes in the scale (seven of them) will be spaced a second apart.

conductor
{
	tempo .1 = 60
	c
	d
	e
	f
	g
	a
	b
}
Figure 1: example1.r - a simple example

The first line in the above example defines an entity known as the conductor. Every Rubato program must have at least one conductor definition. The curly brackets (also called 'braces') delimit the lines associated with the conductor and tells the compiler to generate code that will cause each element within the braces to be executed sequentially by the conductor.

The first line within the curly braces

	tempo .1 = 60

specifies the speed that the following notes shall be played. This line will cause notes played to be separated by an interval of 1/60th of a minute, i.e. one note per second. Suppose the line had been

	tempo .1 = 120

then notes will be played at the rate of 120 notes per minute, or two notes per second.

The rest of the lines specify which notes should sound in sequence. In this context, c implies Middle C, d implies D above Middle C and so on.

It is well worth pointing out at this stage that Rubato programs are free form in nature, i.e. white spaces between characters are not syntactically meaningful to the compiler and only serve to delimit characters. The format of the above example has been carefully designed to maximise human readability. Indeed, the above example could very well have been coded up thus, with resulting loss in readability:

conductor{tempo .1=60 c d e f g a b}
Figure 2: Another version of example1.r

Also note that the Rubato compiler is case sensitive, i.e. conductor and CONDUCTOR mean entirely different things.

4.4 JAZZING IT UP

Suppose we wanted to play the C major scale from middle C to the C above middle C. example1.r can be easily extended by adding one more note:

conductor
{
	tempo .1 = 60
	c d e f g a b c'
}
Figure 3: example2.r - the C major scale

Note that we have shortened the example considerably by joining all the notes to be played on one line.

Hence, the last note played, c' implies the C above Middle C. The ' operator (the quote), when specified after a note, will cause that note to be played one octave ABOVE its normal pitch(8). Similarly, the ' operator (the backquote) causes a note to be played one octave BELOW its normal pitch. Given this, we can easily construct example3.r, which plays most of the C notes on a piano in ascending order:

conductor
{
	tempo .1 = 60
	c``` c`` c` c c' c'' c'''
}
Figure 4: example3.r - Jumping up octaves

Note the effect of stuttering the quote and backquote operators is to further reduce or augment the pitch of the note in decrements/increments of one octave. In particular, c'' is equivalent to typing c without any trailing operators.

We can also insert accidentals, which modify the pitch of a note by one semitone rather than one octave. The # operator (sharp) will increment (augment) the pitch of the note by one semitone and the $ operator (flat) will decrement (diminish) the pitch of the note by one semitone(9). There is a third operator, the % operator (natural), which restores the note back to an unaugmented or undiminished state.

Be careful when specifying a note containing BOTH accidentals and octave operators to put the accidentals BEFORE the octave operators.

Here is an example playing the scale of A major:

conductor
{
	tempo .1 = 60
	a b c#' d' e' f#' g#' a''
}
Figure 5: example4.r - the A major scale

Of course, there is a much simpler way of specifying the A major scale and that is to change the default key associated with the notes. This can be done with a $key statement.

$key a major
conductor
{
	tempo .1 = 60
	a b c' d' e' f' g' a''
}
Figure 6: example5.r - the A major scale

Notice that changing the default key (from C major) to A major will cause the sharps associated with the A major key to be automatically bound to the relevant notes when played. Hence no sharps need to be specified for the actual notes.

One important point with respect to the natural operator must be mentioned here. Suppose the default key is other than C major and it is desired to represent the note e. However, there may be an accidental bound to the note that is due to the default key. Specifying, in this instance e% will ensure that e will be played without bound accidentals.

4.5 DELAYS AND DURATIONS

It would be quite boring if all notes in a piece of music had equal durations and were separated equally. Rubato allows each note to be bound with two attributes: delay and duration. Attributes are things that can be associated with each note. Rubato has quite a few attributes bound to each note, and most of these will be discussed in turn. The delay attribute is a time interval associated with a note. It specifies that the next note specified will not be played until the time interval represented by the current note's delay has elapsed. On the other hand, the duration attribute is a time interval that specifies how long a note should sound.

4.5.1 Delays

Delays are usually specified relative to a 'known' time interval which, for want of a better term, is called a unit. The default delay is usually some multiple of a unit (256). This is somewhat analogous to the specification of notes which are relative to the default key.

Two operators change the delay, the . operator (delaymul) and the : operator (delaydiv).

conductor
{
	tempo .1 = 60
	c:2 d:4 e:4 c.2
}
Figure 7: example6.r - Notes with delays

In example6.r, the first note is played at half the default delay, the next two notes are played at a quarter of the default delay, and the last note is played at twice the default delay.

Hence, the delaymul operator causes a note to have a delay that is equal to the default delay multiplied by the argument that follows the operator. Similarly, the delaydiv operator specifies a delay equal to the default delay divided by the argument following the operator.

4.5.2 Arithmetical expressions

We can form arithmetical expressions with the arguments of the delay operators (indeed, an arithmetical expression can substitute a number whenever a number is required). For example,

c :2+:(4 * 3)-:8*3

causes a note to be played with the following duration:

default / 2 + default / (4 * 3) - default / 8 * 3

Note the use of parenthesis to enforce precedence. arithmetical operators recognized by Rubato include most of the common ones familiar to programmers well versed in traditional computer programming languages.

The following is a list of binary operators in Rubato specified in order of increasing precedence:

or
binary OR
and
binary AND
==
Return 1 if operands are equal, 0 otherwise
<>
Return 1 if operands are not equal, 0 otherwise
<
Return 1 if first operand is less than second operand
>
Return 1 if first operand is greater than second operand
<=
Return 1 if first operand is less than or equal to second operand
>=
Return 1 if first operand is greater than or equal to second operand
+
Addition
-
Subtraction
*
multiplication
/
division
mod
modulus (remainder of first operand divided by second)

In addition, the following unary operators are recognized

not
binary complement
~
unary minus

4.5.3 Duration

The default duration of a note is slightly less than the delay of a note. This is because on most musical instruments, there is a perceptible interval between the playing of one note and the playing of the next. Two operators can change the duration relative to the current note delay.

The ^ operator (decdur) causes the duration of the note to decrease with respect to the current delay. Stuttering the decdur operator will cause the duration to become progressively less and less with respect to the delay and the effect is to cause the playing of notes to become more and more 'disjointed'. This is called staccato.

The _ operator (incdur) causes the duration of the note to increase the duration of the note closer to the delay. A duration that is equal to or very close to the delay causes an effect known as legato or 'smooth-playing' which is commonly associated with bowed stringed musical instruments such as the violin.

4.5.4 Delays and Tempos

The tempo statement, as seen in example1.r, consists of two arguments separated by an equals sign (=). The first argument is the quantity, measured in delay units, and the second argument is the number of times this quantity should count in the period of one minute. Hence,

tempo :4 = 80

means the quantity represented by the current default delay divided by 4 should occur 80 times per minute.

Suppose the current default delay is 256 units. Then the above statement is equivalent to saying

tempo 64 = 80

as 256 / 4 = 64. Hence the rate of flow of units with respect to time is 64 * 80 = 5120 units per minute or around 85.3 units per second.

4.6 LOUDNESS (or VELOCITY)

Yet another attribute associated with a note is the note's velocity. This can be visualized(?!) as the loudness or intensity of the note. The term 'velocity' comes from the action of a pianist hitting the keys on a piano. Obviously, the harder the pianist strikes the keys (the faster the keys move as a result of being struck) the louder and more intense the notes that result. Similarly, the higher the velocity of the note, the louder (or brighter) it will sound on a synthesizer suitably equipped with velocity circuits (Not all synthesizers will respond to the velocity data sent by a player while playing a Rubato program.)

The main velocity operator is ;. It can be used in many ways:

conductor
{
	c;80
	d;+20
	e;-20
	f; 20 + ;;
}
Figure 8: example7.r - Specifying velocities

In the above example, the first note is played with an absolute velocity value of 80. The second note is played at a velocity which is 20 greater than the current velocity. Similarly, the third note is played at a velocity of 20 below the current velocity. The last note is played at a velocity 20 greater than the current velocity, hence it has the same velocity value as the second note. The different syntax simply illustrates how arithmetical expressions can be used when specifying attribute values. Note that stuttering the ; operator returns a number equivalent to the default velocity value, this is then added to 20. The final result is then used as the velocity value.

4.7 SOME OTHER ATTRIBUTES

Some other attributes which may seem a bit obscure but can be quite useful are the patch and channel attributes. These are specified by the patch and channel operators respectively.

conductor
{
	c patch 5 channel 2
}
Figure 9: example8.r - Patch and channels

In the above example, the note when played will be on Patch 5 Channel 2.

The patch attribute specifies the timbre of a note, or how a note will sound. As an example, the sound of a note on a piano is obviously very different from the sound of a note on a bassoon or cello. This can be attributed to (with apologies to the unintentional pun) the different timbres of the instruments. Patch numbers are usually assigned to distinctive timbres within a synthesizer. Each patch number corresponds to some setting of the synthesizer voice generation controls or voice generation algorithms within a synthesizer.

The channel number of a note can be used to identify which synthesizer a note should be played on, if more than one synthesizer is connected to a computer. Even though all the synthesizers may be connected together by a common line to the computer, synthesizers can usually be set up to respond to only selected MIDI channels.

Absolute pitch, delay and duration values of a note can also be specified in a manner similar to the specification of velocity values. The pitch number can be specified with the pitch or & operator. The delay value can be specified with the delay or \ operator. The duration value is specified with the duration or ! operator. Hence, the following notes

&100 \500 !42
&+5 \-20

specifies a note with a pitch number of 100 with a delay of 500 units and a duration of 42 units. The next note has a pitch number 5 greater than the default pitch number and a delay 20 less than the default delay. Absolute attribute specifications are useful when the 'note' to be played is actually directed to an instrument not actually designed as a musical instrument but has a MIDI interface. Examples of MIDI controlled equipment include stage-light controllers, effects generators and other paraphernalia associated with a concert hall. These instruments accept note commands but, instead of playing a note, will respond to the note command by turning on lights or starting a tape recorder.

4.8 CHANGING DEFAULT ATTRIBUTES

So far, all the above descriptions of note attributes have made mysterious references to things called default attribute values. What are default attribute values and how are they changed?

Default attribute values are values taken by attributes of a note unless the attributes are changed using methods outlined previously. Initially, here are the default values for some of the attributes:

Delay
256 units
Duration
10 units less than the delay
Velocity
64
Patch
0
Channel
0

Default values can be changed using the default statement. For example,

conductor
{
	default \512
	default !20
	default ;80
	default patch 42
	default channel 13
	c d e f g a b
}
Figure 10: Changing Default Attributes

will change the default delay to 512 units, the default duration to 20 less than the current delay, the default velocity to 80, the default patch to 42 and the default channel to 13.

Pitch number defaults are changed using a different method:

$default a = a''
conductor
{
	c d e f g a b
}
Figure 11: Changing Pitch defaults

This will cause the default pitch of the note C to be the note C two octaves above Middle C rather than Middle C and so on.

There are actually two sets of pitch defaults:

$default a = a'
$default A = A`
conductor
[
	{ c d e f g a b }
	{ B A G F E D C }
]

The note 'a' is distinguished from the note 'A'. Initially, both uppercase notes and lowercase notes are set to the same default pitch number, and hence will produce the same pitch when specified. The previous example changes the lowercase pitch default to be one octave higher and the uppercase pitch default to be one octave lower.

4.9 WAYS OF COMBINING NOTES

How can notes be combined together?

4.9.1 Phrases

So far, in all the previous examples, notes have been joined together sequentially, separated by the delay values of each note, into a melodic line or phrase.

The phrase constructor in Rubato is the pair of curly braces. Any note within a pair of curly braces will be played in sequential order in the manner outlined in the previous examples. It is important to note that phrases may contain other phrases within themselves. For example, the following two phrases are identical, i.e. they produce the same line of melody.

{ c d e f c d e f g c d e f c }
{ { { c d e f } { c d e f } g } { c d e f } c }

Phrases are entities rather like notes and may possess attributes. Any attributes belonging to a phrase will automatically be inherited as the default attributes of each note within a phrase. Hence,

{ c d e:2 }:4 f

all the notes within the phrase will have a default delay equivalent to the default delay outside the phrase divided by 4. Note that the last note within the phrase will have a delay equivalent to the current default delay divided by 8, as the result of dividing the delays are cumulative. The note outside the phrase, on the other hand, is unaffected by the phrase delay attribute. This is in keeping with the concept of each note (or phrase) being unaffected by entities before or after it.

4.9.2 Chords

Another entity much like phrases or notes is the chord. A chord is a collection of entities delimited by square brackets '[' and ']'. Each entity within a chord is played simultaneously. The delay of a chord is then equivalent to the shortest delay of any one of its entities.

The following is the specification of a chord that plays the C major triad

[ c e g ]:4

Note that chords, like phrases, may possess attributes which are passed to entities within themselves.

Chords may contain other chords within itself, or even phrases.

conductor
[
	{ c d e f g a b c }:8
	[ c e g ]
	{ c c c c }:4
]
Figure 12: example9.r - Chords and Phrases

In example9.r, the two phrases and a chord within the chord tagged to the conductor all play simultaneously, forming three separate melodic lines.

4.10 NAMING THINGS

Everything in Rubato can be assigned a name, which is a string of alphanumeric characters forming a distinct word. Once an entity is assigned to a name, it is considered to be bound to the name until some other assignment is made to the name. Invoking the entity can be accomplished by simplying typing the name which the entity is bound to.

For example, the following example defines a phrase called ditty which contains the chord definition cmajor which is played at the beginning and the end of the phrase. Also, a note called funny is defined outside the conductor. Note that that when an entity is bound to a name, it does not get 'played'. However, the entity will be played when it is invoked by the name it is bound to.

note funny = e#'':16;20
conductor
{
	phrase ditty =
	{
		chord cmajor = [ c e g ]:2
		cmajor
		{
			c d e f funny g a b funny.2
			cmajor
		}:8
		cmajor:2
	}
	ditty
}
Figure 13: example10.r - Namings and block structuring

example10.r is also an excellent example of a concept called block scoping. The phrase ditty is defined within the phrase associated with the conductor. Similarly, the chord cmajor is defined within the phrase ditty.

The point of all these nested definitions is Rubato employs identifier scoping rules when binding entities to names. The chord

[ c e g ]:2

is locally bound to the string cmajor only within the phrase ditty. Outside ditty, such as elsewhere within the conductor, the binding of cmajor is undefined. However, within the anonymous phrase within ditty, the binding of cmajor is still active as the phrase is contained entirely within ditty, which 'holds' the binding of cmajor.

The note funny is defined or bound to outside the conductor. It is therefore technically known as a global binding and will be active for the rest of the Rubato program.

Block scoping is a powerful concept employed in many modern programming languages. Rubato uses block scoping rules in order to 'hide' bindings outside areas where they are useful.

In the context of 'block scoping', a block is simply an entity which may contain other entities, i.e. a phrase or a chord. A note is not considered a block because a note cannot contain other notes or phrases or chords. Bindings can only occur either 'externally', such as in the case of the note funny, or within a block. In a phrase or chord, all bindings must be specified before any specification of entities within the block. Note that in the phrase ditty, the binding of cmajor is done before the specification of phrases and chords within ditty.

4.11 VARIABLES AND CONTROL FLOW

4.11.1 Variables

Rubato allows yet another sort of binding, called variable binding. Variables are names associated with an integer. They can be considered as a 'location' that can hold an integer for later use. If a new integer value is bound to a variable that was previously bound (this is called variable assignment), the old binding is discarded.

Variables can also be declared with no initial binding. In this case, a storage location is allocated for the name of the variable, but the contents of the storage location is left unbound. If an assignment occurs in some later stage, the storage location will then be bound to the new value.

The following example will make this clear.

conductor
{
	var highnote = key c'''
	var longdelay
	longdelay = 10
	&highnote .longdelay
}
Figure 14: example11.r - Variable declarations and assignments

Two variables are declared in example11.r. highnote has an initial binding of the pitch number associated with the note c'''. longdelay has no initial binding but is later assigned to the value 10. A note is then played using the contents of both variables.

Variable declarations are also subject to the scoping rules mentioned above.

4.11.2 Control Flow

Variables are really useful in directing control flow within a phrase. As mentioned previously, entities within a phrase are executed sequentially. Sometimes, however, it may be desired to break the flow of execution within a phrase. Control flow statements are entities whose sole purpose is to modify the control flow within a phrase.(10)

The following are a brief summary of control flow statements available in Rubato. In the context of the syntax descriptions an expression is simply an arithmetical expression that evaluates to an integer value and a statement is either a control flow statement, a variable assignment, or an entity (e.g. note, phrase or chord). A statement can also be a procedure or template call. Procedures and templates will be introduced later.

4.11.2.1 if-then-else

Syntax:
if expression then statement1
if expression then statement1 else statement2

The expression is evaluated. If the result is true (nonzero value) statement1 is executed. If the result is false (zero value) control flow will pass on to the next entity or statement in sequence or, if the else keyword is present, statement2 is executed.

4.11.2.2 while-do

Syntax:
while expression do statement

The expression is evaluated. If the result is true, then statement is executed. The expression is then reevaluated. Statement will be rexecuted until the expression evaluates to false (0), at which point control will be passed onto the next statement after the while-do statement.

4.11.2.3 do-while

Syntax:
do statement while expression

The statement is executed. The expression is then evaluated. If the result is true, the statement is reexecuted. This continues until the expression evaluates to false. Control then passes to the next statement.

4.11.2.4 repeat

Syntax:
repeat expression do statement

The statement is repeated expression number of times.

4.11.3 A control flow example

The following simple example will hopefully make the flavour of the above control flow statements clarified. Try and figure out what it does. If in doubt, key it in and then play it!

conductor
{
	var aa = 1
	while aa < 32 do
	{
		aa = aa * 2
		if aa == 32 then
			repeat 4 do { c:aa d:aa }
		else
			c:aa
	}
}
Figure 15: example12.r - Control flow

4.12 PROCEDURES & TEMPLATES

4.12.1 Procedures

A procedure is analogous to a subroutine call in a computer programming language. A procedure binding is similar to a phrase or chord binding, except during the invocation of the procedure, entities and values may be passed to the procedure body which is the entity bound to the procedure name.

The invocation of the procedure is called a procedure call, and the values and entities passed to the procedure body are called procedure parameters.

Parameters may be values, variables, notes, chords phrases or even other procedure names. A procedure binding (also called 'declaration') binds its parameters to local names and act as if the entities passed to the procedure has been bound locally within the procedure. If a parameter binding is subsequently changed within a procedure, this change is invisible to the outer block that invoked the procedure.

Syntax:
procedure name(parameters) procedure_body
procedure name() procedure_body

The name of a procedure is the string by which the procedure is bound to. The parameters are simply variables, notes, chords or phrases declarations(11) separated by the ',' character. The procedure_body is a block (either a phrase or a chord) that will be executed by the procedure when invoked.

For example,

procedure playtwice(phrase p)
{
	p
	p
}

will play a phrase twice in succession. This procedure can be invoked by passing a name bound to a phrase or an actual phrase:

	playtwice(phrasename)
	playtwice({c d e f g a b})

A more general procedure could be written to play a phrase a certain number of times:

procedure playmany(phrase p, var n)
{
	repeat n do p
}

This can be called by passing either a variable or an expression as the second parameter:

	playmany(phrasename, i)
	playmany({c d e f g a b}, 23+4*7)

We can also define the previous procedure using a while-do statement in the procedure body:

procedure playmany2(phrase p, var n)
{
	var i = 0
	while i < n do
	{
		p
		i = i + 1
	}
}

A procedure with no parameters is declared with nothing in between the parentheses, but the parentheses must still be included in the definition.

4.12.2 Templates

Templates are conceptually similar to procedures but much easier to use. A template is simply a procedure with an implied procedure body. An example will make this clear:

template mybar { :8, :4, :4;20, :8 }

If this is invoked as

mybar(c, d:2, e, f)

this would be equivalent to the phrase

{
	c:8
	d:2:4
	e:4;20
	f:8
}

4.13 TYING IT ALL TOGETHER

This tutorial of Rubato concludes with an example featuring many of the features that has been introduced in the previous pages. It actually plays a 'real' (!) piece of music. The transcription of this piece into the syntax accepted by Rubato is deliberately stylized in order to show off as many features in Rubato as possible. Note that comments in the code are delimited by the strings /* and */.

/*
 *	over.r
 *
 *	OVER THE RAINBOW
 *	From the M-G-M Picture "THE WIZARD OF OZ"
 *	by E.Y. Harburg & Harold Arien
 *
 * Transcribed to Rubato by Chris Tham (christie@basser.cs.su.oz)
 */
$key f major
$default a = a'
conductor
{
	procedure alternate(note lo, note hi)
	{
		{ lo:8 hi:8 lo:8 } hi:8^
		repeat 2 do { lo:8^ hi:8^ }
	}
	phrase song =
	{
		/* this plays the melody of the song from start to end */
		phrase common =
		{
			template motif(:4, :8, :8, :4, :4)
			phrase theme =
			{
				/* Plays the main theme of the song
				 * This is repeated at least 6 times
				 * in the song */
				f`:2 f:2
				motif(e, c, d, e, f)
				f`:2 d:2
				c
				d`:2 b`:2
				motif(a`, f`, g`, a`, b`)
				motif(g`, e`, f`, g`, a`)
			}
			/* common part, song has structure common common */
			theme f`:2 r:2
			theme f`:2+:4 r:8 c:8^
			alternate(a, c) alternate(b, c)
			d:2 d:4*5 r:8 c:8^
			alternate(a, c) alternate(b%, d)
			e:2_ e:2_ g:2 d:2
			theme
		}
		common f`:2 r:2
		common f`:4*3 r:8 c:8^
		alternate(a, c)
		{ b:8 c:8 b:8 } c:8^ { b:8 c:8 d:8 e:8 }
		f:4*(4+2+1)
		r:4
	}
	/* set up global (absolute) values */
	tempo :4=80
	song
}
Figure 16: over.r - Over The Rainbow

Next: Chapter 5: A SPECIFICATION OF THE RUBATO LANGUAGE
Previous: Chapter 4: A TUTORIAL TO THE RUBATO LANGUAGE
Back to: Table of Contents


Created on Sat Feb 21 20:21:24 1998 using a perl script called m2h from original troff mm document.
Click here to download a copy of m2h.

Author: Chris Tham
Email: Chris_Tham@hp.com