CLM

CLM (originally an acronym for Common Lisp Music) is a sound synthesis package in the Music V family. It provides much the same functionality as Stk, Csound, SuperCollider, PD, CMix, cmusic, and Arctic — a collection of functions that create and manipulate sounds, aimed primarily at composers (in CLM's case anyway). The instrument builder plugs together these functions (called generators here), along with general programming glue to make computer instruments. These are then called in a note list or through some user interface (provided by Snd, for example).

CLM exists in several forms: the original Common Lisp implementation (clm-4.tar.gz), a C version (sndlib.tar.gz), a Scheme version (sndlib.tar.gz with s7), Ruby (sndlib again but using Ruby), and Forth (sndlib). The Scheme, Ruby, and Forth versions are also built into the Snd editor (snd-13.tar.gz). This document is aimed at the Common Lisp version in clm-4.tar.gz. See sndclm.html in the Snd tarball for the Scheme/Ruby/Forth/C version (sndclm.html has much more information than this file). There are a variety of unavoidable differences between these versions, but in general, the differences are obvious and consistent: Lisp "-" becomes C "_", "?" becomes "_p", "->" becomes "_to_", and so on, so the function named mus_oscil in C, becomes oscil elsewhere, mus_oscil_p becomes oscil?, and mus_hz_to_radians becomes hz->radians in Lisp/Scheme. If you'd like to compare a standard instrument in the various implementations, check out the fm-violin: v.ins (Common Lisp), v.scm (Scheme), v.rb (Ruby), clm-ins.fs (Forth), and sndlib.html (C).

CLM has several sections: "generators", instruments (definstrument and *.ins), examples of note lists (with-sound, *.clm), a "make" facility for sound files (with-mix), and various functions that are useful in sound file work. CLM is available free, via anonymous ftp (pub/Lisp/clm-4.tar.gz at ccrma-ftp.stanford.edu).

Bill Schottstaedt (bil@ccrma.stanford.edu)
Contents
Introduction

CLM provides functions to experiment with sounds. The easiest way to make a new sound is 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. Compile v.ins and load v, then call with-sound:

(compile-file "v.ins")
(load "v")
(with-sound () (fm-violin 0 1 440 .1)) 

and the note should emerge from the speakers. (In CMU-CL, load v.cmucl, not v.x86f). The compile and load sequence can be abbreviated in most lisps. Once loaded, we don't need to reload v unless we change it in some way. To get an arpeggio:

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

clm-example.lisp shows how to create such a note list algorithmically. To listen to the last computed sound again:

(play)

or, if you have some saved sound file:

(play "a-great.snd")

Although you can use CLM simply as a bunch of canned functions, it's a lot more fun to make your own. In CLM, these are called instruments, and a sequence of instrumental calls is a note list. To create your own instrument, you need to write the function that expresses in CLM's terms the sound processing actions you want. In the simplest case, you can just calculate your new value, and add it into the current output:

(definstrument simp (start-time duration frequency amplitude)
  (let* ((beg (floor (* start-time *srate*)))
	 (end (+ beg (floor (* duration *srate*))))
	 (j 0))
    (run
      (loop for i from beg below end do
        (outa i (* amplitude (sin (* j 2.0 pi (/ frequency *srate*)))))
	(incf j)))))

Now to hear our sine wave, place this code in a file, say simp.ins, compile and load it, then:

(with-sound () (simp 0 0.25 440.0 0.2))

This creates a sine-wave at 440.0 Hz, 0.2 amplitude, between times 0 and 0.25 seconds. The line:

(definstrument simp (start-time duration frequency amplitude) 

says that we are defining an instrument (via definstrument) named simp which takes the four parameters start-time, duration, frequency, and amplitude. The next two lines:

(let* ((beg (floor (* start-time *srate*)))
       (end (+ beg (floor (* duration *srate*))))) 

turn the start-time and duration values, passed by the caller in terms of seconds, into samples. The variable *srate* holds the current sampling rate. The "run" macro is an optimizer; it turns its body into a C foreign function call. The next line:

(loop for i from beg below end and j from 0 by 1 do 

uses the Lisp loop construct to loop through the samples between the start time in samples (beg) and the end point (end) calculating simp's output on each sample. We are also using the variable j to increment the current phase in the last line:

(outa i (* amplitude (sin (* j 2.0 pi (/ frequency *srate*)))))))) 

This is the heart of our instrument. The call (outa i ...) adds its third argument (in this case a complicated expression) into channel 0 of the current output stream at sample i. The expression:

(* amplitude (sin (* j 2.0 pi (/ frequency *srate*)))))))) 

is creating a sinusoid (via the "sin" function) at the specified volume ("amplitude" is passed as an argument to simp), and the desired frequency ("frequency" is also an argument to simp). The caller passes simp a frequency in cycles per second (440.0 for example), but we need to turn that into the corresponding phase value for the "sin" function. We do that by translating from cycles per second to radians per sample by multiplying by two pi (this multiply gives us radians per second), then dividing by the sampling rate (samples per second) to give us radians per sample (i.e. radians/second divided by samples/second gives radians/sample); we then multiply by "j" to step forward on each sample. Finally, the line:

(with-sound () (simp 0 0.25 440.0 0.2))

opens an output sound file, calls simp, closes the file, and plays the result. We need to put the instrument definition in a separate file and compile and load it; we can't just paste it into the listener (this limitation applies only to the Common Lisp CLM).

We can simplify simp by using oscil for the sinusoid and hz->radians. make-oscil creates an oscil generator; similarly make-env creates an envelope generator:

(definstrument simp (start-time duration frequency amplitude &optional (amp-env '(0 0 .5 1.0 1.0 0)))
  (multiple-value-bind (beg end) (times->samples start-time duration)
    (let ((osc (make-oscil :frequency frequency))
	  (amp-env (make-env amp-env :scaler amplitude :duration duration)))
      (run 
       (loop for i from beg below end do
	 (outa i (* (env amp-env) (oscil osc))))))))

Our envelope is a list of (x y) break-point pairs. The x-axis bounds are arbitrary, but it is conventional (here at ccrma) to go from 0 to 1.0. The y-axis values are normally between -1.0 and 1.0, to make it easier to figure out how to apply the envelope in various different situations. In this case, our envelope is a ramp up to the middle of the note: "(0.0 0.0 0.5 1.0)", then a ramp down to 0. The env generator produces the envelope on a sample-by-sample basis.

If you make a change to an instrument, just recompile and reload it to use the changed version; there's no need to restart lisp, or unload the old version (in most lisps there's actually no way to unload it).

Instruments

The normal structure of an instrument is:

(definstrument name (args) (setup code (run run-time code)))

The setup code creates any needed generators for the run-time code which actually generates the samples. The run-time code can contain any of the lisp functions (generators etc) described in the next several sections. Since life is short, not every feature of lisp is supported by the run macro; I've concentrated on those that have been useful in the past, so let me know if you need something new!

Lisp functions that can occur within the body of the run macro:

+  /  *  -  1+  1-  incf decf setf setq
 =  /=  <  >  <=  >=  zerop plusp  
minusp oddp evenp max min abs mod rem identity
floor ceiling round truncate signum sqrt random float
ash log expt exp sin cos tan asin acos atan cosh sinh tanh asinh acosh atanh
erf erfc lgamma bes-j0 bes-j1 bes-jn bes-y0 bes-y1 bes-yn bes-i0
or and not null if unless when cond progn prog1 prog2 case tagbody go 
error warn print princ terpri probe-file
block return return-from let let* loop do do* dotimes declare
lambda apply loop-finish
aref elt svref array-total-size array-in-bounds-p array-rank array-dimension
integerp numberp floatp realp eq eql arrayp

The function clm-print stands in for Lisp's format — I don't support all of format's options, but enough to be useful, I hope. clm-print's syntax is (clm-print format-string &rest args). It is also possible to write to a file:

(definstrument fileit ()
  (let ((file (c-open-output-file "test.clm-data")))
    (run
     (loop for i from 0 to 10 do
       (clm-print file "hiho ~D " i)))
    (c-close file)))

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).

Declare can be used to set the variable types and debugging options. Since the run macro can't always tell what type a variable is, it will generate run-time code to figure out the type. The generated code will be faster and tighter (and a lot easier to read) if you use declare to tell run what the types are. In Common Lisp, the recognized types are :integer, :float, :string, :boolean, :bignum (sample number), :double*, :int*, :mus-any, and :mus-any* (the keyword package is used to avoid endless CL package name troubles).

Generators
all-passall-pass filter
asymmetric-fmasymmetric fm
combcomb filter
convolveconvolution
delaydelay line
envline segment envelope
filterdirect form FIR/IIR filter
filtered-combcomb filter with filter on feedback
fir-filterFIR filter
formantresonance
granulategranular synthesis
iir-filterIIR filter
in-anysound file input
locsigstatic sound placement
move-soundsound motion
moving-averagemoving window average
ncossum of equal amplitude cosines
notchnotch filter
nsinsum of equal amplitude sines
nrxycossum of n scaled cosines
nrxysinsum of n scaled sines
one-poleone pole filter
one-zeroone zero filter
oscilsine wave and FM
out-anysound output
polywave and polyshapewaveshaping
phase-vocodervocoder analysis and resynthesis
pulse-trainpulse train
rand,rand-interprandom numbers, noise
readinsound input
sawtooth-wavesawtooth
square-wavesquare wave
srcsampling rate conversion
ssb-amsingle sideband amplitude modulation
table-lookupinterpolated table lookup
tapdelay line tap
triangle-wavetriangle wave
two-poletwo pole filter
two-zerotwo zero filter
wave-trainwave train

A generator is a function that returns the next sample in an infinite stream of samples each time it is called. An oscillator, for example, returns an endless sine wave, one sample at a time. Each generator consists of a set of functions: Make-<gen> sets up the data structure associated with the generator at initialization time; <gen> produces a new sample; <gen>? checks whether a variable is that kind of generator. Internal fields are accessible via various generic functions such as mus-frequency.

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

prepares oscillator to produce a sine wave when set in motion via

(oscil oscillator)

(oscil? oscillator) returns t, and (mus-frequency oscillator) returns 330. The initialization function (make-oscil above) normally takes a number of optional arguments, setting whatever choices need to be made to specify the generator's behavior. The run-time function (oscil above) always takes the generator as its first argument. Its second argument is nearly always something like an FM input; in a few cases, it is a function to provide input data or editing operations. 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))

Frequencies are always in cycles per second (also known as Hz). 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 Hz, so the function hz->radians can be used to convert from units of cycles per second to radians per sample.

Finally, one special aspect of the make-<gen> functions is the way they read their arguments. I use the word optional-key in the function definitions in this document to indicate that the arguments are keywords, but the keywords themselves are optional. Take the make-oscil call, defined as:

make-oscil &optional-key (frequency *clm-default-frequency*) (initial-phase 0.0)

When make-oscil is called, it scans its arguments; if a keyword is seen, that argument and all following arguments are passed unchanged, but if a value is seen, the corresponding keyword is prepended in the argument list:

(make-oscil :frequency 440.0)
(make-oscil :frequency 440.0 :initial-phase 0.0)
(make-oscil 440.0)
(make-oscil)
(make-oscil 440.0 :initial-phase 0.0)
(make-oscil 440.0 0.0)

are all equivalent, but

(make-oscil :frequency 440.0 0.0)
(make-oscil :initial-phase 0.0 440.0)

