COMMON LISP MUSIC

"The most apparent difference between animals and vegetables is that animals have the powers of sound" -- William Bartram, "Travels", 1791.

Common Lisp Music (CLM, for lack of a catchier name) is a music synthesis and signal processing package in the Music V family. The new version, named CLM-2, represents a large-scale revision of the version that existed from 1990 to 1999. This document describes CLM-2. See translate.cl for information on moving from the previous version to this one. CLM exists in four forms: a Common Lisp implementation with optional C foreign functions, a C version, and Scheme and Ruby versions built on the C version through the Guile, or Ruby libraries. 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 "2", etc). The Common Lisp and Scheme versions try to be completely compatible, and this document concentrates on former. (To compare the three versions, see v.ins, the fm-violin in examp.scm, and the same code in sndlib.htm).

CLM has several sections: the "generators", instruments (definstrument and *.ins), examples of note lists (with-sound, *.clm), a "make" facility for sound files (with-mix), various functions that are useful in sound file work (sound-let, mix), and a connection to the Snd sound editor. CLM runs on the Macintosh (MacOS or LinuxPPC), SGI, Sun, Alpha, Windoze, and any machine running Linux. It is available free, via anonymous ftp (pub/Lisp/clm-2.tar.gz at ccrma-ftp.stanford.edu; the Snd editor is snd-6.tar.gz).

Bill Schottstaedt (bil@ccrma.stanford.edu)

related documentationsnd.htm extsnd.htm grfsnd.htm sndlib.htm sndscm.htm libxm.htm index.htm

Contents


Introduction

CLM provides functions to experiment with sounds. The easiest way to make a new sound is with-sound. Say we want to hear one second of the fm violin (in v.ins, named fm-violin) at 440 Hz, and a somewhat soft amplitude. Compile and load v.ins, 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. The compile and load sequence can be abbreviated in most lisps. In ACL:cl v.ins or perhaps even :cl v will call compile-file if needed, then load. Once loaded, we don't need to reload v unless we change it in some way. (In CMU-CL, load v.cmucl, not v.x86f). 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:

(dac)

or, if you have some saved sound file:

(dac "a-great.snd")

"dac" is jargon, but the function name "play" was already in use way back in the dark ages when CLM emerged into the light.

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 "generators" and "instruments", and a sequence of instrumental calls is a "note list". To create your own generators and instruments, you need to write the Lisp 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:

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

Now to hear our sine wave:

(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:

(defun simp (start-time duration frequency amplitude) 

declares that our instrument is a lisp function (defun) 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 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 start-time (beg) and start-time+duration (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 meat of our instrument. The call (outa i whatever) adds its third argument (in this case a complicated expression) into channel 0 of the current output stream at location i. The expression itself:

(* 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. All this is far more detailed and explicit than would be the case in a normal clm instrument. For example, we would normally use the clm macro hz->radians to replace the multiply by (/ (* 2.0 pi) sampling-rate); and we would use oscil to deal with the phase increments. 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. (This example may not work in "no-ffi" versions of CLM, Clisp in particular; in those cases, you need to use the "run" macro as explained below). Lisp compilers generate less-than-ideal numerical code, so we always wrap up the "run-time" portion of our instrument in the macro run. If it can, run expands into C code to speed up the work. To take care of various system bookkeeping functions during this process, we use definstrument, rather than defun to define the instrument function. Its syntax can be considered to be identical to defun:

(definstrument simp (start-time duration frequency amplitude)
  (multiple-value-bind (beg end) (times->samples start-time duration)
    (let ((s (make-oscil frequency)))
      (run
       (loop for i from beg below end do 
	 (outa i (* amplitude (oscil s))))))))

This version of simp performs the same task as the earlier one, but a lot faster. We use run and definstrument which work together to speed up the computation, and replace the explicit phase calculation with oscil. Oscil returns a sine wave at the frequency specified in the make-oscil call. Each generator in clm has a corresponding make function to create and initialize an instance of that generator. One such generator, env, handles envelopes -- let's put an amplitude envelope on simp's output:

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

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, then a ramp down to 0. Make-env packages up the envelope information, pre-computing increments and so on. The envelope is then actually applied by calling env.

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

In Scheme/Snd, the exact same sequence can be used: with-sound has the same syntax, and the run macro optimizes compute speed. The main difference is that "run" takes a "thunk":

(definstrument (scm-simp start-time duration frequency amplitude)
  (let* ((beg (inexact->exact (floor (* start-time (mus-srate)))))
         (dur (inexact->exact (floor (* duration (mus-srate)))))
         (s (make-oscil :frequency frequency)))
    (run
      (lambda ()
        (do ((i 0 (1+ i)))
            ((= i dur))
          (outa (+ i beg) (* amplitude (oscil s)) *output*))))))

CLM instruments

The normal structure of an instrument is:

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

The setup code creates any needed generator structures for the run-time code which actually generates the samples. The run-time code can contain any of the lisp functions, generators, or other special functions described in the next several sections. Since life is short, not every feature of lisp is supported by the run macro; if you want to use the Lisp-to-C translation feature, only the following Lisp functions can occur within the body of the run macro:

    +  /  *  -  1+  1-  incf decf setf setq psetf psetq shiftf rotatef
     =  /=  <  >  <=  >=  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 
    or and not null if unless when cond progn prog1 prog2 case tagbody go 
    error warn print princ terpri print-object 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 -- we 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). If the first argument is an integer, rather than the format string, it is assumed to be a file identifier, and the text output is written to that file. In Scheme, clm-print falls back on whatever version of format is loaded, so it is much less limited in that regard; it doesn't take an optional output file integer however.

Loop is expanded as a macro and anything in the loop syntax is ok if it expands into something else mentioned above (i.e. a lambda form with go's and so forth). The same holds for psetf, rotatef, and shiftf. (I think only MCL has problems in this regard).

Declare can be used to set the safety and debugging options, as well as set variable types (this can result in tighter code in many cases). In Common Lisp, the recognized types are integer and float; in the resultant C code these become either integers or doubles. In Scheme (in the Snd run macro), the declarable types are: int, float, boolean, char, string, vct, reader, clm, number, vector, integer, real, and any def-clm-struct name.


Generators


all-pass all-pass filter out-any sound output
asymmetric-fm asymmetric fm phase-vocoder vocoder analysis and resynthesis
buffer block oriented processing pulse-train pulse train
comb comb filter rand,rand-interp random numbers, noise
convolve convolution readin sound input
delay delay line sawtooth-wave sawtooth
env line segment envelope sine-summation sine summation synthesis
filter direct form FIR/IIR filter square-wave square wave
fir-filter FIR filter src sampling rate conversion
formant resonance sum-of-cosines band-limited pulse train
granulate granular synthesis table-lookup interpolated table lookup
iir-filter IIR filter tap delay line tap
in-any sound file input triangle-wave triangle wave
locsig static sound placement two-pole two pole filter
notch notch filter two-zero two zero filter
one-pole one pole filter wave-train wave train
one-zero one zero filter waveshape waveshaping
oscil sine wave and FM


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 a 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 normally takes a number of optional arguments, setting whatever state the given generator needs to operate on. The run-time function's first argument is always its associated structure. Its second argument is nearly always something like an FM input, whatever run-time modulation might be desired. Amplitude envelopes are handled with a separate env generator. 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), internal table size is two pi. The fm (or frequency change) argument is assumed to be a phase change in radians, applied on each sample. Normally composers would rather think in terms of Hz, so the function hz->radians can be used to convert from units of cycles per second to radians per sample. Since all the generators agree that their internal period length is two-pi, you can always use hz->radians to convert the frequency change (or fm) argument from a more easily interpreted value.

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 440.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. So, for example,

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


OSCIL

  make-oscil &optional-key (frequency 440.0) (initial-phase 0.0)
  oscil os &optional (fm-input 0.0) (pm-input 0.0)
  oscil? os
  oscil-bank amps oscils fms len 

Oscil produces a sine wave 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 (defaults to 0). Its third argument is the (sample-wise) phase change (in addition to the carrier increment and so on). See fm.htm for a discussion of fm.

oscil methods
mus-frequency frequency in Hz
mus-phase phase in radians
mus-cosines 1 (no setf)

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


(definstrument simple-fm (beg end freq amp mc-ratio index
             &optional (amp-env '(0 0 50 1 100 1)) (index-env '(0 1 100 1)))
  (let* ((cr (make-oscil freq))
         (md (make-oscil (* freq mc-ratio)))
         (fm-index (hz->radians (* index mc-ratio freq)))
         (ampf (make-env amp-env amp :start beg :end end))
         (indf (make-env index-env fm-index :start beg :end end)))
    (run
      (loop for i from beg below end do
        (outa i (* (env ampf) (oscil cr (* (env indf) (oscil md)))))))))

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 start) => duration in samples (can be used instead of duration)
      (start 0)     ; can be used in conjunction with end
  env e
  env? e
  restart-env e     ; return to start of envelope
  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. This list is passed to make-env along with the scaler applied to the y axis, the offset added to every y value, and optionally the desired start and end times. The resulting object, when accessed with the env generator, creates an envelope of the same shape as the original, but stretched along the x-axis to fit the duration, and transformed along the y-axis by offset + scaler * y-value. The kind of interpolation used to get y-values between the break points is determined by the envelope's base.

Make-env produces a structure that env can handle at run-time. Each call on env gets the next value of the envelope. With the arguments to make-env, you can apply a given shape to any situtation. Say we want a ramp moving from .3 to .5 over 1 second. The corresponding make-env call would be

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

Base determines how the break-points are connected. If it is 1.0 (the default), you get straight line segments. Base = 0.0 gives a step function (the envelope changes its value suddenly to the new one without any interpolation). Any other positive value becomes the exponent of the exponential curve connecting the points (for an explicit example see env.lisp). Base < 1.0 gives convex curves (i.e. bowed out), and Base > 1.0 gives concave curves (i.e. sagging). To get arbitrary connecting curves between the break points, treat the output of env as input to the connecting function. Here's an example that maps the line segments into sin x^3:


(definstrument mapenv (beg dur frq amp en)
  (let* ((bg (floor (* beg *srate*)))
	 (nd (+ bg (floor (* dur *srate*))))
	 (o (make-oscil frq))
         (half-pi (* pi 0.5))
	 (zv (make-env en 1.0 dur)))
    (run
     (loop for i from bg below nd do
       (let ((zval (env zv))) ;zval^3 is [0.0..1.0], as is sin between 0 and half-pi.
	 (outa i (* amp (sin (* half-pi zval zval zval)) (oscil o))))))))

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

Or create your own generator that traces out the curve you want. For example, 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))))

