SONITICA
Sonitica Program Structure
The basic structure of a Sonitica program is the program block.

{
	statement;
	...
	statement[;]
}

All statements must be separated by semicolons. Note that the semicolon is optional before the closing bracket.
Statements
Most statements in Sonitica take the form

variable = expression

As in BASIC, you don't have to declare your variables. That is because there is only one data type in Sonitica, which is the number. Numbers in Sonitica have a value from -32767 to 32767, and an accurary of 1/2^16, or about 0.000015. There's no range checking in Sonitica, so if you exceed the limits with an addition or multiplication, it just truncates the result.

A variable must begin with an alphabetic character or an underscore, and may contain any number of alphanumeric characters or underscores. The language is case-sensitive, so there's a difference between uppercase and lowercase.

Also like BASIC, if you assign a different expression to a variable, the variable is replaced with the new value. All variables are global; that is, once assigned a value, they retain that value throughout the program.
Expressions
Expressions look just like they do in any other language: you can use the operators +, -, *, /, and parenthesis. You can use constants and variables in your expressions with wild abandon. One exception is that you can only divide a number by a constant; you can't divide by a variable.

Some functions also require the use of set constants. These are syntatically similar to Pascal's sets, and are surrounded by brackets [ ]. Distinct elements are separated by commas, and ranges are denoted by two values separated by a colon. So the expression

[19:31,33:34,37]

would denote a set containing the values 19 through 31 inclusive, 33, 34, and 37.
Functions
You can't define your own functions in Sonitica. You can, however, define parameterized macro expressions with the preprocessor (described below). All functions in Sonitica are pre-defined. You call these functions like this:

variable = function(parameter[, parameter])

Each function has a certain minimum number of parameters. Some have optional parameters. We'll talk about specifics later.
The Preprocessor
Before going to the compiler, a Sonitica program goes through a preprocessing stage, similar to that of a C program.
An Example
Before we get too confused, let's start looking at some examples, and see what this language is really good for. Here's a simple Sonitica program:

{
	out = 0;
	outdevice(0, 22050, 1, 1, out);
}

Here's what this program does. The first statement, "out = 0", assigns the value 0 to the variable out. The second line, with the function outdevice, specifies that the variable out should be sent to your sound card. It is an output function, which means that it does not need to be assigned to a variable. In fact, it is an error to do so. You need at least one output function in your program for it to execute.

The parameters of outdevice are as follows:

outdevice(device_id, sample_rate, sample_size, channels, output)

The first parameter, device_id, specifies the device to which you want the sound to be output. Putting zero here specifies the default sound card. If you have more than one sound card, you need to pay attention to this parameter.

The next parameter specifies the sample rate in Hz, which is a real number and may go as high as you like.

The last parameter, output, is the expression you want to be sent to the sound device. This expression will be output to your sound card for the duration of the program.

However, you can see that the output parameter is equal to out, and in the first statement the variable out has been assigned the value of zero. Therefore, the value of zero will be sent to your sound device for the duration of the program. The result: Complete silence.

Okay, this example was boring, but it makes a point. Let's now make some noise:

#include "sonc.h"
{
	out = wavegen(TRIANGLE, delta(440));
	outdevice(0, 22050, 1, 1, out);
}

The line before the opening bracket, #include "sonc.h", tells the preprocessor to read in some macros from the file sonc.h to be used in the program. We need these to get the TRIANGLE macro. Don't worry, if you're not hip to this preprocessor jibe, we'll talk about it later.

The revised first statement now does something a little different. It assigns a wavegen function to the variable out. The wavegen function creates a simple waveform (triangle, sawtooth, or square) at a given frequency. In the parameters to wavegen, it tells it that it should be a TRIANGLE wave and that it should have a frequency of 440 Hz. The next line, again, outputs it (as the variable out) to the sound device.

When you run this program, you should hear a steady but annoying tone emenating from your speaker. Press a key to kill it. See how easy it is to make sounds?

Let's quiet that tone down a little bit. Change the third line of the program (the first line after the opening bracket) to:

	out = wavegen(TRIANGLE, delta(440))*0.25;

The wave output from the wavegen function has an amplitude of 1. Adding the *0.25 multiplies the waveform by a factor of .25, effectively reducing the waveform to one-quarter of its original amplitude. Run the program. The tone should be a little quieter now; is it?

Now try this:

	out = wavegen(TRIANGLE, delta(440))*wavegen(TRIANGLE, delta(10));

What does this do? Well, this multiplies the original triangle wave at 440 Hz by another triangle wave at 10 Hz. Run the program again. You should hear the original tone with a tremelo effect added.
Making Music
That stuff's pretty neato, but it's not music. Just watch: converting our little program to play a MIDI file is easier than you think.
MIDI: A Refresher
Before we go any further, we need to explain what exactly MIDI is. If you already know all about messages, channels, programs, and so forth, feel free to skip to the next section. Otherwise, here's the bare essentials of the MIDI protocol.

