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-5.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-17.tar.gz). This document is aimed at the Common Lisp version in clm-5.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: (clm-5.tar.gz clm tarball).

Bill Schottstaedt (bil@ccrma.stanford.edu)
related documentation: snd.html extsnd.html grfsnd.html sndscm.html sndclm.html sndlib.html cl-fm.html
Contents

- Introduction
- Instruments
  - Generators
  - Generic Functions
  - Sound file IO
  - Useful functions
  - def-clm-struct
  - Definstrument
- Note Lists
  - With-sound
  - With-mix
  - Sound-let
- Debugging
- Appendices
  - Header and data types supported in CLM
  - Example of clm-init.lisp
  - About CLM in saved images

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-pass | all-pass filter
asymmetric-fm | asymmetric fm
comb | comb filter
convolve | convolution
delay | delay line
env | line segment envelope
filter | direct form FIR/IIR filter
filtered-comb | comb filter with filter on feedback
fir-filter | FIR filter
formant | resonance
granulate | granular synthesis
iir-filter | IIR filter
in-any | sound file input
locsig | static sound placement
move-sound | sound motion
moving-average | moving window average
ncos | sum of equal amplitude cosines
notch | notch filter
nsin | sum of equal amplitude sines
nrxycos | sum of n scaled cosines
nrxysin | sum of n scaled sines

one-pole | one pole filter
one-zero | one zero filter
oscil | sine wave and FM
out-any | sound output
polywave and polyshape | waveshaping
phase-vocoder | vocoder analysis and resynthesis
pulse-train | pulse train
rand,rand-interp | random numbers, noise
readin | sound input
sawtooth-wave | sawtooth
square-wave | square wave
src | sampling rate conversion
ssb-am | single sideband amplitude modulation
table-lookup | interpolated table lookup
tap | delay line tap
triangle-wave | triangle wave
two-pole | two pole filter
two-zero | two zero filter
wave-train | wave 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 0.0) (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 0.0) (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-increment | base 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 `e` | reverse an envelope
envelope-repeat `e num &optional refl xnorm` | repeat an envelope
envelope-concatenate `&rest es` | concatenate any number of envelopes
envelope+ `es` | add together any number of envelopes
envelope* `es` | same but multiply
envelope-simplify `e &optional yg xg` | simplify an evelope
meld-envelopes `e0 e1` | meld two envelopes together
map-across-envelopes `func es` | map a function across any number of envelopes
envelope-exp `e &optional pow xg` | create exponential segments of envelopes
window-envelope `beg end e` | return portion of e between two x values
stretch-envelope `e a0 a1 &optional d0 d1` | attack and decay portions
scale-envelope `e scale &optional offset` | scale e
normalize-envelope `e &optional norm` | normalize 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 0.0)   ; 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-type | interpolation 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 0.0)
            (partials '(1 1)) (type mus-chebyshev-first-kind)
    polywave w &optional (fm 0.0)
    polywave? w

    make-polyshape &optional-key (frequency 0.0)
            (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 0.0) (amplitude 1.0) (initial-phase pi)
    triangle-wave s &optional (fm 0.0)
    triangle-wave? s

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

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

    make-pulse-train &optional-key (frequency 0.0) (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 0.0) (n 1)
    ncos cs &optional (fm 0.0)
    ncos? cs

    make-nsin &optional-key (frequency 0.0) (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 0.0) (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 0.0) (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-type | interpolation 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 0.0)          ; 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 0.0)
            (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-increment | srate 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-length | fft 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 0.0) (ratio 1.0) (n 1) (r .5)
    nrxysin s &optional (fm 0.0)
    nrxysin? s

    make-nrxycos &optional-key (frequency 0.0) (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 0.0) (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 the Scheme version of 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-channel | channel being read/written
mus-channels | channels open
mus-data | array of data
mus-describe | description of current state
mus-feedback | feedback coefficient
mus-feedforward | feedforward coefficient
mus-file-name | file being read/written
mus-frequency | frequency (Hz)
mus-hop | hop size for block processing
mus-increment | various increments
mus-interp-type | interpolation type (mus-interp-linear, etc)
mus-length | data array length
mus-location | sample location for reads/writes
mus-name | generator name ("oscil")
mus-offset | envelope offset
mus-order | filter order
mus-phase | phase (radians)
mus-ramp | granulate grain envelope ramp setting
mus-reset | set gen to default starting state
mus-run | run any generator
mus-scaler | scaler, normally on an amplitude
mus-width | width of interpolation tables, etc
mus-xcoeff | x (input) coefficient
mus-xcoeffs | array of x (input) coefficients
mus-ycoeff | y (output, feedback) coefficient
mus-ycoeffs | array 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-framples 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))

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 ->frample), and make-sample->file (or frample->). 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?`obj` | t if obj performs sound input
mus-output?`obj` | t if obj performs sound output
file->sample?`obj` | t if obj reads a sound file returning a sample
sample->file?`obj` | t if obj writes a sample to a sound file
frample->file?`obj` | t if obj writes a frample to a sound file
file->frample?`obj` | t if obj reads a sound file returning a frample
make-file->sample `name buffer-size` | return gen that reads samples from sound file name
make-sample->file `name &optional chans format type comment` | return gen that writes samples to sound file name
make-file->frample `name buffer-size` | return gen that reads framples from sound file name
make-frample->file `name &optional chans format type comment` | return gen that writes framples to sound file name
file->sample `obj samp &optional chan` | return sample at samp in channel chan
sample->file `obj samp chan val` | write (add) sample val at samp in channel chan
file->frample `obj samp &optional outf` | return frample at samp
frample->file `obj samp val` | write (add) frample val at samp
file->array `file channel beg dur array` | read samples from file into array
array->file `file data len srate channels` | write samples in array to file
continue-frample->file `file` | reopen file for more output
continue-sample->file `file` | reopen file for more output
mus-close `obj` | close 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 frample->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 framples 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 frample 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 `freq` | convert freq to radians per sample
radians->hz `rads` | convert rads to Hz
db->linear `dB` | convert dB to linear value
linear->db `val` | convert val to dB
times->samples `start duration` | convert start and duration from seconds to samples (beg+dur in latter case)
samples->seconds `samps` | convert samples to seconds
seconds->samples `secs` | convert seconds to samples
degrees->radians `degs` | convert degrees to radians
radians->degrees `rads` | convert radians to degrees
clear-array `arr` | set all values in arr to 0.0
sound-samples `filename` | samples of sound according to header (can be incorrect)
sound-framples `filename` | number of framples (number of samples / channels)
sound-datum-size `filename` | bytes per sample
sound-data-location `filename` | location of first sample (bytes)
sound-chans `filename` | number of channels (samples are interleaved)
sound-srate `filename` | sampling rate
sound-header-type `filename` | header type (aiff etc)
sound-data-format `filename` | data format (alaw etc)
sound-length `filename` | true file length (for error checks)
sound-duration `filename` | file length in seconds
sound-maxamp `name vals` | get max amp vals and times of file name
sound-loop-info `name vals` | get 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)
    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. 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 |  |  |
bandedwg | Juan Reyes banded waveguide instrument | bandedwg.ins | bandedwg.cms |  |
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
singbowl | Juan Reyes Tibetan bowl instrument | bowl.ins | bowl.cms |  |
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
rotates | Juan Reyes Leslie instrument | leslie.ins | leslie.cms |  |
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.ins | stochastic.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-frample *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-frample *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-frample (* 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-frample 0) (output-frample 0) framples 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-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-pi | 2*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 (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.