are in error, because once we see any keyword, all the rest of the arguments have to use keywords too (we can't reliably make any assumptions after that point about argument ordering). If this is confusing, just use the keywords all the time. I implemented this somewhat unusual argument interpretation because in many cases it is silly to insist on the keyword; for example, in make-env, the envelope argument is obvious and can't be confused with any other argument, so it's an annoyance to have to say ":envelope" over and over. Keyword arguments are also useful when there are so many arguments to a function that it becomes impossible to remember what they are and what order they come in.

oscil
make-oscil &optional-key (frequency *clm-default-frequency*) (initial-phase 0.0)
oscil os &optional (fm-input 0.0) (pm-input 0.0)
oscil? os

oscil produces a sine wave (using sin) with optional frequency change (i.e. FM). Its first argument is an oscil created by make-oscil. Oscil's second (optional) argument is the current (sample-wise) frequency change. The optional third argument is the (sample-wise) phase change (in addition to the carrier increment and so on). So the second argument can be viewed as FM, while the third is PM (phase modulation). The initial-phase argument to make-oscil is in radians. You can use degrees->radians to convert from degrees to radians. To get a cosine (as opposed to sin), set the initial-phase to (/ pi 2): (make-oscil 440.0 (/ pi 2)) .

oscil methods
mus-frequency frequency in Hz
mus-phase phase in radians
mus-length 1 (no setf)
mus-increment frequency in radians per sample

Oscil might be defined:

  (prog1
    (sin (+ phase pm-input))
    (incf phase (+ (hz->radians frequency) fm-input)))

oscil takes both FM and PM arguments; here is an example of FM:

(definstrument simple-fm (beg dur freq amp mc-ratio index &optional amp-env index-env)
  (let* ((start (floor (* beg *srate*)))
	 (end (+ start (floor (* dur *srate*))))
	 (cr (make-oscil freq))                     ; our carrier
         (md (make-oscil (* freq mc-ratio)))        ; our modulator
         (fm-index (hz->radians (* index mc-ratio freq)))
         (ampf (make-env (or amp-env '(0 0 .5 1 1 0)) :scaler amp :duration dur))
         (indf (make-env (or index-env '(0 0 .5 1 1 0)) :scaler fm-index :duration dur)))
    (run
      (loop for i from start to end do
        (outa i (* (env ampf) (oscil cr (* (env indf) (oscil md)))))))))

See cl-fm.html for a discussion of FM. The standard additive synthesis instruments use an array of oscillators to create the individual spectral components:

(definstrument simple-osc (beg dur freq amp)
  (let* ((start (floor (* beg *srate*)))
	 (end (+ start (floor (* dur *srate*))))
	 (arr (make-array 20))) ; we'll create a tone with 20 harmonics
    (do ((i 0 (1+ i)))
	((= i 20))
      (setf (aref arr i) (make-oscil (* (1+ i) 100))))
    (run
     (loop for i from start to end do
       (let ((sum 0.0))
	 (do ((i 0 (1+ i)))
	     ((= i (length arr)))
	   (incf sum (oscil (aref arr i))))
	 (outa i (* amp .05 sum)))))))
env
make-env &optional-key 
      envelope      ; list of x,y break-point pairs
      (scaler 1.0)  ; scaler on every y value (before offset is added)
      duration      ; seconds
      (offset 0.0)  ; value added to every y value
      base          ; type of connecting line between break-points
      end           ; end point in samples (similar to dur)
      length        ; duration in samples (can be used instead of end)
env e
env? e
env-interp x env &optional (base 1.0)
envelope-interp x envelope &optional (base 1.0)
env methods
mus-location call counter value (number of calls so far on env)
mus-incrementbase value (no setf)
mus-data original breakpoint list
mus-scaler original scaler
mus-offset original offset
mus-length original duration in samples

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 (that is, we have (x0 y0 x1 y1), so we're going from (0, 0) to (100, 1)). This list is passed to make-env along with the scaler applied to the y axis, the offset added to every y value, and the time in samples or seconds that the x axis represents. make-env returns an env generator which returns the next sample of the envelope each time it is called. The actual envelope value, leaving aside the base is offset + scaler * envelope-value.

The kind of interpolation used to get y-values between the break points (the connecting curve) is determined by the envelope's base. The default (base = 1.0) gives a straight line connecting the points. Say we want a ramp moving from .3 to .5 over 1 second. The corresponding make-env call would be

(make-env '(0 0 100 1) :scaler .2 :offset .3 :duration 1.0)
or
(make-env '(0 .3 1 .5) :duration 1.0)

base = 0.0 gives a step function (the envelope changes its value suddenly to the new one without any interpolation). Any other positive 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). If you'd rather think in terms of e^-kt, set the base to (exp k). To get arbitrary connecting curves between the break points, treat the output of env as the input to the connecting function. Here's an instrument that maps the line segments into sin x^3:

(definstrument mapenv (beg dur frq amp en)
  (let* ((start (floor (* beg *srate*)))
	 (end (+ start (floor (* dur *srate*))))
	 (osc (make-oscil frq))
         (half-pi (* pi 0.5))
	 (zv (make-env en 1.0 dur)))
    (run
     (loop for i from start below end 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 osc))))))))

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

Or create your own generator that traces out the curve you want. J.C.Risset's bell curve could be:

(defmacro bell-curve (x)
  ;; x from 0.0 to 1.0 creates bell curve between .64e-4 and nearly 1.0
  ;; if x goes on from there, you get more bell curves; x can be
  ;; an envelope (a ramp from 0 to 1 if you want just a bell curve)
  `(+ .64e-4 (* .1565 (- (exp (- 1.0 (cos (* two-pi ,x)))) 1.0))))

mus-reset of an envelope causes it to start all over again from the beginning. To jump to any position in an envelope, use mus-location.

This instrument repeats the same envelope over and over:

(definstrument strummer (beg dur env-dur)
  (let* ((os (make-oscil))
	 (e (make-env '(0 0 50 1 100 0) :length env-dur :scaler .1)))
    (run
     (loop for i from beg below (+ beg dur) do 
       (if (> (mus-location e) (mus-length e))
           (mus-reset e))
       (outa i (* (env e) (oscil os)))))))

;;; (with-sound () (strummer 0 22050 2000))

env-interp and envelope-interp return the value of the envelope at some point on the x axis; env-interp operates on an 'env' (the output of make-env), whereas envelope-interp operates on an 'envelope' (a list of breakpoints). To get weighted random numbers, use the output of (random 100.0) as the lookup index into an envelope whose x axis goes from 0 to 100. Then the envelope y values are the numbers returned, and the amount of the x-axis taken by a given value is its weight. Say we want 40% .5, and 60% 1.0,

(loop for i from 0 to 10 collect 
  (envelope-interp (random 100.0) (list 0 .5 40 .5 40.01 1.0 100 1.0)))
=> '(1.0 1.0 0.5 1.0 1.0 0.5 0.5 1.0 0.5 1.0 1.0) 

This idea is also available in the rand and rand-interp generators. Other env-related functions are:

envelope-reverse ereverse an envelope
envelope-repeat e num &optional refl xnormrepeat an envelope
envelope-concatenate &rest esconcatenate any number of envelopes
envelope+ esadd together any number of envelopes
envelope* essame but multiply
envelope-simplify e &optional yg xgsimplify an evelope
meld-envelopes e0 e1meld two envelopes together
map-across-envelopes func esmap a function across any number of envelopes
envelope-exp e &optional pow xgcreate exponential segments of envelopes
window-envelope beg end ereturn portion of e between two x values
stretch-envelope e a0 a1 &optional d0 d1attack and decay portions
scale-envelope e scale &optional offsetscale e
normalize-envelope e &optional normnormalize e

See env.lisp for more such functions. To copy an existing envelope while changing one aspect (say duration), it's simplest to use make-env:

(defun change-env-dur (e dur)
  (make-env (mus-data e)            ; the original breakpoints
	    :scaler (mus-scaler e)  ; these are the original values passed to make-env
	    :offset (mus-offset e)
            :base (mus-increment e) ; the base (using "mus-increment" because it was available...)
	    :duration dur))
table-lookup
make-table-lookup &optional-key 
        (frequency *clm-default-frequency*)   ; in Hz
        (initial-phase 0.0) ; in radians 
        wave                ; double-float array
        size                ; table size if wave not specified
        type                ; interpolation type (mus-interp-linear)
table-lookup tl &optional (fm-input 0.0)
table-lookup? tl

table-lookup performs interpolating table lookup. Indices are first made to fit in the current table (FM input can produce negative indices), then 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 double-floats (the function make-double-array can be used to create it). type sets the type of interpolation used: mus-interp-none, mus-interp-linear, mus-interp-lagrange, mus-interp-bezier, or mus-interp-hermite.

table-lookup methods
mus-frequency frequency in Hz
mus-phase phase in radians (wave-size/(2*pi))
mus-data wave array
mus-length wave size (no setf)
mus-interp-typeinterpolation choice (no setf)
mus-increment table increment per sample

Table-lookup might be defined:

(prog1
  (array-interp wave phase)
  (incf phase (+ (hz->radians frequency) 
                 (* fm-input 
                    (/ (length wave) 
                       (* 2 pi))))))

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

partials->wave synth-data table &optional (norm t)
phase-partials->wave 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 phase-partials->wave; in this case the synth-data is a list of (partial amp phase) triples with phases in radians.

(definstrument simple-table (dur)
  (let ((tab (make-table-lookup :wave (partials->wave '(1 .5 2 .5)))))
    (run
     (loop for i from 0 to dur do
       (outa i (* .3 (table-lookup tab)))))))

spectr.clm has a steady state spectra of several standard orchestral instruments, courtesy of James A. Moorer. bird.clm (using bird.ins and bigbird.ins) has about 50 North American bird songs.

polywave
make-polywave &optional-key (frequency *clm-default-frequency*) 
        (partials '(1 1)) (type mus-chebyshev-first-kind)
polywave w &optional (fm 0.0)
polywave? w

make-polyshape &optional-key (frequency *clm-default-frequency*) 
        (initial-phase 0.0) coeffs (partials '(1 1)) (kind mus-chebyshev-first-kind)
polyshape w &optional (index 1.0) (fm 0.0)
polyshape? w

partials->polynomial partials &optional (kind mus-chebyshev-first-kind)

polywave is the new form of polyshape. These two generators drive a sum of scaled Chebyshev polynomials with a sinusoid, creating a sort of cross between additive synthesis and FM; see "Digital Waveshaping Synthesis" by Marc Le Brun in JAES 1979 April, vol 27, no 4, p250. kind or type can be mus-chebyshev-first-kind or mus-chebyshev-second-kind.

polywave methods
mus-frequency frequency in Hz
mus-scaler index (polywave only)
mus-phase phase in radians
mus-data polynomial coeffs
mus-length number of partials
mus-increment frequency in radians per sample

Polywave and polyshape:

(prog1
  (array-interp wave (* (length wave) 
                        (+ 0.5 (* index 0.5 (sin phase)))))
  (incf phase (+ (hz->radians frequency) fm)))

(prog1
  (polynomial wave (sin phase))
  (incf phase (+ (hz->radians frequency) fm)))

In its simplest use, waveshaping is an inexpensive additive synthesis:

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

Bigbird is another example:

(definstrument bigbird (start duration frequency freqskew amplitude freq-env amp-env partials)
  (multiple-value-bind (beg end) (times->samples start duration)
    (let* ((gls-env (make-env freq-env (hz->radians freqskew) duration))
           (polyos (make-polyshape frequency
                     :coeffs (partials->polynomial (normalize-partials partials))))
           (fil (make-one-pole .1 .9))
           (amp-env (make-env amp-env amplitude duration)))
      (run
        (loop for i from beg below end do
          (outa i 
            (one-pole fil   ; for distance effects
              (* (env amp-env) 
                 (polyshape polyos 1.0 (env gls-env))))))))))

(with-sound ()
  (bigbird beg .05 1800 1800 .2
           '(.00 .00 .40 1.00 .60 1.00 1.00 .0)         ; freq env
           '(.00 .00 .25 1.00 .60 .70 .75 1.00 1.00 .0) ; amp env
           '(1 .5 2 1 3 .5 4 .1 5 .01)))                ; partials (bird song spectrum)

See also pqw.ins for phase quadrature waveshaping (single-sideband tricks).

sawtooth-wave and friends
make-triangle-wave &optional-key (frequency *clm-default-frequency*) (amplitude 1.0) (initial-phase pi)
triangle-wave s &optional (fm 0.0)
triangle-wave? s

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

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

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

These generators produce some standard old-timey wave forms that are still occasionally useful (well, triangle-wave is useful; the others are silly). sawtooth-wave ramps from -1 to 1, then goes immediately back to -1. Use a negative frequency to turn the "teeth" the other way. 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. These are not band-limited; if the frequency is too high, you can get foldover, but as far as I know, no-one uses these as audio frequency tone generators — who would want to listen to a square wave? A more reasonable square-wave can be generated via tanh(n * sin(theta)), where "n" (a float) sets how squared-off it is. Even more amusing is this algorithm:

(defun cossq (c theta)    ; as c -> 1.0+, more of a square wave (try 1.00001)
  (let* ((cs (cos theta)) ; (+ theta pi) if matching sin case (or (- ...))
	 (cp1 (+ c 1.0))
	 (cm1 (- c 1.0))
	 (cm1c (expt cm1 cs))
	 (cp1c (expt cp1 cs)))
    (/ (- cp1c cm1c)
       (+ cp1c cm1c))))  ; from "From Squares to Circles..." Lasters and Sharpe, Math Spectrum 38:2

(defun sinsq (c theta) (cossq c (- theta (* 0.5 pi))))
(defun sqsq (c theta) (sinsq c (- (sinsq c theta)))) ; a sharper square wave

(let ((angle 0.0))
  (loop ...
    (let ((val (* 0.5 (+ 1.0 (sqsq 1.001 angle))))) 
      (set! angle (+ angle .02)) 
    ...)))
saw-tooth and friends' methods
mus-frequency frequency in Hz
mus-phase phase in radians
mus-scaler amplitude arg used in make-<gen>
mus-width width of square-wave pulse (0.0 to 1.0)
mus-increment frequency in radians per sample
One popular kind of vibrato is:
  (+ (triangle-wave pervib) 
     (rand-interp ranvib))

Just for completeness, here's an example:

(definstrument simple-saw (beg dur amp)
  (let* ((os (make-sawtooth-wave 440.0))
	 (start (floor (* beg *srate*)))
	 (end (+ start (floor (* dur *srate*)))))
    (run
     (loop for i from start to end do
       (outa i (* amp (sawtooth-wave os)))))))
ncos and nsin
make-ncos &optional-key (frequency *clm-default-frequency*) (n 1)
ncos cs &optional (fm 0.0)
ncos? cs

make-nsin &optional-key (frequency *clm-default-frequency*) (n 1)
nsin cs &optional (fm 0.0)
nsin? cs

ncos produces a band-limited pulse train containing n cosines. I think this was originally viewed as a way to get a speech-oriented pulse train that would then be passed through formant filters (see pulse-voice in examp.scm). There are many similar formulas: see ncos2 and friends in generators.scm. "Trigonometric Delights" by Eli Maor has a derivation of a nsin formula and a neat geometric explanation. For a derivation of the ncos formula, see "Fourier Analysis" by Stein and Shakarchi, or multiply the left side (the cosines) by sin(x/2), use the trig formula 2sin(a)cos(b) = sin(b+a)-sin(b-a), and notice that all the terms in the series cancel except the last.

ncos methods
mus-frequency frequency in Hz
mus-phase phase in radians
mus-scaler (/ 1.0 cosines)
mus-length n or cosines arg used in make-<gen>
mus-increment frequency in radians per sample
ncos is based on:
  cos(x) + cos(2x) + ... cos(nx) = 
    (sin((n + .5)x) / (2 * sin(x / 2))) - 1/2

  known as the Dirichlet kernel
(definstrument simple-soc (beg dur freq amp)
  (let* ((os (make-ncos freq 10))
	 (start (floor (* beg *srate*)))
	 (end (+ start (floor (* dur *srate*)))))
    (run
     (loop for i from start to end do
       (outa i (* amp (ncos os)))))))

If you sweep ncos upwards in frequency, you'll eventually get foldover; the generator produces its preset number of cosines no matter what. It is possible to vary the spectrum smoothly (without stooping a filter): multiply the output of ncos by an exponential — there's an example in sndclm.html.

nsin produces a sum of n equal amplitude sines. It is very similar (good and bad) to ncos.

nsin methods
mus-frequency frequency in Hz
mus-phase phase in radians
mus-scaler dependent on number of sines
mus-length n or sines arg used in make-<gen>
mus-increment frequency in radians per sample
nsin is based on:
  sin(x) + sin(2x) + ... sin(nx) = 
    sin(n * x / 2) * (sin((n + .5)x) / sin(x / 2))

  known as the conjugate Dirichlet kernel
ssb-am
make-ssb-am &optional-key (frequency *clm-default-frequency*) (order 40)
ssb-am gen &optional (insig 0.0) (fm 0.0)
ssb-am? gen

ssb-am provides single sideband suppressed carrier amplitude modulation, normally used for frequency shifting.

ssb-am methods
mus-frequency frequency in Hz
mus-phase phase (of embedded sin osc) in radians
mus-order embedded delay line size
mus-length same as mus-order
mus-interp-type mus-interp-none
mus-xcoeff FIR filter coeff
mus-xcoeffs embedded Hilbert transform FIR filter coeffs
mus-data embedded filter state
mus-increment frequency in radians per sample
ssb-am is based on:
  cos(freq) * delay(insig) +/- sin(freq) * hilbert(insig) 
  which shifts insig spectrum by freq 
  and cancels upper/lower sidebands

See the instrument under amplitude-modulate for an explicit version of this generator. Here's a complicated way to get a sine wave at 550 Hz:

(definstrument shift-pitch (beg dur freq amp shift)
  (let* ((os (make-oscil freq))
	 (start (floor (* beg *srate*)))
	 (end (+ start (floor (* dur *srate*))))
	 (am (make-ssb-am shift)))
    (run
     (loop for i from start to end do
       (outa i (* amp (ssb-am am (oscil os))))))))
wave-train
make-wave-train &optional-key (frequency *clm-default-frequency*) (initial-phase 0.0) wave size type
wave-train w &optional (fm 0.0)
wave-train? w

wave-train produces a wave train (an extension of pulse-train and table-lookup). 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.

wave-train methods
mus-frequency frequency in Hz
mus-phase phase in radians
mus-data wave array (no setf)
mus-length length of wave array (no setf)
mus-interp-typeinterpolation choice (no setf)

Here is a FOF instrument based loosely on fof.c of Perry Cook and the article "Synthesis of the Singing Voice" by Bennett and Rodet in "Current Directions in Computer Music Research".

(definstrument fofins (beg dur frq amp vib f0 a0 f1 a1 f2 a2 &optional ve ae)
  (let* ((start (floor (* beg *srate*)))
         (end (+ start (floor (* dur *srate*))))
         (ampf (make-env (or ae (list 0 0 25 1 75 1 100 0)) :scaler amp :duration dur))
         (frq0 (hz->radians f0))
         (frq1 (hz->radians f1))
         (frq2 (hz->radians f2))
         (foflen (if (= *srate* 22050) 100 200))
         (vibr (make-oscil 6))
	 (vibenv (make-env (or ve (list 0 1 100 1)) :scaler vib :duration dur))
         (win-freq (/ two-pi foflen))
         (foftab (make-double-float-array foflen))
         (wt0 (make-wave-train :wave foftab :frequency frq)))
    (loop for i from 0 below foflen do
      (setf (aref foftab i) (double-float      
        ;; this is not the pulse shape used by B&R
            (* (+ (* a0 (sin (* i frq0))) 
                  (* a1 (sin (* i frq1))) 
                  (* a2 (sin (* i frq2)))) 
               .5 (- 1.0 (cos (* i win-freq)))))))
    (run
     (loop for i from start below end do
       (outa i (* (env ampf) (wave-train wt0 (* (env vibenv) (oscil vibr)))))))))

(with-sound () (fofins 0 1 270 .2 .001 730 .6 1090 .3 2440 .1)) ; "Ahh"

(with-sound () 
  (fofins 0 4 270 .2 0.005 730 .6 1090 .3 2440 .1 '(0 0 40 0 75 .2 100 1) 
          '(0 0 .5 1 3 .5 10 .2 20 .1 50 .1 60 .2 85 1 100 0))
  (fofins 0 4 (* 6/5 540) .2 0.005 730 .6 1090 .3 2440 .1 '(0 0 40 0 75 .2 100 1) 
          '(0 0 .5 .5 3 .25 6 .1 10 .1 50 .1 60 .2 85 1 100 0))
  (fofins 0 4 135 .2 0.005 730 .6 1090 .3 2440 .1 '(0 0 40 0 75 .2 100 1) 
          '(0 0 1 3 3 1 6 .2 10 .1 50 .1 60 .2 85 1 100 0)))
rand
make-rand &optional-key 
        (frequency *clm-default-frequency*)          ; freq at which new random numbers occur
        (amplitude 1.0)            ; numbers are between -amplitude and amplitude
        (envelope '(-1 1 1 1))     ; distribution envelope (uniform distribution between -1 and 1 is the default)
        distribution               ; pre-computed distribution
rand r &optional (sweep 0.0)
rand? r

make-rand-interp &optional-key 
        (frequency *clm-default-frequency*) 
        (amplitude 1.0) 
        (envelope '(-1 1 1 1) 
        distribution)
rand-interp r &optional (sweep 0.0)
rand-interp? r

centered-random amp 
clm-random amp
mus-random amp ; same as centered-random (for C-side compatibility)
mus-set-rand-seed seed

rand returns a sequence of random numbers between -amplitude and amplitude (it produces a sort of step function). rand-interp interpolates between successive random numbers; it could be defined as (moving-average agen (rand rgen)) where the averager has the same period (length) as the rand. Lisp's function random returns a number between 0.0 and its argument. In both cases, the envelope argument determines the random number distribution. centered-random returns a number between -amp and amp. clm-random returns a random number between 0 and amp. In the latter two cases, mus-set-rand-seed sets the seed for the random number generator. This provides a way around Lisp's clumsy mechanism for repeating a random number sequence.

rand and rand-interp methods
mus-frequency frequency in Hz
mus-phase phase in radians
mus-scaler amplitude arg used in make-<gen>
mus-length distribution table length
mus-data distribution table, if any
mus-increment frequency in radians per sample
rand:
  (if (>= phase (* 2 pi))
      (setf output (centered-random amplitude)))
  (incf phase (+ (hz->radians frequency) sweep))

There are a variety of ways to change rand's uniform distribution to some other: (random (random 1.0)) or (sin (random 3.14159)) are simple examples. Exponential distribution could be:

  (/ (log (max .01 (random 1.0))) (log .01))

where the ".01"'s affect how tightly the resultant values cluster toward 0.0 — set it to .0001 for example to get most of the random values close to 0.0. The central-limit theorem says that you can get closer and closer to gaussian noise by adding rand's together. Orfanidis in "Introduction to Signal Processing" says 12 calls on rand will do perfectly well. We could define our own generator:

(defmacro gaussian-noise (r)
  ;; r=a rand generator allocated via make-rand
  `(let ((val 0.0))
     (dotimes (i 12) (incf val (rand ,r)))
     val))

For a discussion of the central limit theorem, see Korner "Fourier Analysis" and Miller Puckette's dissertation: http://www-crca.ucsd.edu/~msp/Publications/thesis.ps. Another method is the "rejection method" in which we generate random number pairs until we get a pair that falls within the desired distribution; see random-any in dsp.scm (Snd) for code to do this. It is faster at run time, however, to use the "transformation method". The make-rand and make-rand-interp envelope arguments specify the desired distribution function; the generator takes the inverse of the integral of the envelope, loads that into an array, and uses (array-interp (rand array-size)) at run time. This gives random numbers of any arbitrary distribution at a computational cost equivalent to the polyshape generator (which is very similar). The x axis sets the output range (before scaling by amplitude), and the y axis sets the relative weight of the corresponding x axis value. So, the default is '(-1 1 1 1) which says "output numbers between -1 and 1, each number having the same chance of being chosen". An envelope of '(0 1 1 0) outputs values between 0 and 1, denser toward 0. If you already have the distribution table (the result of (inverse-integrate envelope)), you can pass it through the distribution argument.

You can, of course, filter the output of rand to get a different frequency distribution (as opposed to the "value distribution" above, all of which are forms of white noise). Orfanidis also mentions a clever way to get reasonably good 1/f noise: sum together n rand's, where each rand is running an octave slower than the preceding:

(defun make-1f-noise (n)
  ;; returns an array of rand's ready for the 1f-noise generator
  (let ((rans (make-array n)))
    (dotimes (i n) (setf (aref rans i) (make-rand :frequency (/ *srate* (expt 2 i)))))
    rans))

(defmacro 1f-noise (rans)
  `(let ((val 0.0)
         (len (length ,rans)))
     (dotimes (i len) (incf val (rand (aref ,rans i))))
     (/ val len)))

See also green.cl (bounded brownian noise that can mimic 1/f noise in some cases). And we can't talk about noise without mentioning fractals:

(definstrument fractal (start duration m x amp)
  ;; use formula of M J Feigenbaum
  (let* ((beg (floor (* *srate* start)))
	 (end (+ beg (floor (* *srate* duration)))))
    (run
     (loop for i from beg below end do
       (outa i (* amp x))
       (setf x (- 1.0 (* m x x)))))))

;;; quickly reaches a stable point for any m in[0,.75], so:
(with-sound () (fractal 0 1 .5 0 .5)) 
;;; is just a short "ftt"
(with-sound () (fractal 0 1 1.5 .20 .2))

With this instrument you can easily hear the change over from the stable equilibria, to the period doublings, and finally into the combination of noise and periodicity that has made these curves famous. See appendix 2 to Ekeland's "Mathematics and the Unexpected" for more details. Another instrument based on similar ideas is:

(definstrument attract (beg dur amp c) ; c from 1 to 10 or so
  ;; by James McCartney, from CMJ vol 21 no 3 p 6
  (let* ((st (floor (* beg *srate*)))
	 (nd (+ st (floor (* dur *srate*))))
	 (a .2) (b .2) (dt .04)
	 (scale (/ (* .5 amp) c))
	 (x1 0.0) (x -1.0) (y 0.0) (z 0.0))
    (run
     (loop for i from st below nd do
       (setf x1 (- x (* dt (+ y z))))
       (incf y (* dt (+ x (* a y))))
       (incf z (* dt (- (+ b (* x z)) (* c z))))
       (setf x x1)
       (outa i (* scale x))))))

which gives brass-like sounds!

one-pole and friends
 make-one-pole &optional-key a0 b1    ; b1 < 0.0 gives lowpass, b1 > 0.0 gives highpass
 one-pole f input 
 one-pole? f

 make-one-zero &optional-key a0 a1    ; a1 > 0.0 gives weak lowpass, a1 < 0.0 highpass
 one-zero f input 
 one-zero? f

 make-two-pole &optional-key a0 b1 b2 frequency radius
 two-pole f input 
 two-pole? f

 make-two-zero &optional-key a0 a1 a2 frequency radius
 two-zero f input 
 two-zero? f
simple filter methods
mus-xcoeff a0, a1, a2 in equations
mus-ycoeff b1, b2 in equations
mus-order 1 or 2 (no setf)
mus-scaler two-pole and two-zero radius
mus-frequency two-pole and two-zero center frequency
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)

The "a0, b1" 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 the more general filters such as fir-filter. In make-two-pole and make-two-zero you can specify either the actual desired coefficients (a0 and friends), or the center frequency and radius of the filter (frequency and radius). radius should be between 0 and 1 (but less than 1), and frequency should be between 0 and srate/2.

The bird instrument uses a one-pole filter for a distance cue:

(definstrument bird (startime dur frequency freq-skew amplitude freq-envelope amp-envelope 
	             &optional (lpfilt 1.0) (degree 0) (reverb-amount 0))
  (multiple-value-bind (beg end) (times->samples startime dur)
    (let* ((amp-env (make-env amp-envelope amplitude dur))
	   (gls-env (make-env freq-envelope (hz->radians freq-skew) dur))
	   (loc (make-locsig :degree degree :distance 1.0 :reverb reverb-amount))
	   (fil (make-one-pole lpfilt (- 1.0 lpfilt)))
	   (s (make-oscil :frequency frequency)))
      (run
       (loop for i from beg to end do
	 (locsig loc i (one-pole fil (* (env amp-env) 
                                        (oscil s (env gls-env))))))))))
formant
make-formant &optional-key frequency radius
formant f input       ; resonator centered at frequency, bandwidth set by radius above
formant? f

make-firmant &optional-key frequency radius
firmant f input       ; resonator centered at frequency, bandwidth set by radius above
firmant? f
formant methods
mus-frequency formant center frequency
mus-order 2 (no setf)
mus-scaler gain
formant:
    y(n) = x(n) - 
           r * x(n-2) + 
           2 * r * cos(frq) * y(n-1) - 
           r * r * y(n-2)

    firmant:
    x(n+1) = r * (x(n) - 2 * sin(frq/2) * y(n)) + input
    y(n+1) = r * (2 * sin(frq/2) * x(n+1) + y(n))

formant and firmant are resonators (two-pole, two-zero bandpass filters) centered at "frequency", with the bandwidth set by "radius". The formant generator is described in "A Constant-gain Digital Resonator Tuned By a Single Coefficient" by Julius O. Smith and James B. Angell in Computer Music Journal Vol. 6 No. 4 (winter 1982) and "A note on Constant-Gain Digital Resonators" by Ken Steiglitz, CMJ vol 18 No. 4 pp.8-10 (winter 1994). The formant bandwidth is a function of the "radius", and its center frequency is set by "frequency". As the radius approaches 1.0 (the unit circle), the resonance gets narrower. Use mus-frequency to change the center frequency, and mus-scaler to change the radius. The radius can be set in terms of desired bandwidth in Hz via:

(exp (* -0.5 (hz->radians bandwidth)))

If you change the radius, the peak amplitude of the output changes. The firmant generator is the "modified coupled form" of the formant generator, developed by Max Mathews and Julius Smith in "Methods for Synthesizing Very High Q Parametrically Well Behaved Two Pole Filters". grapheq.ins uses a bank of formant generators to implement a graphic equalizer, and fade.ins uses it for frequency domain mixing. Here is an instrument for cross-synthesis with a bank of 128 formants:

(definstrument cross-synthesis (beg dur file1 file2 amp &optional (fftsize 128) (r two-pi) (lo 2) (hi nil))
  ;; file1: input sound, file2: gives spectral shape
  ;; r: controls width of formants (1.0 is another good value here)
  ;; lo and hi: which of the formants are active (a sort of filter on top of the filter)
  ;; we use the on-going spectrum of file2 to scale the outputs of the formant array
  (let* ((fil1 (open-input* file1))
	 (fil2 (and fil1 (open-input* file2))))
    (when fil1
      (if (not fil2)
          (close-input fil1)
        (unwind-protect
	  (let* ((start (floor (* beg *srate*)))
	         (end (+ start (floor (* dur *srate*))))
	         (freq-inc (floor fftsize 2))
	         (fdr (make-double-float-array fftsize))
	         (fdi (make-double-float-array fftsize))
	         (diffs (make-double-float-array freq-inc))
	         (spectrum (make-double-float-array freq-inc))
	         (filptr 0)
	         (ctr freq-inc)
	         (radius (- 1.0 (/ r fftsize)))
	         (bin (float (/ *srate* fftsize)))
	         (fs (make-array freq-inc)))
	    (if (null hi) (setf hi freq-inc))
	    (loop for k from lo below hi do 
              (setf (aref fs k) (make-formant (* k bin) radius)))
	    (run
	     (loop for i from start below end do
	       (when (= ctr freq-inc)
	         (dotimes (k fftsize)
		   (setf (aref fdr k) (ina filptr fil2))
		   (incf filptr))
	         (clear-array fdi)
	         (decf filptr freq-inc)
	         (fft fdr fdi fftsize 1)
	         (rectangular->magnitudes fdr fdi)
	         (dotimes (k freq-inc) 
                   (setf (aref diffs k) 
                     (/ (- (aref fdr k) (aref spectrum k)) freq-inc)))
	         (setf ctr 0))
	       (incf ctr)
	       (dotimes (k freq-inc) 
                 (incf (aref spectrum k) (aref diffs k)))
	       (let ((outval 0.0)
		     (inval (ina i fil1)))
	         (loop for k from lo below hi do 
                   (incf outval (* (aref spectrum k) (formant (aref fs k) inval))))
	         (outa i (* amp outval))))))
        (progn
	  (close-input fil1)
	  (close-input fil2)))))))

(with-sound () (cross-synthesis 0 1 "oboe" "fyow" .5 256 1.0 3 100))
filter, iir-filter, fir-filter
 make-filter &optional-key order xcoeffs ycoeffs
 filter fl inp 
 filter? fl

 make-fir-filter &optional-key order xcoeffs
 fir-filter fl inp 
 fir-filter? fl

 make-iir-filter &optional-key order ycoeffs
 iir-filter fl inp 
 iir-filter? fl

 envelope->coeffs &key order envelope dc

These are the general FIR/IIR filters of arbitrary order. The order argument is one greater than the nominal filter order (it is the size of the arrays).

general filter methods
mus-order filter order
mus-xcoeff x (input) coeff
mus-xcoeffs x (input) coeffs
mus-ycoeff y (output) coeff
mus-ycoeffs y (output) coeffs
mus-data current state (input values)
mus-length same as mus-order
filter:
  (let ((xout 0.0))
    (setf (aref state 0) input)
    (loop for j from order downto 1 do
      (incf xout (* (aref state j) (aref xcoeffs j)))
      (decf (aref state 0) (* (aref ycoeffs j) (aref state j)))
      (setf (aref state j) (aref state (1- j))))
    (+ xout (* (aref state 0) (aref xcoeffs 0))))

dsp.scm in the Snd package has a number of filter design functions, and various specializations of the filter generators, including such perennial favorites as biquad, butterworth, hilbert transform, and notch filters. Similarly, analog-filter.scm in the Snd tarball has the usual IIR suspects: Butterworth, Chebyshev, Bessel, and Elliptic filters.

Say we want to put a spectral envelope on a noise source.

(definstrument filter-noise (beg dur amp &key xcoeffs)
  (let* ((st (floor (* beg *srate*)))
         (noi (make-rand :frequency (* .5 *srate*) :amplitude amp))
         (flA (make-filter :xcoeffs xcoeffs))
         (nd (+ st (floor (* *srate* dur)))))
    (run
      (loop for i from st below nd do
        (outa i (filter flA (rand noi)))))))

(with-sound () 
  (filter-noise 0 1 .2 
    :xcoeffs (envelope->coeffs :order 12 :envelope '(0 0.0 .125 0.5 .2 0.0 .3 1.0 .5 0.0 1.0 0.0))))

envelope->coeffs translates a frequency response envelope into the corresponding FIR filter coefficients. The order of the filter determines how close you get to the envelope.

The Hilbert transform can be implemented with an fir-filter:

(defun make-hilbert (&optional (len 30))
  ;; create the coefficients of the Hilbert transformer of length len
  (let* ((arrlen (1+ (* 2 len)))
	 (arr (make-array arrlen)))
    (do ((i (- len) (1+ i)))
	((= i len))
      (let* ((k (+ i len))
	     (denom (* pi i))
	     (num (- 1.0 (cos (* pi i)))))
	(if (= i 0)
	    (setf (aref arr k) 0.0)
	    (setf (aref arr k) (/ num denom)))))
    (make-fir-filter arrlen (loop for i from 0 below arrlen collect (aref arr i)))))

(defmacro hilbert (f in) `(fir-filter ,f ,in))
delay
make-delay &optional-key size initial-contents initial-element max-size type
delay d input &optional (pm 0.0)
delay? d
tap d &optional (offset 0)
delay-tick d input

delay is a delay line. size is in samples. Input fed into a delay line reappears at the output size samples later. initial-element defaults to 0.0. tap returns the current value of the delay generator. Its offset is the distance of the tap from the current delay line sample. If max-size is specified, and larger than size, the delay line can provide fractional delays. It should be large enough to accommodate the largest actual delay requested at run-time. pm determines how far from the normal index we are; that is, it is difference between the nominal delay length (size) and the current actual delay length (size + pm). A positive pm corresponds to a longer delay line. The type argument sets the interpolation type: mus-interp-none, mus-interp-linear, mus-interp-all-pass, mus-interp-lagrange, mus-interp-bezier, or mus-interp-hermite. delay-tick just puts a sample in the delay line. 'ticks' the delay forward, and returns its input argument. This is aimed at physical modeling instruments where a tap is doing the actual delay line read.

delay methods
mus-length length of delay
mus-order same as mus-length
mus-data delay line itself (no setf)
mus-interp-type interpolation choice (no setf)
mus-scaler unused internally, but available for delay specializations
delay:
(prog1
  (array-interp line (- loc pm))
  (setf (aref line loc) input)
  (incf loc)
  (if (<= size loc) (setf loc 0)))
(definstrument echo (beg dur scaler secs file)
  (let ((del (make-delay (round (* secs *srate*))))
	(inf (open-input file))
	(j 0))
    (run
     (loop for i from beg below (+ beg dur) do
       (let ((inval (ina j inf)))
	 (outa i (+ inval (delay del (* scaler (+ (tap del) inval)))))
	 (incf j))))
    (close-input inf)))

;;; (with-sound () (echo 0 60000 .5 1.0 "pistol.snd"))
comb and notch
make-comb &optional-key scaler size initial-contents initial-element max-size
comb cflt input &optional (pm 0.0)
comb? cflt

make-filtered-comb &optional-key scaler size initial-contents initial-element max-size filter
filtered-comb cflt input &optional (pm 0.0)
filtered-comb? cflt

make-notch &optional-key scaler size initial-contents initial-element max-size
notch cflt input &optional (pm 0.0)
notch? cflt

comb is a delay line with a scaler on the feedback term. notch is a delay line with a scaler on the feedforward term. size is the length in samples of the delay line. Other arguments are handled as in delay. filtered-comb is a comb filter with a one-zero filter on the feedback.

comb, filtered-comb, and notch methods
mus-length length of delay
mus-order same as mus-length
mus-data delay line itself (no setf)
mus-feedback scaler (comb only)
mus-feedforward scaler (notch only)
mus-interp-type interpolation choice (no setf)
 comb:           y(n) = x(n - size) + scaler * y(n - size)
 notch:          y(n) = x(n) * scaler  + x(n - size)
 filtered-comb:  y(n) = x(n - size) + scaler * filter(y(n - size))

As a rule of thumb, the decay time of the feedback part is 7.0 * size / (1.0 - scaler) samples, so to get a decay of dur seconds, scaler <= 1.0 - 7.0 * size / (dur * *srate*). The peak gain is 1.0 / (1.0 - (abs scaler)). The peaks (or valleys in notch's case) are evenly spaced at *srate* / size. The height (or depth) thereof is determined by scaler — the closer to 1.0, the more pronounced. See Julius Smith's "An Introduction to Digital Filter Theory" in Strawn "Digital Audio Signal Processing", or Smith's "Music Applications of Digital Waveguides". The following instrument sweeps the comb filter using the pm argument:

(definstrument zc (time dur freq amp length1 length2 feedback)
  (multiple-value-bind
      (beg end) (times->samples time dur)
    (let ((s (make-pulse-train :frequency freq))  ; some raspy input so we can hear the effect easily
          (d0 (make-comb :size length1 :max-size (max length1 length2) :scaler feedback))
          (zenv (make-env '(0 0 1 1) :scaler (- length2 length1) :duration dur)))
      (run
       (loop for i from beg to end do
	 (outa i (comb d0 (* amp (pulse-train s)) (env zenv))))))))

(with-sound () (zc 0 3 100 .1 20 100 .5) (zc 3.5 3 100 .1 90 100 .95))
all-pass
make-all-pass &optional-key feedback feedforward size initial-contents initial-element max-size
all-pass f input &optional (pm 0.0)
all-pass? f

all-pass or moving average comb is just like comb but with an added feedforward term. If feedforward = 0, we get a comb filter. If both scale terms = 0, we get a pure delay line.

all-pass methods
mus-length length of delay
mus-order same as mus-length
mus-data delay line itself (no setf)
mus-feedback feedback scaler
mus-feedforward feedforward scaler
mus-interp-type interpolation choice (no setf)
 y(n) = feedforward * x(n) + x(n - size) + feedback * y(n - size)

all-pass filters are used extensively in reverberation; see jcrev.ins or nrev.ins for examples.

moving-average
make-moving-average &optional-key size initial-contents initial-element
moving-average f input
moving-average? f

moving-average or moving window average returns the average of the last 'size' values input to it. This is used both to track rms values and to generate ramps between 0 and 1 in a "gate" effect in new-effects.scm and in rms-envelope in env.scm (Snd). It could also be viewed as a low-pass filter.

moving-average methods
mus-length length of table
mus-order same as mus-length
mus-data table of last 'size' values
result = sum-of-last-n-inputs / n

moving-average is used in Snd's dsp.scm to implement several related functions: moving-rms, moving-sum, and moving-length. I might make these CLM generators someday.

src
make-src &optional-key input (srate 1.0) (width 5)
src s &optional (sr-change 0.0) input-function
src? s
src methods
mus-incrementsrate arg to make-src

src performs sampling rate conversion by convolving its input with a sinc function. srate is the ratio between the old sampling rate and the new; an srate of 2 causes the sound to be half as long, transposed up an octave. width is how many neighboring samples to convolve with sinc. If you hear high-frequency artifacts in the conversion, try increasing this number; Perry Cook's default value is 40, and I've seen cases where it needs to be 100. It can also be set as low as 2 in some cases. The greater the width, the slower the src generator runs. The sr-change argument is the amount to add to the current srate on a sample by sample basis (if it's 0.0 and the original make-src srate argument was also 0.0, you get a constant output because the generator is not moving at all). Here's an instrument that provides time-varying sampling rate conversion:

(definstrument simple-src (start-time duration amp srt srt-env filename)
  (let* ((senv (make-env srt-env :duration duration))
         (beg (floor (* start-time *srate*)))
         (end (+ beg (floor (* duration *srate*))))
         (src-gen (make-src :input filename :srate srt)))
    (run
      (loop for i from beg below end do
        (outa i (* amp (src src-gen (env senv))))))))

src can provide an all-purpose "Forbidden Planet" sound effect:

(definstrument srcer (start-time duration amp srt fmamp fmfreq filename)
  (let* ((os (make-oscil :frequency fmfreq))
         (beg (floor (* start-time *srate*)))
         (end (+ beg (floor (* duration *srate*))))
         (src-gen (make-src :input filename :srate srt)))
    (run
      (loop for i from beg below end do
        (outa i (* amp (src src-gen (* fmamp (oscil os)))))))))

(with-sound () (srcer 0 2 1.0   1 .3 20 "fyow.snd"))   
(with-sound () (srcer 0 25 10.0   .01 1 10 "fyow.snd"))
(with-sound () (srcer 0 2 1.0   .9 .05 60 "oboe.snd")) 
(with-sound () (srcer 0 2 1.0   1.0 .5 124 "oboe.snd"))
(with-sound () (srcer 0 10 10.0   .01 .2 8 "oboe.snd"))
(with-sound () (srcer 0 2 1.0   1 3 20 "oboe.snd"))    

(definstrument hello-dentist (beg dur file frq amp)
  (let ((rd (make-src :input file))
        (rn (make-rand-interp :frequency frq :amplitude amp))
        (end (+ beg dur)))
    (run
      (loop for i from beg below end do
        (outa i (src rd (rand-interp rn)))))))

The input argument to make-src and the input-function argument to src provide the generator with input as it is needed. The input function takes one argument (the desired read direction, if the reader can support it); it is funcall'd each time the src generator needs another sample of input. The input argument to src can also be an input file structure, as returned by open-input, or as here, just the filename itself. The simple-src instrument above could be written to use an input function instead:

(definstrument src-with-readin (start-time duration amp srt srt-env filename)
  (let* ((senv (make-env srt-env :duration duration))
         (beg (floor (* start-time *srate*)))
	 (rd (make-readin filename))
         (end (+ beg (floor (* duration *srate*))))
         (src-gen (make-src :srate srt)))
    (run
      (loop for i from beg below end do
        (outa i (* amp (src src-gen (env senv) #'(lambda (dir) (readin rd)))))))))

If you jump around in the input (via mus-location for example), you can use the mus-reset function to clear out any lingering state before starting to read at the new position. (src, like many other generators, has an internal buffer of recently read samples, so a sudden jump to a new location will otherwise cause a click).

convolve
make-convolve &optional-key input filter fft-size filter-size
 convolve ff &optional input-function
 convolve? ff
 convolve-files file1 file2 &optional (maxamp 1.0) (output-file "tmp.snd")
convolve methods
mus-lengthfft size used in the convolution

convolve convolves its input with the impulse response filter. The filter argument can be an array, the result of open-input, or a filename as a string. When not file based, input and input-function are functions of one argument (currently ignored) that are funcall'd whenever convolve needs input.

(definstrument convins (beg dur filter file &optional (size 128))
  (let* ((start (floor (* beg *srate*)))
         (end (+ start (floor (* dur *srate*))))
         (ff (make-convolve :input file :fft-size size :filter filter)))
    (run
      (loop for i from start below end do 
        (outa i (convolve ff))))))

convolve-files handles a very common special case: you often want to convolve two files, normalizing the result to some maxamp. The convolve generator does not know in advance what its maxamp will be, and when the two files are more or less the same size, there's no real computational savings to using overlap-add (i.e. the generator), so a one-time giant FFT saved as a temporary sound file is much handier.

granulate
make-granulate &optional-key   
        input
        (expansion 1.0)   ; how much to lengthen or compress the file
        (length .15)      ; length of file slices that are overlapped
        (scaler .6)       ; amplitude scaler on slices (to avoid overflows)
        (hop .05)         ; speed at which slices are repeated in output
        (ramp .4)         ; amount of slice-time spent ramping up/down
        (jitter 1.0)      ; affects spacing of successive grains
        max-size          ; internal buffer size
        edit              ; grain editing function (Scheme/Ruby, not CL)
granulate e &optional input-function edit-function
granulate? e
granulate methods
mus-frequency time (seconds) between output grains (hop)
mus-ramp length (samples) of grain envelope ramp segment
mus-hop time (samples) between output grains (hop)
mus-scaler grain amp (scaler)
mus-increment expansion
mus-length grain length (samples)
mus-data grain samples (a vct)
mus-location granulate's local random number seed
result = overlap add many tiny slices from input

granulate "granulates" its input (normally a sound file). 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 length — the longer the slice, the more like reverb the effect. The portion of the length (on a scale from 0 to 1.0) spent on each ramp (up or down) is ramp. This can control the smoothness of the result of the overlaps.

jitter sets the accuracy with which we hop. If you set it to 0, you can get very strong comb filter effects, or tremolo. The more-or-less average time between successive segments is hop. If jitter is 0.0, and hop is very small (say .01), you're asking for trouble (a big comb filter). If you're granulating more than one channel at a time, and want the channels to remain in-sync, make each granulator use the same initial random number seed (via mus-location).

The overall amplitude scaler on each segment is scaler — this is used to try to avoid overflows as we add all these zillions of segments together. expansion determines the input hop in relation to the output hop; 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. input and input-function are the same as in src and convolve.

(definstrument granulate-sound (file beg &optional dur (orig-beg 0.0) (exp-amt 1.0))
  (let* ((f-srate (sound-srate file))
	 (f-start (round (* f-srate orig-beg)))
         (f (open-input file :start f-start))
	 (st (floor (* beg *srate*)))
	 (new-dur (or dur (- (sound-duration file) orig-beg)))
	 (exA (make-granulate :input f :expansion exp-amt))
	 (nd (+ st (floor (* *srate* new-dur)))))
    (run
     (loop for i from st below nd do
       (outa i (granulate exA))))
    (close-input f)))

See expsrc.ins. Here's an instrument that uses the input-function argument to granulate. It cause the granulation to run backwards through the file:

(definstrument grev (beg dur exp-amt file file-beg)
  (let* ((exA (make-granulate :expansion exp-amt))
	 (fil (open-input* file file-beg))
	 (ctr file-beg))
    (run
     (loop for i from beg to (+ beg dur) do
       (outa i (granulate exA
			  #'(lambda (dir)
			      (let ((inval (ina ctr fil)))
				(if (> ctr 0) (setf ctr (1- ctr)))
				inval))))))
    (close-input fil)))

(with-sound () (grev 0 100000 2.0 "pistol.snd" 40000))

The edit argument can be a function of one argument, the current granulate generator. It is called just before a grain is added into the output buffer. The current grain is accessible via mus-data. The edit function, if any, should return the length in samples of the grain, or 0.

phase-vocoder
make-phase-vocoder &optional-key input (fft-size 512) (overlap 4) interp (pitch 1.0) analyze edit synthesize
phase-vocoder pv input-function analyze-function edit-function synthesize-function
phase-vocoder? pv
phase-vocoder methods
mus-frequency pitch shift
mus-length fft-size
mus-increment interp
mus-hop fft-size / overlap
mus-location outctr (counter to next fft)

phase-vocoder provides a generator to perform phase-vocoder analysis and resynthesis. The process is split into three pieces, the analysis stage, editing of the amplitudes and phases, then the resynthesis. Each stage has a default that is invoked if the analyze, edit, or synthesize arguments are omitted from make-phase-vocoder or the phase-vocoder generator. The edit and synthesize arguments are functions of one argument, the phase-vocoder generator. The analyze argument is a function of two arguments, the generator and the input function. The default is to read the current input, take an fft, get the new amplitudes and phases (as the edit function default), then resynthesize using sines; so, the default case returns a resynthesis of the original input. interp sets the time between ffts (for time stretching etc).

(definstrument simple-pvoc (beg dur amp size file)
  (let* ((start (floor (* beg *srate*)))
	 (end (+ start (floor (* dur *srate*))))
	 (sr (make-phase-vocoder file :fft-size size)))
      (run
       (loop for i from start to end do
	 (outa i (* amp (phase-vocoder sr)))))))

See ug3.ins for instruments that use the various function arguments. In Snd, clm23.scm has a variety of instruments calling the phase-vocoder generator, including pvoc-e that specifies all of the functions with their default values (that is, it explicitly passes in functions that do what the phase-vocoder would have done without any function arguments).

nrxycos and nrxysin
make-nrxysin &optional-key (frequency *clm-default-frequency*) (ratio 1.0) (n 1) (r .5)
nrxysin s &optional (fm 0.0)
nrxysin? s

make-nrxycos &optional-key (frequency *clm-default-frequency*) (ratio 1.0) (n 1) (r .5)
nrxycos s &optional (fm 0.0)
nrxycos? s
nrxysin methods
mus-frequency frequency in Hz
mus-phase phase in radians
mus-scaler "a" parameter; sideband scaler
mus-length "n" parameter
mus-increment frequency in radians per sample
mus-offset "ratio" parameter
(/ (- (sin phase) (* a (sin (- phase (* ratio phase))))
      (* (expt a (1+ n)) (- (sin (+ phase (* (+ N 1) (* ratio phase))))
			    (* a (sin (+ phase (* N (* ratio phase))))))))
   (- (+ 1 (* a a)) (* 2 a (cos (* ratio phase)))))

These three generators produce a kind of additive synthesis. "n" is the number of sidebands (0 gives a sine wave), "r" is the amplitude ratio between successive sidebands (don't set it to 1.0), and "ratio" is the ratio between the carrier frequency and the spacing between successive sidebands. A "ratio" of 2 gives odd-numbered harmonics for a (vaguely) clarinet-like sound. The basic idea is very similar to that used in the ncos generator, but you have control of the fall-off of the spectrum and the spacing of the partials.

The peak amplitude of the nrxysin is hard to predict. I think nrxysin is close to the -1.0..1.0 ideal, and won't go over 1.0. nrxycos is normalized correctly.

(definstrument ss (beg dur freq amp &optional (n 1) (r 0.5) (ratio 1.0))
  (let* ((st (floor (* *srate* beg)))
         (nd (+ st (floor (* *srate* dur))))
         (sgen (make-nrxycos freq ratio n r)))
    (run
     (loop for i from st below nd do
       (outa i (* amp (nrxycos sgen)))))))
asymmetric-fm
make-asymmetric-fm &optional-key (frequency *clm-default-frequency*) (initial-phase 0.0) (r 1.0) (ratio 1.0)
asymmetric-fm af index &optional (fm 0.0)
asymmetric-fm? af
asymmetric-fm methods
mus-frequency frequency in Hz
mus-phase phase in radians
mus-scaler "r" parameter; sideband scaler
mus-offset "ratio" parameter
mus-increment frequency in radians per sample
(* (exp (* index (* 0.5 (- r (/ 1.0 r)))
	   (cos (* ratio phase))))
   (sin (+ phase (* index (* 0.5 (+ r (/ 1.0 r)))
		    (sin (* ratio phase))))))

asymmetric-fm provides a way around the symmetric spectra normally produced by FM. See Palamin and Palamin, "A Method of Generating and Controlling Asymmetrical Spectra" JAES vol 36, no 9, Sept 88, p671-685. The generator's output amplitude is not always easy to predict. r is the ratio between successive sideband amplitudes, r > 1.0 pushes energy above the carrier, r < 1.0 pushes it below. (r = 1.0 gives normal FM). ratio is the ratio between the carrier and modulator (i.e. sideband spacing). It's somewhat inconsistent that asymmetric-fm takes index (the fm-index) as its second argument, but otherwise it would be tricky to get time-varying indices.

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

For the other kind of asymmetric-fm, and for asymmetric spectra via "single sideband FM", see dsp.scm in Snd.

Other generators

There are a number of other generators in the CLM distribution that aren't loaded by default. Among these are:

  rms         ; trace the rms of signal
  gain        ; modify signal to match rms power
  balance     ; combination of rms and gain

green.cl defines several special purpose noise generators. butterworth.cl has several Butterworth filters. (See analog-filter.scm in the Snd package for functions to design all the usual analog filters; the output is compatible with CLM's filter generator).

generic functions

The generators have internal state that is sometimes of interest at run-time. To get or set this state, use these functions (they are described in conjunction with the associated generators):

mus-channelchannel being read/written
mus-channelschannels open
mus-dataarray of data
mus-describedescription of current state
mus-feedbackfeedback coefficient
mus-feedforwardfeedforward coefficient
mus-file-namefile being read/written
mus-frequencyfrequency (Hz)
mus-hophop size for block processing
mus-incrementvarious increments
mus-interp-typeinterpolation type (mus-interp-linear, etc)
mus-lengthdata array length
mus-locationsample location for reads/writes
mus-namegenerator name ("oscil")
mus-offsetenvelope offset
mus-orderfilter order
mus-phasephase (radians)
mus-rampgranulate grain envelope ramp setting
mus-resetset gen to default starting state
mus-runrun any generator
mus-scalerscaler, normally on an amplitude
mus-widthwidth of interpolation tables, etc
mus-xcoeffx (input) coefficient
mus-xcoeffsarray of x (input) coefficients
mus-ycoeffy (output, feedback) coefficient
mus-ycoeffsarray of y (feedback) coefficients

Many of these are settable: (setf (mus-frequency osc1) 440.0) sets osc1's current frequency to (hz->radians 440.0).

(definstrument backandforth (onset duration file src-ratio)
  ;; read file forwards and backwards until dur is used up
  ;; a slightly improved version is 'scratch' in ug1.ins
  (let* ((last-sample (sound-frames file))
         (beg (floor (* *srate* onset)))
         (end (+ beg (floor (* *srate* duration))))
	 (input (make-readin file))
         (s (make-src :srate src-ratio))
         (cs 0))
    (run
     (loop for i from beg below end do
       (declare (type :integer cs last-sample)
		(type :float src-ratio))
       (if (>= cs last-sample) (setf (mus-increment s) (- src-ratio)))
       (if (<= cs 0) (setf (mus-increment s) src-ratio))
       (outa i (src s 0.0 #'(lambda (dir) 
			      (incf cs dir)
			      (setf (mus-increment input) dir)
			      (readin input))))))))

;;; (with-sound () (backandforth 0 10 "pistol.snd" 2.0))
Frames and mixers

There are two special data types in CLM: frames and mixers. A frame is an array that represents a multichannel sample. For example, in a stereo file, at time 0.0, there are two samples, one for each channel, and the frame that represents it has 2 samples. A mixer is a array of arrays that represents a set of input to output scalers, as if it were the current state of a mixing console's volume controls. A frame (a multichannel input) can be mixed into a new frame (a multichannel output) by passing it through a mixer (a matrix, the operation being a (left) matrix multiply). These are combined with the notion of a sample (one datum of sampled music), and input/output ports (files, audio ports, etc) to handle all the underlying data IO.

make-empty-frame chanscreate frame of 0's
make-frame chans &rest argscreate frame and load it with args
frame? objis obj a frame
frame+ f1 f2 &optional outfadd f1 and f2 element-wise, return new frame (or outf)
frame* f1 f2 &optional outfmultiply f1 and f2 element-size, return new frame (or outf)
frame-ref f1 chanreturn f1[chan]
frame-set! f1 chan valf1[chan] = val (also setf with frame-ref)
make-empty-mixer chanscreate a mixer of 0's
make-identity-mixer chanscreate a mixer of 1's on the diagonal
make-scalar-mixer chans sclcreate a mixer with scl on the diagonal
make-mixer chans &rest argscreate a mixer and load it with args
mixer? objis obj a mixer
mixer* m1 m2 &optional outmmatrix multiply of m1 and m2, return new mixer (or outm)
mixer+ m1 m2 &optional outmmatrix add of m1 and m2, return new mixer (or outm)
mixer-ref m1 in outm1[in,out] (use setf to change)
mixer-set! m1 in out valm1[in,out] = val (also setf with mixer-ref)
frame->frame mf mf &optional outfpass frame through mixer, return new frame (or outf)
frame->list framereturn list of frame's contents
sample->frame mf sample &optional outfpass sample through mf (a frame or mixer), return new frame (or outf)
frame->sample mf framepass frame through mf (a frame or mixer), return sample
frame and mixer methods
mus-channels number of channels accommodated
mus-length same as mus-channels
mus-data the matrix data (float array)

The arguments to frame*, frame+, mixer*, and mixer+ can be floats as well as mixers and frames. In that case, the mixer or frame is either scaled by the float, or the float is added to each element.

fullmix.ins uses these functions to provide a mixer able to handle any number of channels of data in and out with optional scalers and envelopes on any in->out path. The heart of the instrument is:

       (frame->file *output* i 
         (frame->frame  
           (file->frame file inloc inframe) 
           mx outframe))

Here the input file is read by file->frame producing a frame of data. That is then passed through the mixer frame->frame, and the resultant frame is written to the with-sound output file *output* by frame->file. Within run, the output frames of the various frame producing functions must be provided (I'm trying to avoid run-time memory management). In matrix terminology, a mixer is a square matrix, a frame is a column (or row) vector, mixer* is a matrix multiply, and so on. The form (frame->frame frame mixer frame) multiplies a row vector (the first frame) by a matrix (the mixer), whereas (frame->frame mixer frame frame) multiplies a matrix by a column vector.

Sound IO

Sound file IO is supported by a variety of functions. To read and write sound files into an array, use array->file and file->array. Within the run-loop, out-any, in-any, and readin are the simplest input and output generators; locsig provides a sort of sound placement; dlocsig provides moving sound placement. When you use with-sound, the variable *output* is bound to a sample->file object, so output by default goes to with-sound's output file. You can open (for reading or writing) any sound files via make-file->sample (or ->frame), and make-sample->file (or frame->). These return an IO object which you subsequently pass to file->sample (for input) and sample->file (for output). To close the connection to the file system, you can use mus-close, but it's also called automatically during garbage collection, if needed.

mus-input? objt if obj performs sound input
mus-output? objt if obj performs sound output
file->sample? objt if obj reads a sound file returning a sample
sample->file? objt if obj writes a sample to a sound file
frame->file? objt if obj writes a frame to a sound file
file->frame? objt if obj reads a sound file returning a frame
make-file->sample name buffer-sizereturn gen that reads samples from sound file name
make-sample->file name &optional chans format type commentreturn gen that writes samples to sound file name
make-file->frame name buffer-sizereturn gen that reads frames from sound file name
make-frame->file name &optional chans format type commentreturn gen that writes frames to sound file name
file->sample obj samp &optional chanreturn sample at samp in channel chan
sample->file obj samp chan valwrite (add) sample val at samp in channel chan
file->frame obj samp &optional outfreturn frame at samp
frame->file obj samp valwrite (add) frame val at samp
file->array file channel beg dur arrayread samples from file into array
array->file file data len srate channels write samples in array to file
continue-frame->file filereopen file for more output
continue-sample->file filereopen file for more output
mus-close objclose the output file associated with obj
out-any
outa loc data
out-any loc data &optional (channel 0) (o-stream *output*)

out-any adds data into o-stream at sample position loc. O-stream defaults to the current output file (it is a frame->file instance, not a file name). The reverb stream, if any, is named *reverb*; the direct output is *output*. You can output anywhere at any time, but because of the way data is buffered internally, your instrument will run much faster if it does sequential output. Locsig is another output function.

Many of the CLM examples and instruments use outa and outb. These are macros equivalent to (out-any loc data 0 *output*) etc.

in-any
in-any loc channel i-stream
ina loc

in-any returns the sample at position loc in i-stream as a float. Many of the CLM examples and instruments use ina and inb; one example is the digital zipper instrument zipper.ins.

(definstrument simple-ina (beg dur amp file)
  (let* ((start (floor (* beg *srate*)))
	 (end (+ start (floor (* dur *srate*))))
	 (fil (open-input file)))          ; actually make-file->sample
    (run
     (loop for i from start to end do
       (outa i (* amp (in-any i 0 fil))))) ; actually file->sample
    (close-input fil)))
readin
 make-readin &optional-key file (channel 0) start (direction 1)
 readin rd
 readin? rd
readin methods
mus-channel channel arg to make-readin (no setf)
mus-location current location in file
mus-increment sample increment (direction arg to make-readin)
mus-file-name name of file associated with gen
mus-length number of frames in file associated with gen

readin returns successive samples from file. file should be either an IO instance, as returned by open-input, or a filename. start is the frame at which to start reading file. channel is which channel to read (0-based). size is the read buffer size in samples. It defaults to *clm-file-buffer-size*. Here is an instrument that applies an envelope to a sound file using readin and env (see also the fullmix instrument in fullmix.ins):

(definstrument env-sound (file beg &optional (amp 1.0) (amp-env '(0 1 100 1)))
  (let* ((st (floor (* beg *srate*)))
         (dur (sound-duration file))
         (rev-amount .01)
         (rdA (make-readin file))
         (ampf (make-env amp-env amp dur))
         (nd (+ st (floor (* *srate* dur)))))
    (run
      (loop for i from st below nd do
        (let ((outval (* (env ampf) (readin rdA))))
  	  (outa i outval)
	  (if *reverb* (outa i (* outval rev-amount) *reverb*)))))))
locsig
 make-locsig &optional-key (degree 0.0) (distance 1.0) (reverb 0.0) channels (type *clm-locsig-type*)
 locsig loc i in-sig
 locsig? loc
 locsig-ref loc chan
 locsig-set! loc chan val
 locsig-reverb-ref loc chan
 locsig-reverb-set! loc chan val
 move-locsig loc degree distance
 locsig-type ()
locsig methods
mus-data output scalers (a vct)
mus-xcoeff reverb scaler
mus-xcoeffs reverb scalers (a vct)
mus-channels output channels
mus-length output channels

locsig normally takes the place of out-any in an instrument. It tries to place a signal between channels 0 and 1 (or 4 channels placed in a circle) in an extremely dumb manner: it just scales the respective amplitudes ("that old trick never works"). reverb 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 reverb). distance should be greater than or equal to 1.0. type (returned by the function locsig-type) can be mus-interp-linear (the default) or mus-interp-sinusoidal. This parameter can be set globally via *clm-locsig-type*. The mus-interp-sinusoidal case uses sin and cos to set the respective channel amplitudes (this is reported to help with the "hole-in-the-middle" problem).

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. 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).

Locsig can send output to any number of channels. If channels > 2, the speakers are assumed to be evenly spaced in a circle. You can use locsig-set! and locsig-ref to override the placement decisions. To have full output to both channels,

(setf (locsig-ref loc 0) 1.0) ; or (locsig-set! loc 0 1.0)
(setf (locsig-ref loc 1) 1.0)

These locations can be set via envelopes and so on within the run loop to pan between speakers (but see move-locsig below):

(definstrument space (file onset duration &key (distance-env '(0 1 100 10)) (amplitude-env '(0 1 100 1))
		     (degree-env '(0 45 50 0 100 90)) (reverb-amount .05))
  (let* ((beg (floor (* onset *srate*)))
	 (end (+ beg (floor (* *srate* duration))))
         (loc (make-locsig :degree 0 :distance 1 :reverb reverb-amount))
         (rdA (make-readin :file file))
         (dist-env (make-env distance-env :duration duration))
         (amp-env (make-env amplitude-env :duration duration))
         (deg-env (make-env (scale-envelope degree-env (/ 1.0 90.0)) :duration duration))
         (dist-scaler 0.0))
    (run
      (loop for i from beg below end do
        (let ((rdval (* (readin rdA) (env amp-env)))
	      (degval (env deg-env))
	      (distval (env dist-env)))
          (setf dist-scaler (/ 1.0 distval))
          (setf (locsig-ref loc 0) (* (- 1.0 degval) dist-scaler))
          (if (> (mus-channels *output*) 1) (setf (locsig-ref loc 1) (* degval dist-scaler)))
          (when *reverb* (setf (locsig-reverb-ref loc 0) (* reverb-amount (sqrt dist-scaler))))
          (locsig loc i rdval))))))

For a moving sound source, see either move-locsig, or Fernando Lopez Lezcano's dlocsig. Here is an example of move-locsig:

(definstrument move-osc (start dur freq amp &key (degree 0) (dist 1.0) (reverb 0))
  (let* ((beg (floor (* start *srate*)))
         (end (+ beg (floor (* dur *srate*))) )
         (car (make-oscil :frequency freq))
         (loc (make-locsig :degree degree :distance dist :channels 2))
	 (pan-env (make-env '(0 0 1 90) :duration dur)))
    (run
     (loop for i from beg to end do
       (let ((ut (* amp (oscil car))))
	 (move-locsig loc (env pan-env) dist)
         (locsig loc i ut))))))
move-sound
 make-move-sound dlocs-list (output *output*) (revout *reverb*)
 move-sound dloc i in-sig
 move-sound? dloc

move-sound is intended as the run-time portion of dlocsig. make-dlocsig (described in dlocsig.html) creates a move-sound structure, passing it to the move-sound generator inside the dlocsig macro. All the necessary data is packaged up in a list:

(list
  (start 0)               ; absolute sample number at which samples first reach the listener
  (end 0)                 ; absolute sample number of end of input samples
  (out-channels 0)        ; number of output channels in soundfile
  (rev-channels 0)        ; number of reverb channels in soundfile
  path                    ; interpolated delay line for doppler
  delay                   ; tap doppler env
  rev                     ; reverberation amount
  out-delays              ; delay lines for output channels that have additional delays
  gains                   ; gain envelopes, one for each output channel
  rev-gains               ; reverb gain envelopes, one for each reverb channel
  out-map)                ; mapping of speakers to output channels

Here's an instrument that uses this generator to pan a sound through four channels:

(definstrument simple-dloc (beg dur freq amp)
  (let* ((os (make-oscil freq))
	 (start (floor (* beg *srate*)))
	 (end (+ start (floor (* dur *srate*))))
	 (loc (make-move-sound (list start end 4 0
				     (make-delay 12) 
				     (make-env '(0 0 10 1) :duration dur)
				     (make-env '(0 0 1 0) :duration dur)
				     (make-array 4 :initial-element nil)
				     (make-array 4 :initial-contents 
				       (list
					(make-env '(0 0 1 1 2 0 3 0 4 0) :duration dur)
					(make-env '(0 0 1 0 2 1 3 0 4 0) :duration dur)
					(make-env '(0 0 1 0 2 0 3 1 4 0) :duration dur)
					(make-env '(0 0 1 0 2 0 3 0 4 1) :duration dur)))
				     nil
				     (make-integer-array 4 :initial-contents (list 0 1 2 3))))))
    (run
     (loop for i from start to end do
       (move-sound loc i (* amp (oscil os)))))))
Useful functions

There are several commonly-used functions, some of which can occur in the run macro. These include a few that look for all the world like generators.

hz->radians freqconvert freq to radians per sample
radians->hz radsconvert rads to Hz
db->linear dBconvert dB to linear value
linear->db valconvert val to dB
times->samples start durationconvert start and duration from seconds to samples (beg+dur in latter case)
samples->seconds sampsconvert samples to seconds
seconds->samples secsconvert seconds to samples
degrees->radians degsconvert degrees to radians
radians->degrees radsconvert radians to degrees
clear-array arrset all values in arr to 0.0
sound-samples filenamesamples of sound according to header (can be incorrect)
sound-frames filenamesamples per channel
sound-datum-size filenamebytes per sample
sound-data-location filenamelocation of first sample (bytes)
sound-chans filenamenumber of channels (samples are interleaved)
sound-srate filenamesampling rate
sound-header-type filenameheader type (aiff etc)
sound-data-format filenamedata format (alaw etc)
sound-length filenametrue file length (for error checks)
sound-duration filenamefile length in seconds
sound-maxamp name valsget max amp vals and times of file name
sound-loop-info name valsget loop info of file name in vals (make-integer-array 6)

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

  freq-in-hz * 2 * pi / *srate*.  

Freq-in-hz * 2 * pi gives us the number of radians traversed per second; we then divide by the number of samples per second to get the radians per sample; in dimensional terms: (radians/sec) / (sample/sec) = radians/sample. We need this conversion whenever a frequency-related value is actually being accessed on every sample, as an increment of a phase variable. (We are also assuming our wave table size is 2 * pi). This conversion value was named "mag" in Mus10 and "in-hz" in CLM-1. The inverse is radians->hz.

These names are different from the underlying sndlib names mostly due to confusion and inattention. Nearly all the sndlib constants and functions are imported into clm under names that are the same as the C name except "_" is replaced by "-". So mus-sound-duration exists, and is the same as sound-duration mentioned above. See sndlib.html for some info. (mus-sound-srate (mus-file-name *output*)) for example, returns the current output sampling rate; this is the same as *srate*.

polynomial
polynomial coeffs x

polynomial evaluates a polynomial, defined by giving its coefficients, at a particular point (x). coeffs is an array of coefficients where coeffs[0] is the constant term, and so on. For waveshaping, use the function partials->polynomial. Abramowitz and Stegun, "A Handbook of Mathematical Functions" is a treasure-trove of interesting polynomials. See also the brighten instrument.

array-interp and dot-product
array-interp fn x &optional size
dot-product in1 in2
edot-product freq data [Scheme/C versions]
mus-interpolate type x v size y1

These functions underlie some of the generators, and can be called within run. See mus.lisp for details. array-interp can be used for companding and similar functions — load the array (call it "compander" below) with the positive half of the companding function, then:

  (let ((in-val (readin rd))            ; in-coming signal
        (func-len (length compander)))  ; size of array
    (* (signum in-val) 
       (array-interp compander (abs (* in-val (1- func-len))) func-len)))

dot-product is the usual "inner product" or "scalar product".

mus-interpolate is the function used whenever table lookup interpolation is requested, as in delay or wave-train. The type is one of the interpolation types (mus-interp-linear, for example).

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.

Waveshaping can provide a similar effect:

(definstrument brighten (start duration file file-maxamp partials)
  (multiple-value-bind (beg end) (times->samples start duration)
    (let ((fil (open-input* file)))
      (when fil
        (unwind-protect
	  (let ((coeffs (partials->polynomial (normalize-partials partials)))
		(rd (make-readin fil)))
	    (run (loop for i from beg below end do
		   (outa i (* file-maxamp (polynomial coeffs (/ (readin rd) file-maxamp)))))))
	  (close-input fil))))))

(with-sound () (brighten 0 3 "oboe" .15 '(1 1 3 .5 7 .1)))

In this case, it is important to scale the file input to the waveshaper to go from -1.0 to 1.0 to get the full effect of the Chebyshev polynomials. Unfortunately, if you don't add an overall amplitude envelope to bring the output to 0, you'll get clicks if you include even numbered partials. These partials create a non-zero constant term in the polynomial, so when the sound decays to 0, the polynomial output decays to some (possibly large) non-zero value. In the example above, I've used only odd partials for this reason. Another thing to note here is that the process is not linear; that is the sinusoids that make up the input are not independently expanded into the output spectrum, but instead you get sum and difference tones, (not to mention phase cancellations) much as in FM with a complex wave.

ring-modulate and amplitude-modulate
ring-modulate in1 in2
amplitude-modulate am-carrier input1 input2
ring-modulate returns (* in1 in2). amplitude-modulate returns (* input1 (+ am-carrier input2))

ring-modulation is sometimes called "double-sideband-suppressed-carrier" modulation — that is, amplitude modulation with the carrier subtracted out (set to 0.0 above). The nomenclature here is a bit confusing — I can't remember now why I used these names; think of "carrier" as "carrier amplitude" and "input1" as "carrier". Normal amplitude modulation using this function would be:

(defvar carrier (make-oscil carrier-freq (* .5 pi)))
...
(amplitude-modulate 1.0 (oscil carrier) signal)

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 e^(iwt) 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.

We can use these to shift all the components of a signal by the same amount up or down ("single-sideband modulation").

FFT
fft rdat idat fftsize &optional sign
make-fft-window &optional-key type size (beta 0.0) (alpha 0.0)
multiply-arrays rdat window
rectangular->polar rdat idat
rectangular->magnitudes rdat idat
polar->rectangular rdat idat
spectrum rdat idat window norm-type
convolution rdat idat size
autocorrelate dat1 size
correlate dat1 dat2 size

These provide run-time access to the standard fft routines and their habitual companions. make-fft-window can return many of the standard windows including:

  rectangular-window   ; no change in data
  bartlett-window      ; triangle
  parzen-window        ; raised triangle
  welch-window         ; parzen squared
  hann-window          ; cosine (sometimes known as "hanning-window" — a sort of in-joke)
  hamming-window       ; raised cosine
  blackman2-window     ; Blackman-Harris windows of various orders
  blackman3-window
  blackman4-window     ; also blackman5..10
  exponential-window
  kaiser-window        ; beta argument used here

The magnitude of the spectrum is returned by rectangular->polar. The data can be windowed with multiply-arrays. spectrum calls the fft, translates to polar coordinates, then returns the results (in the lower half of "rdat") in dB (norm-type = 0), or linear normalized to 1.0 (norm-type = 1), or linear unnormalized (norm-type not 0 or 1).

The following instrument implements fft overlap-add, but instead of scaling the various spectral components to filter a sound, it reverses a portion of the spectrum, a distortion that can be effective with speech sounds.

(definstrument inside-out (beg dur file amp lo hi &optional (fftsize 1024))
  ;; fft overlap-add (and buffer), but the fft bins between lo and hi are reversed
  (let ((fil (open-input* file)))
    (when fil
      (unwind-protect
        (let* ((start (floor (* beg *srate*)))
               (end (+ start (floor (* dur *srate*))))
               (fdr (make-double-float-array fftsize))
               (fdi (make-double-float-array fftsize))
               (wtb (make-double-float-array fftsize))
               (filptr 0)
               (fft2 (floor fftsize 2))
               (fft4 (floor fftsize 4))
               (ctr fft2)
               (fftn (/ 1.0 fftsize))
               (first-time 1)
               (mid (* .5 (+ hi lo))))
	  (when (zerop lo) (setf lo 1))
          (run
           (loop for i from start below end do
             (when (= ctr fft2)
               (clear-array fdr)
               (clear-array fdi)
               (dotimes (k fft2)
                 (setf (aref fdr (+ k fft4)) (* (ina filptr fil) fftn))
                 (incf filptr))
               (fft fdr fdi fftsize 1)
               (let ((j1 hi) ; now reverse bins between lo and hi
                     (k0 (- fftsize lo))
                     (k1 (- fftsize hi)))
                 (loop for j0 from lo to mid do
                   (let ((tmprj (aref fdr j0))
                         (tmprk (aref fdr k0))
                         (tmpij (aref fdi j0))
                         (tmpik (aref fdi k0)))
                     (setf (aref fdr j0) (aref fdr j1))
                     (setf (aref fdr j1) tmprj)
                     (setf (aref fdr k0) (aref fdr k1))
                     (setf (aref fdr k1) tmprk)
                     (setf (aref fdi j0) (aref fdi j1))
                     (setf (aref fdi j1) tmpij)
                     (setf (aref fdi k0) (aref fdi k1))
                     (setf (aref fdi k1) tmpik)
                     (incf k1)
                     (decf k0)
                     (decf j1))))
               (fft fdr fdi fftsize -1)
               (dotimes (k fft2)
                 (setf (aref wtb k) (aref wtb (+ k fft2)))
                 (setf (aref wtb (+ k fft2)) 0.0))
               (if (= first-time 1)
                   (progn
                     (dotimes (k fftsize) (setf (aref wtb k) (aref fdr k)))
                     (setf first-time 0)
		     (setf ctr fft4))
                 (progn
                   (dotimes (k fft2) (incf (aref wtb k) (aref fdr k)))
                   (dotimes (k fft2) (setf (aref wtb (+ k fft2)) (aref fdr (+ k fft2))))
		   (setf ctr 0))))
             (outa i (* amp (aref wtb ctr)))
             (incf ctr))))
        (close-input fil)))))

(with-sound () (inside-out 0 1.0 "fyow" 1.0 3 8))

There are many other examples of run-time FFTs: the cross-synthesis instrument above, san.ins, and anoi.ins.

def-clm-struct

def-clm-struct is syntactically like def-struct, but sets up the struct field names for the run macro. There are several examples in prc-toolkit95.lisp, and other instruments. The fields can only be of a numerical type (no generators, for example).

Definstrument

definstrument defines an instrument in CLM. Its syntax is almost the same as defun; it has a few bizarre options (for miserable historical reasons), but they should be resolutely ignored. There are a bazillion example instruments included in CLM and Snd. The following instruments live in *.ins files in the CLM directory (see also the file ins), and in various *.scm, *.rb, and *.fs files in the Snd tarball. If you're reading this file from outside ccrma, and the instrument url has snd/snd, change that to clm/clm.

instrument function CL Scheme Ruby Forth
complete-add additive synthesis add.ins
addflts filters addflt.ins dsp.scm dsp.rb
add-sound mix in a sound file addsnd.ins
bullfrog et al many animals (frogs, insects, birds) animals.scm
anoi noise reduction anoi.ins clm-ins.scm clm-ins.rb clm-ins.fs
autoc pitch estimation (Bret Battey) autoc.ins
badd fancier additive synthesis (Doug Fulton) badd.ins
fm-bell fm bell sounds (Michael McNabb) bell.ins clm-ins.scm clm-ins.rb clm-ins.fs
bigbird waveshaping bigbird.ins bird.scm bird.rb clm-ins.fs, bird.fs
canter fm bagpipes (Peter Commons) canter.ins clm-ins.scm clm-ins.rb clm-ins.fs
cellon feedback fm (Stanislaw Krupowicz) cellon.ins clm-ins.scm clm-ins.rb clm-ins.fs
cnvrev convolution (aimed at reverb) cnv.ins clm-ins.scm
moving sounds sound movement (Fernando Lopez-Lezcano) dlocsig.lisp dlocsig.scm dlocsig.rb
drone additive synthesis (bag.clm) (Peter Commons) drone.ins clm-ins.scm clm-ins.rb clm-ins.fs
expandn granular synthesis (Michael Klingbeil) expandn.ins clm-ins.scm
granulate-sound examples granular synthesis expsrc.ins clm-ins.scm clm-ins.rb clm-ins.fs
cross-fade cross-fades in the frequency domain fade.ins fade.scm
filter-sound filter a sound file fltsnd.ins dsp.scm
stereo-flute physical model of a flute (Nicky Hind) flute.ins clm-ins.scm clm-ins.rb clm-ins.fs
fm examples fm bell, gong, drum (Paul Weineke, Jan Mattox) fmex.ins clm-ins.scm clm-ins.rb clm-ins.fs
Jezar's reverb fancy reverb (Jezar Wakefield) freeverb.ins freeverb.scm freeverb.rb clm-ins.fs
fofins FOF synthesis sndclm.html clm-ins.scm clm-ins.rb clm-ins.fs
fullmix a mixer fullmix.ins clm-ins.scm clm-ins.rb clm-ins.fs
grani granular synthesis (Fernando Lopez-Lezcano) grani.ins grani.scm
grapheq graphic equalizer (Marco Trevisani) grapheq.ins clm-ins.scm clm-ins.rb clm-ins.fs
fm-insect fm insect.ins clm-ins.scm clm-ins.rb
jc-reverb a reverberator (see also jlrev) jcrev.ins jcrev.scm clm-ins.rb clm-ins.fs
fm-voice fm voice (John Chowning) jcvoi.ins jcvoi.scm
kiprev a fancier reverberator (Kip Sheeline) kiprev.ins
lbj-piano additive synthesis piano (Doug Fulton) lbjPiano.ins clm-ins.scm clm-ins.rb clm-ins.fs
maraca Perry Cook's maraca physical models maraca.ins maraca.scm maraca.rb
maxfilter Juan Reyes modular synthesis maxf.ins maxf.scm maxf.rb
mlb-voice fm voice (Marc LeBrun) mlbvoi.ins clm-ins.scm clm-ins.rb clm-ins.fs
moog filters Moog filters (Fernando Lopez-Lezcano) moog.lisp moog.scm
fm-noise noise maker noise.ins noise.scm noise.rb clm-ins.fs
nrev a popular reverberator (Michael McNabb) nrev.ins clm-ins.scm clm-ins.rb clm-ins.fs
one-cut "cut and paste" (Fernando Lopez-Lezcano) one-cut.ins
p Scott van Duyne's piano physical model piano.ins piano.scm piano.rb
pluck Karplus-Strong synthesis (David Jaffe) pluck.ins clm-ins.scm clm-ins.rb clm-ins.fs
pqw waveshaping pqw.ins clm-ins.scm clm-ins.rb clm-ins.fs
pqw-vox waveshaping voice pqwvox.ins clm-ins.scm clm-ins.rb clm-ins.fs
physical models physical modelling (Perry Cook) prc-toolkit95.lisp prc95.scm prc95.rb clm-ins.fs
various ins from Perry Cook's Synthesis Toolkit prc96.ins clm-ins.scm clm-ins.rb clm-ins.fs
pvoc phase vocoder (Michael Klingbeil) pvoc.ins pvoc.scm pvoc.rb
resflt filters (Xavier Serra, Richard Karpen) resflt.ins clm-ins.scm clm-ins.rb clm-ins.fs
reson fm formants (John Chowning) reson.ins clm-ins.scm clm-ins.rb clm-ins.fs
ring-modulate ring-modulation of sounds (Craig Sapp) ring-modulate.ins examp.scm examp.rb
rmsenv rms envelope of sound (Bret Battey) rmsenv.ins
pins spectral modelling san.ins clm-ins.scm clm-ins.rb clm-ins.fs
scanned Juan Reyes scanned synthesis instrument scanned.ins dsp.scm
scentroid spectral scentroid envelope (Bret Battey) scentroid.ins dsp.scm
shepard Shepard tones (Juan Reyes) shepard.ins sndscm.html
singer Perry Cook's vocal tract physical model singer.ins singer.scm singer.rb
sndwarp Csound-like sndwarp generator (Bret Battey) sndwarp.ins sndwarp.scm
stochastic Bill Sack's stochastic synthesis implementation stochastic.insstochastic.scm
bow Juan Reyes bowed string physical model strad.ins strad.scm strad.rb
track-rms rms envelope of sound file (Michael Edwards) track-rms.ins
fm-trumpet fm trumpet (Dexter Morrill) trp.ins clm-ins.scm clm-ins.rb clm-ins.fs
various ins granular synthesis, formants, etc ugex.ins clm-ins.scm clm-ins.rb
test ins CLM regression tests — see clm-test.lisp ug(1,2,3,4).ins clm23.scm
fm-violin fm violin (fmviolin.clm, popi.clm) v.ins v.scm v.rb clm-ins.fs
vowel vowels (Michelle Daniels) vowel.ins
vox fm voice (cream.clm) vox.ins clm-ins.scm clm-ins.rb clm-ins.fs
zc, zn interpolating delays zd.ins clm-ins.scm clm-ins.rb clm-ins.fs
zipper The 'digital zipper' effect. zipper.ins zip.scm zip.rb

The file clm-test.lisp exercises most of these instruments. If you develop an interesting instrument that you're willing to share, please send it to me (bil@ccrma.stanford.edu).

Although all the examples in this document use run followed by a loop, you can use other constructs instead:

(definstrument no-loop-1 (beg dur)
  (let ((o (make-oscil 660)))
    (run 
     (let ((j beg)) 
       (loop for i from 0 below dur do
	 (outa (+ i j) (* .1 (oscil o))))))))

(definstrument no-loop-2 (beg dur)
  (let ((o (make-oscil 440)))
    (run
     (dotimes (k dur)
       (outa (+ k beg) (* .1 (oscil o)))))))

And, of course, out-any and locsig can be called any number of times (including zero) per sample and at any output location. Except in extreme cases (spraying samples to random locations several seconds apart), there is almost no speed penalty associated with such output, so don't feel constrained to write an instrument as a sample-at-a-time loop. That form was necessary in the old days, so nearly all current instruments still use it (they are translations of older instruments), but there's no good reason not to write an instrument such as:

(definstrument noisey (beg dur)
  (run
   (dotimes (i dur)
     (dotimes (k (random 10))
       (outa (+ beg (floor (random dur))) (centered-random .01))))))
Note lists

A note list in CLM is any lisp expression that opens an output sound file and calls an instrument. The simplest way to do this is with with-sound or clm-load.

with-sound
 with-sound &key 
   ;; "With-sound: check it out!" — Duane Kuiper, Giants broadcaster after Strawberry homer
   (output *clm-file-name*)        ; name of output sound file ("test.snd" normally)
   (channels *clm-channels*)       ; can be any number (defaults to 1, see defaults.lisp)
   (srate *clm-srate*)             ; also 'sampling-rate' for backwards compatibility
   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; an unquoted list
   (reverb-channels *clm-reverb-channels*) ; chans in temp reverb stream (input to reverb)
   revfile                         ; reverb file name
   (play *clm-play*)               ; play new sound automatically?
   (notehook *clm-notehook*)       ; function evaluated on each instrument call
   (statistics *clm-statistics*)   ; print out various fascinating numbers
   (decay-time 1.0)                ; ring time of reverb after end of piece
   comment                         ; comment placed in header (set to :none to squelch comment)
   info                            ; non-comment header string
   (header-type *clm-header-type*) ; output file type (see also header types)
   (data-format *clm-data-format*) ; output data format (see header types)
   save-body                       ; if t, copy the body (as a string) into the header
   scaled-to                       ; if a number, scale results to have that max amp
   scaled-by                       ; scale output by some number
   (clipped *clm-clipped*)         ; if t, clip output rather than allowing data to wrap-around
   (verbose *clm-verbose*)         ; some instruments use this to display info during computation
   (force-recomputation nil)       ; if t, force with-mix calls to recompute

with-sound is a macro that performs all the various services needed to produce and play a sound file; it also wraps an unwind-protect around its body to make sure that everything is cleaned up properly if you happen to interrupt computation; at the end it returns the output file name. with-sound opens an output sound file, evaluates its body (normally a bunch of instrument calls), applies reverb, if any, as a second pass, and plays the sound, if desired. The sound file's name defaults to "test.snd" or something similar; use the output argument to write some other file:

  (with-sound (:output "new.wave") (fm-violin 0 1 440 .1))

The channels, srate, data-format, and header-type arguments set the sound characteristics. The default values for these are set in defaults.lisp. Reverberation is handled as a second pass through a reverb instrument (nrev.ins for example). The reverb argument sets the choice of reverberator.

(with-sound (:output "new.snd") (simp 0 1 440 .1))
(with-sound (:srate 44100 :channels 2) ...)
(with-sound (:reverb jc-reverb) ...)
(with-sound (:reverb nrev :reverb-data (:reverb-factor 1.2 :lp-coeff .95))...)

With-sound can be called within itself, so you can make an output sound file for each section of a piece as well as the whole thing, all in one run. Since it is the basis of with-mix and sound-let, all of these can be nested indefinitely:

(with-sound () 
  (mix (with-sound (:output "hiho.snd") 
            (fm-violin 0 1 440 .1))))

(with-sound ()
  (with-mix () "s1" 0
    (sound-let ((tmp ()
                  (fm-violin 0 1 440 .1)))
      (mix tmp))))

(with-sound (:verbose t)
  (with-mix () "s6" 0
    (sound-let ((tmp ()
                  (fm-violin 0 1 440 .1))
                (tmp1 (:reverb nrev)
                  (mix "oboe.snd")))
      (mix tmp1)
      (mix tmp :output-frame *srate*))
    (fm-violin .5 .1 330 .1)))

(with-sound (:verbose t)
  (sound-let ((tmp ()
                (with-mix () "s7" 0
                  (sound-let ((tmp ()
                                (fm-violin 0 1 440 .1))
                              (tmp1 ()
                                (mix "oboe.snd")))
                   (mix tmp1)
                   (mix tmp :output-frame *srate*))
                 (fm-violin .5 .1 330 .1))))
    (mix tmp)))

You can call with-sound within an instrument:

(definstrument msnd (beg dur freq amp)
  (let ((os (make-oscil freq)))
    (run
     (loop for i from beg below (+ beg dur) do
       (outa i (* amp (oscil os)))))))

(definstrument call-msnd (beg dur sr amp)
  (let* ((temp-file (with-sound (:output "temp.snd") (msnd 0 dur 440.0 .1)))
	 (tfile (open-input temp-file))
	 (reader (make-src :input tfile :srate sr))
	 (new-dur (/ dur sr)))
    (run
     (loop for i from beg below (+ beg new-dur) do
       (outa i (* amp (src reader)))))
    (close-input tfile)
    (delete-file temp-file)))

Besides :channels, :reverb, and :srate, the most useful options are :scaled-to and :statistics. statistics, if t, causes clm to keep track of a variety of interesting things and print them out at the end of the computation. scaled-to tells clm to make sure the final output file has a maxamp of whatever the argument is to :scaled-to — that is,

(with-sound (:scaled-to .5) 
  (dotimes (i 32) (mix "oboe.snd" :output-frame (* i *srate*))))

will produce test.snd with a maxamp of .5, no matter how loud the intermediate mix actually is. Similarly, the scaled-by argument causes all the output to be scaled (in amplitude) by its value.

(with-sound (:scaled-by 2.0) (fm-violin 0 1 440 .1)) 

produces a note that is .2 in amplitude.

If revfile is specfied, but not reverb, the reverb stream is written to revfile, but not mixed with the direct signal in any way. Normally the reverb output is not deleted by with-sound; you can set *clm-delete-reverb* to t to have it deleted automatically.

The macro scaled-by scales its body by its first argument (much like with-offset):

(with-sound () 
  (fm-violin 0 1 440 .1)
  (scaled-by 2.0
    (fm-violin 0 .25 660 .1)) ; actual amp is .2
  (fm-violin .5 440 .1))

There is also the parallel macro scaled-to. These are built on the macro with-current-sound which sets up an embedded with-sound call with all the current with-sound arguments in place except output, comment, scaled-to, and scaled-by.

Other with-sound options that might need explanation are :notehook and :continue-old-file.

Notehook declares a function that is evaluated each time any instrument is called. The arguments passed to the notehook function are the current instrument name (a string) and all its arguments. The following prints out the instrument arguments for any calls on simp that are encountered:

(with-sound (:notehook
              #'(lambda (name &rest args) 
		  (when (string-equal name "simp")
	            (print (format nil "(simp ~{~A ~})" args))
                    (force-output))))
  (simp 0 1 440 .1)
  (toot .5 .5 660 .2))

If the notehook function returns :done, the instrument exits immediately.

Continue-old-file, if t, re-opens a previously existing file for further processing. Normally with-sound 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. The option data-format underlies :scaled-to. CLM can read and write sound data in all the currently popular formats, leaving aside proprietary compression schemes. The names used in :data-format can be found in initmus.lisp, along with the headers CLM knows about.

You can make your own specialized versions of with-sound:

(defmacro with-my-sound ((&rest args) &body body)
  `(let ((filename (with-sound ,args ,.body)))
     ;; any post-processing you like here
     filename))

One such specialization is with-threaded-sound, available in sbcl if you built sbcl with threads. with-threaded-sound looks exactly like with-sound, but each note (each separate expression in the with-sound body) is handled by a separate thread.

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

If start a thread for each note, then join them all at once, the computation slows down a lot due to all the thread overhead, so *clm-threads* sets the number of threads running through the note list at any one time. It defaults to 4. You can speed up with-threaded-sound if you set *clm-file-buffer-size* large enough to accommodate the entire output, then pass :output-safety 1 to with-threaded-sound. Even so, my tests indicate that it is sometimes faster to use with-sound; I need to figure out why...

clm-load is the same as with-sound, but its first argument is the name of a file containing clm instrument calls (i.e. the body of with-sound), the reverb argument is the name of the reverb function, and the reverb-data argument is the list; that is, clm-load's arguments look like normal lisp, whereas with-sound's are unquoted in these two cases.

(with-sound (:reverb jc-reverb :reverb-data (:volume .3)) ...)
(clm-load "test.clm" :reverb 'jc-reverb :reverb-data '(volume .3))

The with-sound output is normally sent to the speakers via the play function. There are several associated functions:

play &optional file start end wait
dac &optional file start end wait
sl-dac file &optional (output-device mus-audio-default)
stop-playing
stop-dac

play (or dac) starts playing file (or the last file played, if no argument is given); in some cases (MCL and ACL) it then returns to the lisp listener; to interrupt the dac in those cases, use stop-playing (or stop-dac). Currently, play calls the sndplay program if possible; sl-dac is the same thing, but calls the sl_dac function. The latter gives you control over the output device (sndplay will also someday). In some cases, sndplay's default buffer size is not ideal; you can use *clm-player* and sndplay's bufsize argument to set it to the correct value for your audio system. play's start and end arguments are in seconds, and default to playing the entire sound. The wait argument in some cases causes the play call to wait until the complete sound has been played before returning to the listener.

The *clm-* variables (like *clm-srate*) set the default values. The corresponding un-clm'd versions (*srate*) hold the current values. So, if with-sound doesn't include the :srate argument, *srate* is the same as *clm-srate*; otherwise it reflects the :srate value for the duration of the with-sound call. The local variables that are currently exported are: *srate*, *safety*, and *debug*. Unexported, but available in the clm package are *channels*, *data-format*, *header-type*, *notehook*, *clipped*, *verbose*, and *statistics*.

mus-float-equal-fudge-factor     how far apart values can be and still be considered equal
mus-array-print-length ()        how many array (vct) elements to print in mus-describe
mus-file-buffer-size ()          size of input/ouput buffers (default 8192)
make-fir-coeffs (order spectr)
mus-srate ()                     current sampling rate
with-mix
with-mix options file begin &body body

With-mix is a macro, callable within with-sound or clm-load, which 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 does not need to be recomputed.

(with-sound () 
  (fm-violin 0 .1 440 .1)
  (with-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 "/zap/slow.snd"))

Now, if we change just the first note in the with-mix call, the second 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.

With-mix performs a string comparison of its body to decide whether it needs to recompute its note calls. It then loads that body from a separate saved file. This can be confusing if global variables are present.
  > USER(2): (let ((rstr .1)) (with-sound () (with-mix () "sec" 0 (fm-violin 0 1 440 rstr))))
  > ; Loading /zap/sec.clm
  > Error: Attempt to take the value of the unbound variable `RSTR'.
Here the code evaluated is basically (let ((rstr .1)) (load "/zap/sec.clm")) where rstr has lexical scope. To make rstr visible within the load,
  (let ((rstr1 .1)) 
    (declare (special rstr1))
    (with-sound () (with-mix () "sec" 0 (fm-violin 0 1 440 rstr1))))
but if you then evaluate the same form again, changing rstr1 to (say) .5, with-mix does not notice that rstr1's value has changed, so it does not recompute its body, leaving the resultant amplitude at .1.

The fastest way to mix sound files is with mix:

mix &optional-key filename (input-frame 0) (output-frame 0) frames output
c-level IO
open-input &optional name &key start channel restartable
close-input i-stream
open-input* name &key start channel restartable

These functions open and close input sound files. open-input takes either a string or a pathname and returns an IO object. Various clm functions use that object as a handle on the file. The variable *clm-file-name*, used as the default name in most such calls, is "/zap/test.snd" at CCRMA.

Open-input normally opens the sound file name and returns a list or perhaps a 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. If you want the convenience of the directory search (see *clm-search-list*) open-input*. Normally if open-input* can't find a file, it prints a warning and returns nil. If you would rather that it drop into the debugger with an option to specify a new file name at that time, set the restartable argument to t.

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, providing 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. start is the sample to start at when reading the first data buffer. end is the sample to stop at when reading the initial buffer (it defaults to buffer-size). If you are reading only a small portion of a file many times, you can save some time by setting explicitly the bounds of the initial read via start and end. 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).

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)))
  (granulate-sound temp-1 0 2 0 2);temp-1's value is the name of the temp file
  (granulate-sound temp-2 1 1 0 2))

This creates two temporary files and passes them along to the subsequent calls on granulate-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, which can contain embedded sound-lets. The difference between sound-let and a simple embedded with-sound is primarily that sound-let names and later deletes the temporary files it creates, whereas with-sound leaves its output intact.

clm defaults

These default values are set in defaults.lisp. Generally, the default value is *clm-<var>, and the current dynamic value of that variable is *<var>*.

*clm-array-print-length*number of IO data buffer elements printed
*clm-channels*default output channels (1)
*clm-clipped*default for clipped arg in with-sound
*clm-dac-wait-default*default choice of whether play function should wait for completion
*clm-data-format*default output sound file data format
*clm-date*creation date of the current version
*clm-default-frequency*default make-* frequency (0.0)
*clm-delete-reverb*should with-sound delete the temporary reverb output (default nil)
*clm-file-buffer-size*IO buffer sizes (in samples)
*clm-file-name*default sound file name
*clm-header-type*default output sound file header type
*clm-init*name of site-specific initializations (see clm-init.lisp)
*clm-instruments*list of the currently loaded clm instruments
*clm-locsig-type*locsig interpolation choice (mus-interp-linear or mus-interp-sinusoidal)
*clm-news*brief list of recent changes (HISTORY.clm)
*clm-notehook*default for notehook arg in with-sound
*clm-play*default for play arg in with-sound
*clm-player*user-supplied DAC function
*clm-reverb-channels*reverb stream chans in with-sound
*clm-safety*default safety setting (run loop debugging choices)
*clm-search-list*pathname list for file searches (open-input*)
*clm-srate*default sampling rate (44100)
*clm-statistics*default statistics arg in with-sound
*clm-table-size*default table-lookup table size (in Scheme, the associated function is clm-table-size)
*clm-tempfile-data-format*intermediate with-sound file data format
*clm-tempfile-header-type*intermediate with-sound file header type
*clm-version*version identifier (a number — also *clm-revision*)
*output*current output stream (for outa and friends)
*reverb*current reverb stream
two-pi2*pi

*clm-player* can be used to override CLM's normal play routine (which calls sndplay in most cases).

(setf *clm-player* (lambda (name) (clm::run-in-shell "sndplay" (format nil "~A -bufsize 1024" name))))

On machines with plenty of memory and slow disks, you can speed up CLM computations by setting *clm-file-buffer-size* to some number larger than its default (65536):

  (let ((*clm-file-buffer-size* (* 1024 1024))) (with-sound ...) 

The macro with-offset can be used to set local begin time offsets. Its argument is in seconds:

(with-sound () 
  (fm-violin 0 1 440 .1)
  (with-offset 1.0
    (fm-violin 0 .25 660 .1)) ; actually starts at 1.0
  (fm-violin .5 440 .1))
examples

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 several *.clm files included in the clm distribution. clm-test.lisp runs my standard set of regression tests, exercising many of the instruments. pitches.cl provides the standard pitch names as lisp variables (a4 = 440.0 and so on).

run*

run* takes two arguments, a list of variables, and the usual run macro body. The run body is executed (in C normally) and then the variables are set to the values they had when the run loop exited. This extension of run is needed because in C instruments, everything that happens within the run loop is normally hidden from the lisp interpreter; if you set a global variable's value, for example, only the run-specific version of that variable is affected. You need run* to return such values back to Lisp.

(definstrument p (beg dur frq amp)
  (let* ((s (make-oscil frq))
	 (start (floor (* beg *srate*)))
	 (end (+ start (floor (* dur *srate*))))
	 (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 instrument is Michael Edwards' track-rms.ins; see also the sr3 instrument in ug.ins. Here's another instrument that implements legato between notes by using the previous note's phases:

(defstruct fmins carrier modulator)
(definstrument fmsimp (beg dur frq amp ind &optional previous-oscils)
  (let* ((start (floor (* *srate* beg)))
	 (end (+ start (floor (* *srate* dur))))
	 (carrier (if previous-oscils
		      (fmins-carrier previous-oscils)
		    (make-oscil)))
	 (modulator (if previous-oscils
			(fmins-modulator previous-oscils)
		      (make-oscil))))
    (setf (mus-frequency carrier) frq)
    (setf (mus-frequency modulator) frq)
    (run* (carrier modulator)
     (loop for i from start below end do
       (outa i (* amp (oscil carrier (* ind (oscil modulator)))))))
    (if previous-oscils
	(progn
	  (setf (fmins-carrier previous-oscils) carrier)
	  (setf (fmins-modulator previous-oscils) modulator)))))

;;; (defvar oscs (make-fmins :carrier (make-oscil) :modulator (make-oscil)))
;;; (with-sound () (fmsimp 0 1.01 440 .1 0.0 oscs) (fmsimp 1.01 1 660 .1 0.0 oscs))
;;; (with-sound () (fmsimp 0 1.01 440 .1 0.0) (fmsimp 1.01 1 660 .1 0.0))
;;;     the 1.01 (as opposed to 1.0) is needed because the phases line up just by chance in the 1.0 case
;;;     for portamento, the instrument could notice an in-coming osc set and
;;;     change the frequency envelope accordingly
Debugging

CLM provides several built-in data display and instrument debugging aids. But debugging an instrument is still too much pain. I suggest that you develop the algorithm in Snd/Scheme where there are elaborate and robust debugging tools.

The optimize safety option can be used to check for array index and null generator problems (these will be reported as bus errors and segmentation faults, sigh). (pushnew :debug *features*) before loading CLM to see what it is sending to the C compiler.

The error handler

When you hit an error within with-sound, depending on the context of the error and the lisp you're running, you'll see a variety of restart options:

  Restart actions (select using :continue):
   0: return from break.
   1: try to exit current note cleanly and go on.
   2: abort current note.
   3: close files and return to top-level.
   4: jump past remaining notes.

The last four are provided by CLM. The first tries to jump to the end of the current instrument, allowing open input files to be closed and so forth. The second jumps out of the current note, but tries to continue processing the body of with-sound. The third closes all files and jumps out of with-sound. The fourth jumps to the end of the body of with-sound and tries to handle all the usual with-sound closing options such as reverb, statistics, and scaling.

If you hit a C error (segfault, etc), start gdb with lisp ('gdb /usr/local/lisp/acl'), 'run', load clm, run your instrument, then when the error drops you into the gdb debugger, 'where'. This should give you some idea where the problem is. In the worst case, trace clm::run-in-shell and compile/load the instrument to find out what the C compilation sequence is on your machine; next, make whatever changes you like to the instrument C code (produced by the run macro, named clm_INSNAME.c); to add a print statement that will send its output to the lisp listener, use the function mus_error with a first argument of 0; next run the C compiler and loader, making a new instrument object file, start gdb with lisp, run lisp loading clm, load your instrument, and run it.

In Windows using ACL, you can get into the debugger via:

c:\program files\acl80\mlisp.exe -! -I mlisp.dxl 

or some facsimile thereof. When you hit a segfault, get a call stack (stack trace). I don't know how to print out function arguments in this case. In ACL, you can maximize the lisp-level information at an error with:

:zoom :all t :verbose t :count t

ffi-test.lisp has a bunch of FFI calls outside the with-sound/definstrument context. The base CLM test suite is clm-test.lisp, but it's more of a "regression" test — I run it in the various lisps whenever I make some code change.

Appendices
header and data types

CLM can write NeXT/Sun, AIFF/AIFC, RIFF ("wave"), RF64, CAFF, raw (no header), NIST-sphere, and "old-style" IRCAM headers. The default choice is set by *clm-header-type* set in defaults.lisp. The output data format is normally 16-bit signed (2's complement) integer; the default is set by *clm-data-format*. CLM can read most standard headers, and can read and write most uncompressed data formats.

read/write (many data formats):

    NeXT/Sun/DEC/AFsp
    AIFF/AIFC
    RIFF (Microsoft wave)
    RF64
    IRCAM (old style)
    NIST-sphere
    CAFF
    no header ("raw")

read-only (in selected data formats):

    8SVX (IFF), EBICSF, INRS, ESPS, SPPACK, ADC (OGI), AVR, VOC, PVF,
    Sound Tools, Turtle Beach SMP, SoundFont 2.0, Sound Designer I, PSION, MAUD, Kurzweil 2000,
    Gravis Ultrasound, ASF, PAF, CSL, Comdisco SPW, Goldwave sample, omf, quicktime, sox,
    Sonic Foundry, SBStudio II, Delusion digital, Digiplayer ST3, Farandole Composer WaveSample,
    Ultratracker WaveSample, Sample Dump exchange, Yamaha SY85, SY99, and TX16, Covox v8, AVI, 
    Impulse tracker, Korg, Akai, Turtle Beach, Matlab-5

automatically translated to a readable format:

    IEEE text, Mus10, SAM 16-bit (modes 1 and 4), AVI, NIST shortpack, HCOM, Intel, 
    IBM, and Oki (Dialogic) ADPCM, G721, G723_24, G723_40, MIDI sample dump, Ogg, Speex, 
    Flac, Midi, Mpeg, Shorten, Wavepack, tta (via external programs)

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

The CLM names for the output header types, as used with the :header-type argument to with-sound, are mus-aiff, mus-aifc, mus-next, mus-riff, mus-rf64, and mus-ircam. The data-formats that are exported from the clm package are mus-bshort, mus-lshort, mus-bint, mus-lint, mus-bfloat, mus-lfloat, mus-mulaw, mus-alaw, mus-byte, mus-ubyte, mus-b24int, mus-l24int, mus-bdouble, and mus-ldouble. The "b" stands for big-endian, "l" for little-endian, "u" for unsigned. The other header and data format possibilities are listed in initmus.lisp.

If you are trying to read raw (no header) sound files, CLM's default settings for the sampling rate, channels, and data format are 44100, 2, and mus-bshort respectively. To change these, call (mus-set-raw-header-defaults srate chans format):

(mus-set-raw-header-defaults 8012 1 mus-mulaw)
(open-input "raw.snd")

treats "raw.snd" as mono µlaw data at 8012 Hz.

clm-init.lisp

If the file clm-init.lisp exists in the same directory as all.lisp, or if you set the clm variable *clm-init* to point to some file, then CLM loads that file upon initialization. Here is my clm-init.lisp:

(compile-and-load "v")
(compile-and-load "jcrev")
;;; my two favorite instruments

(setf *clm-search-list* 
  (append *clm-search-list* 
	  (list "/home/bil/cl/oboe.snd"
		"/home/bil/test/sounds/test.snd")))
;;; these are my standard sound file directories — by including 
;;; these in the search list I don't need to remember where each 
;;; file happens to be.  The file names are just fillers —
;;; the important part of the path is the directory.
saved images

Many lisps have some mechanism to dump the current lisp image as an executable file. In ACL or MCL, some of CLM's state at run-time is handled in C-based foreign-function modules that are opaque to Lisp, so there are cases where the naive use of dumplisp (acl), or save-application (mcl) can fail with a segmentation fault or some other equally un-informative error message. This should only be a problem when the saved image has called clm-initialize-links (within with-sound or dac or some such function); if you build a clm image and immediately save it, everything should work without problem. Once clm-initialize-links has been called, the C modules assume they have been initialized; if code in the saved version of a module is then executed, the un-initialized variables may be accessed. To get around this problem, call restart-clm before doing anything in the newly executed image.

Index
all-passenvelope->coeffsmake-granulatemus-nameraw data
all-pass?envelope-expmake-identity-mixermus-nextreadin
amplitude-modulateenvelope-interpmake-iir-filtermus-offsetreadin?
array->fileEnvelopesmake-locsigmus-orderrectangular->magnitudes
array-interpfftmake-mixermus-output?rectangular->polar
asymmetric-fmfile->arraymake-move-soundmus-phaserestart-clm
asymmetric-fm?file->framemake-moving-averagemus-ramp*reverb*
autocorrelatefile->frame?make-ncosmus-randomring-modulate
Checkpointsfile->samplemake-notchmus-resetrun
clear-arrayfile->sample?make-nrxycosmus-rf64run*
CLM Initialization filtermake-nrxysinmus-riffRun support for Lisp
*clm-array-print-length*filter?make-nsinmus-runsample->file
*clm-channels*filtered-combmake-one-polemus-scalersample->file?
*clm-clipped*filtered-comb?make-one-zeromus-set-rand-seedsample->frame
*clm-dac-wait-default*Filtersmake-oscilmus-set-raw-header-defaultssamples->seconds
*clm-data-format*fir-filtermake-phase-vocodermus-sratesampling-rate conversion
*clm-date*fir-filter?make-polyshapemus-widthsaved images
*clm-default-frequency*firmantmake-polywavemus-xcoeffsawtooth-wave
*clm-delete-reverb*firmant?make-pulse-trainmus-xcoeffssawtooth-wave?
*clm-file-buffer-size*formantmake-randmus-ycoeffscale-envelope
*clm-file-name*formant?make-rand-interpmus-ycoeffsseconds->samples
*clm-header-type*Fourier transforms make-readinncossl-dac
*clm-init*frame*make-sample->filencos?Sound file formats
*clm-instruments*frame+make-sawtooth-wavenormalize-envelopeSound file IO
clm-loadframe->filemake-scalar-mixernotchSound placement
*clm-locsig-type*frame->file?make-square-wavenotch?sound-chans
*clm-news*frame->framemake-srcNote lists sound-data-format
*clm-notehook*frame->listmake-ssb-amnotehooksound-data-location
*clm-play*frame->samplemake-table-lookupnrxycossound-datum-size
*clm-player*frame-refmake-triangle-wavenrxycos?sound-duration
clm-printframe-set!make-two-polenrxysinsound-frames
clm-randomframe?make-two-zeronrxysin?sound-header-type
*clm-reverb-channels*Generatorsmake-wave-trainnsinsound-length
*clm-safety*Granular synthesismixnsin?sound-let
*clm-search-list*granulatemixer*one-polesound-loop-info
*clm-srate*granulate?mixer+one-pole?sound-maxamp
*clm-statistics*Headers mixer-refone-zerosound-samples
*clm-table-size*hz->radiansmixer-set!one-zero?sound-srate
*clm-tempfile-data-format*iir-filtermixer?open-inputspectrum
*clm-tempfile-header-type*iir-filter?move-locsigopen-input*square-wave
*clm-version*in-anymove-sound*open-input-pathname*square-wave?
close-inputInput and outputmove-sound?*open-input-truename*src
combInstrumentsmoving-average*open-input-verbose*src?
comb?linear->dbmoving-average?optional-keyssb-am
continue-frame->filelocsigmultiply-arraysoscilssb-am?
continue-sample->filelocsig-refmus-aifcoscil?stop-dac
contrast-enhancementlocsig-reverb-refmus-aiffout-anystretch-envelope
convolutionlocsig-reverb-set!mus-array-print-length*output*table-lookup
convolvelocsig-set!mus-bshortpartials->polynomialtable-lookup?
convolve-fileslocsig-typemus-channelpartials->wavetap
convolve?locsig?mus-channelsphase-partials->wavetimes->samples
correlatemake-all-passmus-closephase-vocodertriangle-wave
cross synthesismake-asymmetric-fmmus-dataphase-vocoder?triangle-wave?
dacmake-combmus-describepolar->rectangulartwo-pi
Data formats make-convolvemus-feedbackpolynomialtwo-pole
db->linearmake-delaymus-feedforwardpolyshapetwo-pole?
Debugging make-envmus-file-buffer-sizepolyshape?two-zero
def-clm-structmake-fft-windowmus-file-namepolywavetwo-zero?
definstrumentmake-file->framemus-float-equal-fudge-factorpolywave?wave-train
degrees->radiansmake-file->samplemus-frequencypulse-trainwave-train?
delaymake-filtermus-hoppulse-train?window-envelope
delay-tickmake-filtered-combmus-incrementradians->degreeswith-mix
delay?make-fir-coeffsmus-input?radians->hzwith-offset
dot-productmake-fir-filtermus-interp-typerandwith-sound
edot-productmake-firmantmus-interpolaterand-interpwith-threaded-sound
envmake-formantmus-ircamrand-interp?
env-interpmake-framemus-lengthrand?
env?make-frame->filemus-locationRandom Numbers