Restart-env causes an envelope to start all over again from the beginning. To jump to any position in an envelope, use mus-location (there's an example of this in ug.ins).


(definstrument restartable-simp (beg dur env-dur)
  (let* ((os (make-oscil))
	 (en (make-env '(0 0 50 1 100 0) :end env-dur :scaler .1))
	 (j beg)
	 (env-stop (+ beg env-dur)))
    (run
     (loop for i from beg below (+ beg dur) do 
       (let ((val (* (env en) (oscil os))))
	 (incf j)
	 (when (> j env-stop)
	   (incf env-stop env-dur)
	   (restart-env en))
	 (outa i val))))))

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) 

Other 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 440.0)   ; in Hz
        (initial-phase 0.0) ; in radians 
        wave                ; double-float array
        size                ; table size if wave not specified
  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 linear interpolation returns the table value. Table-lookup scales its frequency change argument (fm-input) to fit whatever its table size is (that is, it assumes the caller is thinking in terms of a table size of two pi, and fixes it up). The wave table should be an array of double-floats (the function make-double-float-array can be used to create it).


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)

(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 simp (dur)
  (let ((hi (make-table-lookup :wave (partials->wave '(1 .5 2 .5)))))
    (run
     (loop for i from 0 to dur do
       (outa i (* .3 (table-lookup hi)))))))

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.


WAVESHAPE

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

  partials->waveshape &optional-key partials (norm t) (size *clm-table-size*)
  partials->polynomial partials &optional (kind 1)

Waveshape performs waveshaping; see "Digital Waveshaping Synthesis" by Marc Le Brun in JAES 1979 April, vol 27, no 4, p250.

waveshape methods
mus-frequency frequency in Hz
mus-phase phase in radians
mus-data wave array (no setf)
mus-length wave size (no setf)

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



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

See bigbird below for an example of partials->polynomial, and pqw.ins for an example of phase quadrature waveshaping. Partials->polynomial takes a list of harmonic amplitudes and returns a list of Chebyshev polynomial coefficients. The argument kind determines which kind of Chebyshev polynomial we are interested in.


SAWTOOTH-WAVE, TRIANGLE-WAVE, PULSE-TRAIN, SQUARE-WAVE

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

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

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

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

These generators produce some occasionally useful wave forms. Sawtooth-wave ramps from -1 to 1, then goes immediately back to -1. Triangle-wave ramps from -1 to 1, then ramps from 1 to -1. Pulse-train produces a single sample of 1.0, then zeros. Square-wave produces 1 for half a period, then 0. All have a period of two-pi, so the fm argument should have an effect comparable to the same fm applied to the same waveform in table-lookup. These are not band-limited; if the frequency is too high, you can get foldover. David Lowenfels has provided band-limited versions based on the sum-of-cosines generator: good-sqr.ins, good-saw.ins, and good-tri.ins.

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)

One popular kind of vibrato is:
  (+ (triangle-wave pervib) 
     (rand-interp ranvib))


SUM-OF-COSINES

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

Sum-of-cosines produces a band-limited pulse train containing cosines cosines. See also legendre-summation in dsp.scm. "Trigonometric Delights" by Eli Maor has a derivation of a very similar formula (producing a sum of sines) and a neat geometric explanation.

sum-of-cosines methods
mus-frequency frequency in Hz
mus-phase phase in radians
mus-scaler (/ 1.0 (+ 1 (* 2 cosines)))
mus-cosines cosines arg used in make-<gen>
mus-length same as mus-cosines

based on:
  1 + 2(cos(x) + cos(2x) + ... cos(nx)) = 
    sin((n+.5)x) / sin(x/2)

  known as the Dirichlet kernel
  see "Special Functions", Andrews, Askey, Roy, 5.1.10
  also cosine-summation in dsp.scm for an extension
  a similar sum involves cos for sin above, AAR 5.6.2


The mus-cosines setf method does not try to fix up the scaler. If you change the number of cosines on the fly, you'll probably want to set the scaler to (/ 1.0 (+ 1 (* 2 cosines))).


WAVE-TRAIN

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

Wave-train produces a wave train (an extension of pulse-train). Frequency is the repetition rate of the wave found in wave. Successive waves can overlap. With some simple envelopes, or filters, you can use this for VOSIM and other related techniques.

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)

;;; 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 :envelope (or ae (list 0 0 25 1 75 1 100 0)) amp dur))
         (frq0 (hz->radians f0))
         (frq1 (hz->radians f1))
         (frq2 (hz->radians f2))
         (foflen (if (= *srate* 22050) 100 200))
         (vibr (make-oscil :frequency 6))
	 (vibenv (make-env :envelope (or ve (list 0 1 100 1)) vib 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, RAND-INTERP

  make-rand &optional-key 
        (frequency 440.0) ;freq at which new random numbers occur
        (amplitude 1.0)   ;numbers are between -amplitude and amplitude
  rand r &optional (sweep 0.0)
  rand? r
  make-rand-interp &optional-key (frequency 440.0) (amplitude 1.0)
  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. Lisp's function random returns a number between 0.0 and its argument. 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>

rand:
  (if (>= phase (* 2 pi))
      (setf output (centered-random amplitude)))
  (incf phase (+ (hz->radians frequency) sweep))


The central-limit theorem says that you can get closer and closer to gaussian noise simply 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. 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 an example of 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, ONE-ZERO, TWO-POLE, TWO-ZERO

   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
   two-pole f input 
   two-pole? f

   make-two-zero &optional-key a0 a1 a2
   two-zero f input 
   two-zero? f

   make-zpolar radius frequency
   make-ppolar radius frequency
simple filter methods
mus-a0 a0 in equations
mus-a1 a1 in equations
mus-a2 a2 in equations
mus-b1 b1 in equations
mus-b2 b2 in equations
mus-x1 x(n-1) in equations
mus-x2 x(n-2) in equations
mus-y1 y(n-1) in equations
mus-y2 y(n-2) in equations
mus-order 1 or 2 (no setf)
one-zero  y(n) = a0 x(n) + a1 x(n-1)
one-pole  y(n) = a0 x(n) - b1 y(n-1)
two-pole  y(n) = a0 x(n) - b1 y(n-1) - b2 y(n-2)
two-zero  y(n) = a0 x(n) + a1 x(n-1) + a2 x(n-2)

This nomenclature is taken from Julius Smith's "An Introduction to Digital Filter Theory" in Strawn "Digital Audio Signal Processing", and is different from that used in the more general filters such as fir-filter.



In make-ppolar ("poles polar") you can specify the radius and angle of a pole while make-two-pole requires actual filter coefficients. The filter provided by make-ppolar has two poles, one at (radius,frequency), the other at (radius,-frequency). Radius is between 0 and 1 (but less than 1), and frequency is between 0 and srate/2. This is the standard resonator form with poles specified by the polar coordinates of one pole. Similar remarks apply to make-two-zero and make-zpolar. Use two-pole in conjunction with make-ppolar.


FORMANT

  make-formant &optional-key radius frequency (gain 1.0)
  formant f input       ; resonator centered at frequency, bandwidth set by r
  formant? f
simple filter methods
mus-a0 a0 in equations
mus-a1 0.0 (no setf)
mus-a2 a2 in equations
mus-b1 b1 in equations
mus-b2 b2 in equations
mus-x1 x(n-1) in equations
mus-x2 x(n-2) in equations
mus-y1 y(n-1) in equations
mus-y2 y(n-2) in equations
mus-formant-radius formant radius
mus-frequency formant center frequency
mus-order 2 (no setf)

y(n) = x(n) - 
       r*x(n-2) + 
       2*r*cos(2*pi*frequency/srate)*y(n-1) - 
       r*r*y(n-2)


Formant is recommended for resonators (simple bandpass filters), vocal tract or instrument cavity simulations, etc. It provides bandpass filtering (a simple resonance) using a two-pole and a two-zero filter, as described above. Only one coefficient need be changed in order to move the filter center frequency. The filter coefficients are set as a function of desired pole-radius radius (set by desired bandwidth from radius=1-bw/2), center frequency frequency, and peak gain gain. To change the frequency in the run loop use (setf (mus-frequency f) freq). The radius is the field (mus-formant-radius) which can be set at run time using (setf (mus-formant-radius f) val). For details see "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). There radius ~= e^(-pi*Bw*T) where Bw is the bandwidth in Hz and T is the sampling period (1/sampling-rate); also "A note on Constant-Gain Digital Resonators" by Ken Steiglitz, CMJ vol 18 No. 4 pp.8-10 (winter 1994). The bandwidth can be specified in the run loop in Hz by using a macro such as:

  (defmacro compute-radius (bw) ; bw in Hz
    `(exp (/ (* (- pi) ,bw) *srate*)))

The gain argument to make-formant is not used directly; it becomes gain * (1 - radius) or some variation thereof (see make-formant in mus.lisp). When you set the mus-formant-radius in the run-loop, the gain is also adjusted. We can use a bank of formant generators to implement a graphic equalizer, but consider doing cross-synthesis with an equalizer of 128 channels driven at the sampling rate by a spectral analyzer:


(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 radius (* k bin))))
	    (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->polar 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))

This same idea (an enveloped bank of resonators) can be used for a variety of cross-fade effects, analogs in the sound world to the common graphical sweeps, dissolves, and whatnot. fade.ins has several examples. See also the noi-attempt, filter-white-noise, and testR instruments in ugex.ins.


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. See fltdes.lisp and dsp.scm for functions compatible with these filters that provide the coefficients for various FIR and IIR filters. 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-xcoeffs x (input) coeffs
mus-ycoeffs y (output) coeffs
mus-data current state (input values)
mus-length same as mus-order

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


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 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, TAP

  make-delay &optional-key size initial-contents initial-element max-size
  delay d input &optional (pm 0.0)
  delay? d
  tap d &optional (offset 0)

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. If specified, initial-contents need not be the same length as the delay line. Tap returns the current value of the delay generator. 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 via linear interpolation. The argument pm determines how far from the normal index we are. max-size should be large enough to accommodate the largest actual delay requested at run-time. The pm argument 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.

delay methods
mus-length length of delay (no setf)
mus-order same as mus-length
mus-data delay line itself (no setf)

(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"))

In the Scheme version of CLM, the Scheme built-in function delay is available as %delay.


COMB, NOTCH

  make-comb &optional-key scaler size initial-contents initial-element max-size
  comb cflt input &optional (pm 0.0)
  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. If max-size is different from size, you get an interpolating delay line; otherwise the pm argument to the generator is ignored in some cases (in Scheme and Ruby, not in Common Lisp).

comb and notch methods
mus-length length of delay (no setf)
mus-order same as mus-length
mus-data delay line itself (no setf)
mus-feedback scaler (comb only)
mus-feedforward scaler (notch only)

 comb:  y(n) = x(n-size-1) + scaler * y(n-size)
 notch: y(n) = x(n-1) * scaler  + x(n-size-1)


As a rule of thumb, the decay time of the feedback part is 7*size/(1-scaler) samples, so to get a decay of Dur seconds, scaler <= 1-7*size/(Dur*Srate). The peak gain is 1/(1-(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".


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 feedback = 0, we get a moving average comb filter. If both scale terms = 0, we get a pure delay line.

all-pass methods
mus-length length of delay (no setf)
mus-order same as mus-length
mus-data delay line itself (no setf)
mus-feedback feedback scaler
mus-feedforward feedforward scaler

 y(n) = feedforward*x(n-1) + x(n-size-1) + feedback*y(n-size)


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
mus-location if file input, current location in file
mus-channel if file input, channel of file (no setf)

Src performs sampling rate conversion by convolving its input with a sinc function. srate is the ratio between the new sampling rate and the old. 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. 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, you get a constant output because the generator is not moving at all). Here's an instrument that provides time-varying sampling rate conversion (see also the srctst instrument below):


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

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


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

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

(definstrument hello-dentist (beg dur file frq amp)
  (let ((fil (open-input* file)))
    (when fil
      (unwind-protect
        (let ((rd (make-src :input fil))
              (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)))))
        (close-input fil))))))

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 is a function of one argument (the desired read direction, if the reader can support it), that is funcall'd each time src needs another sample of input. The input argument to src can also be an input file structure, as returned by open-input. If you're using the run macro (the normal case), only file input is supported at the make-src level; otherwise, you should pass in the function in the src call inside the body of run (this is a temporary situation, hopefully; see expsrc.ins for an example).


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 (see src).


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

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
  granulate e &optional input-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)

Granulate "granulates" the sound file 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 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. The more-or-less average time between successive segments is hop. The accuracy at which we handle this hopping is set by the float jitter -- if jitter is very small, you may get an annoying tremolo. The overall amplitude scaler on each segment is scaler -- this is used to try to 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 a short example of using 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))))))))

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

PHASE-VOCODER

  make-phase-vocoder &optional-key input (fftsize 512) (overlap 4) interp (pitch 1.0) analyze edit synthesize
  phase-vocoder pv input
  phase-vocoder? pv
phase-vocoder methods
mus-frequency pitch shift
mus-length fft-size ("N")
mus-increment interp
mus-hop fft-size / overlap ("D")

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. 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 passed to phase-vocoder at run-time (if any). The default is to read input (from the input function, an "as-needed" input source as in src or granulate), take the fft, get the new amplitudes and phases (as the edit function default), then resynthesize using sum-of-sines (the synthesize function default); so, the default case simply returns a resynthesis of the original input. interp sets the time between ffts (for time stretching etc).


BUFFER->SAMPLE

  make-buffer &optional-key size (fill-time 0.0)
  buffer->sample b 
  sample->buffer b val 
  buffer->frame b &optional f 
  frame->buffer b f 
  buffer-empty? b 
  buffer-full? b 
  buffer? b 
buffer methods
mus-data buffer data (no setf)
mus-increment buffer fill time
mus-length buffer length (no setf)

This generator is an extension of a delay line, providing a way to overlap-add stuff into the line, and a way to get notification when the next block of input is needed. Buffer->sample provides sample-at-a-time access to block-oriented data. On each call, buffer->sample returns the next sample in the buffer. Similarly, sample->buffer appends a sample to the buffer. buffer-empty? returns t when a new block of data is needed; mus-increment sets the next block-trigger time.


(definstrument simp (dur)
  (let* ((gen (make-buffer 128 128.0)))
    (loop for i from 0 below 128 do (setf (aref (mus-data gen) i) (double-float (* i .001))))
    (run
     (loop for i from 0 below dur do
       (when (buffer-empty? gen)
	 (loop for k from 0 below 128 do (setf (aref (mus-data gen) k) (double-float (* k .001))))
	 (incf (mus-increment gen) 128))
       (outa i (buffer->sample gen))))))

SINE-SUMMATION

  make-sine-summation &optional-key 
        (frequency 440.0) (initial-phase 0.0) (n 1) (a .5) (ratio 1.0)
  sine-summation s &optional (fm 0.0)
  sine-summation? s
sine-summation methods
mus-frequency frequency in Hz
mus-phase phase in radians
mus-scaler "a" parameter; sideband scaler

sine-summation provides a kind of additive synthesis. See J.A.Moorer, "Signal Processing Aspects of Computer Music" and "The Synthesis of Complex Audio Spectra by means of Discrete Summation Formulae" (Stan-M-5). n is the number of sidebands, a is the amplitude ratio between successive sidebands, ratio is the ratio between the carrier frequency and the spacing between successive sidebands. The basic idea is very similar to that used in the sum-of-cosines generator. (see "Special Functions", Andrews, Askey, Roy chapter 5 for lots of interesting stuff, including the cosine-summation formula used in dsp.scm). The output amplitude of this generator is hard to predict; see Moorer's paper for some normalization functions (and it is numerically a disaster -- don't set a to 1.0!).


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


ASYMMETRIC-FM

  make-asymmetric-fm &optional-key 
        (frequency 440.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

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: this is another extension of the sine-summation and sum-of-cosines approach. 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)))))))

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

prc-toolkit95.lisp and piano.ins define different versions of some of the filter, noise and delay generators. Various special functions and their relatives are defined in bessel.lisp. green.cl defines several special purpose noise generators. butterworth.cl has several Butterworth filters.


Generic Functions

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

  mus-frequency  mus-cosines         mus-phase     mus-location
  mus-length     mus-increment       mus-data      mus-order
  mus-channel    mus-formant-radius  mus-channels  mus-xcoeffs
  mus-scaler     mus-ycoeffs         mus-ramp      mus-feedback
  mus-a0         mus-feedforward     mus-a1        mus-hop
  mus-a2         mus-b1              mus-b2        mus-run
  mus-file-name  mus-offset          mus-width

(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* ((f (open-input file))
         (last-sample (sound-frames file))
         (beg (floor (* *srate* onset)))
         (end (+ beg (floor (* *srate* duration))))
         (s (make-src :file f :start 0 :srate src-ratio))
         (cs 0))
    (run
     (loop for i from beg below end do
       (setf cs (mus-location s))
       (if (>= cs last-sample) (setf (mus-increment s) (- src-ratio)))
       (if (<= cs 0) (setf (mus-increment s) src-ratio)
       (outa i (src s)))))
    (close-input f)))

mus-location is useful in cases where you're doing some complicated processing on a file, and want to use envelopes, but for whatever reason can't easily say how long the output will be. The envelopes can be defined in terms of the input file's length, then applied by watching mus-location:


(definstrument srctst (file beg srcenv in-file-start)
  (let ((f (open-input file :start (floor (* in-file-start (sound-srate file))))))
    (unwind-protect
        (let* ((st (floor (* beg *srate*)))
               (dur (sound-duration file))
               (samples (1- (sound-frames file)))
               (srcA (make-src :input f :srate 0.0))
               (env-val 0.0)
               (pass 0)
               (senv (make-env :envelope srcenv :duration dur)))
	  (setf env-val (env senv))           ;get initial src value
          (run
           (loop for i from st do
             (let ((pos (mus-location srcA)))
               (when (/= pass pos)            ;position in input has changed
                 (let ((passes (- pos pass))) ;move env forward by same amount
                   (setf pass pos)
                   (dotimes (k passes) (setf env-val (env senv)))))
               (outa i (src srcA env-val))
               ;; perhaps this should protect against an env-val of 0.0
               (if (>= pass samples) (loop-finish))))))
      (close-input f))))

This instrument puts an envelope on the sampling rate, and defines the envelope to apply over the entire input file:

  (with-sound () (srctst  "oboe.snd" 0 '(0 1 1 .5 2 1)))

If you write your own generators, and want them to respond to any of the generic functions, you need to use add-clm-method. It takes 4 arguments, the name of the generic function to which you're adding a method, the type identifier for the generator this method applies to, and two functions that produce the code to access the data or set it to some new value. The type can be any integer greater than 50 or so -- currently CLM's built-in types stop at +mixer+ which is 42. There are numerous examples of add-clm-method in cmus.lisp. When a generic function reference occurs in the run body, a function is built up at compile time that collects all the known methods for that generic function.

mus-bank gens amps &optional in1 in2
(let ((sum 0.0))
  (dotimes (i (length gens)) 
    (incf sum (* (aref amp i) 
                 (mus-run (aref gens i) (aref in1 i) (aref in2 i)))))
  sum)

An example is oscil-bank.


Frames, Mixers, Sound IO

Frames and Mixers

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

  make-empty-frame chans         ;create frame of 0's
  make-frame chans &rest args    ;create frame and load it with args
  frame? obj                     ;is obj a frame
  frame+ f1 f2 &optional outf    ;add f1 and f2 element-wise, return new frame (or outf)
  frame* f1 f2 &optional outf    ;multiply f1 and f2 element-size, return new frame (or outf)
  frame-ref f1 chan              ;return f1[chan]
  frame-set! f1 chan val         ;f1[chan] = val (also setf with frame-ref)

  make-empty-mixer chans         ;create a mixer of 0's
  make-identity-mixer chans      ;create a mixer of 1's on the diagonals
  make-mixer chans &rest args    ;create a mixer and load it with args
  mixer? obj                     ;is obj a mixer
  mixer* m1 m2 &optional outm    ;matrix multiply of m1 and m2, return new mixer (or outm)
  mixer-ref m1 in out            ;m1[in,out] (use setf to change)
  mixer-set! m1 in out val       ;m1[in,out] = val (also setf with mixer-ref)

  frame->frame mixer frame &optional outf
                        ;pass frame through mixer, return new frame (or outf)
  frame->list frame              ;return list of frame's contents
  sample->frame frame-or-mixer sample &optional outf
                        ;pass sample through frame-or-mixer, return new frame (or outf)
  frame->sample frame-or-mixer frame  
                        ;pass frame through frame-or-mixer, return sample
frame and mixer methods
mus-channels number of channels accommodated
mus-length same as mus-channels

In Ruby, frame* is frame_multiply, frame+ is frame_add, and mixer* is mixer_multiply.

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

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

Here the input file is read by file->frame producing a frame of data. That is then passed through the mixer frame->frame, and the resultant frame is written to the with-sound output file *output* by frame->file. Within run, the output frames of the various frame producing functions must be provided (I'm trying to avoid run-time memory management).

Sound file IO is supported by a variety of low-level functions:

  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
  frame->file? obj              ;t if obj writes a frame to a sound file
  file->frame? obj              ;t if obj reads a sound file returning a frame
   
  make-file->sample name        ;return gen that reads samples from sound file name
  make-sample->file name chans &optional format type
                                ;return gen that writes samples to sound file name
  make-file->frame name         ;return gen that reads frames from sound file name
  make-frame->file name chans &optional format type
                                ;return gen that writes frames 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->frame obj samp &optional outf     ;return frame at samp
  frame->file obj samp val      ;write (add) frame val at samp

  file->array filename channel start-sample samples array
  array->file file data srate channels
  ;; these two read and write entire files to or from float arrays

  mus-open-write filename       ;low-level open call (O_RDRW), returning file id
  mus-reopen-write filename     ;low-level open call (O_RDRW), returning file id
  mus-open-read filename        ;low-level open call (O_RDONLY), returning file id
  mus-create filename           ;low-level creat call (make new file), returning file id
  mus-close fd                  ;low-level close call (fd can be int or IO struct)

  continue-sample->file file    ;reopen file for more output (C/Scheme, not CL yet)

In the Scheme version of CLM, the Guile built-in (but unused) function frame? is available as %frame?.

OUTA, OUTB, OUTC, OUTD, OUT-ANY

  outa loc data &optional (o-stream *output*)
  outb loc data &optional (o-stream *output*)
  outc loc data &optional (o-stream *output*)
  outd loc data &optional (o-stream *output*)
  out-any loc data &optional (channel 0) (o-stream *output*)

Outa and friends add data into o-stream at sample position loc. O-stream defaults to the current output file (it is an IO instance, not a file name). The reverb stream, if any, is named *reverb*; the direct output is *output*. See the env-sound instrument for an example of *reverb*. 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 the other output function.


INA, INB, IN-ANY

  ina loc &optional (i-stream *input*)
  inb loc &optional (i-stream *input*)
  in-any loc &optional (channel 0) (i-stream *input*)

Ina and friends get the sample at position loc in i-stream as a float. The data is normally between -1.0 and 1.0. I-stream defaults to the current input file (it is an IO struct, not a file name). See the digital zipper instrument zipper.ins for an example.


REC-ANY

  rec-any chan

Within defpinstrument and with-psound, access to the current real-time input is through rec-any. The chan argument refers to the recording input channel. Which input is accessed depends on the rec-line argument to with-psound or with-dac (0=microphone, 1=analog line in, 2=digital line in).


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

Readin returns successive samples (as floats) from file. File should be an IO instance, as returned by open-input. Start is the frame at which to start reading file. 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 ((f (open-input* file)))
    (when f
      (unwind-protect
        (let* ((st (floor (* beg *srate*)))
               (dur (sound-duration file))
               (rev-amount .01)
               (rdA (make-readin f))
               (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*)))))
          (close-input f))))))

LOCSIG

 make-locsig &optional-key (degree 0.0) (distance 1.0) reverb channels 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 normally takes the place of outa and outb in an instrument. It tries to place a signal between outa and outb in an extremely dumb manner, it just scales the respective amplitudes ("that old trick never works"). 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 can be mus-linear (the default) or mus-sinusoidal. This parameter can be set globally via *clm-locsig-type*. The mus-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 -- that is, a diffuse sound source is the ideal! By sending an arbitrary mix of signal and reverberation to various speakers, locsig gives you a very diffuse source; it does the opposite of what it claims to do, and by some perversity of Mother Nature, that is what you want. (See "Binaural Phenomena" by J Blauert).

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 ((f (open-input file)))
    (when f
      (unwind-protect
	(let* ((beg (floor (* onset *srate*)))
	       (end (+ beg (floor (* *srate* duration))))
	       (loc (make-locsig :degree 0 :distance 1 :reverb reverb-amount))
	       (rdA (make-readin :file f))
	       (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)))))
        (close-input f)))))

See also make-roomsig in roomsig.cl. For a moving sound source, see either move-locsig, or Fernando Lopez Lezcano's dlocsig, documented in dlocsig/index.htm. mus-channels of a locsig generator returns the size of the direct-signal array (the channels argument to make-locsig, or the number of channels in the output sound).

Locsig can be used for moving sound sources via move-locsig:


(definstrument simp (start dur freq amp &key (degree 0) (dist 1.0) (reverb 0))
  (let* ((beg (floor (* start *clm-srate*)))
         (end (+ beg (floor (* dur *clm-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))))))

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)
  seconds->samples secs          convert secs to samples
  degrees->radians degs          convert degs to radians
  radians->degrees rads          convert rads 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-frames filename          samples per channel
  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
  mus-set-raw-header-defaults srate chans data-format
  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)
  sound-set-loop-info name vals  set loop info of file name to vals
  sound-format-name format       format name as string
  sound-type-name type           type name as string
hz->radians converts its argument to radians/sample (for any situation where a frequency is used as an amplitude, glissando or FM, for example). It can be used within run. 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.htm for some info.

sound-maxamps is used to print the maxamp stats in with-sound. It's a bit of a kludge, but here's an example of using it directly:

(defvar maxes (make-integer-array (* 2 chans)))
(sound-maxamp filename maxes)

Now the array "maxes" has two values: the first is the sample number where the max was found, the second is the value itself as a 24-bit int. You can use clm::fix-to-real to turn it into a float.

POLYNOMIAL

   polynomial coeffs x

Coeffs is an array of coefficients, x is the value to be plugged into the polynomial. Coeffs[0] is the constant term, and so on. For waveshaping, use the function partials->polynomial. Abramowitz and Stegun, "A Handbook of Mathematical Functions" is a treasure-trove of interesting polynomials.


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

See also the brighten instrument.


ARRAY-INTERP, DOT-PRODUCT, SUM-OF-SINES

  array-interp fn x &optional size
  dot-product in1 in2 &optional (start 0)
  sum-of-sines amps phases

These simple functions underlie some of the generators, and can be called within run, if you want to roll your own. 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)))

sum-of-sines simply loops through its arrays of amps and phases, summing (* amp (sin phase)) -- it is mostly a convenience function for additive synthesis. dot-product is also known as scalar product, and in orthogonal coordinate systems is the same as the inner product.



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. One way to play with this is to use a simple instrument such as:

(define (waver spectr driver)
  (let ((v0 (make-vct 8192))
	(poly (partials->polynomial spectr)))
    (mix-vct (vct-map! v0 (lambda () (polynomial poly (driver)))) 0 0 0 #f)))

and try:

(waver '(1 .6 2 .4) 
       (let ((g0 (make-oscil 100)) 
             (g1 (make-oscil 1000)))
         (lambda ()
	   (* .5 (+ (g0) (g1))))))

RING-MODULATE, AMPLITUDE-MODULATE

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

Since neither needs any state information, there are no associated make functions.

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

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

FFT

  fft rdat idat fftsize &optional sign
  make-fft-window &optional-key type size (beta 2.5)
  multiply-arrays rdat window
  rectangular->polar rdat idat
  polar->rectangular rdat idat
  spectrum rdat idat window norm-type
  convolution rdat idat 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 (also hanning-window for historical reasons)
  hamming-window       ;raised cosine
  blackman2-window     ;Blackman-Harris windows of various orders
  blackman3-window
  blackman4-window
  exponential-window
  kaiser-window        ;beta argument used here

The magnitude of the spectrum is returned by rectangular->polar. The data can be windowed with multiply-arrays. Spectrum calls the fft, translates to polar coordinates, then returns results 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))

See the cross-synthesis instrument above, ugex.ins, san.ins, and anoi.ins for more examples.


(definstrument spectr (infile outfile &optional (fftsize 128))
  (let* ((fdi (make-double-float-array fftsize))
	 (fdr (make-double-float-array fftsize))
	 (win (make-fft-window blackman2-window fftsize))
	 (in-file (open-input* infile))
	 (out-file (mus-create outfile))
	 (k 0)
	 (freqsize (floor fftsize 2))
	 (end (sound-frames infile)))
    (run 
     (loop for i from 0 below end do
       (let ((inval (ina i in-file)))
	 (setf (aref fdr k) inval)
	 (incf k)
	 (when (>= k fftsize)
	   (setf k 0)
	   (spectrum fdr fdi win 1)
	   (clm-print out-file "~%sample: ~D~%" i)
	   (dotimes (ctr freqsize)
	     (clm-print out-file "  (~,3F ~,3F)"
			(/ (* ctr *srate*) fftsize)
			(aref fdr ctr)))))))
    (close-input in-file)
    (mus-close out-file)))

def-clm-struct

Def-clm-struct is syntactically like def-struct, but sets up the struct field names for the run macro.


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

See prc-toolkit95.lisp for many examples.


Definstrument

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

  language  :c or :lisp ; specify output language (C is the default where possible)
  c-file  nil           ; specify the instrument intermediate C file name
  c-include-file  nil   ; C code to be #include'd in the intermediate file
  c-options  "-c -O"    ; C compiler switches
  parallel nil          ; a "sample-synchronous" instrument -- defpinstrument
  type                  ; can be :parallel or nil -- will replace the "parallel" argument
  print-function

The output language is chosen based on the kind of CLM you load. In most cases, it defaults to :c. The :lisp option is useful during debugging. The syntax for these options is somewhat similar to that of with-sound. To specify that the instrument simp should use C with the "-c -O3" flags,

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

Defpinstrument produces an instrument that is handled by a special scheduler that runs it one sample at a time, in parallel with all other p-instruments that want to run on that sample. P-instruments can be mixed freely with normal instruments. The latter run to completion when they are called, while the currently running p-instruments wait in the background. Obviously, calls on these instruments must be ordered by begin-time.

Any statements after the run loop in a parallel instrument are actually executed as soon as the instrument is called; the run loop contents are deferred until the scheduler starts the loop, but the lisp code has long since exited. This can be a problem if you open input files. In the no-ffi case (i.e. clisp primarily), the open and subsequent close happen immediately, but the rc (or rcp) program re-opens and re-closes such files themselves. In the FFI case, however, the close-input frees the file descriptor and associated data, leading to a segfault once the instrument's run loop starts to run. So... Defer close-input in such a case to a point after the instrument has finished processing. Perhaps it would be better to provide run support for open-input and close-input.

The following instruments are included as separate .ins files in the clm directory (see also ins):

complete-add add.ins additive synthesis
addflts addflt.ins filters
add-sound addsnd.ins mix in a sound file
anoi anoi.ins noise reduction
badd badd.ins fancier additive synthesis
fm-bell bell.ins fm bell sounds
bessel stuff bessel.lisp special functions and related instruments
bigbird bigbird.ins waveshaping (bird.clm and bird.ins)
canter canter.ins fm (bag.clm -- bagpipes)
cellon cellon.ins feedback fm
cnvrev cnv.ins convolution (aimed at reverb)
moving sounds dlocsig/dlocsig.lisp quad sound movement
drone drone.ins additive synthesis (bag.clm)
granulate-sound expsrc.ins examples of the granulate generator (granular synthesis)
cross-fade fade.ins cross-fades and dissolves in the frequency domain
filter-noise fltnoi.ins filter with envelopes on coefficients
filter-sound fltsnd.ins filter a sound file
stereo-flute flute.ins physical model of a flute (Nicky Hind)
fm examples fmex.ins fm bell, gong, drum
Jezar's reverb freeverb/freeverb.ins fancy reverb
fullmix fullmix.ins a mixer
good-sqr good-sqr.ins band-limited square-wave (D Lowenfels)
good-tri good-tri.ins band-limited triangle-wave (D Lowenfels)
good-saw good-saw.ins band-limited sawtooth (D Lowenfels)
grani grani.ins granular synthesis (Fernando Lopez-Lezcano)
grapheq grapheq.ins graphic equalizer
fm-insect insect.ins fm
jc-reverb jcrev.ins an old reverberator (jlrev is a cavernous version)
fm-voice jcvoi.ins jc's fm voice
kiprev kiprev.ins a fancier (temperamental) reverberator
lbj-piano lbjPiano.ins additive synthesis piano
maraca maraca.ins Perry Cook's maraca physical models
maxfilter maxf.ins Juan Reyes modular synthesis
mlb-voice mlbvoi.ins mlb's fm (originally waveshaping) voice
moog filters moog.lisp Moog filters (also filter-noise.ins) (Fernando Lopez-Lezcano)
fm-noise noise.ins noise maker
nrev nrev.ins a popular reverberator
p piano.ins Scott van Duyne's piano physical model
pluck pluck.ins Karplus-Strong synthesis
pqw pqw.ins waveshaping
pqw-vox pqwvox.ins waveshaping voice
physical models prc-toolkit95.lisp several physical modelling instruments
various ins prc96.ins fm and aditive synthesis from Perry Cook's Synthesis Toolkit
preverb preverb.ins realtime version of jc-reverb (jcrev.ins)
pvoc pvoc.ins phase vocoder (Michael Klingbeil)
resflt resflt.ins filters (3 resonators)
reson reson.ins fm (formants)
ring-modulate ring-modulate.ins ring-modulation of sounds (Craig Sapp)
track-rms rms.ins rms envelope of sound file (Michael Edwards)
pins san.ins spectral modelling
scanned scanned.ins Juan Reyes scanned synthesis instrument
singer singer.ins Perry Cook's vocal tract physical model
bow strad.ins Juan Reyes bowed string physical model
tc tc.ins fft-based instruments
fm-trumpet trp.ins fm trumpet (Dexter Morrill)
various ins ugex.ins granular synthesis, formants, etc
test ins ug.ins,ug1.ins
fm-violin v.ins fm violin (fmviolin.clm, popi.clm)
vox vox.ins fm voice (cream.clm)
zc, zn zd.ins examples of interpolating delays
zipper zipper.ins The 'digital zipper' effect.

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, outa 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))))))

See also the stick and stick1 instruments in ug.ins. "Parallel" instruments must use loop (all such instruments are assumed to be running in parallel, generating the current sample for the DACs). It is also possible (albeit slow) to call any of the run-oriented functions in with-sound:

(with-sound (:statistics t) 
  (let ((os (make-oscil 440)))
    (dotimes (i 5000) (outa i (* .1 (oscil os))))))

CLM in Guile, Ruby, C

Although this document is aimed at Common Lisp, CLM instruments can also be written in Scheme (via Guile), Ruby, or C. Examples can be found in the Snd tarball, but a short example might not be out of place. The CL bird instrument is


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

Its (slightly simplified) Scheme counterpart is:


(define (bird start dur frequency freqskew amplitude freq-envelope amp-envelope)
  "(bird start dur frequency freqskew amplitude freq-envelope amp-envelope)"
  (let* ((gls-env (make-env freq-envelope (hz->radians freqskew) dur))
	 (os (make-oscil :frequency frequency))
	 (amp-env (make-env amp-envelope amplitude dur))
	 (len (inexact->exact (round (* (srate) dur))))
	 (beg (inexact->exact (round (* (srate) start))))
	 (local-data (make-vct len)))
    (vct-map! local-data (lambda () (* (env amp-env) (oscil os (env gls-env)))))
    (vct-add! out-data local-data beg)))

And in Ruby:


def bird(start, dur, frequency, freqskew, amplitude, freq_envelope, amp_envelope)
  gls_env = make_env(freq_envelope, hz2radians(freqskew), dur)
  os = make_oscil(frequency)
  amp_env = make_env(amp_envelope, amplitude, dur)
  beg = (srate() * start).round
  len = (srate() * dur).round
  local_data  = make_vct len
  vct_map!(local_data, Proc.new { // env(amp_env) * oscil(os, env(gls_env)) })
  vct_add!($out_data, local_data, beg)
end

In Scheme, a generator is itself a function: (oscil gen 0.0) is the same as (gen 0.0), if "gen" is an oscil. This can simplify instruments that leave the generator type decision until run time:


(define* (chain-dsps beg dur #:rest dsps)
  (let* ((dsp-chain (apply vector (reverse (map (lambda (gen)
						 (if (list? gen)
						     (make-env gen :duration dur)
						     gen))
					       dsps))))
	 (output (make-vct (inexact->exact (floor (* dur (mus-srate))))))
	 (len (vector-length dsp-chain)))
    (vct-map! output (lambda ()
		       (let ((val 0.0))
			 ;; using do and vector here for the run macro's benefit
			 (do ((i 0 (1+ i)))
			     ((= i len))
			   (let ((gen (vector-ref dsp-chain i)))
			     (if (env? gen)
				 (set! val (* (gen) val))
				 (if (readin? gen)
				     (set! val (+ val (gen)))
				     (set! val (gen val))))))
			 val)))
    (mix-vct output (inexact->exact (floor (* beg (mus-srate)))) #f #f #f)))

;(chain-dsps 0 1.0 '(0 0 1 1 2 0) (make-oscil 440))
;(chain-dsps 0 1.0 '(0 0 1 1 2 0) (make-one-zero .5) (make-readin "oboe.snd"))

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 and clm-load

 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 1)       ; 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                ; print out various fascinating numbers
   (decay-time 1.0)          ; ring time of reverb after end of piece
   comment                   ; comment placed in header
   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 (has no effect in no-ffi versions)
   (verbose *clm-verbose*)
   (force-recomputation nil) ; if t, force with-mix calls to recompute
   (explode *clm-explode*)   ; if t, produce "exploded" output -- each instrument call becomes a separately mixed file in Snd

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, then returns the output file name. Here are some examples showing how to use the various options:

  (with-sound (:output "new.snd") (simp 0 1 440 .1))
  (with-sound (:srate 44100 :channels 2) ...)
  (with-sound (:comment (format nil 
     "This version is louder: ~A" (make-header))))
  (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, for example, 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))
          :amplitude .5))

(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 :amplitude .2 :output-frame *srate*))
    (fm-violin .5 .1 330 .1)))

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

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 :file 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)))

But enough fun... Besides the obvious options like :reverb and :srate, the most useful ones are :scaled-to and :statistics. Statistics, if t (the default is nil), causes clm to keep track of a variety of interesting things and print them out at the end of the computation. Scaled-to tells clm to make sure the final output file has a maxamp of whatever the argument is to :scaled-to -- that is,

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

will produce test.snd with a maxamp of .5, no matter how loud the intermediate mix actually is (within reason -- the current true intermediate maxamp is 128.0 (if using the default sndlib settings)). 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. 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))

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.

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 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 (the default is nil), re-opens a previously existing file for further processing. The default 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))

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

All the run-time generators and functions discussed above can be called directly (that is, outside any instrument) in with-sound: (with-sound () (outa 3 .1)). Global amplitude envelopes can be handled with eref, and so on.

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

  dac &optional file
  sl-dac file &optional (output-device mus-audio-default)
  stop-dac
  volume &optional (device mus-audio-default) channel

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-dac. To set the dac volume (setf (volume) 1.0). Similarly (volume) returns the current volume settings. Currently, dac 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).

Direct to DAC and Realtime Controls

  with-psound (&key
    (srate *clm-srate*) 
    (channels *clm-channels*) 
    (rec-chans 0) (rec-line 0)
    reverb reverb-duration
    (bufsize *clm-rt-bufsize*)
    (data-format mus-bshort)) &body body

With-psound runs its body in real-time in the sense that the sound is sent directly to the DAC as it is being computed. The dac buffer size is bufsize, which defaults to 1024. This default avoids clicks and interruptions, but can be sluggish if, for example, you're using with-psound to run a real-time spectrum analyzer. Obviously this only works if the body can be computed faster than real-time. Ordinary C instruments can be handled if there's only one note in the body; for more complex cases, use parallel instruments (defpinstrument). Local input sound files should not be closed until with-psound returns (that is, if you call open-input, you need to save the file pointer -- don't call close-input in the instrument). Record (microphone) input is enabled when rec-chans is greater than 0 (see rec-any). Reverb in this mode is handled currently by the generator previn -- see rt.lisp and preverb.ins. With-psound assumes it is dealing with a precomputed notelist, and runs until all instruments have stopped. That is, in with-psound, time is always moving forward and instruments that arrive behind the current time will screw up. The time moves forward only as instruments come and go, however, and if you don't send it notes, it simply waits for them, sending nothing to the DAC and holding the current time suspended. The current time in seconds from with-psound's point of view is returned by the function current-real-time. This kind of time management is not always (i.e. almost never) what you want in realtime situations. The alternative is:

  with-dac &key
    (srate *clm-srate*) 
    (channels *clm-channels*) 
    (rec-chans 0)
    (rec-line 0) ;0=mic,1=analog in, 2=digital in
    init-func
    run-func
    (data-format mus-bshort)

With-dac opens the dac and starts sending it buffers of data. It runs until told explicitly to stop, processing whatever instruments are currently active. Init-func is a function of two arguments, the srate and number of channels; any realtime control settings that need to be done before you start should be handled here. Run-func is a function of one argument, the current time in seconds, that is called constantly as long as with-dac is active. It can return :quit which exits with-dac, :continue which simply sends out the next buffer of data, or a number which is the number of samples to wait until the next call on the run-func. With-dac currently is limited to ACL on the SGI or Linux.

Realtime control and value readback is handled in CLM through shared memory. In the run loop (or anywhere else for that matter), the function control gets the current (float) value of some portion of this memory; (setf (control n) val) sets it to val. While lisp is running (most usefully while with-psound or with-dac is running), any instrument or program can read or write these values (via sliders or whatever) by accessing the same memory. Besides serving as real-time IO locations, these can also be used for inter-instrument communication in parallel instruments; spectral fusion effects involving shared frequency modulation across instruments can be implemented with control. Several complete examples can be found in the bess*.cl files. The available control-related functions are:

  control n
  open-controls size
  close-controls
  control-allocate (&optional (size 1))
  control-free (var)
  make-controller program &rest args
  fcontrol fc
  make-fcontrol n &optional (mult 0.005)
  graph sc val
  make-graph arr len &optional (trigger 0)
  fft-graph sc val win rl im
  make-fft-graph arr len &optional (trigger 0) (type 0)

The functions control-allocate and control-free allocate and release portions of shared memory. If you don't call open-controls yourself, the first control-allocate call will get a block of 4096 words (*clm-shared-memory-size*). The control function accesses shared memory from Lisp or within the run loop. The external programatic access is through shmget and friends -- see the code generated by make-controller. fcontrol is a filtered version of control to smooth out the staircase functions you get from slider widgets. make-graph and make-fft-graph display waveforms and ffts in realtime. make-controller provides a simple way to get a graphical interface up and running, connected to its associated lisp code. It takes the desired program name (this program will have the widgets), an argument that is currently ignored, and a list of widget descriptions. It then sets up the widgets on the screen in the order listed in the arguments, where currently the descriptions are of the form, (index name &rest description). Index is the control location associated with this widget; name is the label to place on or beside the widget; description describes the widget and can be one of:

 :slider lo hi &opt color
 :push (down = 1, caller should clear)
 :separator &opt (height 4) (type :normal)
 :toggle default &opt color (up = 0, down = 1)
 :label &opt bgcolor fgcolor (shows current value)
 :graph xlabel ylabel -> ready x0 x1 y0 y1 length [data]
 :midi channel status data-byte

If you want to watch a particular variable as an instrument runs, set a control location to its value, and request a label widget in the user-interface. The files bess.cl through bess6.cl give examples of these functions. This portion of CLM currently works only on the SGI or under Linux (though there's no reason it couldn't also work on the Sun).

The :midi input sets the associated control location to the value of the data-byte indicated (byte 1 or 2) if something comes along that matches the specified channel and status. The raw data byte is turned into a float (without scaling) and written to the control location and some sort of indication is posted in the controller. I actually expect midi users to use make-controller to get the basic interface up and running quickly, then edit the midi handler themselves. I'm not too interested in trying to unravel all of midi's wierd cases. Currently the midi support is limited to the SGI Irix 6.2 or later. The examples are:

  bess.cl   simple realtime controls on FM
  bess1.cl  complex FM with waveform and fft graphs, variable readback
  bess2.cl  fft of microphone/line input
  bess3.cl  trigger fm-violin calls via pushbuttons
  bess4.cl  realtime control of global and instrument-local variables
  bess5.cl  lisp composition algorithm with controls
  bess6.cl  midi control of clm instruments

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 need not be recomputed. This is equivalent to open-input.

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

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

With-mix performs a string comparison of its body to decide whether it needs to recompute its note calls. It then loads that body from a separate saved file. This can be confusing if global variables are present.

  > USER(2): (let ((rstr .1)) (with-sound () (with-mix () "sec" 0 (fm-violin 0 1 440 rstr))))
  > ; Loading /zap/sec.clm
  > Error: Attempt to take the value of the unbound variable `RSTR'.

Here the code evaluated is basically (let ((rstr .1)) (load "/zap/sec.clm")) where rstr has lexical scope. To make rstr visible within the load,

  (let ((rstr1 .1)) 
    (declare (special rstr1))
    (with-sound () (with-mix () "sec" 0 (fm-violin 0 1 440 rstr1))))

but if you then evaluate the same form again, changing rstr1 to (say) .5, with-mix does not notice that rstr1's value has changed, so it does not recompute its body, leaving the resultant amplitude at .1.


The fastest way to mix sound files is with mix:

  mix &optional-key filename (input-frame 0) (output-frame 0) frames 
                       (amplitude 1.0) output (prescaler 1.0)

  open-input &optional name &key verbose if-does-not-exist element-type mix-at  
             mix-duration force-recomputation start channel end bufsize prescaler
  open-output &optional (name *clm-file-name*)
  close-input &optional (i-stream *input*)
  close-output &optional (o-stream *output*)
  reopen-output &optional (o-stream *clm-file-name*)
  open-input* name &key start channel restartable

These functions open and close input and output sound files. Open-input and open-output take either strings or pathnames and return an IO object (see io.lisp for a description of the innards of this structure). 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 an IO structure that other clm functions can use to access the file. If you don't give a complete file name (name without the .snd extension), open-input checks to see if there's either no .snd file or a later .cm or .clm file, and in that case, suspends the current computation, makes the sound file from the sources, then resumes the old computation, opening the (newly computed) sound file. If you are working in sections, and keep the sections in separate files, the various layers of mixing can automatically notice when some section has changed, and update everything for you. Similarly, if all your sound files get deleted, the whole piece can still regenerate itself in one operation. 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).


Instrument-let

Instrument-let is like Lisp's flet -- it can be used to define local instruments:


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

Now, when the function Ins is called within with-sound or clm-load, it will add a note from Simp and Toot. For a more useful example, see expsrc.ins. Don't use instrument-let within definstrument.


Sound-let

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

  (sound-let ((temp-1 () (fm-violin 0 1 440 .1))
              (temp-2 () (fm-violin 0 2 660 .1)
                         (fm-violin .125 .5 880 .1)))
    (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 explicitly named output intact.


CLM Defaults

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

  *clm-file-name*          default sound file name
  *clm-header-type*        default output sound file header type
  *clm-data-format*        default output sound file data format
  *clm-table-size*         default table-lookup table size
  *clm-srate*              default sampling rate (22050)
  *clm-channels*           default output channels (1)
  *clm-delete-reverb*      should with-sound delete the temporary reverb output (default nil)
  *clm-play*               default for play arg in with-sound
  *clm-clipped*            default for clipped arg in with-sound
  *clm-notehook*           default for notehook arg in with-sound
  *clm-init*               name of site-specific initializations (see clm-init.lisp)
  *clm-array-print-length* number of IO data buffer elements printed
  *clm-player*             user-supplied DAC funtion
  *clm-file-buffer-size*   IO buffer sizes 
  *clm-search-list*        pathname list for file searches (open-input*)
  *clm-instruments*        list of the currently loaded clm instruments
  *clm-news*               brief list of recent changes (HISTORY.clm)
  *clm-version*            version identifier (a number -- also *clm-revision*)
  *clm-date*               creation date of the current version
  *clm-safety*             default safety setting (run loop debugging choices)
  *clm-locsig-type*        locsig interpolation choice (mus-linear or mus-sinusoidal)
  *output*                 current output stream (for outa and friends)
  *input*                  current input stream (for ina and friends)
  *reverb*                 current reverb stream
  two-pi                   2*pi

*clm-player* can be used to override CLM's normal DAC routine (which calls sndplay in most cases); say we want to send the sound to an ADAT output:

(setf *clm-player* (lambda (name) (sl-dac name mus-audio-adat-out)))

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). On ccrma's PC's running Linux with IDE drives and 64MBytes of RAM, CLM runs as much as 50% faster if you use:

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

CLM examples and whatnot

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. Here's an example:


(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 example is Michael Edwards' rmsp.ins; see also the sr3 instrument in ug.ins. Run* doesn't work in no-ffi versions of CLM, but you can get the same effect (on the SGI or in Linux) by using the control function instead. Here's another example 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, loosely divided into three groups; lisp itself, realtime readback and analysis, and a memo mechanism to communicate with Snd. The realtime support is described above under realtime controls. The lisp support is mostly oriented around the lisp debugger, but includes some additional functions provided by CLM. The most useful first step in debugging is to run the instrument in Lisp; CLM will run very slowly, but you can use all of Lisp's debugging features to see what is going wrong. To run in Lisp, (definstrument (simp :language :lisp) (args) ...), and then recompile and reload the instrument.

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

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, for example), 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_xxxyyy_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. This is incredibly tedious!

Once in gdb, you can use clm_print_object to display the fields of a generator:


Program received signal SIGSEGV, Segmentation fault.
0x804af4d in clm_lnxacl_fm_violin3 (clm_beg=0, clm_end=22050,clm_double=0x8079b08, clm_int=0x8079ea8)
    at /space/home/bil/cl/clm_lnxacl_FM-VIOLIN.c:507
507              ((int *)(clm_int[rIO[CLM_IO_DATS + 1]+i]))[data_end] += (int)(revn[i] * CLM_12263);

Now we look at clm_lnxacl_FM-VIOLIN.c (the C code produced by the run macro), and find the generator locations (toward the start of the violin function):


int clm_lnxacl_fm_violin3 (int clm_beg, int clm_end, double *clm_double, int *clm_int)
... 
  #define c_l_m__REVERB_ 34
  #define c_l_m__REVERB__r 17
  #define CARRIER 38
  #define CARRIER_r 19
 ...

To see the carrier fields, we call clm_print_object with the two locations:


(gdb) p clm_print_object(38,19,0,clm_int,clm_double)
#<oscil: freq: 440.000 Hz, phase: 8.554 deg>

But we're more interested in the segfault, which obviously has something to do with the reverb output file, or the locsig generator:


(gdb) p clm_print_object(34,17,0,clm_int,clm_double)
#<IO: "test.reverb", write, [chan: 0, loc: 0], chans: 1, fil: 8, beg: 0, end: 65535, 
  frames: 0, bufsize: 65536, hdr-end: 28, data-start: 0, data-end: 0, 
  dats: #<integer-array[1]: [0]>>

In this case the output buffer looks suspicious...

Help

To get help for some topic, call clm-help. Its argument can be either a (quoted) symbol or a string. In most cases, Netscape will be used to display the documentation; on the Mac, the variable *clm-help-browser* can be set to :cyberdog to choose that browser instead of Netscape. Another possibility is linux-help with the same arguments -- it is smarter about which browser is used.

To find out the current state of an instrument, use

  describe-instrument               show the state of current instrument locals
  ins-var (var &optional ins-name)  return value of var in instrument ins-name

Except when running in just-lisp mode, each instrument has a variety of properties the may occasionally be useful.

  :c-version-number     lisp/C version to check for libclm conflicts
  :version              CLM version
  :ins-vars             hash table that maps from lisp to C variables
  :run-vars             run* variables
  :ins-args             complete arglist of the instrument (including initial values)
  :c-proc               name of c function called from lisp
  :language             :c (or :lisp if just-lisp)
  :c-file-name          file containing C source code for instrument
  :print-function       function used by describe-instrument

CLM and Snd

If the Snd sound editor is available (currently this is the case on the SGI and some Linux systems, and should work on the Sun, but I haven't been able to try it), there are a variety of hooks in clm-snd.lisp that enable CLM and Snd to talk to each other. This portion of CLM has changed drastically three times now, so it is only for good sports. Currently, the most useful of the functions are:

  start-snd
  send-snd code
  receive-snd 
  send-and-receive-snd code &optional with-eval

  snd-memo file format-string &rest args
  add-mark sample channel
  add-region beg end
  save-signal value format-string &rest args

  snd-sound &optional file-name
  snd-region &optional (reg 0)
  snd-cleanup &body body

  to-snd (&rest args) &body body
  snd-edit-sound new-name &optional old-name
  snd-edit (&optional file) &body body

  snd-envelope env
  clm-envelope env

snd-memo provides access to the ".scm" file that can be associated with any sound in Snd; the memo file can contain any Snd-related Scheme code you want; it will be loaded just after the sound it itself is loaded. The variable memo-sound is set during this process to the index of the current sound. You can save any arbitrary information you like at any time through this function; the predefined macro add-mark is just one simple example:

  (defmacro add-mark (samp &optional (chan 0))
    `(snd-memo *output* "(add-mark ~D memo-sound ~D)~%" ,samp ,chan))

It appends the (add-mark samp memo-sound chan) call to the current output's memo (.scm) file. Similarly, add-region defines a new region in Snd; it can be used to mark a note's begin and end points with a sound. save-signal saves its first argument as an on-going output file, opening it in Snd when the current output file is opened. This provides a simple way to probe an instrument to see what the local signals are. The rest of its arguments are used to create the signal's file name, and it returns its first argument. This facility was handled through the display argument to with-sound in CLM-1.


(definstrument simp (dur)
  (let ((fd (open-input "oboe.snd")))
    (run
     (loop for i from 0 to dur do
       (outa i (save-signal (ina i fd) "ina.snd"))))
  (close-input fd)))

Currently, the save-signal output file is not deleted, nor is the memo file, so before each with-sound evaluation, delete any such files if you want to re-use the same names. Also, save-signal saves each value when it is called, so it is not fast, but you will get data even if the instrument dies in mid-note.

snd-sound returns the current edited state of the desired file in the Snd editor (or the currently active file if no file-name is given). This can be used anywhere a file name occurs in CLM:

  (with-sound () (mix (snd-sound)))

Similarly, snd-region returns the contents of region reg. snd-edit-sound passes sound data back to sound, causing it to appear in the editor as an edit of either the file passed as old-name, or the currently active file if no old-name is given.

  (snd-edit-sound 
    (with-sound () 
      (mix (snd-sound)) 
      (fm-violin .1 .1 660 .1)))

takes the current state of the current active file in Snd (snd-sound), adds a short fm-violin note to it, then returns that to Snd as a kind of editing operation.

Nearly everything that Snd can do is accessible to CLM by sending the appropriate Snd code through the function send-snd and friends.

  (send-snd "(open-sound /"oboe.snd/")")
  (snd-edit () (with-sound () (mix (snd-sound)) (fm-violin .1 .1 660 .1)))
  (send-and-receive-snd "(save-sound 0)")

starts Snd, opens "oboe.snd", edits it by adding a fm-violin note, and saves the result under the same name, exiting Snd. All of this is compatible with the debugging and display uses of Snd. For example, 'display' starts Snd itself if one isn't started yet, and that same Snd process can handle the editing operations described above.

The Snd envelope editor can be used in conjunction with clm; clm-envelope passes its argument (assumed to be a list) to Snd, and snd-envelope returns Snd's notion of that envelope (presumably after it has been edited):

  (defvar hi '(0 0 1 1))
  (clm-envelope hi)
  (setf hi (snd-envelope hi))

send-snd can also be called within the run loop, sending Snd any arbitrary command; the following instrument prints out the seconds as they go by in Snd's minibuffer:

(definstrument call-snd (beg dur frq)
  (let* ((bg (floor (* beg *srate*)))
         (nd (+ bg (floor (* dur *srate*))))
         (o (make-oscil frq))
	 (ctr 0)
	 (secs 0))
    (run
     (loop for i from bg below nd do
       (incf ctr)
       (when (>= ctr *srate*)
	 (incf secs)
	 (setf ctr 0)
	 (send-snd "(report-in-minibuffer (number->string ~D))" secs))
       (outa i (oscil o))))))

In this case, if send-snd has extra arguments, they are treated like format or clm-print arguments.

snd-sound creates a temporary file to pass the current state to CLM, and expects CLM to delete that file when it is done with it. The macros snd-cleanup and snd-edit provide this cleanup action for you. If you don't clean these files up as they are created, they are finally deleted when snd-stop is called, or when CLM is exited. If something goes wrong, just delete any file on the temp directory (/tmp or /var/tmp normally) whose name begins with "snd_".

to-snd is a version of with-sound that sends causes Snd to open the output file when with-sound is done.

  (to-snd (:statistics t) (fm-violin 0 1 440 .1))

There are two gotchas with to-snd, both occurring only when to-snd has to start a new Snd image itself (i.e. there's no Snd already running). In this case, while Snd is active, it gets stdin, so typing to the ACL listener is actually communicating with Snd, not CLM. And, with the change to sndplay (for CLM's dac function) in most cases, sndplay is playing the sound at the very moment that Snd is starting up in the background and trying to figure out what audio devices are available. I added mus-audio-reinitialize to clear this confusion (i.e. if Snd complains it can't play the sound, force it to reinitialize its notion of what's out there).

to-snd is just a simple macro (clm-snd.lisp) that calls send-snd opening the result of the with-sound call, so to open the new sound and make sure the control panel is open:

(defmacro to-snd-with-controls ((&rest args) &body body)
  `(send-snd (format nil 
	             "(let ((snd (open-sound ~S))) 
                        (set! (show-controls snd) #t))" 
	             (with-sound ,args ,@body))))

Appendices


Header and data types

CLM can write NeXT/Sun, AIFF/AIFC, RIFF ("wave"), 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.

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-sun, mus-riff, and mus-ircam. (The type mus-next is a synonym for mus-sun). 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.


Sources of unwanted noise

The major source of unwanted noise in computer music is amplitude quantization. This means soft notes are buzzy. If these are later used as input to a reverberator, the buzziness can easily be magnified. If the soft notes are split between channels (via locsig), you may end up increasing the overall noisiness. My experience has been that anything under .003 in amplitude is asking for trouble unless it is covered up in some way. Since reverb amounts are often less than .01, even a loud note can produce noisy input to the reverberator. The simplest way around this in CLM is to make one run and get the reverb stream max amp (reported if :statistics is t in with-sound). Then scale all the reverb signals up by the inverse of this (leaving some room for slop, of course). For example, if the reverb max amp is .01, you're throwing away about 7 bits of amplitude resolution in the reverb input. So, in this case, multiply all the locsig reverb by (say) 50, then in the reverberator, divide the reverb output by 50 before sending it out. See jcrev.ins for an example (the volume argument). Similarly, if your notes are soft, send them directly out channel 0 or 1; the spatial effects are going to add less to your piece than the noise will detract in this case. And if you're making repeated iterative passes over sound files, try to keep them scaled close to 1.0 in maxamp through all the passes. The exact maxamp is not important; the difference between 1.0 and .5 is one bit of resolution. If you want to hear this buzz, either run a slowly decaying envelope on a sine wave, or listen to the tail end of the standard reverbs as they decay into "silence"; you'll be horrified. The latter effect is so noticeable that many of the reverbs have an amplitude envelope argument that can be used to cut them off quickly at the end of the piece; it is against my religion to add noise (i.e. the dithering used currently by commercial CD's; the theory is that this dithering makes the buzz (a periodic toggling of the low order bits) uncorrelated, and therefore we're happier because we've come to expect tape noise anyway; but turn up the soft portions of your favorite CD, if it has any, and decide whether the cure is perhaps worse than the disease).

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

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

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

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

Here are some interesting comments from Lamar Owen (a radio engineer):

Properly designed balanced in and out A/D and D/A converters can be completely immune to the PC's internal noise. But it all goes to proper design. The Antex SX series of cards likewise contain internal A/D converters. I have measured the noise figures of the Antex SX-36 we have at WGCR using state of the art audio systems analyzers, and the noise floor is below the LSB threshold. All due to balanced I/O, sound PC layout techniques, and top of the line components. With unbalanced I/O all bets are off, of course.

In a high RF environment, unless the converters are optically isolated from the PC, you might be asking for trouble. When I say 'high RF' I'm talking 10KW of AM transmitter fifteen feet away, with a measured RF field intensity of 105V/m (the ANSI exposure limit is around 645V/m). This means a one meter piece of wire that isn't properly grounded can develop 105V of RF energy. I have suffered RF burns of appreciable intensity touching wires that weren't connected to anything on either end -- they were just oriented along the field gradient.

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. As an example of the sorts of customizations that can be included, here is my clm-init.lisp:


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

(setf (volume) 1.0)
;;; dac output volume

(setf *print-array* nil)
;;; keep lisp from printing out endless arrays in trace/zoom, etc

(setf *clm-search-list* 
  (append *clm-search-list* 
	  (list "/me/cl/oboe.snd" 
		"/me/mus/hdr/aiff-8.snd" 
		"/me/snd/now.snd" 
		"/snd1/zap/test.snd" 
		"/snd1/snds/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.

User-defined generators

Most special-case generators can be expressed as macros and defined in lisp; see zd.ins or prc-toolkit95.lisp for extensive examples. There's rarely a real need to drop into C! If you want to call your own C code within the run loop, you have to tell the run macro how to make the call. The run macro looks up identifiers in a table built by def-clm-fun; as an example, we'll make a generator that averages its arguments:


(in-package :clm)
(export '(average))
(def-clm-fun 'average #'(lambda (var x) (package-op 'c-average var x :clm-real)))
(defmacro c-average (result a b)
  (format *c-file* "  ~A = (~A + ~A)/2.0;~%" 
    (lc (second result)) (lc-num-ref a) (lc-num-ref b))
  nil)

def-clm-fun tells the run macro how to handle the function average -- actual C output needs to be deferred until the entire tree has been examined, so in this case, when "average" is encountered in an instrument's run section, the macro "c-average" is queued up with its arguments in an internal form; The macro c-average sends out the associated c code (to the global file identifier *c-file*) when that point is reached in the translation. See cmus.lisp for more examples. Once this code is loaded into CLM,


(definstrument average-stuff (beg dur freq amp)
  (let ((os1 (make-oscil freq))
	(os2 (make-oscil (* freq 3/2)))
	(bg (floor (* *srate* beg)))
	(nd (+ beg (floor (* *srate* dur)))))
    (run
     (loop for i from bg below nd do
       (outa i (* amp (average (oscil os1) (oscil os2))))))))

To define a CLM-in-C generator (i.e. within the context of clm.c and clm2xen.c), see A Note on User-defined Generators in C-CLM.


About CLM in 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, as in make-clm.cl, everything should work without problem. Once clm-initialize-links has been called, the C modules assume they have been initialized; if the saved version of modules is then executed, the arrays associated with these modules will not exist, but the modules won't know that. In this case, you need to call the function restart-clm before doing anything in the newly executed image. In Linux, I use the Scheme window manager (scwm), and have the following as a top-level menu item:

(menuitem "CLM" 
  #:action (lambda () 
             (execute "rxvt -e /space/home/bil/cl/CLM")))

The "CLM" script is:

#!/bin/csh -f
exec /space/home/bil/acl5/lisp -I /space/home/bil/cl/clm.dxl -e '(restart-clm)'

where the file clm.dxl was created by ACL's dumplisp command:

(excl:dumplisp :name "clm.dxl")

In Linux, you also need to avoid using any function that tickles the Linux audio drivers (OSS or Alsa) since they are not compatible with saved images.


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

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

  (clm-initialize-links)
  (setf *srate* 44100)
  (setf *output* (open-output "/zap/test.snd" :chans 2 :srate 44100))

< insert instrument calls here >

  (close-output *output*)

An instrument can be called outside with-sound and friends, or notice that it is being called in that way:


  (definstrument auto (dur)
    (let ((os (make-oscil))
          (need-close (not *output*))
          (end (floor (* dur *srate*))))
      (if (not *output*) 
        (setf *output* (open-output "/zap/test.snd")))
      (run
        (loop for i from 0 to end do (outa i (* .1 (oscil os)))))
    (when need-close
      (close-output *output*)
      (dac "/zap/test.snd"))))

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

An instrument need not perform sound output. See rmsp.ins for an example.


Misfeatures and Unfinished Business

Doubly nested with-mix triggers paranoia

With-mix called within with-mix called within with-sound causes the outer with-mix to be such a worrier that it always recomputes its note list.

:no-ffi limitations

Currently the :no-ffi versions of CLM (affecting Clisp and CMU-CL) do not implement convolve with huge files, notehook, run*, instrument-let, error within run, nor the multi-host support. In this version of CLM, instruments are written out as C modules and loaded into the rc (or saved-rc) program whenever CLM notices that rc and CLM are not in sync. With-sound arguments and instrument parameters are then passed from CLM to rc through a binary data file, normally called test.cdt. The instrument cannot be loaded dynamically (on the SGI -- this might be do-able on other systems, but I haven't had time to check it out), so currently rc is rebuilt from scratch each time the instrument list changes. To accomplish this, CLM needs continuous access to various header files, the libmus.a library built at CLM build time, and the instrument code. The variables clm::*clm-source-directory* and clm::*clm-binary-directory* tell CLM where to look for the library files; *rc-directory* tells CLM where to write the rc program and its various header files and modules. See rc.lisp for more details.

Instruments as generators

Instruments as generators are not implemented yet in common lisp (not difficult) or in the run macro (difficult). They are working in the Scheme/Snd versions however -- see fmv.scm for an example of treating the fm-violin (v.ins) as a generator; pvoc.scm for a phase-vocoder generator; and zip.scm for the zipper. There are two main differences between an instrument and a generator: first, the generator does not know its duration, so envelopes have to be packaged as functions (closures), and second, sound input (as from a sound file) should be handled as an "as-needed" function, rather than calling in-any or whatever. The other difference is one of approach; you need to separate the note-list-oriented portion of an instrument from its signal-processing or synthesis section. Once split apart, the same code can be used in "real-time" situations (the control function and the closures-as-envelopes are interchangeable).

I may rewrite CLM's instruments to use this format even if it doesn't work in the run macro; it irks me that progress is seriously impeded by a desperate attempt to speed up Lisp. The first step toward run-macro revision is to make cmus.c use clm.c -- that is, imbed the current C clm implementation into lisp.


Index


add-clm-method dot-product make-fft-graph mus-output? sampling-rate conversion
add-mark env make-fft-window mus-phase save-signal
add-region env-interp make-file->frame mus-ramp saved images
all-pass env? make-file->sample mus-random sawtooth-wave
all-pass? envelope* make-filter mus-reopen-write sawtooth-wave?
amplitude-modulate envelope+ make-fir-filter mus-riff scale-envelope
array->file envelope->coeffs make-formant mus-run seconds->samples
array-interp envelope-concatenate make-frame mus-scaler send-and-receive-snd
asymmetric-fm envelope-exp make-frame->file mus-set-rand-seed send-snd
asymmetric-fm? envelope-interp make-granulate mus-set-raw-header-defaults sine-summation
buffer->frame envelope-repeat make-graph mus-sun sine-summation?
buffer->sample envelope-reverse make-identity-mixer mus-width sl-dac
buffer-empty? envelope-simplify make-iir-filter mus-xcoeffs snd-cleanup
buffer-full? Envelopes make-locsig mus-ycoeffs snd-edit
buffer? fcontrol make-mixer normalize-envelope snd-edit-sound
centered-random fft make-notch notch snd-envelope
Checkpoints fft-graph make-one-pole notch? snd-memo
clear-array file->array make-one-zero Note lists snd-region
CLM Initialization file->frame make-oscil notehook snd-sound
*clm-array-print-length* file->frame? make-phase-vocoder one-pole Sound file formats
*clm-binary-directory* file->sample make-ppolar one-pole? Sound file IO
*clm-channels* file->sample? make-pulse-train one-zero Sound placement
*clm-clipped* filter make-rand one-zero? sound-chans
*clm-data-format* filter? make-rand-interp open-controls sound-data-format
*clm-date* Filters make-readin open-input sound-data-location
*clm-delete-reverb* fir-filter make-roomsig open-input* sound-datum-size
clm-envelope fir-filter? make-sample->file *open-input-pathname* sound-duration
*clm-file-buffer-size* formant make-sawtooth-wave *open-input-truename* sound-format-name
*clm-file-name* formant? make-sine-summation *open-input-verbose* sound-frames
*clm-header-type* Fourier transforms make-square-wave open-output sound-header-type
clm-help frame* make-src optional-key sound-length
*clm-help-browser* frame+ make-sum-of-cosines oscil sound-let
*clm-init* frame->buffer make-table-lookup oscil-bank sound-loop-info
*clm-instruments* frame->file make-triangle-wave oscil? sound-maxamp
clm-load frame->file? make-two-pole out-any sound-samples
*clm-locsig-type* frame->frame make-two-zero outa sound-set-loop-info
*clm-news* frame->list make-wave-train outb sound-srate
*clm-notehook* frame->sample make-waveshape outc sound-type-name
*clm-play* frame-ref make-zpolar outd spectrum
*clm-player* frame-set! map-across-envelopes *output* square-wave
clm-print frame? meld-envelopes partials->polynomial square-wave?
clm-random Generators mix partials->wave src
*clm-safety* Granular synthesis mixer* partials->waveshape src?
*clm-search-list* granulate mixer-ref phase-partials->wave start-snd
*clm-source-directory* granulate? mixer-set! phase-vocoder stop-dac
*clm-srate* graph mixer? phase-vocoder? stretch-envelope
*clm-table-size* Headers move-locsig polar->rectangular sum-of-cosines
*clm-verbose* Help multiply-arrays polynomial sum-of-cosines?
*clm-version* hz->radians mus-a0 pulse-train sum-of-sines
close-controls iir-filter mus-a1 pulse-train? table-lookup
close-input iir-filter? mus-a2 radians->degrees table-lookup?
close-output in-any mus-aifc radians->hz tap
comb ina mus-aiff rand times->samples
comb? inb mus-b1 rand-interp to-snd
continue-sample->file *input* mus-b2 rand-interp? triangle-wave
contrast-enhancement Input and output mus-bank rand? triangle-wave?
control ins-var mus-bshort Random numbers two-pi
control-allocate instrument-let mus-channel raw data two-pole
control-free Instruments mus-channels rc two-pole?
convolution linear->db mus-close *rc-directory* two-zero
convolve linux-help mus-cosines readin two-zero?
convolve-files locsig mus-create readin? User-defined generators
convolve? locsig-ref mus-data Realtime Controls volume
cross synthesis locsig-reverb-ref mus-feedback rec-any wave-train
dac locsig-reverb-set! mus-feedforward receive-snd wave-train?
Data formats locsig-set! mus-file-name rectangular->polar waveshape
db->linear locsig? mus-formant-radius reopen-output waveshape?
Debugging make-all-pass mus-frequency restart-clm window-envelope
def-clm-fun make-asymmetric-fm mus-hop restart-env with-current-sound
def-clm-struct make-buffer mus-increment *reverb* with-dac
definstrument make-comb mus-input? ring-modulate with-mix
defpinstrument make-controller mus-ircam run with-offset
degrees->radians make-convolve mus-length run* with-psound
delay make-delay mus-location Run support for Lisp with-sound
delay? make-empty-frame mus-offset sample->buffer
describe-instrument make-empty-mixer mus-open-read sample->file
Direct to DAC make-env mus-open-write sample->file?
dlocsig

related documentationsnd.htm extsnd.htm grfsnd.htm sndlib.htm sndscm.htm libxm.htm index.htm