CLM

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

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

CLM has several sections: "generators", instruments (definstrument and *.ins), examples of note lists (with-sound, *.clm), a "make" facility for sound files (with-mix), and various functions that are useful in sound file work. CLM is available free: (clm-5.tar.gz clm tarball).

Bill Schottstaedt (bil@ccrma.stanford.edu)

Contents

Introduction

CLM provides functions to experiment with sounds. The easiest way to make a new sound is with-sound. Say we want to hear one second of the fm violin (in v.ins, named fm-violin) at 440 Hz, and a somewhat soft amplitude. Compile v.ins and load v, then call with-sound:

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

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

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

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

(play)

or, if you have some saved sound file:

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

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

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

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

(definstrument simp (start-time duration frequency amplitude)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Instruments

The normal structure of an instrument is:

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

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

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

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

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

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

Loop is expanded as a macro and anything in the loop syntax is ok if it expands into something else mentioned above (i.e. a lambda form with go's and so forth).

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

Generators

all-pass all-pass filter asymmetric-fm asymmetric fm comb comb filter convolve convolution delay delay line env line segment envelope filter direct form FIR/IIR filter filtered-comb comb filter with filter on feedback fir-filter FIR filter formant resonance granulate granular synthesis iir-filter IIR filter in-any sound file input locsig static sound placement move-sound sound motion moving-average moving window average ncos sum of equal amplitude cosines notch notch filter nsin sum of equal amplitude sines nrxycos sum of n scaled cosines nrxysin sum of n scaled sines one-pole one pole filter one-zero one zero filter oscil sine wave and FM out-any sound output polywave and polyshape waveshaping phase-vocoder vocoder analysis and resynthesis pulse-train pulse train rand,rand-interp random numbers, noise readin sound input sawtooth-wave sawtooth square-wave square wave src sampling rate conversion ssb-am single sideband amplitude modulation table-lookup interpolated table lookup tap delay line tap triangle-wave triangle wave two-pole two pole filter two-zero two zero filter wave-train wave train

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

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

(oscil? oscillator) returns t, and (mus-frequency oscillator) returns 330. The initialization function (make-oscil above) normally takes a number of optional arguments, setting whatever choices need to be made to specify the generator's behavior. The run-time function (oscil above) always takes the generator as its first argument. Its second argument is nearly always something like an FM input; in a few cases, it is a function to provide input data or editing operations. Frequency sweeps of all kinds (vibrato, glissando, breath noise, FM proper) are all forms of run-time frequency modulation. So, in normal usage, our oscillator looks something like:

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

Frequencies are always in cycles per second (also known as Hz). The FM (or frequency change) argument is assumed to be a phase change in radians, applied on each sample. Normally composers would rather think in terms of Hz, so the function hz->radians can be used to convert from units of cycles per second to radians per sample.

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

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

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

are all equivalent, but

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

oscil

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

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

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

Oscil might be defined:

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

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

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

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

(definstrument simple-osc (beg dur freq amp) (let* ((start (floor (* beg *srate*))) (end (+ start (floor (* dur *srate*)))) (arr (make-array 20))) ; we'll create a tone with 20 harmonics (do ((i 0 (1+ i))) ((= i 20)) (setf (aref arr i) (make-oscil (* (1+ i) 100)))) (run (loop for i from start to end do (let ((sum 0.0)) (do ((i 0 (1+ i))) ((= i (length arr))) (incf sum (oscil (aref arr i)))) (outa i (* amp .05 sum)))))))

env

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

env methods mus-location call counter value (number of calls so far on env) mus-increment base value (no setf) mus-data original breakpoint list mus-scaler original scaler mus-offset original offset mus-length original duration in samples

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

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

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

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

(definstrument mapenv (beg dur frq amp en) (let* ((start (floor (* beg *srate*))) (end (+ start (floor (* dur *srate*)))) (osc (make-oscil frq)) (half-pi (* pi 0.5)) (zv (make-env en 1.0 dur))) (run (loop for i from start below end do (let ((zval (env zv))) ; zval^3 is [0.0..1.0], as is sin between 0 and half-pi. (outa i (* amp (sin (* half-pi zval zval zval)) (oscil osc)))))))) (with-sound () (mapenv 0 1 440 .4 '(0 0 50 1 75 0 86 .5 100 0)))

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

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

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

This instrument repeats the same envelope over and over:

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

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

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

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

envelope-reverse e reverse an envelope envelope-repeat e num &optional refl xnorm repeat an envelope envelope-concatenate &rest es concatenate any number of envelopes envelope+ es add together any number of envelopes envelope* es same but multiply envelope-simplify e &optional yg xg simplify an evelope meld-envelopes e0 e1 meld two envelopes together map-across-envelopes func es map a function across any number of envelopes envelope-exp e &optional pow xg create exponential segments of envelopes window-envelope beg end e return portion of e between two x values stretch-envelope e a0 a1 &optional d0 d1 attack and decay portions scale-envelope e scale &optional offset scale e normalize-envelope e &optional norm normalize e

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

(defun change-env-dur (e dur) (make-env (mus-data e) ; the original breakpoints :scaler (mus-scaler e) ; these are the original values passed to make-env :offset (mus-offset e) :base (mus-increment e) ; the base (using "mus-increment" because it was available...) :duration dur))

table-lookup

make-table-lookup &optional-key (frequency 0.0) ; in Hz (initial-phase 0.0) ; in radians wave ; double-float array size ; table size if wave not specified type ; interpolation type (mus-interp-linear) table-lookup tl &optional (fm-input 0.0) table-lookup? tl

table-lookup performs interpolating table lookup. Indices are first made to fit in the current table (FM input can produce negative indices), then interpolation returns the table value. Table-lookup scales its frequency change argument (fm-input) to fit whatever its table size is (that is, it assumes the caller is thinking in terms of a table size of two pi, and fixes it up). The wave table should be an array of double-floats (the function make-double-array can be used to create it). type sets the type of interpolation used: mus-interp-none, mus-interp-linear, mus-interp-lagrange, mus-interp-bezier, or mus-interp-hermite.

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

Table-lookup might be defined:

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

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

partials->wave synth-data table &optional (norm t) phase-partials->wave synth-data table &optional (norm t)

The synth-data argument is a list of (partial amp) pairs: '(1 .5 2 .25) gives a combination of a sine wave at the carrier (1) at amplitude .5, and another at the first harmonic (2) at amplitude .25. The partial amplitudes are normalized to sum to a total amplitude of 1.0 unless the argument norm is nil. If the initial phases matter (they almost never do), you can use phase-partials->wave; in this case the synth-data is a list of (partial amp phase) triples with phases in radians.

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

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

polywave

make-polywave &optional-key (frequency 0.0) (partials '(1 1)) (type mus-chebyshev-first-kind) polywave w &optional (fm 0.0) polywave? w make-polyshape &optional-key (frequency 0.0) (initial-phase 0.0) coeffs (partials '(1 1)) (kind mus-chebyshev-first-kind) polyshape w &optional (index 1.0) (fm 0.0) polyshape? w partials->polynomial partials &optional (kind mus-chebyshev-first-kind)

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

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

Polywave and polyshape:

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

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

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

Bigbird is another example:

(definstrument bigbird (start duration frequency freqskew amplitude freq-env amp-env partials) (multiple-value-bind (beg end) (times->samples start duration) (let* ((gls-env (make-env freq-env (hz->radians freqskew) duration)) (polyos (make-polyshape frequency :coeffs (partials->polynomial (normalize-partials partials)))) (fil (make-one-pole .1 .9)) (amp-env (make-env amp-env amplitude duration))) (run (loop for i from beg below end do (outa i (one-pole fil ; for distance effects (* (env amp-env) (polyshape polyos 1.0 (env gls-env)))))))))) (with-sound () (bigbird beg .05 1800 1800 .2 '(.00 .00 .40 1.00 .60 1.00 1.00 .0) ; freq env '(.00 .00 .25 1.00 .60 .70 .75 1.00 1.00 .0) ; amp env '(1 .5 2 1 3 .5 4 .1 5 .01))) ; partials (bird song spectrum)

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

sawtooth-wave and friends

make-triangle-wave &optional-key (frequency 0.0) (amplitude 1.0) (initial-phase pi) triangle-wave s &optional (fm 0.0) triangle-wave? s make-square-wave &optional-key (frequency 0.0) (amplitude 1.0) (initial-phase 0) square-wave s &optional (fm 0.0) square-wave? s make-sawtooth-wave &optional-key (frequency 0.0) (amplitude 1.0) (initial-phase pi) sawtooth-wave s &optional (fm 0.0) sawtooth-wave? s make-pulse-train &optional-key (frequency 0.0) (amplitude 1.0) (initial-phase two-pi) pulse-train s &optional (fm 0.0) pulse-train? s

These generators produce some standard old-timey wave forms that are still occasionally useful (well, triangle-wave is useful; the others are silly). sawtooth-wave ramps from -1 to 1, then goes immediately back to -1. Use a negative frequency to turn the "teeth" the other way. triangle-wave ramps from -1 to 1, then ramps from 1 to -1. pulse-train produces a single sample of 1.0, then zeros. square-wave produces 1 for half a period, then 0. All have a period of two-pi, so the fm argument should have an effect comparable to the same FM applied to the same waveform in table-lookup. These are not band-limited; if the frequency is too high, you can get foldover, but as far as I know, no-one uses these as audio frequency tone generators — who would want to listen to a square wave? A more reasonable square-wave can be generated via tanh(n * sin(theta)), where "n" (a float) sets how squared-off it is. Even more amusing is this algorithm:

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

saw-tooth and friends' methods mus-frequency frequency in Hz mus-phase phase in radians mus-scaler amplitude arg used in make-<gen> mus-width width of square-wave pulse (0.0 to 1.0) mus-increment frequency in radians per sample

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

Just for completeness, here's an example:

(definstrument simple-saw (beg dur amp) (let* ((os (make-sawtooth-wave 440.0)) (start (floor (* beg *srate*))) (end (+ start (floor (* dur *srate*))))) (run (loop for i from start to end do (outa i (* amp (sawtooth-wave os)))))))

ncos and nsin

make-ncos &optional-key (frequency 0.0) (n 1) ncos cs &optional (fm 0.0) ncos? cs make-nsin &optional-key (frequency 0.0) (n 1) nsin cs &optional (fm 0.0) nsin? cs

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

ncos methods mus-frequency frequency in Hz mus-phase phase in radians mus-scaler (/ 1.0 cosines) mus-length n or cosines arg used in make-<gen> mus-increment frequency in radians per sample

ncos is based on: cos(x) + cos(2x) + ... cos(nx) = (sin((n + .5)x) / (2 * sin(x / 2))) - 1/2 known as the Dirichlet kernel

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

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

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

nsin methods mus-frequency frequency in Hz mus-phase phase in radians mus-scaler dependent on number of sines mus-length n or sines arg used in make-<gen> mus-increment frequency in radians per sample

nsin is based on: sin(x) + sin(2x) + ... sin(nx) = sin(n * x / 2) * (sin((n + .5)x) / sin(x / 2)) known as the conjugate Dirichlet kernel

ssb-am

make-ssb-am &optional-key (frequency 0.0) (order 40) ssb-am gen &optional (insig 0.0) (fm 0.0) ssb-am? gen

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

ssb-am methods mus-frequency frequency in Hz mus-phase phase (of embedded sin osc) in radians mus-order embedded delay line size mus-length same as mus-order mus-interp-type mus-interp-none mus-xcoeff FIR filter coeff mus-xcoeffs embedded Hilbert transform FIR filter coeffs mus-data embedded filter state mus-increment frequency in radians per sample

ssb-am is based on: cos(freq) * delay(insig) +/- sin(freq) * hilbert(insig) which shifts insig spectrum by freq and cancels upper/lower sidebands

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

(definstrument shift-pitch (beg dur freq amp shift) (let* ((os (make-oscil freq)) (start (floor (* beg *srate*))) (end (+ start (floor (* dur *srate*)))) (am (make-ssb-am shift))) (run (loop for i from start to end do (outa i (* amp (ssb-am am (oscil os))))))))

wave-train

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

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

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

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

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

rand

make-rand &optional-key (frequency 0.0) ; freq at which new random numbers occur (amplitude 1.0) ; numbers are between -amplitude and amplitude (envelope '(-1 1 1 1)) ; distribution envelope (uniform distribution between -1 and 1 is the default) distribution ; pre-computed distribution rand r &optional (sweep 0.0) rand? r make-rand-interp &optional-key (frequency 0.0) (amplitude 1.0) (envelope '(-1 1 1 1) distribution) rand-interp r &optional (sweep 0.0) rand-interp? r centered-random amp clm-random amp mus-random amp ; same as centered-random (for C-side compatibility) mus-set-rand-seed seed

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

rand and rand-interp methods mus-frequency frequency in Hz mus-phase phase in radians mus-scaler amplitude arg used in make-<gen> mus-length distribution table length mus-data distribution table, if any mus-increment frequency in radians per sample

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

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

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

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

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

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

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

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

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

(definstrument fractal (start duration m x amp) ;; use formula of M J Feigenbaum (let* ((beg (floor (* *srate* start))) (end (+ beg (floor (* *srate* duration))))) (run (loop for i from beg below end do (outa i (* amp x)) (setf x (- 1.0 (* m x x))))))) ;;; quickly reaches a stable point for any m in[0,.75], so: (with-sound () (fractal 0 1 .5 0 .5)) ;;; is just a short "ftt" (with-sound () (fractal 0 1 1.5 .20 .2))

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

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

which gives brass-like sounds!

one-pole and friends

make-one-pole &optional-key a0 b1 ; b1 < 0.0 gives lowpass, b1 > 0.0 gives highpass one-pole f input one-pole? f make-one-zero &optional-key a0 a1 ; a1 > 0.0 gives weak lowpass, a1 < 0.0 highpass one-zero f input one-zero? f make-two-pole &optional-key a0 b1 b2 frequency radius two-pole f input two-pole? f make-two-zero &optional-key a0 a1 a2 frequency radius two-zero f input two-zero? f

simple filter methods mus-xcoeff a0, a1, a2 in equations mus-ycoeff b1, b2 in equations mus-order 1 or 2 (no setf) mus-scaler two-pole and two-zero radius mus-frequency two-pole and two-zero center frequency

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

The "a0, b1" nomenclature is taken from Julius Smith's "An Introduction to Digital Filter Theory" in Strawn "Digital Audio Signal Processing", and is different from that used in the more general filters such as fir-filter. In make-two-pole and make-two-zero you can specify either the actual desired coefficients (a0 and friends), or the center frequency and radius of the filter (frequency and radius). radius should be between 0 and 1 (but less than 1), and frequency should be between 0 and srate/2.

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

(definstrument bird (startime dur frequency freq-skew amplitude freq-envelope amp-envelope &optional (lpfilt 1.0) (degree 0) (reverb-amount 0)) (multiple-value-bind (beg end) (times->samples startime dur) (let* ((amp-env (make-env amp-envelope amplitude dur)) (gls-env (make-env freq-envelope (hz->radians freq-skew) dur)) (loc (make-locsig :degree degree :distance 1.0 :reverb reverb-amount)) (fil (make-one-pole lpfilt (- 1.0 lpfilt))) (s (make-oscil :frequency frequency))) (run (loop for i from beg to end do (locsig loc i (one-pole fil (* (env amp-env) (oscil s (env gls-env))))))))))

formant

make-formant &optional-key frequency radius formant f input ; resonator centered at frequency, bandwidth set by radius above formant? f make-firmant &optional-key frequency radius firmant f input ; resonator centered at frequency, bandwidth set by radius above firmant? f

formant methods mus-frequency formant center frequency mus-order 2 (no setf) mus-scaler gain

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

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

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

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

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

filter, iir-filter, fir-filter

make-filter &optional-key order xcoeffs ycoeffs filter fl inp filter? fl make-fir-filter &optional-key order xcoeffs fir-filter fl inp fir-filter? fl make-iir-filter &optional-key order ycoeffs iir-filter fl inp iir-filter? fl envelope->coeffs &key order envelope dc

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

general filter methods mus-order filter order mus-xcoeff x (input) coeff mus-xcoeffs x (input) coeffs mus-ycoeff y (output) coeff mus-ycoeffs y (output) coeffs mus-data current state (input values) mus-length same as mus-order

filter: (let ((xout 0.0)) (setf (aref state 0) input) (loop for j from order downto 1 do (incf xout (* (aref state j) (aref xcoeffs j))) (decf (aref state 0) (* (aref ycoeffs j) (aref state j))) (setf (aref state j) (aref state (1- j)))) (+ xout (* (aref state 0) (aref xcoeffs 0))))

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

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

(definstrument filter-noise (beg dur amp &key xcoeffs) (let* ((st (floor (* beg *srate*))) (noi (make-rand :frequency (* .5 *srate*) :amplitude amp)) (flA (make-filter :xcoeffs xcoeffs)) (nd (+ st (floor (* *srate* dur))))) (run (loop for i from st below nd do (outa i (filter flA (rand noi))))))) (with-sound () (filter-noise 0 1 .2 :xcoeffs (envelope->coeffs :order 12 :envelope '(0 0.0 .125 0.5 .2 0.0 .3 1.0 .5 0.0 1.0 0.0))))

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

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

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

delay

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

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

delay methods mus-length length of delay mus-order same as mus-length mus-data delay line itself (no setf) mus-interp-type interpolation choice (no setf) mus-scaler unused internally, but available for delay specializations

delay: (prog1 (array-interp line (- loc pm)) (setf (aref line loc) input) (incf loc) (if (<= size loc) (setf loc 0)))

(definstrument echo (beg dur scaler secs file) (let ((del (make-delay (round (* secs *srate*)))) (inf (open-input file)) (j 0)) (run (loop for i from beg below (+ beg dur) do (let ((inval (ina j inf))) (outa i (+ inval (delay del (* scaler (+ (tap del) inval))))) (incf j)))) (close-input inf))) ;;; (with-sound () (echo 0 60000 .5 1.0 "pistol.snd"))

comb and notch

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

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

comb, filtered-comb, and notch methods mus-length length of delay mus-order same as mus-length mus-data delay line itself (no setf) mus-feedback scaler (comb only) mus-feedforward scaler (notch only) mus-interp-type interpolation choice (no setf)

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

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

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

all-pass

make-all-pass &optional-key feedback feedforward size initial-contents initial-element max-size all-pass f input &optional (pm 0.0) all-pass? f

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

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

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

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

moving-average

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

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

moving-average methods mus-length length of table mus-order same as mus-length mus-data table of last 'size' values

result = sum-of-last-n-inputs / n

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

src

make-src &optional-key input (srate 1.0) (width 5) src s &optional (sr-change 0.0) input-function src? s

src methods mus-increment srate arg to make-src

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

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

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

(definstrument srcer (start-time duration amp srt fmamp fmfreq filename) (let* ((os (make-oscil :frequency fmfreq)) (beg (floor (* start-time *srate*))) (end (+ beg (floor (* duration *srate*)))) (src-gen (make-src :input filename :srate srt))) (run (loop for i from beg below end do (outa i (* amp (src src-gen (* fmamp (oscil os))))))))) (with-sound () (srcer 0 2 1.0 1 .3 20 "fyow.snd")) (with-sound () (srcer 0 25 10.0 .01 1 10 "fyow.snd")) (with-sound () (srcer 0 2 1.0 .9 .05 60 "oboe.snd")) (with-sound () (srcer 0 2 1.0 1.0 .5 124 "oboe.snd")) (with-sound () (srcer 0 10 10.0 .01 .2 8 "oboe.snd")) (with-sound () (srcer 0 2 1.0 1 3 20 "oboe.snd")) (definstrument hello-dentist (beg dur file frq amp) (let ((rd (make-src :input file)) (rn (make-rand-interp :frequency frq :amplitude amp)) (end (+ beg dur))) (run (loop for i from beg below end do (outa i (src rd (rand-interp rn)))))))

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

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

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

convolve

make-convolve &optional-key input filter fft-size filter-size convolve ff &optional input-function convolve? ff convolve-files file1 file2 &optional (maxamp 1.0) (output-file "tmp.snd")

convolve methods mus-length fft size used in the convolution

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

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

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

granulate

make-granulate &optional-key input (expansion 1.0) ; how much to lengthen or compress the file (length .15) ; length of file slices that are overlapped (scaler .6) ; amplitude scaler on slices (to avoid overflows) (hop .05) ; speed at which slices are repeated in output (ramp .4) ; amount of slice-time spent ramping up/down (jitter 1.0) ; affects spacing of successive grains max-size ; internal buffer size edit ; grain editing function (Scheme/Ruby, not CL) granulate e &optional input-function edit-function granulate? e

granulate methods mus-frequency time (seconds) between output grains (hop) mus-ramp length (samples) of grain envelope ramp segment mus-hop time (samples) between output grains (hop) mus-scaler grain amp (scaler) mus-increment expansion mus-length grain length (samples) mus-data grain samples (a vct) mus-location granulate's local random number seed

result = overlap add many tiny slices from input

granulate "granulates" its input (normally a sound file). It is the poor man's way to change the speed at which things happen in a recorded sound without changing the pitches. It works by slicing the input file into short pieces, then overlapping these slices to lengthen (or shorten) the result; this process is sometimes known as granular synthesis, and is similar to the freeze function.

The duration of each slice is length — the longer the slice, the more like reverb the effect. The portion of the length (on a scale from 0 to 1.0) spent on each ramp (up or down) is ramp. This can control the smoothness of the result of the overlaps.

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

The overall amplitude scaler on each segment is scaler — this is used to try to avoid overflows as we add all these zillions of segments together. expansion determines the input hop in relation to the output hop; an expansion-amount of 2.0 should more or less double the length of the original, whereas an expansion-amount of 1.0 should return something close to the original speed. input and input-function are the same as in src and convolve.

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

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

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

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

phase-vocoder

make-phase-vocoder &optional-key input (fft-size 512) (overlap 4) interp (pitch 1.0) analyze edit synthesize phase-vocoder pv input-function analyze-function edit-function synthesize-function phase-vocoder? pv

phase-vocoder methods mus-frequency pitch shift mus-length fft-size mus-increment interp mus-hop fft-size / overlap mus-location outctr (counter to next fft)

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

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

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

nrxycos and nrxysin

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

nrxysin methods mus-frequency frequency in Hz mus-phase phase in radians mus-scaler "a" parameter; sideband scaler mus-length "n" parameter mus-increment frequency in radians per sample mus-offset "ratio" parameter

(/ (- (sin phase) (* a (sin (- phase (* ratio phase)))) (* (expt a (1+ n)) (- (sin (+ phase (* (+ N 1) (* ratio phase)))) (* a (sin (+ phase (* N (* ratio phase)))))))) (- (+ 1 (* a a)) (* 2 a (cos (* ratio phase)))))

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

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

(definstrument ss (beg dur freq amp &optional (n 1) (r 0.5) (ratio 1.0)) (let* ((st (floor (* *srate* beg))) (nd (+ st (floor (* *srate* dur)))) (sgen (make-nrxycos freq ratio n r))) (run (loop for i from st below nd do (outa i (* amp (nrxycos sgen)))))))

asymmetric-fm

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

asymmetric-fm methods mus-frequency frequency in Hz mus-phase phase in radians mus-scaler "r" parameter; sideband scaler mus-offset "ratio" parameter mus-increment frequency in radians per sample

(* (exp (* index (* 0.5 (- r (/ 1.0 r))) (cos (* ratio phase)))) (sin (+ phase (* index (* 0.5 (+ r (/ 1.0 r))) (sin (* ratio phase))))))

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

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

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

Other generators

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

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

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

generic functions

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

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

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

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

Sound IO

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

mus-input? obj t if obj performs sound input mus-output? obj t if obj performs sound output file->sample? obj t if obj reads a sound file returning a sample sample->file? obj t if obj writes a sample to a sound file frample->file? obj t if obj writes a frample to a sound file file->frample? obj t if obj reads a sound file returning a frample make-file->sample name buffer-size return gen that reads samples from sound file name make-sample->file name &optional chans format type comment return gen that writes samples to sound file name make-file->frample name buffer-size return gen that reads framples from sound file name make-frample->file name &optional chans format type comment return gen that writes framples to sound file name file->sample obj samp &optional chan return sample at samp in channel chan sample->file obj samp chan val write (add) sample val at samp in channel chan file->frample obj samp &optional outf return frample at samp frample->file obj samp val write (add) frample val at samp file->array file channel beg dur array read samples from file into array array->file file data len srate channels write samples in array to file continue-frample->file file reopen file for more output continue-sample->file file reopen file for more output mus-close obj close the output file associated with obj

out-any

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

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

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

in-any

in-any loc channel i-stream ina loc

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

(definstrument simple-ina (beg dur amp file) (let* ((start (floor (* beg *srate*))) (end (+ start (floor (* dur *srate*)))) (fil (open-input file))) ; actually make-file->sample (run (loop for i from start to end do (outa i (* amp (in-any i 0 fil))))) ; actually file->sample (close-input fil)))

readin

make-readin &optional-key file (channel 0) start (direction 1) readin rd readin? rd

readin methods mus-channel channel arg to make-readin (no setf) mus-location current location in file mus-increment sample increment (direction arg to make-readin) mus-file-name name of file associated with gen mus-length number of framples in file associated with gen

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

(definstrument env-sound (file beg &optional (amp 1.0) (amp-env '(0 1 100 1))) (let* ((st (floor (* beg *srate*))) (dur (sound-duration file)) (rev-amount .01) (rdA (make-readin file)) (ampf (make-env amp-env amp dur)) (nd (+ st (floor (* *srate* dur))))) (run (loop for i from st below nd do (let ((outval (* (env ampf) (readin rdA)))) (outa i outval) (if *reverb* (outa i (* outval rev-amount) *reverb*)))))))

locsig

make-locsig &optional-key (degree 0.0) (distance 1.0) (reverb 0.0) channels (type *clm-locsig-type*) locsig loc i in-sig locsig? loc locsig-ref loc chan locsig-set! loc chan val locsig-reverb-ref loc chan locsig-reverb-set! loc chan val move-locsig loc degree distance locsig-type ()

locsig methods mus-data output scalers (a vct) mus-xcoeff reverb scaler mus-xcoeffs reverb scalers (a vct) mus-channels output channels mus-length output channels

locsig normally takes the place of out-any in an instrument. It tries to place a signal between channels 0 and 1 (or 4 channels placed in a circle) in an extremely dumb manner: it just scales the respective amplitudes ("that old trick never works"). reverb determines how much of the direct signal gets sent to the reverberator. distance tries to imitate a distance cue by fooling with the relative amounts of direct and reverberated signal (independent of reverb). distance should be greater than or equal to 1.0. type (returned by the function locsig-type) can be mus-interp-linear (the default) or mus-interp-sinusoidal. This parameter can be set globally via *clm-locsig-type*. The mus-interp-sinusoidal case uses sin and cos to set the respective channel amplitudes (this is reported to help with the "hole-in-the-middle" problem).

Locsig is a kludge, but then so is any pretence of placement when you're piping the signal out a loudspeaker. It is my current belief that locsig does the right thing for all the wrong reasons; a good concert hall provides auditory spaciousness by interfering with the ear's attempt to localize a sound. A diffuse sound source is the ideal! By sending an arbitrary mix of signal and reverberation to various speakers, locsig gives you a very diffuse source; it does the opposite of what it claims to do, and by some perversity of Mother Nature, that is what you want. (See "Binaural Phenomena" by J Blauert).

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

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

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

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

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

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

move-sound

make-move-sound dlocs-list (output *output*) (revout *reverb*) move-sound dloc i in-sig move-sound? dloc

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

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

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

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

Useful functions

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

hz->radians freq convert freq to radians per sample radians->hz rads convert rads to Hz db->linear dB convert dB to linear value linear->db val convert val to dB times->samples start duration convert start and duration from seconds to samples (beg+dur in latter case) samples->seconds samps convert samples to seconds seconds->samples secs convert seconds to samples degrees->radians degs convert degrees to radians radians->degrees rads convert radians to degrees clear-array arr set all values in arr to 0.0 sound-samples filename samples of sound according to header (can be incorrect) sound-framples filename number of framples (number of samples / channels) sound-datum-size filename bytes per sample sound-data-location filename location of first sample (bytes) sound-chans filename number of channels (samples are interleaved) sound-srate filename sampling rate sound-header-type filename header type (aiff etc) sound-data-format filename data format (alaw etc) sound-length filename true file length (for error checks) sound-duration filename file length in seconds sound-maxamp name vals get max amp vals and times of file name sound-loop-info name vals get loop info of file name in vals (make-integer-array 6)

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

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

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

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

polynomial

polynomial coeffs x

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

array-interp and dot-product

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

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

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

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

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

contrast-enhancement

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

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

Waveshaping can provide a similar effect:

(definstrument brighten (start duration file file-maxamp partials) (multiple-value-bind (beg end) (times->samples start duration) (let ((fil (open-input* file))) (when fil (unwind-protect (let ((coeffs (partials->polynomial (normalize-partials partials))) (rd (make-readin fil))) (run (loop for i from beg below end do (outa i (* file-maxamp (polynomial coeffs (/ (readin rd) file-maxamp))))))) (close-input fil)))))) (with-sound () (brighten 0 3 "oboe" .15 '(1 1 3 .5 7 .1)))

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

ring-modulate and amplitude-modulate

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

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

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

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

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

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

FFT

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

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

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

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

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

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

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

def-clm-struct

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

Definstrument

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

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

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

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

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

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

Note lists

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

with-sound

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

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

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

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

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

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

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

You can call with-sound within an instrument:

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

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

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

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

produces a note that is .2 in amplitude.

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

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

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

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

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

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

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

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

Continue-old-file, if t, re-opens a previously existing file for further processing. Normally with-sound clobbers any existing file of the same name as the output file (see output above). By using continue-old-file, you can both add new stuff to an existing file, or (by subtracting) delete old stuff to any degree of selectivity. When you erase a previous note, remember that the subtraction has to be exact; you have to create exactly the same note again, then subtract it. By the same token, you can make a selected portion louder or softer by adding or subtracting a scaled version of the original. The option data-format underlies :scaled-to. CLM can read and write sound data in all the currently popular formats, leaving aside proprietary compression schemes. The names used in :data-format can be found in initmus.lisp, along with the headers CLM knows about.

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

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

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

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

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

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