







                          Sapphire
                    An Acoustic Compiler

                             by

                         Jim Finnis
















































                                                           1










                         CHAPTER  1


                        Introduction




1.  Overview

     Sapphire is an acoustic compiler : it takes a text file
written in the sapphire programming language,  and  converts
it  into sound (usually a WAV file).  I've assumed that any-
one reading this document has a reasonable understanding  of
electronic  music  techniques  -  if  you don't know what an
envelope  generator,  oscillator  or  low-pass  filter  are,
please find out before reading further.

     The  way  sapphire works is similar to ray-tracing pro-
grams like POVray.  You give sapphire a text description  of
a  sound  (or  a whole piece of music), and it will generate
the sound you have described. It may take several  hours  to
'render' a complex sound, even though that sound may be only
a few seconds long. However, there is no limit to  the  com-
plexity  of  the  sound.   You could design a sound which no
existing synthesizer can generate, using hundreds of  oscil-
lators,  samples,  filters, delays and so on, which might be
only 5 seconds long but will take six hours for sapphire  to
render.

     There  are  three  main uses for sapphire. Firstly, you
can use it to generate short samples of complex sound, which
can  be  loaded into a sampler and played at any pitch. Sec-
ondly, using the score language and  instrument  management,
you  can write an entire piece in sapphire which it can then
generate.  Finally, you can use it to read  in  and  process
samples in novel ways, outputting new samples.

1.1.  A Brief Example

     Here's a quick example of the sapphire language to give
you some idea of what it looks like. Note  that  '--'  means
the rest of the line is a comment.

    include sine.wave

    fosc: osc &sine 0.5 100.0;  -- frequency oscillator
    freq: add fosc 400.0;       -- find the frequency for..
    CH0: osc &sine freq 1.0;    -- ... the main oscillator

    end at (100000);















The  first  line automatically reads in the file 'sine.wave'
(which is in sapphire's 'sample' directory) and contains the
definition of a sine wave. The next line creates an oscilla-
tor which uses this sine wave, has a frequency of 0.5Hz, and
an  amplitude  of  100.0. This oscillates between -100.0 and
100.0 every two seconds. The output of  this  oscillator  is
stored in a variable called 'fosc'.

     The  next line creates an 'add' object which adds 400.0
to the output of the oscillator, storing  the  result  in  a
variable  called  'freq'.  This will oscillate between 300.0
and 500.0 every two seconds.

     Then we create another sine wave  oscillator,  with  an
amplitude  of  1.0, which oscillates at the frequency speci-
fied by the previous line. That  is,  this  oscillator  will
sweep between 300Hz and 500Hz every two seconds.  The output
of this oscillator is fed to the special variable CH0, which
is  the left hand (or mono) output channel. The output chan-
nel converts values into sample values to  be  stored  in  a
file.

     The  last  line  in  the program tells sapphire to stop
compiling after 100000 samples.  We could also  have  speci-
fied an absolute time. For example,

    end at 01:00:00;

would  create a result 1 minute (0 seconds and 0 hundredths)
long. If you want to hear the result, copy the program  into
a file called (say) 'test1.s' and execute the command:

    sapphire -fwav16 -s8000 -c1 -ofoo.wav test1.s

This  creates  a 16-bit WAV file, at a sampling frequency of
8000Hz, with one output channel (i.e. mono), called foo.wav,
from  the  sapphire  file  test1.s.  Play the result using a
program like 'vplay' - whatever you use to  play  WAV  files
normally. It's not very exciting, though.

     First, sapphire has to parse the input - turn your text
file into internal lists of objects  and  events.  This  can
take  a long time for a long sapphire file (I've recorded up
to 10 minutes on my machine, but then  that  was  for  a  20
minute  piece). So don't worry if sapphire seems to be doing
nothing. Really, it's working its little heart out trying to
understand  your program. Once this phase has finished, sap-
phire can use these lists to generate the sound.

     You'll notice that as it does this, sapphire produces a
kind  of  running  commentary  on  how it's doing. Each line
looks like this:



                                                           2










    ** 7.54% (max out=0.5565 [*1.0000]) **

The first number is what percent of the  file  sapphire  has
generated  so  far,  to  give you an idea of how much it has
left to do. The second number is the  maximum  amplitude  of
the generated sound. Sapphire's output channels take samples
in the range -1 to 1 and convert  them  to  the  appropriate
range for your output format. The output should never exceed
this range - which brings us neatly to the third number.  If
the amplitude of the output does exceed 1, sapphire automat-
ically starts dividing it by a sufficient amount to make  it
fall  back  into  the correct range. The third number is the
number sapphire multiplies the output by.  It's  usually  1,
unless the limiting has been used.

     When  the limiting kicks in, there'll be a click in the
output sample, so you'll need to run sapphire  again  having
fixed the problem that caused the output to go out of range.
A simplistic method of doing this is just  to  multiply  the
output by the final value of the limiting factor.

2.  Sapphire Programs

     Sapphire   programs   generally  describe  two  things:
objects and events.  Object definitions, for example

    fosc: osc &sine 0.5 100.0;

describe a web of objects,  linked  together  by  variables.
Each time sapphire generates a sample in the output file, it
runs each object by reading the object's inputs  (which  can
be  variables  or  parameters), running the object code, and
storing the results in the object's output variables.  (most
objects  only  have  one  output).  There are many different
types of object, all described below.

     Event definitions describe changes  in  variables  that
take place at a certain time. For example,

    event at 01:00:00
         freq=440.0,
         ampl=1.0,
         noteon=1.0;

describes a change in the values of three variables which is
set to take place one minute into the result. The  'end  at'
command  is a special type of event, which tells sapphire to
stop generating samples.

     There are several other types of statements, for  exam-
ple  wave,  scale,  sample  and  instrument definitions, but
these are just there to make life easier. The heart of  sap-
phire is the web of objects and the list of events.


                                                           3










2.1.  How to define an object

     An  object takes values or variables, manipulates them,
and outputs the result into a variable. An object definition
looks like this:

    output1,output2,... : object_type input1 input2 ... ;

