COMMON LISP MUSIC

Common Lisp Music (CLM, for lack of a catchier name) is a music synthesis and signal processing package in the Music V family, written primarily in Common Lisp. The CLM instrument design language is a subset of Common Lisp (its numerical functions, and nearly all its control functions), extended with a large number of "unit generators": oscil, env, table-lookup, and so on. The instruments can run as straight Common Lisp software, or can be compiled into C or DSP code. Since CLM instruments are Lisp functions, a CLM note list is just any Lisp expression that happens to call those functions. The notes need not even be in any order. The actual computation is done one note at a time, and the results are overlayed, building up the sound note by note. CLM is available free, via anonymous ftp (pub/Lisp/clm.tar.Z at ccrma-ftp.stanford.edu).

Originally CLM was intended primarily for machines that had a 56000 available, but over the years the main processors have caught up and passed the 56000 in computational speed, so the current version is trying to keep a foot in both camps. If you're running a NeXT with an Ariel QP board, you should probably stick with the 56000 version of CLM, but otherwise I suggest using the C version -- even on the NeXT the latter is almost as fast as the former, if you're using just the built-in 56000. The choice can be made in all.lisp when CLM is compiled and loaded, or can be delayed to instrument compilation time (the two forms can co-exist without problem; CLM will use the 56000 if it is available). The file README.clm contains some benchmarks comparing various hardware platforms. On the newer processors, even relatively complex instruments can run in "real-time", so I may re-implement the real-time control portion of CLM. There are a variety of small differences between current implementations of CLM -- for example, due to the stupidity of GCL's defentry function, CLM's instrument-let only works in GCL for Lisp or the 56000, but not for C. Similarly, funcall is supported in Lisp and on the 56000, but not in C. Most such differences are minor, and there's always a way around them. But, to keep this document from expanding beyond anyone's patience, I've abbreviated such gotchas as [DSP] = only on the 56000, [C] = only in C, [ACL] = only in Franz's Allegro CL, [GCL] = only in GCL, and [MCL] = only on the Mac. Others may pop up as I sit and type.

CLM has several sections: the "unit generators", instruments (definstrument, *.ins, and ins.txt), examples of note lists (with-sound, *.clm), a "make" facility for sound files (mix), and various functions that are useful in sound file work (sound-let, fasmix, etc). The rest of this document concentrates on the functions CLM provides to the composer at this "top level". The file ins.txt is a tutorial on writing instruments. I haven't yet written a tutorial for composition with CLM.

Currently, CLM runs on the NeXT, Macintosh, SGI, and Sun, and Intel machines running NeXTStep. It can take advantage of Ariel's QuintProcessor board on the NeXT, and the Ariel PC56-D and Ilink i56 boards on the 486-style machines. For benchmarks comparing these platforms, and comparing CLM to its friends in the C world (CSound, CMix, and CMusic) see the clm file README.clm.

Contents

  1. Introduction
  2. Instruments
    1. Lisp Functions (Run)
    2. Unit Generators
  3. Sound Processing (Mix)
  4. Definstrument
  5. Note Lists (With-Sound)
  6. Phrasing
  7. Structs
  8. Appendices
    1. How to use CLM outside with-sound
    2. Fast sound file mixing and format changes
    3. Header and data types supported in CLM
    4. Low level debugging aids
    5. Sources of unwanted noise
  9. Index

Introduction

CLM is a sound compiler. It provides a Lisp environment in which various kinds of expressions generate sound files. Lisp from time immemorial has presented itself to the user as an "interpreter". As you type in its window, it reads what you type, tries to make sense of it, and does something, returning a result. To use CLM either from the lisp listener or from Rick Taube's Common Music you first need to learn the rudiments of Lisp. The basic idea in Lisp is that something of the form
    (name arg1 arg2)

calls the function "name" and passes it the two arguments. In C this would be
    name(arg1,arg2);

In C, 4+3 is an expression yielding 7. In Lisp this is expressed (+ 4 3). Similarly C's 4+3*2 becomes (+ 4 (* 3 2)). To learn Lisp, just follow the examples given below or in ins.txt, and try making simple changes. For full information see "Common Lisp" by Steele, "Lisp" by Winston and Horn, or "Common Lisp" by Touretzky.

The easiest way to create a new sound file is to use the macro with-sound. Say we want to hear one second of the fm violin (in v.ins, named fm-violin) at 440 Hz, and a somewhat soft amplitude. Load v.fasl (or v.o in GCL);(it is automatically included in the Common Music image at CCRMA), Then type

    (with-sound () (fm-violin 0 1 440 .1))

and the note should emerge from the speakers. Once loaded, we don't need to reload v unless we change it in some way. To get a chord:
    (with-sound () (fm-violin 0 1 440 .1) (fm-violin 0 1 660 .1))

To get a reverberated version, load a reverberator (jcrev.fasl from jcrev.ins, for example), and
    (with-sound (:reverb jc-reverb) (fm-violin 0 .1 440 .2))

The with-sound macro takes a number of optional arguments -- :reverb is one of them. It gives the name of the reverberator (jc-reverb in the case above). The favorite reverberator for the last few years has been nrev in nrev.ins. If you want to hear the sound again without recomputing it, call the function (dac). To stop in the middle of playback, type (stop-dac). The (dac) call starts up a background process playing the sound and returns to the lisp listener. The function dac without any argument plays the last file clm created (normally /zap/test.snd, the default output of the with-sound macro). If you want to play some other sound file, give the file name as the argument to dac: (dac "saved.snd").

A large number of instruments are already compiled and ready for use -- see ins.txt for a tutorial on building instruments, and the various *.ins files which usually contain a few sample calls. For more examples of note lists, see the various files *.clm. Any Lisp-ism is ok in the note list:

    (with-sound ()   
      (loop for i from 0 to 7 do
        (fm-violin (* i .25) .5 (* 100 (1+ i)) .1)))

creates a little arpeggio of slightly overlapping notes. clm-example.lisp shows how to create such a note list algorithmically. Judicious use of mix and with-mix can speed up the computation by an arbitrary amount. The rest of this document discusses clm instrument structure, the built-in unit-generators, various useful sound file functions (mix, definstrument, with-sound, and local versions thereof), the 56000/C library, the "phrasing" mechanism, and the structs that make the innards of the generators available at run-time.

CLM Instruments