MIDI stands for Musical Instrument Digital Interface and was originally established as a communications protocol to link together keyboards. However, before long, many other uses were discovered for MIDI, most recently that of linking keyboards and tone generators to computers.

MIDI devices communicate by sending each other messages. There are several types of messages, each corresponding to a specific MIDI event. For instance, the NoteOn message is transmitted whenever a key is pressed on the transmitting keyboard. A NoteOff message is sent when the key is released.
How Sonitica Makes Music
The MIDI file interacts with the Sonitica program through the midi function. The midi function acts as a receiver for MIDI messages. Each declaration of the function is assigned to a particular MIDI message, and when that message is sent, it modifies a given variable or constant in the Sonitica program.

All instances of the midi function must be contained within a midivoice block. A midivoice block encapsulates the statements that make up a single instrument or voice in the Sonitica program. Each midivoice statement is responsible for the generation of a single note in the song to be played.

The parameters to the midivoice block specify the channel range, program range, and note range for the voice. Only MIDI messages that fall within these limits will get passed to the midi functions inside the midivoice block. Messages outside the given values will be filtered out. This way you can make instruments that will only sound on given channels, or for given programs. For instance, you can make a trumpet instrument that will only sound notes on program number 56. Or you can make a snare drum that is only activated on channel 10, note number 38.

The midi function takes one or two parameters. The first parameter is always the value of the MIDI message you want the function to receive. Most MIDI messages are defined as macros in sonc.h.

The second parameter, which is optional, specifies a variable or constant that you want to hold the value of the message when it is received.

#include sonc.h
{
	midifile("g-fugue.mid");
	midivoice([0:9,11:14],[0:127],[0:127]) {
		vel = midi(NOTE_OFF, midi(NOTE_ON_VEL));
		mod = midi(NOTE_ON_PITCH, delta);
		tone = wavegen(TRIANGLE, mod);
		out = tone*vel;
	}
	outdevice(0, 22050, 1, 1, out);
}

This program will play a standard MIDI file using our goofy little tones that we invented in the preceding program.

The first statement of this program, midifile("g-fugue.mid"), specifies the MIDI file to be used for this program. It can also be given on the command line with the -m <filename> option.

The next statement is a midivoice statement that begins the definition of one instrument voice in the program. The three parameters to midivoice are set constants.

The statements within the midivoice block show a simple way of defining an instrument in Sonitica. In this example, the voice will be a simple triangle wave of variable pitch, and will have no envelope. That means that it will switch on instantly when the NoteOn message is sent, and switch off instantly when the NoteOff message is sent.

We choose the vel variable to represent the voice's velocity, or intensity. The statement

		vel = midi(NOTE_ON_VEL);

indicates that when a note is pressed (a NoteOn message is sent), the vel variable should contain the value of the note's velocity. The velocity falls within the range 0 to 1.

Great, we've got a variable that gets set to the velocity of a note when it's pressed. But what about when the key is released? We've said nothing about when the NoteOff message is sent. If we left the statement like this, the note would never turn off. So we add another midi function to the last one, like this:

		vel = midi(NOTE_OFF, midi(NOTE_ON_VEL));

The mod variable will represent the modulation of the note, or the pitch of the tone that will be generated.

The tone variable will actually contain the waveform itself, and will take the mod variable as input to define the frequency of the generated wave. Finally, the out variable is set to tone*vel. Remember that the vel variable not only controls the loudness of the note, but also turns it on and off. Since the vel variable is equal to zero when the note is not playing, the value of out will also be zero.

This completes our definition of a single voice in the Sonitica program. The last statement performs the same duty it did in our last program, that of sending the out variable to the sound device.

Now run this program. Wow, groovy tune, man. But this piece has four parts, and I only hear one part playing! Remember that each midivoice block is responsible for the generation of only one note at a time. If we want more polyphony, we just duplicate the midivoice block as many times as we want simultaneous notes. We can use the magic of the preprocessor to perform this task for us, using the #repeat directive:

#include sonc.h
{
	midifile("g-fugue.mid");
	out = 0;
#repeat 4
	midivoice([0:9,11:14],[0:127],[0:127]) {
		vel = midi(NOTE_OFF, midi(NOTE_ON_VEL));
		mod = midi(NOTE_ON_PITCH, delta);
		tone = wavegen(TRIANGLE, mod);
		out = out + tone*vel;
	}
#enddef
	outdevice(0, 22050, 1, 1, out/4);
}

Try the program now. You should now hear four of the cheesiest organ parts ever played since the Doors broke up. Look at what we did: We started with the out variable equal to zero. Then, in the midivoice block, we added tone*vel to it. The block was repeated four times, so four different tone*vel expressions were added to the out variable.

In the outdevice function, we divide out by 4 because since there are four parts now, there is the potential for the sum of the parts to be 4 times more intense than a single part by itself. Try the program without dividing out by 4 and hear some awful-sounding clipping taking place.