There  are  several  examples  of  objects above, so I'm not
going to bother with another. Very  few  object  types  have
multiple  outputs  (the  only  one I can think of is "stere-
omix"), but you can still specify more than one  output,  in
which  case the single output from the object will be copied
to all the output variables specified. Got that?  Here's  an
example:

    a,b,c:    osc &sine 440.0 1.0;

will set up a single sine wave oscillator playing a middle A
(440Hz), which will write its output to all three  variables
a,b and c. This can be handy sometimes. And that's all there
is to defining an object.

2.2.  Object expressions

     This is a new feature which I've  added  to  make  life
much  easier. If we look at something like our earlier exam-
ple,

    include sine.wave

    fosc: osc &sine 0.5 100.0;  -- frequency oscillator
    freq: add fosc 400.0;       -- find the frequency for..
    CH0: osc &sine freq 1.0;    -- ... the main oscillator

    end at (100000);


wouldn't it be nice to not have to worry  about  those  fosc
and  freq  variables, which are just used to store temporary
values, and write proper expressions like you do in any nor-
mal programming language. Well, you can:

    include sine.wave
    CH0: osc &sine {add {osc &sine 0.5 100.0} 400.0} 1.0;

As  you  can  probably  figure  out,  when sapphire meets an
object definition enclosed in curly brackets,  it  processes
it and invents a variable (which you don't see) to store the
results in. Then it pretends the whole thing in curly brack-
ets  is  that  variable.  And  you  can  have objects within
objects down to any depth.



                                                           4










     You'll find it makes things much more legible and  eas-
ier  to understand if you split the lines a bit and indent a
little. Look at the sample code for more examples:

    CH0: osc &sine
         {add
              {osc &sine 0.5 100.0}
         400.0} 1.0;


2.3.  How to set an event

     Use an event definition  if  you  want  a  variable  to
change  at  a  particular  point  in a sound, for example to
change the frequency or amplitude of an  oscillator,  or  to
trigger  a sample or envelope shaper. Event definitions take
the form:

    event at some_time var1=value,var2=value... ;

where some_time is a time specified as a  sample  number,  a
beat number, or an absolute time:

 + (x)  specifies  the  time  as  a sample number within the
   result, e.g. (100000). The first sample is sample 0.
 + /xx:yy:zz/ specifies the time as xx bars,  yy  beats,  zz
   64ths  of  a beat, e.g. /08:00:00/. This is also based on
   zero, so the fourth beat of the third bar is  /02:03:00/.
 + xx:yy:zz  specifies  an absolute time in minutes, seconds
   and hundredths.
 + "a number by itself" just specifies  a  time  in  seconds
   (you can use a decimal point if you like).

     I've  described  the  basics of objects and events, and
that's the most important part of this manual over with. The
rest  of  sapphire  is  essentially just extra stuff to make
life easier. For example, setting up instruments with multi-
ple  voices  and playing scores on them is possible in 'raw'
sapphire, but the instrument  and  score  definition  syntax
makes it much easier.

     Before moving on to instruments and scores, we'll cover
command line options, how to include files, and then how the
various objects work.

3.  Command Line Options

     The  command  line  options accepted by sapphire are as
follows:

 +  -snumber specifies the sampling rate  of  the  resulting
   file.



                                                           5










 +   -Ipath  adds a directory to the search path. The direc-
   tory "must contain the trailing /" .
 +  -cnumber sets the number of output channels. Only 1  and
   2 are currently supported.
 +   -fformatname  sets  the output format. To get a list of
   the available formats, use '-f?'.
 +  -ofilename sets the name of the output file.  If  it  is
   not specified, the data is output as floating-point ASCII
   numbers  to  the  standard  output,  and  the  format  is
   ignored.
 +  -d<number> turns on debugging and produces reams of use-
   less data. If you mail me with a problem,  I'll  probably
   ask  you  to run sapphire again with this option set so I
   can see what's going on.  If  you're  really  curious,  I
   might  even  tell  you what the various debugging options
   mean. You could even figure it out yourself (if you  have
   the  source, and you should have, look at the end of sap-
   phire.h).
 +  -D turns on parser debugging and produces even more use-
   less data. Yeuch.
 +   -l  turns  off  the  status listing. Normally, sapphire
   gives a running percentage of how far through the file it
   is, every 100 samples. This option switches that off.
 +  -E prints a listing of the events and objects created by
   the program before it is actually executed. This is  use-
   ful for debugging things like complex object expressions:
   what you'll get is all the information about the 'hidden'
   variables that sapphire made when it read your expression
   and how it linked the objects together using them.

4.  File Inclusion

     The 'include' statement, seen at the top of the example
given  earlier, interpolates another sapphire file into your
file. It is usually used to include instruments,  waves  and
scales  into  a program. It behaves much like the '#include'
directive of the C programming language. One  major  differ-
ence,  however, is that once a file is included, any attempt
to include it again will do nothing.  This means you can add
'include  sine.wave'  (for  example)  at the top of all your
instrument definitions, and include lots of instruments into
your  main sapphire file, without the 'sine.wave' file being
read more than once.

     Sapphire looks in the current directory,  and  then  in
the  directories  'insts', to the list of search paths using
the '-I' command line option.

5.  The Objects

     In this section, I'll divide the objects sapphire comes
with  into functional groups and explain how each one works.
If an object requires some extra syntax, I'll  explain  that


                                                           6










too. For example, in order to use an oscillator, you need to
know how to define a wave.

     A few conventions are used in the object  descriptions.
Each  object's  title includes details of its parameters and
how many results it has. <Angle  brackets>  indicate  things
that  you  change in your own code. Dots '...' indicate that
the previous item may be repeated any number of times.

5.1.  Generators

     These are objects which actually generate sounds  based
on  their  parameters.   They're  probably  the most complex
objects in Sapphire, and the most  useful,  so  I'm  dealing
with  them  first  to  get  them  out  of the way.  You have
already seen one - the wave-table  oscillator,  'osc'.   The
others are used for playing samples.

5.1.1.  signal: osc &<wave> <frequency> <amplitude>

     This  is  a  wave-table oscillator, which generates the
wave at the given frequency and  amplitude.  The  wave  must
have  already  been  defined  with  a 'wave' statement. This
takes the form:

    wave <name>
    {
         <value>
         <value>
         ...
    }

A wave can have up to 64000 values. The values in  the  wave
constitute  a single wavelength.  Usually, you won't need to
write your own waves, just include the  waves  provided  for
you  in  the  'samples'  directory, as I did in the examples
above. If you do need to write your own waves, make sure you
provide  a  large number of values, otherwise aliasing - the
inherent 'steppiness' of a digitised wave - will be  obvious
and  unpleasant to the ear. Remember that you can easily use
the 'oneshot' and 'loopsamp' objects to  play  samples  from
WAV (or other) files much more easily.

     A useful application of multiple oscillators is FM syn-
thesis. Here's a little example of  an  FM  tone,  using  an
'expodecay'  envelope  shaper  you  can  find out more about
below.








                                                           7










    include sine.wave

    -- work out the modulated frequency of the
    -- main oscillator. I'm not using {} here to
    -- make it easier to grok.

    -- multiply the main freq by the ratio
    modf:  mul freq rat;

    -- this is the modulating oscillator
    osc1:  osc &sine modf 0.5;

    -- convert to range 0.5 .. 1.5
    mult:  add 1.0 osc1;

    -- and work out the modulated frequency
    mfrq:  mul freq mult;


    thesound: osc &sine mfrq 1.0;

    -- we generate the sound, with an amplitude of 1.0,
    -- and shape it. Here we use the 'expodecay' envelope
    -- shaper. When noteon is 1, this is triggered to
    -- start multiplying the signal by 1.0.  Once noteon
    -- becomes zero, this multiplier will decay, reaching
    -- 0.0625 (1/16) after 1.5 second.

    CH0:   expodecay noteon 1.0 1.5
              {osc &sine mfrq 1.0};

    event at (0)    -- event at first sample
        noteon=1,   -- triggers the expodecay
        freq=440,   -- sets the frequency
        rat=0.785;  -- and the modulation ratio
    event at (1)    -- event at second sample
        noteon=0;   -- clears the trigger

    end at 00:02:00;

That's a pretty heavy example, and you have to understand FM
synthesis  to get the most of it. Also, you may think it's a
lot of trouble messing about with triggers  just  to  get  a
single  note. Well, don't worry - playing notes is made much
easier by the instrument/score syntax I'll deal with  later.

5.1.2.    signal:   oneshot   &<sample>   <noteon>   <pitch>
<amplitude>

     This is a simple sample player, which  plays  a  sample
through  from  beginning to end and then stops. The 'noteon'
parameter triggers the sample - if noteon is 1,  the  sample
starts playing again from the beginning. Because it restarts


                                                           8










every time noteon is 1, you must set  it  to  zero  straight
after.  Scores  and instruments do this for you (see below),
but the next example shows you how to do this by hand.

     Before you can play a sample, you need to tell sapphire
about  it  with  a 'sample' statement.  This is because sap-
phire needs more information than is stored in your  average
sample file.  The sample definition takes the form

    sample <samplename>
         format <formatname>
         file "<filename>"
         pitch <pitch>

You  can  see  some  examples in the 'insts' directory (some
might be a bit longer - more on this  later).   <formatname>
is  the format of the file. You can see a list of valid for-
mats using 'sapphire -f?'  as described earlier.  Here,  you
can use the formats marked 'for input only'. For example, if
you are using WAV files, you don't need to specify 'wav8' or
'wav16'  - just 'wav' will do; sapphire will figure out from
the file whether the sample you wish to  play  is  8  or  16
bits.

     <filename> is the name of the actual file the sample is
stored in. For example, if you are  playing  a  WAV  sample,
this  will be the name of the WAV file. Sapphire will search
for this file in the search path, as it does for  'included'
files.

     Finally,  <pitch>  is the pitch of the note in the sam-
ple, assuming it's a  musical  note.  For  example,  if  you
recorded someone playing a middle C on a waterphone (look it
up!) in a file called "waterphone.wav", your sample  defini-
tion in sapphire would look like:

    sample waterphone
         format wav
         file "waterphone.wav"
         pitch 261.6      -- a middle C

To actually play the sample, at the pitch it was recorded (a
middle C) and at maximum amplitude, we could use the follow-
ing sapphire program:

    sample waterphone
         format wav
         file "waterphone.wav"
         pitch 261.6      -- a middle C

    CH0: oneshot &waterphone noteon 261.6 1.0;
    event at (0) noteon=1;
    event at (1) noteon=0;


                                                           9










Easy,  yes?  And  you can then manipulate the sample further
with any of the other objects. You can even do wacky  things
like  modulating  one  oneshot sample - tweaking its 'pitch'
input, as we did with the oscillator in the last  example  -
with another oneshot sample. Ugh.

     If you want to override the sample rate recorded in the
file (why you'd want to is  beyond  me  -  just  change  the
pitch)  you  can use the Extended form of the sample defini-
tion. This is just the same except for a samprate thingy  at
the end:

    sample waterphone
         format wav
         file "waterphone.wav"
         pitch 261.6      -- a middle C
         samprate 11025

Finally  -  and  this is really important - if you are using
samples in "raw" format (just the linear  raw  data  itself)
you'll have to specify the number of channels too, using the
Really Extended form:

    sample waterphone
         format raw8
         file "waterphone.raw"
         pitch 261.6      -- a middle C
         samprate 11025
         channels 1

If you don't specify sample rate and channels for raw  data,
sapphire  will  ask you for these when you run it. This gets
to be a pain after a while.

5.1.3.  loopsamp &<sample> <noteon> <gate>  <pitch>  <ampli-
tude> <loop start> <loop end> <loop transition>

     This  is,  for the most part, the same as the 'oneshot'
object - except for the gate parameter, and those last three
loop  parameters.  As you may have guessed, it lets you play
samples in a loop for as long as gate is non-zero.

     The behaviour of the loop is set by  those  last  three
parameters,  all of which are percentages.  Loopstart is how
far through the sample the looped section starts - i.e.  the
point  to  which the sample 'rewinds' when it loops; loopend
is the end of the looped section,  when  it  jumps  back  to
loopstart, and the final "loop transition" parameter is very
special indeed. It describes the  percentage  "of  the  loop
time" spent in fading from the end of the loop to the begin-
ning. Normally, a loop would just  jump  straight  from  the
last  sample  in  the looped section to the first, giving an
audible click unless  you'd  got  the  loop  points  exactly


                                                          10










right.  Loopsamp lets you fade out the last part of the loop
and simultaneously fade in the first part, giving  a  smooth
transition.  The  loop  transition time is the percentage of
the loop taken up in this fade operation.  For  example,  if
you set it at 50%, the crossfade would start halfway through
the loop.

     When the gate parameter is zero, the  transition  never
happens  and  the object just keeps playing the sample until
it stops, just like a normal oneshot sample player.

5.2.  Arithmetic Operators

     These are objects whose purpose is to  manipulate  num-
bers; these numbers may be actual sound data or just numbers
you're using internally as parameters.  We've  already  come
across  'add'  and  'mul' to add and multiply numbers; there
are a few others.

     Of course, when I say 'number', I mean any variable  or
signal  or  object expression or whatever. I'm sure you knew
that, but I'd hate anyone to get confused.

5.2.1.  result: add <number1> <number2>...

     This object adds all the numbers together and pipes out
the result.

5.2.2.  result: mul <number1> <number2>...

     This  one multiplies all the numbers together and pipes
out the result.

5.2.3.  result: div <number1> <number2>

     This divides number1 by number2 and outputs the result.
For speed's sake, I'm not doing any divide-by-zero checking,
so if you get an unexpected  floating  point  error,  divide
statements are good places to look.

5.2.4.  result: sub <number1> <number2>

     This  subtracts  number2  from  number1 and returns the
result.

5.2.5.  result: mix <number1> <number2>...

     All this does is take all its inputs and average  them.
Useful,  as  the name suggests, for mixing - but see 'stere-
omix' below for vastly more powerful mixability.





                                                          11










5.2.6.  result: copy <number>

     This is a bit of an oddity. Still, you  might  find  it
useful.  All  it  does  is  copy  its input to its output. I
haven't had cause to use it yet.

5.2.7.  result: switch <test1> <test2> <number1> <number2>

     A  bit  weird,  this one, but I've found it useful when
dealing with thresholds and stuff like that. If test1<test2,
then  number1  is  output,  otherwise number2 is output. For
example, you could add some really unpleasant  digital  dis-
tortion to a signal using:

    distorted_signal : switch in_signal 0.5 in_signal 0.5;

which would cause any signal above 0.5 to be clipped to 0.5.
Ugly.

5.3.  Envelope Shapers

     An envelope shaper takes a signal and multiplies it  by
a  number  which  it  calculates  internally  each sample to
'shape' the amplitude of the sound.


5.3.1.  signal: adsr <trig> <gate> <att> <dec>  <sus>  <rel>
<input>

     This  is  a good, old fashioned Attack, Decay, Sustain,
Release envelope shaper. When trig is not zero,  the  multi-
plier  is  set to 0 and the envelope started off. As soon as
trig becomes zero (usually the next sample), the  multiplier
rises  until,  after  att seconds, it is at 1 - the maximum.
Then it starts to drop, until, after another dec seconds, it
is  equal  to  sus .  The multiplier stays at sus until gate
becomes zero, whereupon it takes  rel  seconds  to  drop  to
zero,  ready  to  start again. All the time, input is multi-
plied by the multiplier and output to signal .

     In brief  then,  att/dec/sus/rel  describe  the  actual
shape  of the envelope, trig starts it off and gate keeps it
going. Here's a self-contained example  of  how  to  play  a
sound  - just a sine wave here - through an envelope shaper.











                                                          12










    include sine.wave

    --  Here's our envelope shaper.
    --  The attack phase is 0.1sec, decay 0.1s, the sustain
    --  phase is at level 0.6, and the sound takes 0.5s to
    --  release. The envelope is shaping a sine wave,
    --   at frequency 440Hz (middle A) with amplitude 1.

    CH0 : adsr noteon gate 0.1 0.1 0.6 0.5
            {osc &sine 440.0 1};

    -- here are the events we need to play the note.
    -- First, set noteon and gate to 1, then switch off
    -- noteon (the trigger) immediately afterwards.

    event at (0)
         noteon=1,gate=1;
    event at (1) noteon=0;

    -- Turn off the note and let it decay after 1 second.

    event at 1 gate=0;

    -- and end the sample after 2sec, to give time for the
    -- release phase.

    end at 2;

It looks like using envelope shapers  and  actually  playing
notes  and  stuff  is  quite tough, yes? Actually, it's made
much easier by the instrument and score facilities I'll talk
about  later.  These  will  let you say 'play this note' and
will create the necessary  events  behind  the  scenes.  But
before  I  deal  with that, I'd better cover the rest of the
objects.

5.3.2.  signal:  expodecay  <trig>  <startval>  <timeto16th>
<input>

     This  is  a different kind of envelope shaper: an expo-
nential decay.  Using this  envelope  gives  a  naturalistic
'plucked string' sort of sound.

     Here,  trig is the same as in the adsr envelope shaper:
it tells the envelope when to start. When  it  is  non-zero,
the  envelope's  internal  multiplier  is  set to startval .
This is the peak value, the starting value, of the envelope.
Thereafter,  the envelope will decay by a fraction each time
- but it will never quite  reach  zero.  In  practice,  this
doesn't matter since it won't take long before it goes below
the threshold of audibility. In any case, timeto16th is  the
speed  of  the  decay  -  the time it takes for the sound to
decay to 1/16th of its starting value. The larger  this  is,


                                                          13










the slower the decay. It can take some time to get right. As
in the adsr envelope, the signal  parameter  is  the  signal
which is being shaped.

5.4.  Filters

5.4.1.  filter <signal> <frequency> <gain> <Q>

     A  filter changes the frequencies present in the output
signal. In  sapphire,  we  use  a  band-gain  filter,  which
increases  the amount of sound present around a certain fre-
quency.  For example, to boost the  bass  end  of  a  signal
(frequencies around 100Hz) we could use

    output_signal : filter my_signal 100 6 1;

This  would  boost frequencies around 100Hz by 6dB. The last
number is the quality factor, which controls  the  bandwidth
of the filter. The bandwidth is the centre frequency divided
by the Q factor, so the above filter would boost frequencies
between 0 at 200Hz, peaking at 6dB at 100Hz.

                            NOTE


    Be  very  careful  with  filters! They can easily go
    wild, and resonate - producing a loud whine  at  the
    centre  frequency (or worse) complete chaos. It will
    take some practice to get the parameters of a filter
    right in any given situation.

For technically minded folks, the filter used is an IIR fil-
ter with two poles and two zeroes.

5.5.  Other Objects

     There are quite a few objects in  sapphire  that  don't
fit into any of the above categories.  Here they are.

5.5.1.     leftsig,rightsig:    stereomix    <sig1>   <pan1>
<amp1>...

     This very useful object lets you build  complex  stereo
images.  It takes up to 10 sets of three parameters, each of
which contains signal, pan and amplitude parameters.  Sig is
the  signal  to be mixed in, pan is the desired stereo posi-
tion of that signal in the output (from -1 to 1), and ampli-
tude  is  the  amplitude of that signal. All the results are
averaged, like in the 'mix' object.

     An important thing to realise about this object is that
it  has  two  outputs - currently it is the only object with
this distinction. The two outputs are seperated  by  commas.


                                                          14










The  first  output is the left signal, and the second output
is the right.

     Another useful thing to know is that it's  quite  legal
to  change  any  of the pan and amplitude parameters 'on the
fly', so you can  have  signals  sweeping  about  the  final
stereo image like wild things.

5.5.2.  result: delay <signal> <maxtime> <curtime>

     This  simple  object  delays the signal by curtime sec-
onds. The delay time can be altered during a piece, but this
can  lead to clicks and buzzes - so-called 'zipper noise'. I
have yet to find a way around this. Help!   Maxtime  is  the
maximum permitted length of this delay, in seconds. The cur-
rent delay time must always be less than or  equal  to  this
value.

5.5.3.  output: samphold <signal> <interval>

     This  object  samples  the input signal at intervals of
interval seconds, and sets the output to  that  value  until
the next sample.  Users of old-style modular analogue synths
will recognise the familiar "sample and hold" module.

5.5.4.  output: random <range>

     This object produces a  random  number  each  time  the
object  runs, between 0 and range .  It's not a good idea to
use this to generate white noise, because computer-generated
pseudorandom  numbers  aren't  truly random - there are pat-
terns in the result, which will  make  themselves  heard  as
truly horrible noise. The best way of getting white noise is
to sample some, and use the sample.

     The random object, however, is useful when you want  to
'tweak'  frequencies slightly on the fly, for example, or in
association with a samphold object.

6.  Making Things Easier  :  Ramp  Events,  Instruments  and
Instances

     Writing complex pieces and sounds in sapphire is possi-
ble with all you've learnt so far,  but  it's  like  kicking
dead  whales  down  a  beach  -  difficult  and  unpleasant.
Because of this, I've added several extra facilities to make
certain tasks much easier.

6.1.  Ramps

     The first of these is the ramp event. You give sapphire
the name of a variable, a start time and value, and  an  end
time and value; and sapphire will smoothly ramp the variable


                                                          15










from one value to the other between those times.  For  exam-
ple,

    event at (0) my_amplitude=0;
    ramp my_amplitude from 0 at 10 to 1.0 at 30;
    ramp my_amplitude from 1.0 at 40 to 0 at 60;

will  set  the  variable my_amplitude to zero to start with,
then at 10 seconds start fading it up until it is  1  at  30
seconds.  It  will hold the value 1.0 until 40 seconds, when
it will start to ramp it down until it's zero again after 60
seconds.

     Now,  you could have done that using some kind of enve-
lope shaper object, but for simple, one-off  ramps  this  is
much  easier.  As  a  bonus, it also takes considerably less
processing time than a whole object.

6.2.  Instruments and Instances

     Instruments and instances are the most complex parts of
sapphire,  so pay attention. Generally, you'll only use them
when you are designing actual pieces of music, where instru-
ments play notes, rather than just sound effects.

     An instrument is essentially a collection of objects as
a kind of 'template'.  For example:

    instrument simplesine durationadd 0.5
    {
         %osc1:    osc &sine {div $freq 2} 0.5;
         %osc2:  osc &sine $freq 1;

         out: adsr $noteon $gate 0.1 0.1 $amp 0.5
                        {mix %osc1 %osc2};
    }

describes an instrument which consists of  two  sine  waves,
one  of which is an octave lower than the other, being mixed
and passed through  an  adsr  envelope  shaper.  The  'dura-
tionadd'  value  tells  sapphire that each note will last at
most 0.5 second after it has stopped being played - this  is
because  of  the  0.5 second release on the envelope shaper.
It's important because sapphire needs to  know  how  long  a
note will last in order to correctly allocate voices when it
plays a score.  I'll cover  the  funny  '%'  and  '$'  stuff
later.

     The  instrument  definition  just stores a template for
the instrument. It doesn't actually make any objects. To  do
this, we need to use make an instance of the instrument. You
can specify in your instance how many  voices  the  instance
will  have - i.e. how many notes it can play simultaneously.


                                                          16










Sapphire will make one set of objects out  of  the  template
you provided for each voice as you need, and them wire these
voices together with a special object, so that  all  of  the
voices' outputs are mixed. For example,

    my_sine_wave: instance sine1 8 of simplesine;

will  create an instance of the simplesine instrument called
'sine1'. This instance will have 8 voices, so sapphire  will
make  8  sets  of  objects from the instrument template. The
special 'out'  variables  from  each  voice  will  be  added
together   (NOT   averaged!),  and  put  into  the  variable
'my_sine_wave'. You can make as many  instances  of  instru-
ments  as you like, all with a different number of voices if
you wish.

6.2.1.  Instance variables and parameters

     So what about the variables in the instrument  template
which  start  with  '$'  or  '%'?  Well, these are variables
which are private to each voice.  If  you  use  an  ordinary
variable name, such as 'fred' or 'amplitude', the value will
be shared across all the voices of all the instances of  all
the instruments. However, any variable which starts with '$'
or '%' is unique to each voice. For example, the '%osc1'  in
voice  1  of  instance  'sine1'  is not the same variable as
'%osc1' in voice 2 of that instance.

     Technical bit: how sapphire does this. Skip this  para-
graph if you're not interested... If you run sapphire with a
-S option, on a file which uses instruments,  sapphire  will
print out the objects it has created before it generates the
output. You'll see then that it's turned  each  '%'  or  '$'
variable into something like '_sine1.2.e1', meaning 'the %e1
variable from the 2nd  voice  of  the  sine1  instance.  And
that's  how  it  works.  Knowing how to read the '-S' output
can help you debug sapphire programs.

     The difference between '$' and '%' is just a matter  of
convention.  You  use  '%' variables, called "instance vari-
ables" , to hold private information to that voice - such as
the  oscillator  outputs  in  the example. Sapphire uses '$'
variables, called "instance parameters" ,  to  hold  special
values used by the instrument/score/instance mechanism. Gen-
erally, they are the variables the  'playscore'  and  'note'
commands  generate  events  for  when you want to play notes
(see below). They are used in the sine wave instrument exam-
ple  above.   You'll  need  to know these to use instruments
properly, so here they are.






                                                          17










6.2.2.  The instance parameters

 +  $noteon This is the trigger to tell  the  voice  that  a
   note  has started. It's only true for the first sample of
   the voice, so it's usually used as the trigger for  enve-
   lope shapers.
 +  $gate This is the gate for the voice - it's true while a
   note is being played, and corresponds to the  gate  input
   of the envelope shapers.
 +   $freq  This is the frequency for the current note being
   played. You normally feed this to oscillators  or  sample
   generators,  although  you  might want to base the centre
   frequency of a filter on it.
 +  $amp Finally, $amp is the amplitude  of  the  note  cur-
   rently  being  played  on this voice. You can either feed
   this into the appropriate part of an envelope shaper,  or
   just multiply the output of the voice by this value.

     Actually,  that's  not  quite  finally. If you put some
extra variables or values on your instance  statement,  such
as

    my_thing: instance my_instance 4 of my_instrument
              0.1 ampl pan;

the  extra  values  -  0.1,  ampl and pan in this case - are
available to the instance as the variables $0,  $1  and  $2.
You  can  go up to $9.  This way, you can make each instance
of an instrument slightly different. For example:

    instrument simplesine durationadd 0.5
    {
         %osc1:    osc &sine {mul $freq $0} $1;
         %osc2:  osc &sine $freq 1;

         out: adsr $noteon $gate 0.1 0.1 $amp 0.5
                        {mix %osc1 %osc2};
    }

    CH0: mix
              {instance sine1 4 of simplesine 2 0.5}
              {instance sine2 4 of simplesine 0.5 1};

I'll leave it to you to  work  out  the  difference  between
those two instances.

7.  Scores and Scales

     Now  you  have  your  instruments and instances, you'll
want to play notes on them. You do  this  using  scores  and
scales.   I've  already  defined a few scales for you, which
you can find in the 'scales' directory ready to include. The
one you'll use most (if not exclusively!) is Equal440, which


                                                          18










is the equal tempered scale with middle A at 440Hz. You  can
use this scale by putting the line

    include Equal440.sc

at  the  beginning  of  your sapphire program. I'll describe
below how to define your own scale, should you need to,  but
first we'll look at how to play single notes.

7.1.  Playing a Note

     Once  you've defined a scale, or included the scale (or
scales) you're going to use, you can play a single  note  or
group  of  notes  using  the note statement, which takes the
form

    note <time> <instance> <scale> <notelist>
         <duration in beats> <amplitude>;

Here's an example:

    note /00:03:01/ my_inst Equal440 a2+e2 2.0 0.5;

This will play, for 2 beats, starting at the second beat  of
the  fourth  bar,  the  notes  a2 and e2 (that's the A and E
below middle C) in the scale Equal440. You can specify up to
10  notes  in  a notelist, seperated by '+' signs. Each note
has its name, optionally followed by its octave number.  The
default  octave  number  is  3 - both "c" and "c3" represent
middle C.  Note that the Equal440 scale starts at C, so  you
could play a scale of C major with the commands

    note /00:00:00/ my_inst Equal440 c 1.0 1.0;
    note /00:00:01/ my_inst Equal440 d 1.0 1.0;
    note /00:00:02/ my_inst Equal440 e 1.0 1.0;
    note /00:00:03/ my_inst Equal440 f 1.0 1.0;
    note /00:01:00/ my_inst Equal440 g 1.0 1.0;
    note /00:01:01/ my_inst Equal440 a 1.0 1.0;
    note /00:01:02/ my_inst Equal440 b 1.0 1.0;
    note /00:01:03/ my_inst Equal440 c4 5.0 1.0;

However,  there's  a  much easier way of playing a series of
notes, which I'll describe a little later.  The above  exam-
ple  will  play  the  notes using the instance 'my_inst'. It
will automatically look in that instance for  a  free  voice
for  each  note  to  be played, and play the note using that
voice. You must define the instance with as  least  as  many
voices  as  your  maximum  number  of  simultaneous  notes -
including the release time, after the note proper  has  fin-
ished.

     All  note  (and  all the note playing commands) does is
create events.  For example,


                                                          19










    note (0) my_inst Equal440 a4 1.0 1.0;

is equivalent to

    event at (0) _my_inst.0.freq=880,
                   _my_inst.0.amp=1.0,
                   _my_inst.0.noteon=1.0,
                   _my_inst.0.gate=1.0;

    event at (1) _myinst.0.noteon=0;

    event at /00:00:01/ _myinst.0.gate=0;

Note works out which voice number to use (0 here, hence  all
the  _my_inst.0...   instance  parameters) and automatically
generates events to set the $freq, $amp, $noteon  and  $gate
parameters for that voice. Makes life easier, doesn't it?

7.2.  Tempo and Time Signatures

     By  default,  the  tempo sapphire uses is 120 beats per
minute. However, you can change this from  within  the  sap-
phire  program  by  using  the 'tempo' command. For example,
after the command

    tempo 150;

all scores later in the  program  will  be  played  at  that
tempo.

     Also, the time signature that sapphire uses to work out
how many beats there are in a bar is  4/4.  You  can  change
this  using  the  'signature' command. Like tempo, this only
affects scores, notes or events parsed later in the program.
Normally,  you would put both statements at the beginning of
the program:

    tempo 180;
    signature 7 8;

and then, perhaps,  change  the  tempo  and  time  signature
before  the  playscore  or event statement you wish to use a
different tempo with. Incidentally, note that the  signature
is  given as '7 8' - not Also, be warned - all bar times are
calculated from the beginning of the piece, so if (for exam-
ple)  you  wanted  to play in 3/4 after ten bars of 4/4, you
couldn't do

    signature 3 4;
    playscore ... at /10:00:00/;

because the previous ten bars would have been four beats  to
the bar, and playscore will assume they all had three beats.


                                                          20










You'd have to do

    signature 3 4;
    playscore ... at /00:40:00/;

to explicitly tell sapphire you  had  40  beats  previously.
Messy,  I  know. Sorry!  It gets worse - the same applies to
tempo! If you suddenly started playing at double the  speed,
you'd have to double the number of beats.  The best thing to
do is stick to one time signature as far as sapphire is con-
cerned,  and  use  the  'speed'  option on playscore to fake
tempo changes.

7.3.  Scores

     There's an even easier way to play lots of notes -  the
score.  This  is a list of notes (or even '+' seperated note
lists) with durations and amplitudes for each one. For exam-
ple,  here's  that scale of C major written as a score. Just
for a change, we'll end on a quiet chord of C major:

    score scale_c_major Equal440
    {
         -- you can have as many note items as you
         -- like per line - sapphire is a 'free-format'
         -- language: it doesn't care about new lines
         -- (except in comments!)

         -- these have just note and duration, no amplitude,
         -- so the amplitude is implied to be 1.

         c,1; d,1; e,1; f,1;
         g,1; a,1; b,1;

         -- chord at the end (Cmaj, 1st inversion)
         -- this does have an amplitude: 0.5

         e+g+c4,1,0.5
    }

This won't actually play anything, it  will  just  define  a
score called playscore statement, which like the note state-
ment will create the necessary events. Here's an example:

    playscore scale_c_major my_inst at /00:07:00/;

will play the above scale on the instance 'my_inst' starting
at  the eighth bar.  You can play as many scores as you like
on as many instances, overlapping them as much as you  want.
Just make sure you have set up instances with enough voices.
If they don't have enough, sapphire  will  print  a  warning
message and not play the note.



                                                          21










     There  are  several  options  to the playscore command,
which you put after the instance name and  before  the  'at'
keyword. They are:

 + "speed  <number>" This will play the score at a different
   tempo. For example, 'speed 2' will play  twice  as  fast,
   and  'speed  0.5'  will play at half speed.  "trans <num-
   ber>" This will transpose the notes by a given number  of
   steps  in the scale. In the Equal440 scale, this is semi-
   tones - so 'trans 7' will transpose up a 5th, and  'trans
   -12'  will transpose down an octave.  "amp <number>" This
   will multiply the amplitudes of all the note by the  num-
   ber  given,  so  'amp  0.5'  will play at half the normal
   amplitude.  "times <number>" This will repeat  the  score
   as many times as you specify.

     Here's  an  example  of  those.  It will play a scale 8
times, and, overlapping that, play the same scale once at an
eighth of the speed, quieter and one octave down:

    playscore scale_c_major my_inst times 8 at 0;
    playscore scale_c_major my_inst amp 0.5 trans -12
         speed 0.125 at 0;


7.4.  Defining a Scale

     If  you're  into wacky tunings, you can define your own
quite easily. I've already  given  you  two  in  the  scales
directory  -  an  ancient Greek Lydian tuning and a medieval
Arab tuning. There might be some more in there too. In fact,
sapphire's  original  raison d'etre was so that I could hear
the weird tunings I found in my travels, and it sort of grew
out of control from there.

     You  define  a scale with the scale statement, giving a
list of the frequencies of the notes. The last in  the  list
is  taken  to  be the 'octave frequency' - that is, when you
specify different octaves in your scores, sapphire uses  the
ratio  of this pitch with the first in the list to determine
how big an octave is. In any normal scale,  the  last  pitch
the list should therefore be twice the first item. For exam-
ple, here's a Javese scale you might care to try:












                                                          22










    --
    -- The Salendro Scale, assumed. Source:
    -- Hermann Helmholtz, On the Sensations of Tone
    -- Start pitch - 261Hz (middle C)
    -- (The note names are my own convention)
    --
    scale Salendro
    {
         a:261,
         b:297.7,
         c:345.2,
         d:397.4,
         e:454.4,
         A:522          -- the octave
    }


8.  Applications

     Here's how you might go about doing the sorts of things
people will typically want to do with sapphire.

8.1.  Making a Sample

     If you want to make a simple sample, for use by another
program, you don't really need to worry  about  instruments,
instances,  scores  and  so on (although you can if you want
some tonal effects).  All you need to do is  write  code  to
make a simple sound, setting the trigger and gate properties
for any sample players and envelope shapers by hand. A  sim-
ple  example, that of a shaped sine wave, can be found under
'adsr' in the section on Objects.

     You can  make  samples  by  mixing  samples,  generated
waves,  filters  and  envelopes all together. I imagine that
most people will use sapphire for this purpose.

8.2.  Making a Piece

     If you have a lot of computer power and disk space, and
want  to  write  a  whole  piece in sapphire, you'll need to
worry about instruments and scores. You'll also need to work
around  sapphire's  speed  constraints:  on  my home machine
(albeit just a 486/33) it takes an hour to  generate  a  two
minute piece of moderate complexity.

     Probably  the best thing to do is compose your piece as
several different sapphire programs, each generating a  one-
minute  section  or  one  of a group of overlapping samples.
Then, when you've made sure you've got each  section  right,
bring  them  all  into one big program and change the event,
ramp and playscore times to match when you want the  various
sections  to  happen.  Mix  the  outputs of all the sections


                                                          23










together with a stereomix object, and then leave sapphire to
run for a few hours.

     It  probably  won't work first time - you'll get filter
noise, range overrun, all sorts of problems - but by working
on  each  section  in  isolation  the  problems will be much
smaller.

8.3.  Processing a Sample

     Another use for sapphire is to process a sample  -  for
example,  to  shape it with an envelope or to filter it. You
can do this by using the 'oneshot' sample player to play the
sample, manipulating it, and outputting the manipulated ver-
sion. Here's a brief example.

    --
    -- Impose an envelope on the 'woof.wav' sample, and filter it
    -- a bit.
    --

    -- here's the sample
    -- it doesn't matter what pitch number we use..

    sample woof format wav file "woof.wav" pitch 400.0;

    event at (0) noteon=1,gate=1;
    event at (1) noteon=0;
    event at 2 gate=0;

    -- process the sample

    CH0: adsr
              noteon gate 0 0.1 0.9 0.2
              {filter
                   {oneshot &woof noteon 400.0 1.0}
                   50 6 1.0};

    end at 3;
















                                                          24










                        APPENDIX  A


                          Support



If you wish to talk to other sapphire users, (including me),
to  discuss  problems, neat things you've done, and new fea-
tures you'd like to see, join the sapphire mailing  list  by
sending  a message to 'majordomo@yoyo.cc.monash.edu.au' with
'subscribe sapphire-l' in the body of the message.

     You can also mail me as 'white@elf.dircon.co.uk'.









































                                                          25










                        APPENDIX  B


                  Writing your Own Objects



If you have full access to the source (and you  should)  and
access  to  the flex(1) and bison(1) utilities used to build
it, you can recompile sapphire and even add objects of  your
own.   This  is  actively encouraged - there are quite a few
facilities missing from sapphire which I haven't had time to
add.  You  do,  however, need to know the C programming lan-
guage!

     First, work out exactly what your object  is  going  to
do. You need to know

 + How  many inputs and outputs the object is going to have,
   or if the object has a variable number of inputs;
 + Whether the object is going to make use of a wave or sam-
   ple (like 'osc' and 'oneshot');
 + Whether the object needs an initialisation routine, which
   will run only once before any sound is generated. This is
   useful  for  allocating  memory,  initialising variables,
   etc.

     It's a good idea to look  at  the  objects  already  in
objects.c  to  see  how  they work, particularly the simpler
objects like stereomix, add, copy etc.  From  these,  you'll
notice  that  each  object  has one or two functions - an f_
function, which actually does the work of the object; and an
optional i_ function, to initialise the object.

     The  f_  functions make heavy use of two special macros
called INPUT and OUTPUT. INPUT takes one argument, the index
of  the  input  you  which  to  get data from, and returns a
floating point value from -1 to 1 -  the  current  value  of
that input.

     OUTPUT  takes  two arguments - the index of the output,
and the value you wish to write there. It's a good  idea  to
try  to limit the normal range of outputs from -1 to 1.  You
can see exactly how to use INPUT and OUTPUT  by  looking  at
the  code  I've  already written. Note that you can only use
them from within f_ functions. Trying to use  them  anywhere
else  doesn't  make  sense,  and  the  program will probably
crash.

     Once you've worked out how to do your object, you  need
to  enter  it into the tables in objects.h. Add declarations
for your f_ and i_ functions just before the line



                                                          26










    /* END_OF_OBJECT_FUNCTIONS */

Then add an entry to the end of the object table at the  end
of the file, just before

    /* END_OF_OBJECT_TABLE */

Those  comments are there so I can quickly write patches for
older versions of sapphire. The fields in the  object  table
are  described  in  a comment at the beginning of the table,
but a few words are necessary to explain the OF_HASWAVE  and
OF_HASSAMPL  flags.  You can have either of these flags, but
both both, and they tell sapphire to expect a wave or sample
respectively  as the first argument. For example, let's look
at the oscillator object, osc.

     This has the following definition:

    {"osc",f_osc,2,1,i_osc,OF_HASWAVE}

meaning it's called "osc", it uses f_osc as it's main  func-
tion,  it  has  2  inputs and 1 output, it uses i_osc to set
itself up, and it uses a wave. When we use osc, we do  some-
thing like this:

    CH0: osc &sine 440.0 1.0;

which  looks  like 3 arguments. However - the first argument
is actually the wave, and is ignored by the signal  process-
ing  stuff.  The  440.0 is therefore input 0, and the 1.0 is
input 1.

     To get a pointer to the wave structure from your f_  or
i_  function, use self->wave. This points to the first float
in an array. self->wavsize is the length of the wave.

     Samples are rather more complicated -  self->wave  here
is  actually  a pointer to a filedata struct, which contains
details of the sample file. You use the f_getsample  routine
to  get data from the sample - see 'oneshot' for an example.

     Another flag is the OF_VARARGS flag,  which  means  the
object  can  have any number (up to about 32) of inputs. You
use self->realins to tell you how  many  inputs  you  really
have.

     Now,  you should write your f_ and i_ functions and put
them at the end of objects.c. Recompile sapphire,  and  test
your object!






                                                          27










                        APPENDIX  C


              Objects it would be nice to have




 + A  better envelope generator, with an arbitrary number of
   levels and rates.
 + A better set of filters - the current  filter  is  pretty
   hard to control.
 + A reverb object.
 + A  delay  object which can cope with real-time changes in
   delay time somewhat better.








































                                                          28










                        APPENDIX  D


                           Index



Chapter 1, Introduction ...............................    1
Overview ..............................................    1
A Brief Example .......................................    1
Sapphire Programs .....................................    3
How to define an object ...............................    4
Object expressions ....................................    4
How to set an event ...................................    5
Command Line Options ..................................    5
File Inclusion ........................................    6
The Objects ...........................................    6
Generators ............................................    7
signal: osc &<wave> <frequency> <amplitude> ...........    7
signal: oneshot &<sample> <noteon>  <pitch>  <ampli-
     tude> ............................................    8
loopsamp  &<sample>  <noteon> <gate> <pitch> <ampli-
     tude> <loop start> <loop end> <loop transition>
     ..................................................   10
Arithmetic Operators ..................................   11
result: add <number1> <number2>...  ...................   11
result: mul <number1> <number2>...  ...................   11
result: div <number1> <number2> .......................   11
result: sub <number1> <number2> .......................   11
result: mix <number1> <number2>...  ...................   11
result: copy <number> .................................   12
result:  switch  <test1> <test2> <number1> <number2>
     ..................................................   12
Envelope Shapers ......................................   12
signal: adsr <trig> <gate> <att> <dec>  <sus>  <rel>
     <input> ..........................................   12
signal:  expodecay  <trig>  <startval>  <timeto16th>
     <input> ..........................................   13
Filters ...............................................   14
filter <signal> <frequency> <gain> <Q> ................   14
Other Objects .........................................   14
leftsig,rightsig: stereomix <sig1> <pan1>  <amp1>...
     ..................................................   14
result: delay <signal> <maxtime> <curtime> ............   15
output: samphold <signal> <interval> ..................   15
output: random <range> ................................   15
Making  Things Easier : Ramp Events, Instruments and
     Instances ........................................   15
Ramps .................................................   15
Instruments and Instances .............................   16
Instance variables and parameters .....................   17
The instance parameters ...............................   18
Scores and Scales .....................................   18


                                                          29










Playing a Note ........................................   19
Tempo and Time Signatures .............................   20
Scores ................................................   21
Defining a Scale ......................................   22
Applications ..........................................   23
Making a Sample .......................................   23
Making a Piece ........................................   23
Processing a Sample ...................................   24
Appendix A, Support ...................................   25
Appendix B, Writing your Own Objects ..................   26
Appendix C, Objects it would be nice to have ..........   28
Appendix D, Index .....................................   29











































                                                          30