An instrument is a lisp function that can send its output to any currently open output file, and get input from any currently open input file (or from the output files, for that matter). If you're willing to run entirely in Lisp software, there's no limitation on what you can do -- you are just running an arbitrary lisp function. If you are hoping to compile the lisp code into DSP or C code, there are a number of limitations (life is short, and many of Lisp's functions are not used in any current instruments). The main two instrument-defining entities are definstrument and run. Definstrument replaces Lisp's defun, and run marks the "run-time" portion of the instrument -- the portion that we want to optimize as much as possible for speed. In its simplest use, definstrument looks just like defun. Here is a simple instrument:

   (definstrument simp (start duration frequency amplitude)
    (let* ((beg (floor (* start sampling-rate)))
           (end (+ beg (floor (* duration sampling-rate))))
           (sinewave (make-oscil :frequency frequency)))
      (run (loop for i from beg to end do
              (outa i (* amplitude (oscil sinewave)))))))

This creates an oscillator (make-oscil), then calls it once on each sample between the start and end points, adding its output into the current output file via outa. Once compiled and loaded, we can call this instrument:

    (with-sound () (simp 0 1 440 .1))

to produce a 1 second sine wave at 440Hz. The normal structure of an instrument is

    (definstrument name (args)
       setup code
        (run
          (loop for i from beg to end do
             run-time code
             )))

The run macro is our dsp/C-compiler. Use only one call on run per instrument -- our run-time loader is not yet smart enough to thread its way through more than one dsp code vector. See sound-let and instrument-let (and expsrc.ins) for ways to get around this limitation.

[DSP: If the instrument uses delay lines, it should end with (end-run) to free up the lines.]

Lisp Functions that Run can handle

The following Lisp functions can occur inside the run loop:

    +  /  *  -  1+  1-  incf decf setf setq psetf psetq
     =  /=  <  >  <=  >=  zerop  plusp  
    minusp  oddp  evenp  max
    min  abs  mod  rem  gcd  lcm
    floor  ceiling  round  truncate  signum  sqrt  random  float
    ash  log  expt  exp  sin  cos  tan  asin  acos  atan  cosh  sinh  tanh
    asinh  acosh  atanh 

    or  and  not  null   if  unless  when  cond   progn  prog1  prog2  case
    tagbody  go 
    break  error  warn  print  princ  terpri  y-or-n-p  yes-or-no-p
    block  return  return-from   let  let*  loop  do  do*  dotimes declare
    lambda  apply  funcall  
    aref  elt  array-total-size  array-in-bounds-p array-rank array-dimension
    eref  nth

    various logical and byte manipulation functions

Within the outer loop body, loop is expanded as a macro and anything in the loop syntax is ok if it expands into something else mentioned above (i.e. a lambda form with go's and so forth). The outermost loop, however, is special -- it is expected to be the first thing run sees, but cannot be anything but a simple loop (just one iteration variable, counting by 1 etc).

Generally user variables are assumed to be either integer, short-float, one of the struct types defined in mus.lisp (see Structs), or a user-defined struct that has been defined for run's benefit with def-clm-struct. Variables are currently assumed to be of one type (that is, I haven't yet fully implemented Lisp's run-time typing). [DSP: The loop control variable is a long integer -- not a "normal" integer, and any arithmetic on it returns a long integer. It should only be used where you want a sample counter result.] Lisp's declare can be used to force a variable to be a particular type. Run currently recognizes the types fixnum, float, and bignum -- the latter should be used for long integers (see ins.txt for an example). Very large arrays (or wave-tables) should be written out as sound files and read using ina or readin.

[DSP: Funcall provides an escape to Lisp -- (funcall #'some-function some-args...) passes the arguments (dsp run-time numerical values) to the function "some-function" in the Lisp interpreter, and any numerical result is passed back to the dsp. This escape is 1000 times slower than a normal dsp function call. Break drops into both the DSP breakpoint handler and the lisp debugger; error halts the DSP and drops into the Lisp debugger; warn prints a warning; print and princ print a string or a variable's value. All but print and princ can take the usual optional formatting commands and arguments. If the optimize quality safety is set to be greater than 0 (the default), run inserts code to catch and report overflows -- these affect mainly those unit generators that require "fractional" input on the 56000]. In addition, the function clm-print stands in for Lisp's print+format -- we don't support all of format's options, but enough to be useful, I hope. Its syntax is (clm-print format-string &rest args).

[C: If the optimize quality safety is set to be greater than 1, each unit generator is checked for plausibility before being called; this helps protect against bus errors when a null generator is called.]

Unit Generators

You could stop reading here, and work directly with the mathematical description of the instrument's behavior translated into Lisp, but if you take this approach you have to write out the code to handle each algorithm every time it appears in the formula. And the algorithm is not just the simple mathematical function call that you would naively plug into your code. Mathematicians assume that numbers (arguments to sin for example) run to infinity and have infinite precision. The computer's numbers run to some relatively small maximum number and the step size between successive numbers depends on a variety of factors. If you shrug and take the most inclusive representation available, you not only lose in computation time, sometimes by many orders of magnitude, but you still hit these numerical limits in normal musical situations. As the phase increments into the stratosphere, for example, your sin call starts to wobble, and spectra fall apart. In addition, many of these functions are quite complex, and it would be crazy to repeat all that code every time the need for it arises. So nearly every computer music implementation, CLM included, provides highly optimized versions of the synthesis and processing functions that have proved most useful to date. We follow the ancients and call them "unit generators" -- weird inexplicable jargon, but at least we no longer talk about "slew rate" and "mag".

Each generator consists of a pair of functions. Make-<gen> sets up the data structure associated with the generator at initialization time, and <gen> runs the generator, producing a new sample each time it is called. For example,

    (setf oscillator (make-oscil :frequency 330))

prepares a structure (of type osc in this case), ready to produce a sine wave when set in motion via

    (oscil oscillator)

All generators follow this calling sequence. The initialization function normally takes a number of optional arguments, setting whatever state the given generator needs to operate on. The run-time function's first argument is always its associated structure. Its second argument is nearly always something like an FM input -- whatever run-time modulation might be desired. Amplitude envelopes are handled with a separate env generator, and attack and decay characteristics are handled by divseg at initialization time. Frequency sweeps of all kinds (vibrato, glissando, breath noise, FM proper) are all forms of run-time frequency modulation. So, in normal usage, our oscillator looks something like:

    (oscil oscillator (+ vibrato glissando frequency-modulation))

All these functions and structures are defined in mus.lisp. In the documentation below, we give the calling sequences and defaults, and mention any peculiarities that come to mind. Frequencies are always in cycles per second (also known as Hz), internal table size is (nearly) always two pi. The fm (or frequency change) argument is assumed to be a phase change in radians, applied on each sample. Normally composers would rather think in terms of Hertz, so the function In-hz can be used to convert from units of cycles per second to radians per sample (this conversion factor used to be called "mag"). Since all the generators agree that their internal period length is two-pi, you can always use In-hz to convert the frequency change (or fm) argument from a more easily interpreted value.

OSCIL

Oscil produces a sine wave with optional frequency change (i.e. fm). Its first argument is an osc structure, normally created by make-oscil. Oscil's second (optional) argument is the current (sample-wise) frequency change (defaults to 0). Its third argument is the (sample-wise) phase change (in addition to the carrier increment and so on). See fm.wn for a discussion of fm.

    make-oscil &key (frequency 440.0)       ; Hz
                    (initial-phase 0.0)     ; radians 
               ; (to convert degrees to radians multiply by .0174532925)
    oscil os &optional (fm-input 0.0) (pm-input 0.0)

For example, a simple-FM instrument is:

(definstrument fm (beg end freq amp mc-ratio index
             &optional (amp-env '(0 0 50 1 100 1)) (index-env '(0 1 100 1)))
  (let* ((cr (make-oscil :frequency freq))
         (md (make-oscil :frequency (* freq mc-ratio)))
         (fm-index (in-Hz (* index mc-ratio freq)))
         (ampf (make-env :envelope amp-env :scaler amp :start beg :end end))
         (indf (make-env :envelope index-env :scaler fm-index
                         :start beg :end end)))
    (run
      (loop for i from beg to end do
        (outa i (* (env ampf)
                   (oscil cr (* (env indf) 
                                (oscil md)))))))))

Oscil is essentially: returned-val = sin(current-phase+pm-input); current-phase += freq+fm-input;

ENV

In CLM, an envelope is a list of break point pairs: '(0 0 100 1) is a ramp from 0 to 1 over an x-axis excursion from 0 to 100. This list is passed to make-env along with the desired start and end times, the scaler applied to the y axis, and the offset added to every y value. The resulting object, when accessed with the env generator, creates an envelope of the same shape as the original, but stretched along the x-axis to fit the desired start time and duration, and transformed along the y-axis by offset + scaler * y-value. In normal use, the x-axis can be anything you like. Here at CCRMA, we use 0..100, but it might make more sense to use 0..1. The kind of interpolation used to get y-values between the break points is determined by the envelope's base.

  make-env &key envelope      ; list of x,y break-point pairs
                start-time    ; in seconds
                duration      ; seconds
                start         ; in samples (can be used instead of start-time)
                end           ; in samples (can be used instead of duration)
                (offset 0.0)  ; value added to every y value
                (scaler 1.0)  ; scaler on every y value (before offset is added)
                base          ; type of connecting line between break-points
  env  e
  restart-env e               ; return to start of envelope

Make-env produces a structure that env can handle at run-time. Each call on env gets the next value of the envelope, so unless you're being very clever, you should only use each envelope structure once within the run body (that is, make separate envelopes for each occurrence of env, even if everything is the same, or alternatively, use let or let* to hold the envelope value).

With the arguments to make-env, you can apply a given shape to any situtation. Say we want a ramp moving from .3 to .5, starting at 1.0 seconds, and ending a second later. The corresponding make-env call would be

  (make-env :envelope '(0 0 100 1) :scaler .2 :offset .3 :start-time 1.0 :duration 1.0)

The x and y axis limits of the envelope definition are arbitrary -- use whatever limits you find easy to visualize. When the envelope is actually applied to a specific situation, the x axis is stretched to fit the current duration and set to start at the start time. The y axis is scaled by scaler and offset by offset.

Base determines how the break-points are connected. If it is 1.0 (the default), you get straight line segments. Base = 0.0 gives a step function (the envelope changes its value suddenly to the new one without any interpolation). Any other value becomes the exponent of the exponential curve connecting the points. Base < 1.0 gives convex curves (i.e. bowed out), and Base > 1.0 gives concave curves (i.e. sagging). [DSP: Extreme exponentials run out of bits on the dsp -- create a line segment envelope as close as possible to the curve you want, then use exponential curves to connect those break-points.]

To get arbitrary connecting curves between the break points, treat the output of env as input to the connecting function. Here's an example that maps the line segments into sin x^3:

(definstrument mapenv (beg dur frq amp en)
  (let* ((bg (floor (* beg sampling-rate)))
	 (nd (+ bg (floor (* dur sampling-rate))))
	 (o (make-oscil :frequency frq))
	 (zv (make-env :envelope en :start-time beg :duration dur)))
    (run
     (loop for i from bg to nd do
       (let ((zval (env zv))) ;zval^3 is [0.0..1.0], as is sin between 0 and half-pi.
	 (outa i (* amp (sin (* half-pi zval zval zval)) (oscil o))))))))

(with-sound () (mapenv 0 1 440 .4 '(0 0 50 1 75 0 86 .5 100 0)))

Although they may appear otiose, Start-Time and Duration are not entirely useless -- an envelope can have any duration (and perhaps someday any start time). If its duration is shorter than the instrument's, the envelope sticks at its final value. If the envelope's duration is longer than the instrument's, you only march through the portion of the envelope that happens to fit within the instrument's duration.

Restart-env causes an envelope to start all over again from the beginning. [DSP: To be able to restart an envelope on the dsp, set the argument restartable to env to t (all envelopes are automatically restartable in Lisp/C)].

 divseg  fn  old-att  new-att  &optional  old-dec  new-dec
 divenv  fn  dur  &optional  p1  t1  p2  t2  p3  t3  p4  t4  p5  t5

Divseg provides attack and decay controls on an envelope. Very often, the envelope is considered to have two or three independent portions. The first, called the "attack", is very important, and its duration needs to be dealt with independently of the second ("steady state") and third ("decay") portions. Divseg takes the original envelope (Fn), the original x axis point at which the attack portion ends, the new x-axis point where it should end, and similar data for the decay portion if desired, and returns the new envelope that has been squeezed or stretched along the x-axis to produce the desired timings.

    (divseg '(0 0 100 1) 50 25) => '(0 0 25 .5 100 1)
    (divseg '(0 0 100 1) 50 25 75 90) => '(0 0 25 .5 90 .75 100 1)

Divenv provides the same operations as divseg, but gives you up to five segments to push around, and instead of specifying the new x axis values, you supply the times (in seconds). Divenv can be extremely confusing -- it was a kludge in previous CCRMA software to get around severe memory limitations and to avoid inadequate envelope definition software. I urge you to make the envelopes from scratch. For example, say we want the basic shape '(0 0 1 1 2 1 3 0) to rise to 1 in the time att, and decay in the time dec, where the overall duration is dur. All it takes it (list 0 0 att 1 (- dur att dec) 1 dur 0) as the envelope argument to make-env. Surely this is clearer than the equivalent (divseg '(0 0 1 1 2 1 3 0) 1 (* 3 (/ att dur)) 2 (* 3 (/ (- dec att dec) dur)))?

env.lisp has a variety of functions that operate on envelopes. The most useful are:

  env-reverse              reverse an envelope
  env-repeat               repeat an envelope
  env-concatenate          concatenate any number of envelopes
  env+                     add together any number of envelopes
  env*                     same but multiply
  env-max                  find max y value of envelope(s)
  env-simplify             simplify an evelope (smoothing using graphics notions)
  fft-env-simplify         same but use fft-filtering to smooth
  meld-envelopes           return attempt to meld two envelopes together
  map-across-envelopes     map a function across any number of envelopes

mus.lisp has a few other useful, self-evident envelope functions: list-interp, scale-envelope, and normalize-envelope.

OUTA

OUTB

OUTC

OUTD

  outa  loc  data  &optional  (o-stream  *current-output-file*)
  outb  loc  data  &optional  (o-stream  *current-output-file*)
  outc  loc  data  &optional  (o-stream  *current-output-file*)
  outd  loc  data  &optional  (o-stream  *current-output-file*)

Outa and friends merge Data into O-Stream at sample position Loc. O-Stream defaults to the current default output file. Data is a short-float, normally between -1.0 and 1.0 -- the sound file contains 2's complement integers interpreted as fractions between -1 and 1, but not including 1 itself. [DSP: clm's output functions truncate data, so if you (outa 0 1.0), the actual value written is 0]. Locsig is the recommended way to do multi-channel output.

IN-A

IN-B

IN-C

IN-D

INA

INB

  in-a  loc  &optional  (i-stream  *current-input-file*)
  in-b  loc  &optional  (i-stream  *current-input-file*)
  in-c  loc  &optional  (i-stream  *current-input-file*)
  in-d  loc  &optional  (i-stream  *current-input-file*)

In-a and friends get the sample at position Loc in I-Stream as a short-float (normally between -1.0 and 1.0). I-Stream defaults to the current default input file. Ina and inb are the same as in-a and in-b -- "Ind" got too many loader errors, so I added the "-".

REVIN

REVOUT

  revin  i
  revout  i  val

Revin and revout read/write data from/to the reverb stream. In the clm software, reverb is normally handled as a third independent channel, and the actual reverberation is done after all other processing is complete. The current reverb stream is *reverb*.

READIN

READIN-REVERSE

   make-readin  &key  file  start-time  start  (channel  :A)
   readin  rd

   make-reverse  &key  file  start-time  start  (channel :A)
   readin-reverse  rd

Readin provides a way to package up information related to a given input stream (File, Channel, and Start-Time (seconds) or Start (samples)). The field rdin-i is the sample counter (see under Structs below). Input-file-start-time and input-file-start are synonyms for start-time and start.

Here is an instrument that applies an envelope to a sound file using readin and env:

(definstrument env-sound (file beg &optional (amp 1.0) 
                                             (amp-env '(0 1 100 1)))
  (let ((f (open-input file)))
    (unwind-protect
      (let* ((st (floor (* beg sampling-rate)))
        (dur (clm-get-duration f))
        (rev-amount .01)
        (rdA (make-readin :file f :input-file-start-time 0.0))
        (rdB (if (and (stereo f) (stereo *current-output-file*))
                 (make-readin :file f :input-file-start-time 0.0 :channel :B)))
        (ampf (make-env :envelope amp-env :scaler amp :start-time beg :duration dur))
        (nd (+ st (floor (* sampling-rate dur)))))
    (run
      (loop for i from st to nd do
        (let* ((amp-val (env ampf))
               (outa-val (* amp-val (readin rdA))))
          (outa i outa-val)
          (if rdB (outb i (* amp-val (readin rdB))))
          (if *reverb* (revout i (* rev-amount outa-val)))))))
    (close-input f))))

To change direction in the run loop, you can either set rdin-inc (see page 36), or call read-forward or read-backward. The current readin sample number is returned by read-position which is also setf-able. These three functions also apply to all the generators that have embedded readins (or appear to) -- src, resample, expand, fft-filter, and convolve.

LOCSIG

   make-locsig  &key  (degree 0.0)  (distance 1.0)  (revscale 0.01)  revin
   locsig  loc  i  in-sig

Locsig normally takes the place of outa and outb in an instrument. It tries to place a signal between outa and outb in an extremely dumb manner -- it just scales the respective amplitudes ("that old trick never works"). Revscale determines how much of the direct signal gets sent to the reverberator. Distance tries to imitate a distance cue by fooling with the relative amounts of direct and reverberated signal (independent of Revscale). Locsig is a kludge, but then so is any pretence of placement when you're piping the signal out a loudspeaker. It is my current belief that locsig does the "right" thing for all the wrong reasons -- a good concert hall provides "auditory spaciousness" by interfering with the ear's attempt to localize a sound -- that is, a diffuse sound source is the ideal! By sending an arbitrary mix of signal and reverberation to various speakers, locsig gives you a very diffuse "source" -- it does the opposite of what it claims to do, and by some perversity of Mother Nature, that is what you want. (See "Binaural Phenomena" by J Blauert).

If you'd like to mess around with room simulations, see the function roomsig in mus.lisp with examples in room.ins. Locsig now handles quad output -- the speakers are assumed to be in a circle with channel A at 0, channel B at 90, and so on.

TABLE-LOOKUP

Table-lookup performs interpolating table lookup. Indices are first made to fit in the current table (fm input can produce negative indices), then linear interpolation returns the table value. Table-lookup scales its frequency change argument (fm-input) to fit whatever its table size is (that is, it assumes the caller is thinking in terms of a table size of two pi, and fixes it up). The wave table should be an array of floats (short-floats, preferably). The function make-table returns such an object, ready to be loaded.

   make-table-lookup  &key  (frequency 440.0)     ; in Hz
                            (initial-phase 0.0)   ; in radians 
                            wave-table            ; short-float array with wave

   table-lookup  tl  &optional  (fm-input  0.0)
   make-table  &optional  (size default-table-size)

There are several functions available that make it easier to load up various wave forms:

   load-synthesis-table  synth-data  table  &optional  (norm  t)
   load-synthesis-table-with-phases  synth-data table  &optional  (norm t)

The synth-data argument is a list of (partial amp) pairs:

    '(1 .5 2 .25)

gives a combination of a sine wave at the carrier (1) at amplitude .5, and another at the first harmonic (2) at amplitude .25. The partial amplitudes are normalized to sum to a total amplitude of 1.0 unless the argument norm is nil. If the initial phases matter (they almost never do), you can use load-synthesis-table-with-phases. The phases are in radians. [DSP: it takes a long time both to load the table, and to write the table contents to the 56000's private memory, so additive synthesis should be done with waveshaping, not table lookup.]

spectr.clm has a large number of steady state spectra of standard orchestral instruments, courtesy James A. Moorer. bird.clm has about 50 North American bird songs. Very large tables should be handled as sound files and read with ina and friends.

RANDH

RANDI

Randh and randi produce random numbers of various kinds. The Lisp function random is also available.

  make-randh  &key  (frequency  440.0)     ;freq at which new random numbers occur
                    (amplitude  1.0)       ;numbers are between -amplitude and amplitude
                    (increment  0.0)
                    (type  'random)
   randh  r  &optional  (sweep  0.0)

   make-randi  &key  (frequency 440.0)  (amplitude 1.0)
   randi  r  &optional  (sweep  0.0)

Randh returns a sequence of random numbers between -amplitude and amplitude (it produces a sort of step function -- David Mellinger suggests that the "h" stands for "hold"). Randi interpolates between successive random numbers.

[DSP: If Type is 'Sambox-random, you get a simulation of the Samson box's random number generator.]

The "central-limit theorem" says that you can get closer and closer to gaussian noise simply by adding randh's together.

ARRAY-INTERP

TABLE-INTERP

DOT-PRODUCT

These simple functions underlie some of the unit-generators, and can be called within Run, if you want to roll your own. See mus.lisp for details.

SAWTOOTH-WAVE

TRIANGLE-WAVE

PULSE-TRAIN

SQUARE-WAVE

These generators produce some occasionally useful wave forms. Sawtooth-wave ramps from -1 to 1, then goes immediately back to -1. Triangle-wave ramps from -1 to 1, then ramps from 1 to -1. Pulse-train produces a single sample of 1.0, then zeros. Square-wave produces 1 for half a period, then 0. All have a "period" of two-pi, so the fm argument should have an effect comparable to the same fm applied to the same waveform in table-lookup.
    make-triangle-wave  &key  frequency  (amplitude  1.0)  (initial-phase  pi)
    triangle-wave  s  &optional  (fm  0.0)

    make-square-wave  &key  frequency  (amplitude  1.0)  (initial-phase  0)
    square-wave  s  &optional  (fm  0.0)

    make-sawtooth-wave  &key  frequency  (amplitude  1.0)  (initial-phase pi)
    sawtooth-wave  s  &optional  (fm  0.0)

    make-pulse-train  &key  frequency  (amplitude 1.0)  (initial-phase two-pi)
    pulse-train  s  &optional  (fm 0.0)

Triangle-wave appears almost exclusively in the vibrato computation (v.ins for example). One popular kind of vibrato is:

    (+ (triangle-wave pervib) (randi ranvib))

ONE-POLE

ONE-ZERO

TWO-POLE

TWO-ZERO

These are simple filters.

   make-one-pole  a0  b1        ; b1 < 0.0 gives lowpass, b1 > 0.0 gives highpass
   one-pole  f  input
   make-one-zero  a0  a1        ; a1 > 0.0 gives weak lowpass, a1 < 0.0 highpass
   one-zero  f  input
   make-two-pole  a0  b1  b2    ; see ppolar below
   two-pole  f  input
   make-two-zero  a0  a1  a2    ; see zpolar below
   two-zero  f  input

The difference equations are:

        one-zero    y(n) = a0 x(n) + a1 x(n-1)
        one-pole    y(n) = a0 x(n) - b1 y(n-1)
        two-pole    y(n) = a0 x(n) - b1 y(n-1) - b2 y(n-2)
        two-zero    y(n) = a0 x(n) + a1 x(n-1) + a2 x(n-2)

(This nomenclature is taken from Julius Smith's "An Introduction to Digital Filter Theory" in Strawn "Digital Audio Signal Processing", and is different from that used in more general filters given on page 15. It is also different from that used by the MusicKit. For example, the MusicKit's one-pole's A1 is our b1 and their B0 is our a0). The coefficients should be between -2.0 and 2.0.

PPLOAR

ZPOLAR

FORMNT

These generators provide a more intuitive handle on the simple filters given above.

   make-ppolar  R  freq
   ppolar  f  input             ; bandpass, centered at freq, bandwidth set by R
 
   make-zpolar  R  freq
   zpolar  f  input             ; bandstop, notch centered at freq, width set by R

   make-formnt  R  freq  &optional  G
   formnt  f  input             ; resonator centered at freq, bandwidth set by R

  PPolar    y(n) = x(n) + 2*R*cos(2*pi*Freq/Srate)*y(n-1) - R*R*y(n-2)
  ZPolar    y(n) = x(n) - 2*R*cos(2*pi*Freq/Srate)*x(n-1) + R*R*x(n-2)
  Formnt    y(n) = x(n) - R*x(n-2) + 2*R*cos(2*pi*Freq/Srate)*y(n-1) - R*R*y(n-2)

PPolar ("poles polar") allows you to specify the radius and angle of a pole while two-pole requires actual filter coefficients. The filter provided by ppolar has two poles, one at (R,Freq), the other at (R,-Freq). R is between 0 and 1 (but less than 1), and Freq is between 0 and Srate/2. This is the standard resonator form with poles specified by the polar coordinates of one pole. Similar remarks apply to two-zero and zpolar. More than likely you want "polar-coordinate" calling parameters (R and Freq) when setting up a second order section. The "rectangular-coordinate" versions exist for generality (e.g. to make two real poles) and are rarely needed.

The impulse response of a two-pole filter section is an exponentially decaying sinusoid:

        Rn * cos(2*pi*(Freq/Srate)*n+Phi)

where (R,Freq) are as above, and Phi is a phase term. Therefore, the time constant of decay is 1/(1-R) samples which is 1/((1-R)*Srate)) seconds. Seven time constants is approximately the time it takes to decay 60dB. For example, if R=0.9999, then the time constant of decay is 10000 samples. If Srate=50000, then 10000 samples is one fifth of a second. Seven time-constants is then a little more than a second. Thus, the two-pole filter specified by ppolar with R=0.9999 and Freq=400, with Srate=50000, is a resonator which when pulsed will give an exponentially decaying tone at A440 which decays for about a second before becoming inaudible.

Formnt is recommended for resonators (simple bandpass filters), vocal tract or instrument cavity simulations, etc. It provides robust bandpass filtering (simple resonance) using two poles and two zeroes. Only one coefficient need be changed in order to move the filter center frequency. The filter coefficients are set as a function of desired pole-radius R (set by desired bandwidth), center frequency F, and peak gain G.

Here's a little instrument that uses formnt to filter white noise:

(definstrument simp (beg end R freq G)
  (let* ((amp .5)
         (f (make-formnt R freq G))
         (noi (make-randh :frequency 5000 :amplitude .1)))
    (run
      (loop for i from beg to end do
        #-just-lisp (declare (optimize (safety 1)))   ;catch and report overflows
        (outa i (* amp (formnt f (randh noi))))))))

(With-sound () (simp 0 10000 .99 1000 1.0))

FILTER

DIRECT-FILTER

LATTICE-FILTER

LADDER-FILTER

FIR-FILTER

These are the general FIR/IIR filters of arbitrary order. [DSP: all coefficients must be between -1.0 and 1.0].

   make-filter  &key  order
                      m                ; same as order
                      a                ; same as y-coeffs
                      p                ; same as x-coeffs
                      x-coeffs
                      y-coeffs
                      (type direct-form)
                      sn               ; same as a, for ladder-filter
                      rc               ; for lattice-filter
                      tap              ; same as p, for ladder-filter or lattice-filter
                      k                ; same as rc, for lattice-filter
                      cn               ; for ladder-filter

   make-direct-filter  &key  a  p  m
   make-lattice-filter  &key  k  rc  tap  p  a  m
   make-ladder-filter  &key  sn  cn  a  p  tap  so  m
   make-FIR-filter  &key  coeffs  order

   direct-filter  fl  inp
   lattice-filter  fl  inp        ; Markel & Gray's "two multiplier lattice"
   ladder-filter  fl  inp         ; M&G's "normalized ladder"
   filter  fl  inp
   FIR-filter  fl  inp

Here we follow the nomenclature of Markel and Gray, "Linear Prediction of Speech". Many of the keyword arguments are synonyms -- you can use whichever name you're used to. If you give x-coeffs or y-coeffs, but ask for a lattice or ladder-filter, CLM automatically translates to the correct reflection coefficients. Use flt-a and flt-b as arrays if you want time-varying coefficients. We follow Markel and Gray in assuming that the first y-coeff is 1.0. That is, to implement the difference equation yn <= a0*x0 - b1*y1 we would set y-coeffs to (list 1.0 b1) and x-coeffs to (list a0). The possible types are direct-form, ladder-form, and lattice-form.

See fltdes.lisp for functions compatible with these filters that provide the coefficients for various FIR and IIR filters. There are also functions to print the frequency response and so on -- this code could relatively easily be made more flexible, if anyone is interested. fltdes.lisp also contains examples and test cases. An example is given on page 32. FIR-filter is an optimization of direct-filter. Associated with it are three functions:

   make-filter-coefficients order coeffs
   design-FIR-from-env &key order envelope dc
   make-FIR-coeffs order env

These make it easy to specify the frequency response of the FIR filter. The envelope is a line segment approximation to the desired frequency response. The order of the filter determines how close you get to that curve; the higher the order the closer, but also the more slowly the filter runs. make-FIR-coeffs combines the first two. See the example on page 32.

COMB

NOTCH

   make-comb  mlt  length
   comb  cflt  input

   make-notch  mlt  length
   notch  cflt  input

Comb is a delay line with a scaler on the feedback term. Notch is a delay line with a scaler on the feedforward term. Mlt is the scaler (it should be less than 2.0 in magnitude). Length is the length in samples of the delay line. In filter parlance, comb is:

   y(n) <= x(n-length-1) + mlt * y(n-length)

As a rule of thumb, the decay time of the feedback part is 7*length/(1-mlt) samples, so to get a decay of Dur seconds, mlt <= 1-7*length/(Dur*Srate). The peak gain is 1/(1-(abs mlt)). See Julius Smith's "An Introduction to Digital Filter Theory" in Strawn "Digital Audio Signal Processing", or Smith's "Music Applications of Digital Waveguides".

See zd.ins for interpolating versions of the comb, notch, and all-pass generators (zcomb, znotch, zall-pass).

ALL-PASS

   make-all-pass  feedback-scaler feedforward-scaler  length
   all-pass  f  input

All-pass or "moving average comb" is just like comb but with added feed-forward term. If feedback-scaler = 0, we get a moving average comb filter. If both scale terms = 0, we get a pure delay line. In filter parlance,

     y(n) <= feedforward-scaler*x(n-1) + x(n-length-1) + feedback-scaler*y(n-length)

[DSP: the samples in the delay are assumed to be fractional (between -1 and 1).]

DELAY

   make-delay   length  &key  initial-contents  initial-element
   delay  d  input
   tap  d  &optional  (offset  0)

Delay is a straightforward delay line. Length is in samples. Input fed into a delay line reappears at the output Length samples later. Initial-element defaults to 0.0. If specified, initial-contents need not be the same length as the delay line. [DSP: input is assumed to be between -1 and 1.] Tap returns the current value of the delay generator. Offset is the distance of the tap from the current (default) delay line sample -- it is assumed to be positive or zero.

ZDELAY

   make-zdelay  length  &key  initial-contents  initial-element  true-length
   zdelay  z  input  &optional  (pm-input  0.0)
   ztap  z  &optional  (pm-input  0.0)

Zdelay is an "interpolating" delay line. Zdelay is just like delay but the current index into the delay line can have a fractional part. Pm-Input determines how far off from the normal index we are. Obviously there are limits to what you can get away with. Zdelay is intended for things like "flanging" where the delay line is relatively long, and the interpolation motion is relatively small. Zdelay uses "phase modulation" rather than FM, because it is closer to what we want to think about here -- the Pm-Input argument is difference between the nominal delay length (Length) and the current actual delay length (Length + Pm-Input) -- a positive Pm-Input corresponds to a longer delay line. If true-length is not set explicitly, you normally get the smaller of twice the length and the length + 512; a pm-input outside those bounds will cause trouble. To add a tap to a zdelay line, use ztap.


(definstrument zcomb (time duration freq amp length1 length2 feedback)
  (multiple-value-bind
      (beg end) (get-beg-end time duration)
    (let ((s (make-pulse-train :frequency freq))
          (d0 (make-zdelay length1 :true-length (max length1 length2)))
          (zenv (make-env :envelope '(0 0 1 1) :scaler (- length2 length1)
                          :start-time time :duration duration)))
      (run
       (loop for i from beg to end do
	 (let* ((offset (env zenv))
		(dly-val (ztap d0 offset)))
	   (outa i (zdelay d0 (+ (* amp (pulse-train s)) (* feedback dly-val)) offset)))))
      (end-run))))

(with-sound () (zcomb 0 3 100 .1 20 100 .5) (zcomb 3.5 3 100 .1 100 20 .95))

WAVESHAPE

  make-waveshape  &key  (frequency  440.0)  (partials  '(1 1))
  waveshape  w  &optional  (index  1.0)  (fm 0.0)

  signify  Harm-amps
  make-waveshape-table  harm-amps  &optional (norm t) (tblsiz default-table-size)
  make-phase-quad-table  harm-amps  phases  &optional  (tblsiz  default-table-size)
  get-chebychev-coefficients  partials  &optional  (kind  1)

Waveshape performs waveshaping. For a description of waveshaping, see "Digital Waveshaping Synthesis" by Marc Le Brun in JAES 1979 April, vol 27, no 4, p250. [DSP: it is expensive (that is, it takes a long time) to load the table and read it using waveshape. I recommend that you use polynomial instead.]

(definstrument simp ()
  (let ((wav (make-waveshape :frequency 440 :partials '(1 .5 2 .3 3 .2))))
    (run (loop for i from 0 to 1000 do (outa i (waveshape wav))))))

See bigbird below for an example of get-chebychev-coefficients, and pqw.ins for an example of phase quadrature waveshaping. Signify takes a list of harmonic amplitudes and puts a pattern of signs on that list that optimizes the behaviour of the waveshaper at low indices. Get-chebychev-coefficients takes a list of harmonic amplitudes and returns a list of Chebychev polynomial coefficients. The argument kind determines which kind of Chebychev polynomial we are interested in.

POLYNOMIAL

   polynomial  coeffs  x

Coeffs is an array of coefficients, x is the value to be plugged into the polynomial. Coeffs[0] is the constant term, and so on. For waveshaping, use the function get-chebychev-coefficients. [DSP: the coefficients in coeffs, the argument x and all intermediate values produced during polynomial evaluation are assumed to be "fractional".] Abramowitz and Stegun, "A Handbook of Mathematical Functions" is a treasure-trove of interesting polynomials.

(definstrument BigBird (start duration frequency freqskew amplitude
                          Freq-env Amp-env Partials)
  (multiple-value-bind (beg end) (get-beg-end start duration)
    (let* ((gls-env (make-env :envelope Freq-env :scaler (In-Hz freqskew)
                              :start-time start :duration duration))
           (os (make-oscil :frequency frequency))
           (fil (make-one-pole .1 .9))
           (coeffs (get-chebychev-coefficients 
                     (normalize-partials Partials)))
           (amp-env (make-env :envelope Amp-env :scaler (* 8 amplitude)
                              :start-time start :duration duration)))
      (loop for i from 0 below (length coeffs) do
        (setf (aref coeffs i) (* .125 (aref coeffs i))))
      (run
        (loop for i from beg to end do
          (outa i 
            (one-pole fil 
              (* (env amp-env) 
                 (polynomial coeffs (oscil os (env gls-env)))))))))))

RESAMPLE

   make-resample  &key  file (srate  1.0)  start-time  start  (channel  :A)
   resample  s  &optional  (sr-change  0.0)

Resample performs sampling rate conversion by linear interpolation. File is the file of sound samples to be resampled, Start-Time is where to start in that file (in seconds), Start is the start point in samples, Channel is which channel to resample, and Srate is how much to move forward on each sample. Srate = 1.0 therefore causes no change at all, Srate = .5 interpolates a point between each of the originals, so the sound goes down an octave if played at the original sampling-rate. Similarly Srate = 2.0 moves it up an octave. Sr-Change is added to Srate to provide time varying sampling rate changes. Large downward shifts are often marred by the buzz or whine of quantization noise -- use src for extreme cases. Input-file-start-time and input-file-start are synonyms for start-time and start.

SRC

   make-src  &key  file  (srate  1.0)  (channel  :A)  start-time  start  (width  5)
   src  s  &optional  (sr-change  0.0)

Src performs sampling rate conversion in a somewhat more sophisticated manner than resample -- it convolves the input with a sinc function, after filtering the input if Srate > 1.0. As in resample, File is the file to be converted, Srate is the ratio between the new sampling rate and the old, Start-Time is where to start in File (in seconds), Channel is which channel to convert, and Width is how many neighboring samples to convolve with sinc. Input-file-start-time and input-file-start are synonyms for start-time and start. In src, the Sr-Change argument is the amount to add to the current Srate on a sample by sample basis. Currently, the low pass filter is not changed in concert with Sr-Change, so large upward changes may not be engineer-approved. Here's an instrument that provides time-varying sampling rate conversion:

(definstrument simp (start-time duration amp srt srt-env filename)
  (let* ((senv (make-env :envelope srt-env :scaler 1.0 :offset 0.0
                         :start-time start-time :duration duration))
         (beg (floor (* start-time sampling-rate)))
         (end (+ beg (floor (* duration sampling-rate))))
         (f (open-input filename))
         (src-gen (make-src :file f :srate srt)))
    (run
      (loop for i from beg to end do
        (outa i (* amp (src src-gen (env senv))))))
    (close-input f)))

CONTRAST-ENHANCEMENT

   contrast-enhancement  in-samp  &optional  (fm-index  1.0)

Contrast-enhancement phase-modulates a sound file. It's like audio MSG. The actual algorithm is sin(in-samp*pi/2 + (fm-index*sin(in-samp*2*pi))). The result is to brighten the sound, helping it cut through a huge mix and so on.

WAVE-TRAIN

   make-wave-train  &key  wave  (frequency 440.0)  (initial-phase 0.0)
   wave-train  w  &optional  (fm  0.0)

Wave-train produces a wave train (an extension of Pulse-Train). Frequency is the repetition rate of the wave found in Wave. Successive waves can overlap. With some simple envelopes, or filters, you can use this for VOSIM and other related techniques. [DSP: the actual wave samples are assumed to be fractional.] See ins.txt for a number of example instruments.

RUN-BLOCK

   make-block  &key  size  trigger
   run-block  blk

This unit generator is the main working portion of all the block processing generators, and is provided for those who want to build their own fancy generators. There are some examples in the ins.txt file (phase-vocoders and so on). A block (rblk-buf) can be cleared quickly with clear-block.

FFT-FILTER

   make-fft-filter  &key  filter
                          file          ;input file
                          start-time    ;start-time in input file in seconds
                          start         ;same but in samples
                          (channel :A)
                          (fft-size 512)
   fft-filter  ff

Fft-filter performs fft based filtering of File. Filter can be an envelope or an array. In either case, the values should be positive fractions (that is, numbers between 0.0 and 1.0, inclusive). The advantage of using an array is that you can change the filter on the fly in the instrument (changing an envelope is relatively difficult). Filter represents the frequency response of the filter for the frequencies below the Nyquist limit -- if an array is used, it should be 1/2 the Fft-Size. [DSP: fft-size should be small enough to fit in the available DSP memory]. Input-file-start-time and input-file-start are synonyms for start-time and start. Here's an example of using fft-filter with a table to do time-varying filtering. In this case, we are gradually notching out some of the lower frequencies.

(definstrument fftins (beg dur spectrum file)
  (let* ((start (floor (* beg sampling-rate)))
         (end (+ start (floor (* dur sampling-rate))))
         (fil (open-input file))
         (val 1.0)
         (step (/ 1.0 (* (min dur 1.0) sampling-rate))) ;max 1 sec for ramp
         (notch-start 10)
         (notch-end 20)
         (ff (make-fft-filter :file fil :input-file-start-time 0.0
                              :fft-size 128 :filter spectrum)))
    (run
      (loop for i from start to end do
        (when (>= val step) ;ramp down to 0, then leave it there
          (decf val step)
          (loop for k from notch-start to notch-end do
          (setf (aref (fftflt-env ff) k) val)))
        (outa i (fft-filter ff))))
    (close-input fil)))

In this example, we are assuming that "spectrum" is an array, not an envelope (we are accessing fftflt-env using aref inside run). Due to the stupidity of the foreign function interface the array passed must have elements of type 'short-float -- e.g. (make-array 256 :element-type 'short-float). (Of course, a "real" fftins would protect against interruptions and :reset and so on). If fftflt's file is explicitly nil, the input is white noise. This is a feature, not a bug -- an example can be found in ins.txt.

CONVOLVE

   make-convolve  &key  filter
                        file
                        start-time
                        start
                        (channel :A)
                        (fft-size 512)
   convolve  ff

Closely related to fft-filter is the unit generator convolve. Its arguments are the same as in make-fft-filter and fft-filter, and it provides a very similar function. Fft-filter assumes that the imaginary part of the fft of the spectrum is 0, whereas convolve does not. [DSP: If filter is a file, convolve falls into C and can handle any size convolution; if both files to be convolved are bigger than will fit in memory, you'll spend a lot of time swapping]. ("Filter" = impulse response here). Input-file-start-time and input-file-start are synonyms for start-time and start.

(definstrument convins (beg dur filter file 
                        &optional (size 128))
  (let* ((start (floor (* beg sampling-rate)))
         (end (+ start (floor (* dur sampling-rate))))
         (fil (open-input file))
         (ff (make-convolve :file fil  :input-file-start-time 0.0
                            :fft-size size  :filter filter)))
    (run
      (loop for i from start to end do (outa i (convolve ff))))
    (close-input fil))) 

EXPAND

  make-expand  file-name             ;input file name
       &key  start-time              ;start-time (secs) in the input file
             start                   ;same, but in samples
             (channel  :A)           ;which channel this expand gen reads
             (segment-length . 15)   ;length of file slices that are overlapped
             (segment-scaler  .6)    ;amplitude scaler on slices (to avoid overflows)
             (expansion-amount  1.0) ;how much to lengthen or compress the file
             (output-hop  .05)       ;speed at which slices are repeated in output
             (ramp-time .4)          ;amount of slice-time spent ramping up/down
             (accuracy 1)            ;how closely to follow the original
  expand  e

Expand "expands" the sound file File-name. It is the poor man's way to change the speed at which things happen in a recorded sound without changing the pitches. It works by slicing the input file into short pieces, then overlapping these slices to lengthen (or shorten) the result; this process is sometimes known as granular synthesis, and is similar to the "freeze" function. The duration of each slice is segment-length -- the longer, the more like reverb the effect. The portion of the segment-length (on a scale from 0 to 1.0) spent on each ramp (up or down) is ramp-time. This can control the smoothness of the result of the overlaps. The more-or-less average time between successive segments is output-hop. The accuracy at which we handle this hopping is set by accuracy -- too accurate and you'll get an annoying tremolo. The overall amplitude scaler on each segment is segment-scaler -- this is used to try to to avoid overflows as we add all these zillions of segments together. The expansion-amount determines the input hop in relation to the output hop -- as of 27-Feb-95, the output-hop is divided by expansion-amount, so an expansion-amount of 2.0 should more or less double the length of the original, whereas an expansion-amount of 1.0 should return something close to the original speed. (We used to divide by 2*expansion-amount, but everyone found that confusing).

SUM-OF-COSINES

   make-sum-of-cosines  &key  (cosines  1)
                              (frequency  440.0)
                              (initial-phase 0.0)
   sum-of-cosines  cs  &optional  (fm  0.0)

Sum-of-cosines produces a band-limited pulse train containing Cosines cosines. It uses the formula: 1+2(cos(x)+cos(2x)+...cos(nx)) = sin((n+.5)x)/sin(x/2).

RING-MODULATE

AMPLITUDE-MODULATE

   ring-modulate  in1  in2
   amplitude-modulate  am-carrier  input1  input2

Ring-modulate returns (* in1 in2). Amplitude-modulate returns (* input1 (+ am-carrier input2)) Since neither needs any state information, there are no associated "make" functions.

Both of these take advantage of the "Modulation Theorem" -- since multiplying a signal by a phasor (e ^ (j w t)) translates its spectrum by w / two-pi Hz, multiplying by a sinusoid splits its spectrum into two equal parts translated up and down by w/two-pi Hz. The simplest case is:

        cos f1 * cos f2 = (cos (f1 + f2) + cos (f1 - f2)) / 2.

SINE-SUMMATION

  make-sine-summation  &key  (n  1)
                             (a . 5)
                             (b-ratio  1.0)
                             (frequency 440.0)
                             (initial-phase 0.0)
  sine-summation  s  &optional  (fm  0.0)

See J.A.Moorer, "Signal Processing Aspects of Computer Music" and "The Synthesis of Complex Audio Spectra by means of Discrete Summation Formulae" (Stan-M-5) -- the basic idea is very similar to that used in the sum-of-cosines generator.


  (defcinstrument ss (beg dur freq amp &optional (N 1) (a .5) (B-ratio 1.0))
    (let* ((st (floor (* sampling-rate beg)))
           (nd (+ st (floor (* sampling-rate dur))))
           (sgen (make-sine-summation :n N :a a :b-ratio B-ratio :frequency freq)))
      (run
       (loop for i from st to nd do
         (outa i (* amp (sine-summation sgen)))))))


ASYMMETRIC-FM

  make-asymmetric-fm  &key  (r  1.0)
                            (ratio 1.0)
                            (frequency 440.0)
                            (initial-phase 0.0)
  asymmetric-fm  af  index  fm

See Palamin and Palamin, "A Method of Generating and Controlling Asymmetrical Spectra" JAES vol 36, no 9, Sept 88, p671-685: this is another extension of the sine-summation and sum-of-cosines approach.

  (defcinstrument asy (beg dur freq amp index &optional (r 1.0) (ratio 1.0))
    (let* ((st (floor (* beg sampling-rate)))
           (nd (+ st (floor (* dur sampling-rate))))
           (asyf (make-asymmetric-fm :r r :ratio ratio :frequency freq)))
      (run
       (loop for i from st to nd do
         (outa i (* amp (asymmetric-fm asyf index 0.0)))))))

OTHER FUNCTIONS callable within RUN

    stereo &optional (o-stream  *current-output-file*)      ;2 channels
    mono  &optional  (o-stream  *current-output-file*)      ;1 channel
    quad  &optional  (o-stream  *current-output-file*)      ;4 channels

Stereo returns t if its argument is a stereo file (i.e. has exactly 2 channels). Mono returns t if its argument is a mono file. The default argument for both is the current output file, also known as *current-output-file*. (Quad and stereo can't both be true at the same time).

in-Hz converts its argument to radians/sample (for any situation where a frequency is used as an amplitude -- glissando or FM, for example). It can be used within run.

fft, inverse-fft, and fft-window

These provide run-time access to a standard fft routine. Both take as the only argument an fft-data structure, normally created by the function make-fft-data-arrays. Its only argument is the size of the ffts.

(definstrument pvins (beg dur file amp shift &optional (fftsize 128))
  ;;assume shift is a positive integer
  (let* ((start (floor (* beg sampling-rate)))
         (end (+ start (floor (* dur sampling-rate))))
         (fil (open-input file))
         (fftsize-1 (1- fftsize))
         (fd (make-fft-data-arrays fftsize))
         (wtb (make-block :size fftsize :trigger 0))
         (window (make-block :size fftsize))
         (freq-inc (/ fftsize 2))
         (freq-inc-1 (1- freq-inc))
         (rate (/ 1.0 (* freq-inc fftsize)))
         (filptr 0)
         (shift-top (- fftsize shift 1))) 
    (loop for i from 0 below freq-inc and j from fftsize-1 by -1 and 
              angle from 0.0 by rate do
      (setf (aref (rblk-buf window) i) angle)
      (setf (aref (rblk-buf window) j) angle))
    (run
      (loop for i from start to end do
        (when (zerop (rblk-loc wtb))
          (dotimes (k fftsize)
            (setf (aref (fft-data-real fd) k) 
              (* (aref (rblk-buf window) k) (ina filptr fil)))
            (incf filptr))
        (clear-block (fft-data-imaginary fd))
        (decf filptr freq-inc)
        (fft fd)
         ;; now shift the positive frequencies up and the negative frequencies down
        (loop for k from freq-inc-1 downto shift and j from freq-inc to shift-top do
          (setf (aref (fft-data-real fd) k) (aref (fft-data-real fd) (- k shift)))
          (setf (aref (fft-data-imaginary fd) k) (aref (fft-data-imaginary fd) (- k shift)))
          (setf (aref (fft-data-real fd) j) (aref (fft-data-real fd) (+ j shift)))
          (setf (aref (fft-data-imaginary fd) j) (aref (fft-data-imaginary fd) (+ j shift))))
        (dotimes (k shift)
          (setf (aref (fft-data-real fd) k) 0.0)
          (setf (aref (fft-data-imaginary fd) k) 0.0)
          (setf (aref (fft-data-real fd) (- fftsize-1 k)) 0.0)
          (setf (aref (fft-data-imaginary fd) (- fftsize-1 k)) 0.0))
        (inverse-fft fd)
        (dotimes (k fftsize)
          (incf (aref (rblk-buf wtb) k) (aref (fft-data-real fd) k))) 
        (incf (rblk-ctr wtb) freq-inc))
      (outa i (* amp (run-block wtb)))))))

The arguments to fft-window are the fft-data structure and the array containing the window values. Many of the standard windows are available in mus.lisp and, for the enterprising, in lib56.lisp. See ins.txt, sms.lisp, and san.ins for more examples (vocoder tricks and so on).

Definstrument

Definstrument is the most common way to define an instrument in CLM. Its syntax is almost the same as defun, but in addition it has a few options. These fulfill two roles: specifying instrument output language details, and requesting minor optimizations. The options are in a state of flux; currently they are:

       language  :c, :lisp or :56k ; specify output language -- defcinstrument for :c
       c-file  nil                 ; [C: specify the instrument intermediate C file name]
       c-include-file  nil         ; [C: C code to be #include'd in the intermediate file]
       c-options  "-c -O"          ; [C: C compiler switches]
       exp-env  t                  ; [DSP:should exponential envelopes support be loaded]
       save-labels  nil            ; [DSP: should debugging labels be saved]
       parallel nil     ; [C: this is a "sample-synchronous" instrument -- defpinstrument]

The output language is chosen based on the kind of CLM you load -- on any machine but the NeXT, it defaults to :c. On the NeXT the choice is made in all.lisp when you create CLM -- the C output easily matches the DSP in speed for most cases, so unless you have an Ariel QP board, the recommended output language is C. The :lisp option is useful during debugging.

The syntax for these options is somewhat similar to that of with-sound. For example, to specify that the instrument simp should use C with the "-c -O3" flags,

   (definstrument (simp :language :c :c-options "-c -O3") (beg dur
...)

In GCL, the c-file name, if specified, should be different from the instrument file name because GCL creates a .c file as part of its compilation process. That is, if you have simp.ins, don't use simp.c as its c-file name.

Defpinstrument is the same as defcinstrument except that in addition to producing C output the instrument is handled by a special scheduler that runs it one sample at a time, in parallel with all other p-instruments that want to run on that sample. This kind of scheduling is useful when instruments share global data that they are modifying as they run. P-instruments can be mixed freely with "normal" instruments. The latter run to completion when they are called, while the currently running p-instruments wait in the background. There are currently a few limitations associated with p-instruments: local sound output files and phrase-value are not supported, p-instruments are unlikely to behave correctly within mix and with-mix, and calls on these instruments must be ordered by begin-time.

The following instruments are included as separate .ins files in the clm directory:

  complete-add     add.ins           additive synthesis
  addflts          addflt.ins        filters
  add-sound        addsnd.ins        mix in a sound file
  badd             badd.ins          fancier additive synthesis
  fm-bell          bell.ins          fm bell sounds
  bigbird          bigbird.ins       waveshaping bird.clm (also bird.ins)
  canter           canter.ins        fm                       bag.clm (bagpipes)
  cellon           cellon.ins        feedback fm
  clarinet         clar.ins          physical model(?) of a clarinet
  cnvflt-reverb    cnv.ins           convolution-based reverb
  drone            drone.ins         additive synthesis       bag.clm
  filter-noise     fltnoi.ins        filter with envelopes on coefficients
  filter-sound     fltsnd.ins        filter a sound file
  stereo-flute     flute.ins         physical model of a flute (Nicky Hind)
  fm-insect        insect.ins        fm
  fusion           fusion.ins        spectral fusion using fm
  jc-reverb        jcrev.ins         an old reverberator
  fm-voice         jcvoi.ins         jc's fm voice
  kiprev           kiprev.ins        a fancier (temperamental) reverberator
  lbj-piano        lbjPiano.ins      additive synthesis piano 
  mlb-voice        mlbvoi.ins        mlb's fm (originally waveshaping) voice
  fm-noise         noise.ins         noise maker
  nrev             nrev.ins          a popular reverberator
  pins             san.ins           spectral modelling
  pluck            pluck.ins         Karplus-Strong synthesis
  pqw              pqw.ins           waveshaping
  pqw-vox          pqwvox.ins        waveshaping voice
  resflt           resflt.ins        filters
  reson            reson.ins         fm
  reverberate      revsnd.ins        add reverb to sound file
  track-rms        rms.ins           rms envelope of sound file (Michael Edwards)
  singer           singer.ins        Perry Cook's vocal tract physical model
  fm-trumpet       trp.ins           fm trumpet
  fm-violin        v.ins             fm violin               fmviolin.clm, popi.clm
  vox              vox.ins           fm voice                cream.clm
  zc, zn           zd.ins            examples of zdelay (zcomb, znotch, zall-pass)

The file makins.lisp exercises most of these instruments. See also ins.txt for a large number of examples and some tutorial explanations. If you develop an interesting instrument that you're willing to share, please send it to me (bil@ccrma.stanford.edu).

WITH-SOUND and CLM-LOAD

 with-sound  &key  
        (output  default-sound-file-name) ; name of output sound file
        sndfile                   ; same as output
        (channels  1)             ; 1, 2, and 4 are supported
        (srate  22050)            ; other playable rates are 44100 and 8012
        sampling-rate             ; same as srate
        continue-old-file         ; open and continue old output file
        reverb                    ; name of the reverberator, if any -- the reverb
                                  ;   is a normal clm instrument (see nrev.ins)
        reverb-data               ; arguments passed to the reverberator --
                                  ;   passed here as an unquoted list
        (reverb-channels 1)       ; chans in temp reverb stream
        revfile                   ; explicit reverb file name (normally nil)
        (play  t)                 ; play new sound automatically?
        play-options              ; args passed to user's DAC routine
        (cleanup-first  t)        ; if nil, don't try to get clean dsp state
        wait                      ; wait for previous computation to finish
        notehook                  ; form evaluated on each instrument call
        statistics                ; print out various fascinating numbers
        (decay-time  1.0)         ; ring time of reverb after end of piece
        output-buffer-size        ; don't set unless you're a good sport
        comment                   ; comment placed in header (defaults to a time
                                  ;   stamp (clm-get-default-header)
        commentary                ; same as comment
        info                      ; non-comment header string
        type                      ; output sound file type
        data-format               ; output sound data format (defaults to snd-16-linear)
        save-body                 ; if t, copy the body (as a string) into the header
        save-stats                ; (NeXT sound file output only) t=save stats in header
        scaled-to                 ; if a number, scale results to have that max amp
        verbose                   ; local *clm-verbose* value
        (force-recomputation nil) ; if t, force mix/with-mix calls to recompute

With-sound is a macro that wraps an "unwind-protect" around its body to make sure that everything is cleaned up properly if you happen to interrupt computation. Here are some examples showing how to use the various options:

  (with-sound (:output "new.snd") (simp 0 1 440 .1))
  (with-sound (:srate 44100 :channels 2) ...)
  (with-sound (:play 3) ...) ;play resultant sound 3 times
  (with-sound (:comment (format nil 
     "This version is louder: ~A" (clm-get-default-header))))
  (with-sound (:reverb jc-reverb) ...)
  (with-sound (:reverb nrev :reverb-data (:reverb-factor 1.2 :lp-coeff .95))...)

Continue-Old-file: if t (default is nil), a previously existing file is reopened for further processing. The default (nil) clobbers any existing file of the same name as the output file (see Output above). By using Continue-Old-File, you can both add new stuff to an existing file, or (by subtracting) delete old stuff to any degree of selectivity. When you erase a previous note, remember that the subtraction has to be exact -- you have to create exactly the same note again, then subtract it. By the same token, you can make a selected portion louder or softer by adding or subtracting a scaled version of the original.

Notehook: a form that is evaluated each time an instrument is called (defaults to nil). The following example prints a dot each time an instrument is called:

  (with-sound (:notehook '(princ "."))
    (simp 0 110)
    (simp 1 2))

Statistics: currently just a flag -- if t (default is nil), clm keeps track of a variety of interesting things and prints them out at the end of the computation. If your favorite statistic isn't among the chosen few, send bil a complaint.

Data-format: CLM can write output sound data in any of the following formats: snd-16-linear (the default, and with snd-8-mulaw, the only playable data format on the NeXT), snd-8-mulaw, snd-8-alaw, snd-8-linear, snd-8-unsigned (for old Macs), snd-32-float, snd-32-linear, snd-24-linear, and snd-64-double. The most useful of these is snd-32-linear; if you aren't sure what the output max amp will be, write it out as snd-32-linear and get the accurate (possibly greater than 1.0) max amp, then scale that back while transforming to snd-16-linear to hear the result. The easiest way to do this is to use the scaled-to argument in with-sound -- if scaled-to has a value (like .9), with-sound automatically writes its results first to a 32-bit linear intermediate file, then scales that file by whatever amount is needed to end up at the requested max amp.

Clm-load is the same as with-sound, but its argument is the name of a file containing clm instrument calls and so on (i.e. the body of with-sound). You can exit early from the note list with (throw :FINISH) -- (with-sound () (load "file")) is very similar.

Instrument-let

Instrument-let is like Lisp's flet -- it can be used to define local instruments. For example,

(defun ins ()
  (instrument-let ((simp (beg dur freq)
                     (let* ((start (floor (* beg sampling-rate)))
                            (end (+ start (floor (* dur sampling-rate))))
                            (osc (make-oscil :frequency freq)))
                       (run
                         (loop for i from start to end do
                         (outa i (* .25 (oscil osc)))))))
                   (toot (beg dur) 
                     (let* ((start (floor (* beg sampling-rate)))
                            (end (+ start (floor (* dur sampling-rate))))
                            (osc (make-oscil :frequency 220)))
                       (run
                         (loop for i from start to end do
                         (outa i (* .25 (oscil osc))))))))
    (simp 0 1 660)
    (toot 3 1)))

Now, when the function Ins is called within with-sound or clm-load, it will add a note from Simp and Toot. For a more useful example, see expsrc.ins. (Currently in CLM, these instrument names are not purely local; the DSP code vector is stored on the symbol's property list which means, much to my surprise, that you can't have a local and a global instrument with the same name). (Also, instrument-let does not work in GCL with C output). Don't use instrument-let within definstrument.

Sound-Let

Sound-let is a form of let* that creates temporary sound streams within with-sound. Its syntax is like that of let and with-sound:

  (sound-let ((temp-1 () (fm-violin 0 1 440 .1))
              (temp-2 () (fm-violin 0 2 660 .1)
                         (fm-violin .125 .5 880 .1)))
    (expand-sound temp-1 0 2 0 2);temp-1's value is the name of the temp file
    (expand-sound temp-2 1 1 0 2)))

This creates two temporary files and passes them along to the subsequent calls on expand-sound. The first list after the sound file identifier (i.e. after "temp-1" in the example) is the list of with-sound options to be passed along when creating this temporary file. These default to :output with a unique name generated internally, and all other variables are taken from the overall (enclosing) output file. The rest of the list is the body of the associated with-sound.

Sound Processing

Mix  file  begin  &body body
With-mix options file begin &body body

Mix is a macro, callable within with-sound or clm-load, that saves the computation in its body in a separate file named file (without the .snd extension), and can tell when that file's data is up to date and need not be recomputed. Mix is equivalent to open-input or mix-in(see below) with a separate file of clm instrument calls, but lets you keep all the notes in one file. Mix with no body is the same as mix-in. With-mix is the same as mix, but gives you a chance to specify local with-sound options (mainly for local reverb). For example, the following with-sound uses mix, with-mix, mix-in, and normal instrument calls:

  (with-sound () 
    (fm-violin 0 .1 440 .1)
    (mix "sec1" .5 
      (fm-violin 0 .1 550 .1)
      (fm-violin .1 .1 660 .1))
    (with-mix (:reverb jc-reverb) "sec2" 1.0
      (fm-violin 0 .1 880 .1 :reverb-amount .2)
      (fm-violin .1 .1 1320 .1 :reverb-amount .2))
    (fm-violin 2 .1 220 .1)
    (mix-in "/zap/slow.snd" 2))

Now, if we change just the first note in the mix call, the with-mix section will not be recomputed, but will be mixed in from the saved file "sec2.snd". By surrounding stable sections of a piece with calls on mix or with-mix, you can save a huge amount of time that would otherwise be spent waiting for these notes to be recomputed. This check-point or makefile capability is built on open-input.

    open-input  &optional  name 
                &key verbose  if-does-not-exist  element-type  mix-at  
                     mix-duration  force-recomputation
    open-output  &optional  (name  default-sound-file-name)
    close-input  &optional  (i-stream  *current-input-file*)
    close-output  &optional  (o-stream  *current-output-file*)
    reopen-output  &optional  (o-stream  default-sound-file-name)
    mix-in  file-name  begin-time  &optional  duration
    mix-sound  output-file  output-start-samp  input-file  input-start-samp  samples-to-merge
    sound-let
    clm-open-input name

These functions open and close input and output sound files. Open-input and open-output take either strings or pathnames and return an IO object (see mus.lisp for a description of the innards of this structure). Various clm functions use that object as a handle on the file. The variable default-sound-file-name is "/zap/test.snd" at CCRMA. It is set in next-io.lisp.

Open-input normally opens the sound file name and returns an IO structure that other clm functions can use to access the file. If you don't give a complete file name (name without the .snd extension), open-input checks to see if there's either no .snd file or a later .cm or .clm file, and in that case, suspends the current computation, makes the sound file from the sources, then resumes the old computation, opening the (newly computed) sound file. If you are working in sections, and keep the sections in separate files, the various layers of mixing can automatically notice when some section has changed, and update everything for you. Similarly, if all your sound files get deleted, the whole piece can still regenerate itself in one operation. You can avoid all this fanciness by calling clm-open-input.

Open-input's &key parameters are patterned after Lisp's load function: Verbose (the default is nil) turns on some informational printout. Element-type can be nil (the default), or :sound. In the latter case, the file passed to open-input is assumed to contain sound data, no matter what extension it has. This is a way to override the check for out of date sound files and so on. If-does-not-exist can be nil or :error (the default). In the latter case, if no sound file associated with name can be found or created, you get an error message. As to mix-at, mix-duration, and force-recomputation, see mix-in below.

The implicit load triggered by open-input with a non-specific file name sets *open-input-pathname* and *open-input-truename* and notices *open-input-verbose* (if t, print out informational messages).

Finally, if all you want is from open-input is the "make" action, the function mix-in provides the simplest access. Its arguments are the file name and where to place the resulting sound file in the current output file (in seconds) and, optionally, how much of the mixed-in file to include. If you want to avoid the fancy checkpoint stuff in open-input, or start at some arbitrary place in the input file, the next lower level is mix-sound. Mix-sound takes sample numbers, not seconds. Force-recomputation, if t, forces the mix to be recomputed.

Various CLM Functions and Variables of General Interest

sampling-rate                current sampling rate
frequency-mag                scaler used to turn cycles per second into radians per sample
*current-output-file*        current default output stream (for outa and friends)
*current-input-file*         current default input stream (for ina and friends)
*clm-instruments*            list of the currently loaded clm instruments (see cream.clm)
*clm-init*                   name of site-specific clm initializations, if any
*sound-player*               user-supplied DAC funtion (see sound.lisp for details)
*clm-array-print-length*     number of IO data buffer elements printed in trace
*ignore-header-errors*       whether to worry about illegal sound file headers
*fix-header-errors*          whether clm should try to make sense of illegal headers
*grab-dac*                   whether clm should ask before interrupting the DAC 
default-sound-file-type      what kind of output sound file to write
clm-reset                    closes files and the DSP, and cleans up internal state
get-beg-end  start  duration convert Start and Duration from seconds to samples.
set-srate  new-srate         Set the sampling-rate to New-Srate.
clm-get-channels  file       Returns the number of channels in the file File.
clm-get-sampling-rate  file  Returns the sampling rate of File (IO struct).
clm-get-max-amp  file        Returns the maximum amplitude of File (IO struct).
clm-get-duration  file       Returns the duration (secs) of File (IO struct).
clm-get-samples  file        Returns the number of full samples in the file File

As an example of these, here's a function that returns the maximum amplitude in a sound file:
  (defun maxamp (file) 
    (let ((hi (open-input (namestring (merge-pathnames file default-sound-file-name))))) 
      (multiple-value-bind (a b) (clm-get-max-amp hi) 
        (close-input hi) 
        (list a b))))

clm-get-default-header returns a string containing the current date and time, the current version of the clm software and so on. This is used as the default "comment" in every sound file header.

    read-header  name
    write-header  name  &optional  header
    make-header  &key  (channels  1)  (format  snd-16-linear)   data-location
                       (sampling-rate  22050)  (info  "    ")  (type default-sound-file-type)
    update-header  name  header  new-data-size
    edit-header  name &key  channels  datasize  datalocation  offset  dataformat  srate

    dac &optional (file  default-sound-file-name)  (report-errors t)  (start 0.0)
    dac-n  &key  (file  default-sound-file-name)  (times  1)  (worry  t)  (start  0.0)
    stop-dac
    wait-for-dac

Dac is the simple way to hear a sound file. Its first optional argument is the file to be played (it defaults to the default sound file -- normally "/zap/test.snd"). Its second optional argument tells it whether to worry about illegal sound file formats and similar problems. Because NeXT's own sound file handling programs turn out files that are illegal according to NeXT's own software, we have to have a way to simply ignore errors and send arbitrary bytes to the DAC. So, for example, (dac "bad.snd" nil) explicitly overrides the error checking. Normally, the dac function starts playing in a background process and returns control to the lisp listener. You can stop the playing with stop-dac (synonyms). You can cause lisp to wait until the playing is finished with wait-for-dac. In the arguments to dac-n, File is the file name (a string), Times is the number of times to play the file, Worry is whether to worry about errors, and Start is where to start in the sound file. If you have Jean Laroche's Play program on the NeXT, the function jrdac can replace dac -- its arguments are the file name and optionally the new srate.

    end-run &optional phrase

End-run cleans up memory allocation associated with delay lines. Any DSP instrument that calls delay lines (even implicitly as in all-pass) should call end-run after the run loop. See the section on Phrasing, below, for a discussion of the phrase argument.

    volume        current volume.  Can be used with setf.  Values range between 0.0 and 1.0.
    dac-filter    current DAC output filter setting (setf-able).

The default "safety" setting for run (in the absence of any optimize declaration) is determined by *clm-safety* which defaults to 0. A value of 1 or higher causes overflow checks to be enabled. A value of 2 or higher causes array references to include bounds checks. A value of 3 causes some of the unit generators to use "reals" rather than "fractions". (Some of this is not yet implemented). *clm-speed* defaults to 1; a value of 2 or 3 causes some address calculations to be done at load time, and triggers other such optimizations.

*clm-news* is a variable containing information on what has changed recently in the clm software. Similarly *clm-version* is the current version identifier (a date). If you want clm to print out instrument names and begin times as they are computed, set the variable *clm-verbose* to t.

fltdes.lisp has a bunch of filter design functions. As an example of what is there, say we want to put a spectral envelope on a noise source.

(definstrument filter-noise (beg dur amp &key x-coeffs)
  (let* ((st (floor (* beg sampling-rate)))
         (noi (make-randh :frequency (* .5 sampling-rate) :amplitude amp))
         (flA (make-filter :x-coeffs x-coeffs))
         (nd (+ st (floor (* sampling-rate dur)))))
    (run
      (loop for i from st to nd do
        (outa i (filter flA (randh noi)))))))

(with-sound () 
  (filter-noise 0 1 .2 
    :x-coeffs 
      (make-filter-coefficients 12 
        (design-FIR-from-env 
          :order 12 
          :envelope '(0 0.0 .125 0.5 .2 0.0 .3 1.0 .5 0.0 1.0 0.0)))))


The file files describes briefly each of the files in the clm directory; clm-example.lisp shows one way to write notelists; cm-clm.lisp is a brief example of using Rick Taube's Common Music to drive CLM. There are also examples on the cm/clm directory; the cm/220 directory examples use the MusicKit, not CLM.

Phrasing

Sometimes an instrument needs to know the final state of some preceding instrument to decide its initial state (in legato for example, it sometimes helps to match phases and so on). This state can be impossible to calculate outside the actual sound computation (it might depend on random numbers, or a phase might depend on FM), so the following functions are provided to package all this up and make it relatively easy to use.

First, an instrument that uses phrase information:

(definstrument pickup (phrase beg dur frq amp)
  ;; this instrument shows how to get the final state of
  ;; on-chip variables and pass them on to the next call.
  ;; The idea here is that by matching phases and so on
  ;; we might get a cleaner legato in some cases.
  (wait-for-phrase phrase)
  (let* ((s (or (phrase-value phrase 's)
                (make-oscil :frequency frq)))
         (start (floor (* beg sampling-rate)))
         (end (+ start (floor (* dur sampling-rate)))))
    (run
      (loop for i from start below end do
        (outa i (* amp (oscil s)))))
    (End-Run (phrase phrase 's))))

and an example of running it:

    (setf phrase (make-phrase))
    (with-sound () (pickup phrase 0 1 440 .1)
                   (pickup phrase 1 1 440 .1))

The Loop in the run body uses below not to because we want to start the next instrument on the very next sample -- if we used "loop from start to end" in both, we would have written the end sample of the first instrument twice. The variable Phrase above is the identifier for a given phrase. It is created with:

    make-phrase  &optional  anything

You can include anything you like in the phrase information being passed around. Just pass it as the optional argument to make-phrase. Once created, an instrument can wait for that phrase to be ready using
    wait-for-phrase  &rest  phrases

Normally you'd only wait for one phrase to complete, but it is possible to wait on any number of phrases. You then get any saved variable's value with

    phrase-value  phrase  var

Phrase-value returns nil if the given variable has not yet been set (i.e. normally at the start of the phrase). Phrase-value with a nil var argument is a reference to the "anything" that you want passed around with the phrase.

To save state in the phrase structure, call phrase as the optional argument to end-run. The arguments to phrase are the phrase structure and the names of the variables you want saved.

    phrase  phrase  &rest vars

Outside the dsp world (i.e. outside run and end-run) there's no complication because an instrument is just a lisp function and it can return anything it likes. Inside the dsp world, however, many dsp's can be running in parallel, simultaneous phrases may be interleaved, and we have no way in advance of knowing when or where the next portion of a given phrase will be needed, or which dsp will be free to handle it. In addition, the definstrument function returns immediately after setting up the dsp -- it returns long before any sound has been computed, so a subsequent definstrument call has to know how to wait its turn.

Here's another similar example -- in this case we abut each instrument in the phrase, so none of the Pickup instruments gets a begin time. We use the nil option of phrase-value to store the end of the current instrument -- since the variable End does not explicitly appear within the run body (the Loop is a special case), we can't use (phrase phrase 'S 'END). If we had stared at (describe-dsp-state) long enough we might surmise that instead we could save *run-end*, but in general, user variables that don't make it into run need to be saved elsewhere (for example, the nil phrase-value slot might be an association list). Just for fun we'll play the same sort of game with the AMP variable.

(definstrument pickup (phrase dur frq)
  (wait-for-phrase phrase)
  (let* ((s (or (phrase-value phrase 's)
                (make-oscil :frequency frq)))
         (amp (* .9 (or (phrase-value phrase 'amp) .5)))
         (start (or (phrase-value phrase nil) 0))
         (end (+ start (floor (* dur sampling-rate)))))
    (setf (phrase-value phrase nil) end)
    (run
      (loop for i from start below end do
        (outa i (* amp (oscil s)))))
    (end-run (phrase phrase 's 'amp))))

(setf phrase (make-phrase))
(with-sound () (pickup phrase 1 440)
               (pickup phrase 1 440))

Run*

For C instruments (i.e. defcinstrument), there is also the macro run*. It takes two arguments, a list of variables, and the usual run macro body. The run body is executed (in C of course) and then the variables are set to the values they had when the run loop exited. This mimics the phrase-value mechanism without all the overhead of invoking the phrase mechanism. Here's an example:

(defcinstrument p (beg dur frq amp)
  (let* ((s (make-oscil :frequency frq))
	 (start (floor (* beg sampling-rate)))
	 (end (+ start (floor (* dur sampling-rate))))
	 (hi 0.0))
    (run* (amp hi)
      (loop for i from start below end do
	(incf hi .001)
	(outa i (* amp (oscil s)))))
    (print (format nil "~A ~A" hi amp))))

A more useful example is Michael Edwards rms.ins.

Structs

Several of the instruments in CLM make explicit reference to the structs defined in mus.lisp -- the run macro knows these in case you need to goof around with them too. Instead of trying to remember the struct and field names below (which were originally considered internal, and therefore were not prettified), I suggest use of the "generic" functions: frequency, phase, size-of, data, channel, read-position, and read-direction. These are setf-able, where that makes sense, and apply to any struct that happens to support the notion in question. So, for example (setf (frequency osc1) 440.0) sets osc1's osc-freq field to (in Hz 440.0). And (read-position srcg) returns the current position in the input file of srcg. Since the type decision is (currently) done at compile time by the run macro, there is no speed penalty in using these functions rather than the explicit ones. But if you must do it yourself, the structs and fields are:

OSC        (oscil):
    osc-freq                ; in radians per sample
    osc-phase               ; in radians

TBL        (table-lookup):
   tbl-phase                ; table lookup position
   tbl-freq                 ; table locations per sample (freq=(tbl-size*Hz-freq)/srate)
   tbl-table                ; the location of the table
   tbl-table-size
   tbl-internal-mag         ; table size scaler to make period two pi

DLY        (delay lines):
    dly-size                ; length of delay line
    dly-pline               ; location of delay line -- many possibilites here
    dly-loc                 ; current pointer in delay line

ZDLY        (zdelay):
    zdly-del                ; location of zdelay delay generator
    zdly-phase              ; current location in delay line

CMBFLT        (comb and notch filters):
    cmbflt-scaler
    cmbflt-dly-unit         ; location of delay line associated with comb filter

ALLPASSFLT        (all-pass filters):
    allpassflt-feedback     ; scaler on feedback term
    allpassflt-feedforward  ; scaler on feedforward term
    allpassflt-dly-unit     ; location of associated delay unit

FLT        (direct-form and associated filters):
    flt-m                   ; the filter order (0 based)
    flt-typ                 ; 0=direct, 1=lattice, 2=ladder
    flt-so
    flt-a                   ; a table of coefficients -- the "y" coeffs
    flt-b                   ; ditto -- the "x" coeffs, also known as "p"
    flt-c                   ; ditto -- used only by ladder filter
    flt-d                   ; state of filter

FRMNT        (formnt):
    frmnt-g                 ; gain
    frmnt-tz                ; location of two-zero filter
    frmnt-tp                ; location of two-pole filter

COSP        (sum-of-cosines):
    cosp-phase              ; radians
    cosp-freq               ; radians per sample

SMPFLT        (various simple filters -- one-pole et al):
    smpflt-a0 -- these coefficients are divided by 2 on chip
    smpflt-a1, smpflt-a2, smpflt-b1, smpflt-b2, smpflt-x1, smpflt-y1, smpflt-x2, smpflt-y2

The following code fragment from addflt.ins shows one use of these
fields to put envelopes on the filter coefficients:

       (dotimes (k numFilts)
         (incf outsig (ppolar (aref pps k) (* (aref scs k) insig)))
         (setf (smpflt-b1 (aref pps k)) (env (aref ffs k)))
         (setf (smpflt-b2 (aref pps k)) (env (aref rfs k))))

NOI        (randh and randi):
    noi-phase               ; radians
    noi-freq                ; radians per sample

SW        (pulse-train, square-wave, triangle-wave, sawtooth-wave):
    sw-phase                ; radians
    sw-freq                 ; radians per sample

WS        (waveshape):
    ws-tab                  ; location of polynomial table
    ws-os                   ; location of lookup oscillator
    ws-offset               ; midpoint of table

LOCS        (locsig):
    locs-ascl               ; outa scaler -- fractional (i.e. [-1.0,1.0))
    locs-bscl               ; outb scaler
    locs-cscl               ; outc scaler
    locs-dscl               ; outd scaler
    locs-rscl               ; reverb signal scaler
    locs-revname            ; whether reverb is on or not

(setf (locs-ascl loc) (env panning))  puts an envelope on the outa scaler.

SMP        (resample):
    smp-sr                  ; current srate change
    smp-lst                 ; last input value
    smp-nxt                 ; next input value
    smp-i                   ; current interpolation location
    smp-chn                 ; channel

SR        (src):
    sr-incr
    sr-rd                   ; location of readin structure
    sr-x                    ; current src location
    sr-data                 ; input sequence for low pass filtering
    sr-filt                 ; filter coefficients
    sr-left
    sr-right
    sr-width                ; sinc table size

RDIN        (readin and readin-reverse):
    rdin-i                  ;current sample index
    rdin-inc                ;-1 (readin-reverse) or 1 (readin)
    rdin-chn                ; channel

RBLK        (run-block):
    rblk-siz
    rblk-buf                ; location of block data table
    rblk-loc                ; 0=>trigger run-block actions
    rblk-ctr                ; count down from freq samps to 0

WT        (wave-train):
    wt-wave                 ; location of table containing current wave
    wt-wsiz
    wt-freq                 ; cycles per second
    wt-b                    ; location of run-block rblk structure
    wt-phase                ; table lookup position
    wt-internal-mag

FFTFLT        (fft-filter):
    fftflt-env              ; either the filter array or envelope
    fftflt-siz              ; true (power of 2) size of fft
    fftflt-hop              ; hop size of sliding fft's
    fftflt-rd               ; readin structure
    fftflt-b                ; run-block structure
    fftflt-datar            ; real data
    fftflt-datai            ; imaginary data
    fftflt-half-siz

CONV        (convolve):
    conv-siz                ; true (power of 2) size of fft
    conv-hop                ; hop size of sliding fft's
    conv-rd                 ; readin structure
    conv-b                  ; run-block structure
    conv-datar              ; real data
    conv-datai              ; imaginary data
    conv-half-siz, conv-pow, conv-filti, conv-filtr

FFT-DATA        (fft and inverse-fft):
    fft-data-real           ; the real part of the fft data (or the sound input data)
    fft-data-imaginary      ; the imaginary part of the fft data
    fft-data-size           ; data size

SPD        (expand):
    spd-rd, spd-len, spd-rmp, spd-amp, spd-in-spd, spd-out-spd, spd-cur-in, spd-cur-out,
    spd-s20, spd-s50, spd-ctr ; see expsrc.ins for info

ASYMFM        (asymmetric-fm):
    asymfm-phase, asymfm-freq, asymfm-r, asymfm-ratio, asymfm-sinr, asymfm-cosr

SSS        (sine-summation):
    sss-phase, sss-freq, sss-a, sss-an, sss-a2, sss-b, sss-n

IO        (file handlers):
    IO-fil, IO-nam, IO-dat-a/b/c/d, IO-beg, IO-end, IO-hdr, IO-hdr-end, IO-data-end
    IO-dir, IO-open-index, IO-siz, IO-data-start

def-clm-struct

In addition, you can use your own structs in run if you first define them for run, as in the following example:

(def-clm-struct hi ho (silver envelope) (away array integer))
;; this replaces the lisp defstruct statement

(definstrument simp ()
  (let ((ha (make-hi :ho .1 :away 2
                     :silver (make-env :envelope '(0 0 100 1)
                                       :scaler .1 :start 0 :end 10))))
    (run
      (loop for i from 0 to 10 do
        (outa i (+ (hi-ho ha) (* .1 (hi-away ha)) (env (hi-silver ha))))))))

Def-clm-struct is a macro whose first argument is the name of the struct and the rest of the arguments are the field names you intend to access in the run block. If the field type is not real, you should include the type as the second element of the (name type) list describing that field -- these are clm internal types like dly or envelope. [DSP: An array of fractional values can be passed as a table]. An array of any clm type can be passed as an array with the type of the element added as the third element of the field description: (envs array envelope):

  (def-clm-struct hi (ho array envelope) silver away)
  (definstrument simp ()
    (let ((ha (make-hi :ho (make-array 1
                             :element-type 'envelope 
                             :initial-element (make-env :envelope '(0 0 100 1) 
                                                        :scaler .1 :start 0 :end 10)))))
      (run
        (loop for i from 0 to 10 do
          (outa i (env (aref (hi-ho ha) 0)))))))

Bill Schottstaedt (bil@ccrma.stanford.edu)

Appendices

How to use CLM outside with-sound or clm-load

With-sound and clm-load are macros that expand into code similar to the following:

  (set-srate 44100)
  (open-output "/zap/test.snd" (make-header :channels 2 :sampling-rate 44100))
  (setf *reverb* (open-output "/zap/reverb.snd" 
                   (make-header :channels 1 :sampling-rate 44100)))

< insert instrument calls here >

  (close-output *reverb*)
  (open-input "/zap/reverb.snd")
  (jc-reverb 0 350 :double t)
  (close-input)
  (close-output)

When a fatal error occurs (even within with-sound or clm-load), you can use this code in the interpreter to finish the current computation. Similarly, an instrument can be called outside with-sound and friends, or notice that it is being called in that way:

  (definstrument auto (dur)
    (let* ((outf (and (not *current-output-file*)
                      (open-output "/zap/test.snd" 
                        (make-header :channels 1 :sampling-rate 22050))))
           (os (make-oscil :frequency 440))
           (end (floor (* dur 22050))))
    (run
     (loop for i from 0 to end do (outa i (* .1 (oscil os)))))
    (when outf
      (close-output)
      (clm-cleanup)
      (dac "/zap/test.snd"))))

Now (auto) is the same as (with-sound () (auto))

A C instrument need not perform sound output, so it can be called at any time. See rms.ins for an example.

Fast sound file mixing

The fastest way to mix sound files is with fasmix -- it is about 5 times faster than the equivalent clm instrument add-sound. Fasmix tries valiantly to be all things to all people, but it doesn't provide absolutely every possible mixing option.
  fasmix infile 
    &key (start-time 0.0) start                ; start-time in output file
         input-file-start-time input-file-start
         duration  
         srate coeffs
         amplitude
         amp-env
         ampAA ampAB ampAC ampAD ampBA ampBB ampBC ampBD 
         ampCA ampCB ampCC ampCD ampDA ampDB ampDC ampDD
         envAA envAB envAC envAD envBA envBB envBC envBD 
         envCA envCB envCC envCD envDA envDB envDC envDD)

The envelope arguments can be either lists of breakpoints, in which case you'll get line-segment envelopes, or actual CLM envelopes. All possible mixing operations involving 1,2, or 4 channels files in or out are supported. Most of the options should be pretty obvious -- to extract channel A and scale it by .25, set ampAA to .25. The srate argument defaults to 1 -- it can be negative to read backwards. The actual sampling rate conversion is done by linear interpolation. The input can also be processed by an arbitrary order FIR filter. The filter coefficients should be passed via coeffs. Here are a bunch of examples:

  ;; say we have these two sounds:

  (with-sound (:output "/zap/1.snd") 
    (loop for i from 0 to 9 do (fm-violin i 1 (* (1+ i) 100) .1)))

  (with-sound (:output "/zap/2.snd" :channels 2) 
    (loop for i from 0 to 9 do (fm-violin i 1 (* (- 10 i) 120) .1 :degree 60)))


(with-sound ()
  (fasmix "1")   ;add "1.snd" to current output
  (fasmix "1" :duration 1.0 :amplitude .5)
    ;;scale first 1 sec of "1" by .5 and mix into current output
  (fasmix "1" :amplitude .5 :amp-env '(0 0 100 1));scale and envelope sound
  (fasmix "1" :duration 1.0 :start-time 1.5 :input-file-start-time 3.0))
    ;;take section in "1.snd" from 3.0 to 4.0 and mix into output starting at 1.5

  ;; the basic fasmix calls are the same if mixing stereo to stereo:

(with-sound (:channels 2) (fasmix "2" :amp-env '(0 0 100 1)))


  ;; now mono-to-stereo:

(with-sound (:channels 2) 
  (fasmix "1")    ;add "1" into channel 1 of output
  (fasmix "1" :ampAA .5 :ampAB .5) ;add .5 of "1" into each channel
  (fasmix "1" :ampAB 1.0 :duration 2.0 :input-file-start-time 2.0 
              :amp-env '(0 0 1 1 2 1 3 0)))
    ;;put envelope portion (2.0:4.0) only into channel 2 (B)


  ;; and stereo-to-mono:

(with-sound () 
    (fasmix "2")  ;channel 1 of "2" added into output (i.e. extract channel 1)
    (fasmix "2" :ampBA .75)  ;just channel 2 (scaled) of "2" into output
    (fasmix "2" :ampAA .5 :ampB .75) ;mix chanA*.5+chanB*.75 into output
    (fasmix "2" :ampAA 1.0 :duration 3.0 :input-file-start-time 2.0 
                :amp-env '(0 0 1 1 2 1 3 0)))
   ;chanA from 2.0 to 5.0, enveloped

  ;; and stereo to stereo where we want to mix the separate channels as well as scale them

(with-sound (:channels 2)
    (fasmix "2" :ampBB 1.0 :ampAA 1.0 :ampBA .1 :ampAB .2))
   ;outA<-(inA*1.0 + inB*.2), outB<-(inA*.1 + inB*1.0)

  ;; fasmix can perform header and data type translations:

  (with-sound (:srate 8012) 
    (fasmix "/me/cl/mulaw.snd"))  ;from mulaw to 16-bit linear
  (with-sound (:data-format snd-32-float) 
    (fasmix "/me/cl/aiff1.snd"))   ;from 8-bit to 32-bit floats (AIFF->NeXT)
  (with-sound (:type AIFF-sound-file) 
    (fasmix "/me/cl/esps.snd"))    ;from ESPS to AIFF (16-bit linear)

  ;; and sampling rate conversion with an FIR filter:

(with-sound () 
    (fasmix "2" :srate 2 :coeffs (make-FIR-coeffs 12 '(0 0 .1 1.0 .2 0.0 1.0 0.0))))

  ;; the envelope of make-FIR-coeffs is the desired frequency response approximated
  ;; with line segments.  If you know what you want, you can pass it directly:

(with-sound () (fasmix "2" :srate -1.2 :coeffs '(.25 .5 .25)))

Header and Data Types Supported by CLM

CLM normally writes NeXT/Sun or AIFF headers. It can also write RIFF and old-style IRCAM headers. Where applicable, any data type can be read or written (i.e. on the NeXT all types are supported, whereas in IRCAM files only snd-16-linear and snd-32-float are supported). The following headers/types are supported:

 *      NeXT/Sun/DEC 8-bit mulaw, 8-bit linear, 24-bit linear, 
 *                   32-bit linear, 32-bit float, 
 *                   8-bit alaw, 16-bit emphasized, 64-bit double
 *      AIFF 8-bit linear
 *      8SVX 8-bit linear
 *      IRCAM 16-bit linear (BICSF), EBICSF
 *      NIST-SPHERE 16-bit linear
 *      INRS, ESPS, AVR 16-bit linear, AVR 8-bit linear and unsigned
 *      RIFF (wav) 8-bit alaw, 8-bit mulaw, 8-bit unsigned, 32-bit linear
 *      VOC 8-bit signed
 *      no header (CLM will prompt for info it needs)
 *      Sound Tools 8-bit unsigned
 *      Turtle Beach SMP 16-bit linear
 *      Sound Designer II (the data file, not the resource fork)

I am willing to add almost anything to this list, except compressed data that requires block access. See the file headers.c for all the gory details. In with-sound, you can set the output header type with the keyword :type, and the data type with the :data-format keyword.

Debugging Aids

The main debugging aid is describe-ins-state which tries to show the names and values of all the variables local to the currently running instrument. For a C instrument, you need to set the clm variable c-debug to t before calling the instrument to get this debugging information. If you'd like to see what the run macro is actually doing, set the clm variable c56-debug to t. Both C and DSP instruments can call error or break from within the run loop.

Sources of Unwanted Noise

The major source of unwanted noise in computer music is amplitude quantization. This means soft notes are buzzy. If these are later used as input to a reverberator, for example, the buzziness can easily be magnified. If the soft notes are split between channels (via locsig for example), you may end up increasing the overall noisiness. My experience has been that anything under .003 in amplitude is asking for trouble unless it is covered up in some way. Since reverb amounts are often less than .01, even a loud note can produce noisy input to the reverberator. The simplest way around this in CLM is to make one run and get the reverb stream max amp (reported if :statistics is t in with-sound). Then scale all the reverb signals up by the inverse of this (leaving some room for slop, of course). For example, if the reverb max amp is .01, you're throwing away about 7 bits of amplitude resolution in the reverb input. So, in this case, multiply all the locsig revscales by (say) 50, then in the reverberator, divide the reverb output by 50 before sending it out. This only works with reverberators that use C, not the DSP, but C is much faster than the DSP in this case anyway. See jcrev.ins for an example (the volume argument). Similarly, if your notes are soft, send them directly out channel A or B -- the spatial effects are going to add less to your piece than the noise will detract in this case. And if you're making repeated iterative passes over sound files, try to keep them scaled close to 1.0 in maxamp through all the passes. The exact maxamp is not important -- the difference between 1.0 and .5 is one bit of resolution.

Next in importance, in my unhappy experience with headphones, speakers, and amplifiers has been that slow amplitude envelopes can cause annoying artifacts in less than perfect audio equipment. You may hear glissandos or chirps if a soft note is slowly ramping up or down. The function reduce-amplitude-quantization-noise in env.lisp makes the envelope jump through the initial or final section where there are too few bits, hopefully reducing the chirps (these are apparently caused by non-linearities in the audio equipment -- I don't hear them in very good equipment). A similar trick is to use exponential envelopes that are bowed out (a base greater than 1.0 in CLM) -- this is the default in the MusicKit for example. There may be some combination of problems here -- see Robert Maher, "On the Nature of Granulation Noise in Uniform Quantization Systems", JAES vol 40 no 1/2, 1992, p12 -- he says "The common assumption that the quantization noise is additive, white, and uncorrelated with the input signal is simply incorrect for the case of sinusoidal input". Since the ramping portions are often sinusoidal, or nearly so, these chirps might be analyzable as FM caused by quantization.

Another source of "noise" is foldover. This is mostly a problem with FM when the sampling rate is low -- you'll hear inharmonic artifacts caused by components that are beyond half the sampling rate. Then there are the obvious problems like discontinuities in the wave becoming clicks at the speaker, and clipping -- these are easy to fix. Finally, avoid reverberating notes with sharp attacks.

The major sources of noise in audio equipment are bad connections and "ground loops". In the latter case, various pieces of equipment are at slightly different ground voltages due to tiny resistances in ground wires -- this sets up a sort of screen upon which all kinds of interference can be projected, so to speak. These loops are hard to avoid. In my house, for example, the grounds are real -- that is, the third plug on the power cords is actually connected to a solid ground, but I still had to connect all the drives, audio equipment, and NeXT itself to the same outlet (representing altogether about 1000 watts of worst case power consumption). The problem was that the ground wire meanders through the conduits alongside the power wires, so it becomes a serial connection of grounds along a given path -- it's a low gauge wire, but even so, there is a slight resistance along that path, causing equipment connected to different outlets to be at a slightly different ground voltage. Even after putting everything on the same outlet, the external drive was the source of a slight hum -- after unplugging it, I could turn all the volume knobs up all the way and hear only a small hiss which I attribute to the unavoidable randomness in any analog equipment (Johnson noise, for example).

On the NeXT, the DACs are mounted near the monitor, so you can hear interference from that all the time. Similarly speaker cables can sometimes act as antennas -- I think the theory here is that radio frequency interference (which can be picked up by a short wire like a speaker connection) can occur in pulses that become clicks or hiss in the speakers, and hum can apparently be caused by stray inductance from transformers. The simplest answer is to buy properly shielded computer equipment, route the wires away from that equipment, and use the shortest wires possible. If that's not an option, I'm told that it helps to use shielded cables. If there's still interference, you might be able to go to "balanced" connections, but this normally means investing in pro-audio equipment.

Data files

CLM instrument makers often want to read or write files of floating point data, and are willing to live with the fact that such files might not be readable on other machines (similarly for integers). It is an annoyance to have to package such data up as a fake sound file, but Lisp itself does not provide any way to read or write floats. So, several relatively low level functions exist in CLM, but they are only for use by good sports! The IO routines are:

  c-open-input-file name => integer file descriptor (fd below)
  c-open-output-file name => fd
  c-close fd
        
  clm-seek-bytes fd n => go to byte n in file fd
  clm-seek-floats fd n => got to float n in file fd (assuming 4 byte floats)
        
  clm-read-floats fd n arr => read next n floats in fd into (lisp) array arr
  clm-read-ints fd n arr => same for integers (longs)
  clm-write-floats fd n arr => write n floats from arr[0] into fd

From Lisp's point of view, a float is a 'short-float (i.e. set the array element type explicitly in make-array), and an integer is a 'fixnum. sms.lisp and lpc.lisp contain examples of these functions.

Index

all-pass                   divseg                      ppolar 
amplitude-modulate         dlocsig                     pulse-train 
array-interp               dot-product                 quad 
asymmetric-fm              edit-header                 randh,randi  
c56-debug                  end-run                     read-backward 
c-close                    env                         read-direction 
c-debug                    expand                      read-forward 
channel                    fasmix                      read-header 
clear-block                fft                         readin 
*clm-version*              fft-filter                  readin-reverse 
*clm-array-print-length*   fft-window                  read-position 
clm-get-channels           filter                      reopen-output  
clm-get-default-header     *fix-header-errors*         resample 
clm-get-duration           formnt                      restart-env 
clm-get-max-amp            frequency                   *reverb* 
clm-get-samples            frequency-mag               revin,revout
clm-get-sampling-rate      get-beg-end                 ring-modulate 
*clm-init*                 get-chebychev-coefficients  roomsig 
*clm-instruments*          *ignore-header-errors*      run-block 
clm-load                   in-a,in-b,in-c,in-d,ina,inb safety optimization
*clm-news*                 in-hz                       sampling-rate 
clm-open-input             jrdac                       sawtooth-wave 
clm-print                  instruments                 set-srate 
clm-read-ints              instrument-let              signify 
clm-read-floats            inverse-fft                 sine-summation 
clm-reset                  ladder-filter               size-of 
*clm-safety*               lattice-filter              *sound-player* 
clm-seek-bytes             lisp functions              file formats
clm-seek-floats            load-synthesis-table        sound-let 
*clm-verbose*              load-synthesis-table-with-phases  square-wave 
clm-write-floats           locsig                      src 
close-input                make-filter-coefficients    stereo 
close-output               make-fft-data-arrays        stop-dac 
comb                       make-header                 structs 
contrast-enhancement       make-phase-quad-table       sum-of-cosines 
convolve                   make-phrase                 table-interp 
c-open-input-file          make-waveshape-table        table-lookup 
c-open-output-file         mix                         triangle-wave 
*current-input-file*       mix-in                      two-pole 
*current-output-file*      mix-sound                   two-zero 
dac,dac-n                  mono                        update-header 
dac-filter                 notch                       volume 
data                       one-pole                    wait-for-dac 
debugging                  one-zero                    wait-for-phrase 
default-sound-file-name    open-input                  waveshape 
default-sound-file-type    *open-input-pathname*       wave-train 
definstrument              *open-input-truename*       with-mix 
defcinstrument             *open-input-verbose*        with-sound 
defpinstrument             open-output                 write-header 
delay                      oscil                       zall-pass 
def-clm-struct             outa,outb,outc,outd         zcomb 
describe-ins-state         phase                       zdelay 
design-FIR-from-env        phrase                      znotch 
direct-filter              phrase-value                zpolar 
divenv                     polynomial                  ztap 
FIR-filter                 run*