This version of the clm documentation omits all the graphics and font-related stuff of the original (clm.wn, clm.rtf). COMMON LISP MUSIC Common Lisp Music (CLM, for lack of a catchier name) is a music synthesis and signal processing package in the Music V family, written primarily in Common Lisp. The CLM instrument design language is a subset of Common Lisp (its numerical functions, and nearly all its control functions), extended with a large number of "unit generators": oscil, env, table- lookup, and so on. The instruments can run as straight Common Lisp software, or can be compiled into C or DSP code. Since CLM instruments are Lisp functions, a CLM note list is just any Lisp expression that happens to call those functions. The notes need not even be in any order. The actual computation is done one note at a time, and the results are overlayed, building up the sound note by note. CLM is available free, via anonymous ftp (pub/Lisp/clm.tar.Z at ccrma-ftp.stanford.edu). Originally CLM was intended primarily for machines that had a 56000 available, but over the years the main processors have caught up and passed the 56000 in computational speed, so the current version is trying to keep a foot in both camps. If you're running a NeXT with an Ariel QP board, you should probably stick with the 56000 version of CLM, but otherwise I suggest using the C version -- even on the NeXT the latter is almost as fast as the former, if you're using just the built-in 56000. The choice can be made in all.lisp when CLM is compiled and loaded, or can be delayed to instrument compilation time (the two forms can co-exist without problem), or can be left up to CLM (it will try to use the 56000, if available, except when it thinks C will perform better). The file README.clm contains some benchmarks comparing various hardware platforms. On the newer processors, even relatively complex instruments can run in "real-time", so I may re-implement the real-time control portion of CLM. There are a variety of small differences between current implementations of CLM -- for example, due to the stupidity of AKCL's defentry function, CLM's instrument-let only works in AKCL for Lisp or the 56000, but not for C. Similarly, funcall is supported in Lisp and on the 56000, but not in C. Most such differences are minor, and there's always a way around them. But, to keep this document from expanding beyond anyone's patience, I've abbreviated such gotchas as [DSP] = only on the 56000, [C] = only in C, [ACL] = only in Franz's Allegro CL, [KCL] = only in AKCL, and [MCL] = only on the Mac. Others may pop up as I sit and type. CLM has several sections: the "unit generators", instruments (definstrument, *.ins, and ins.lisp), examples of note lists (with-sound, *.clm), a "make" facility for sound files (mix), and various functions that are useful in sound file work (sound-let, fasmix, etc). The rest of this document concentrates on the functions CLM provides to the composer at this "top level". The file ins.lisp is a tutorial on writing instruments. I haven't yet written a tutorial for composition with CLM. Currently, CLM runs on the NeXT, Macintosh, SGI, and Intel machines running NeXTStep. It can take advantage of Ariel's QuintProcessor board on the NeXT, and the Ariel PC56-D and Ilink i56 boards on the 486-style machines. For benchmarks comparing these platforms, and comparing CLM to its friends in the C world (CSound, CMix, and CMusic) see the clm file README.clm. Contents: Introduction 2 Instruments 3 Lisp Functions (Run) 4 Unit Generators 5 Sound Processing (Mix) 24 Definstrument 26 Note Lists (With-Sound) 28 Phrasing 33 Structs 35 Appendices How to use CLM outside with-sound 38 Fast sound file mixing and format changes 39 Header and data types supported in CLM 40 Low level debugging aids 40 Sources of unwanted noise 40 Index 42 INTRODUCTION CLM is a sound compiler. It provides a Lisp environment in which various kinds of expressions generate sound files. Lisp from time immemorial has presented itself to the user as an "interpreter". As you type in its window, it reads what you type, tries to make sense of it, and does something, returning a result. To use CLM either from the lisp listener or from Rick Taube's Common Music you first need to learn the rudiments of Lisp. The basic idea in Lisp is that something of the form (name arg1 arg2) calls the function "name" and passes it the two arguments. In C this would be name(arg1,arg2); In C, 4+3 is an expression yielding 7. In Lisp this is expressed (+ 4 3). Similarly C's 4+3*2 becomes (+ 4 (* 3 2)). To learn Lisp, just follow the examples given below or in ins.lisp, and try making simple changes. For full information see "Common Lisp" by Steele, "Lisp" by Winston and Horn, or "Common Lisp" by Touretzky. The easiest way to create a new sound file is to use the macro with-sound. Say we want to hear one second of the fm violin (in v.ins, named fm-violin) at 440 Hz, and a somewhat soft amplitude. Load v.fasl (or v.o in KCL);(it is automatically included in the Common Music image at CCRMA), Then type (with-sound () (fm-violin 0 1 440 .1)) and the note should emerge from the speakers. Once loaded, we don't need to reload v unless we change it in some way. To get a chord: (with-sound () (fm-violin 0 1 440 .1) (fm-violin 0 1 660 .1)) To get a reverberated version, load a reverberator (jcrev.fasl from jcrev.ins, for example), and (with-sound (:reverb jc-reverb) (fm-violin 0 .1 440 .2)) The with-sound macro takes a number of optional arguments -- :reverb is one of them. It gives the name of the reverberator (jc-reverb in the case above). The favorite reverberator for the last few years has been nrev in nrev.ins. If you want to hear the sound again without recomputing it, call the function (dac). To stop in the middle of playback, type (stop-dac). The (dac) call starts up a background process playing the sound and returns to the lisp listener. The function dac without any argument plays the last file clm created (normally /zap/test.snd, the default output of the with-sound macro). If you want to play some other sound file, give the file name as the argument to dac: (dac "saved.snd"). A large number of instruments are already compiled and ready for use -- see ins.lisp for a tutorial on building instruments, and the various *.ins files which usually contain a few sample calls. For more examples of note lists, see the various files /dist/lisp/clm/*.clm. Any Lisp-ism is ok in the note list: (with-sound () (loop for i from 0 to 7 do (fm-violin (* i .25) .5 (* 100 (1+ i)) .1))) creates a little arpeggio of slightly overlapping notes. clm-example.lisp shows how to create such a note list algorithmically. Judicious use of mix and with-mix (page 23) can speed up the computation by an arbitrary amount. The rest of this document discusses clm instrument structure, the built-in unit-generators, various useful sound file functions (mix, definstrument, with-sound, and local versions thereof), the 56000/C library, the "phrasing" mechanism, and the structs that make the innards of the generators available at run-time. CLM INSTRUMENTS An instrument is a lisp function that can send its output to any currently open output file, and get input from any currently open input file (or from the output files, for that matter). If you're willing to run entirely in Lisp software, there's no limitation on what you can do -- you are just running an arbitrary lisp function. If you are hoping to compile the lisp code into DSP or C code, there are a number of limitations (life is short, and many of Lisp's functions are not used in any current instruments). The main two instrument-defining entities are definstrument and run. Definstrument replaces Lisp's defun, and run marks the "run-time" portion of the instrument -- the portion that we want to optimize as much as possible for speed. In its simplest use, definstrument looks just like defun. Here is a simple instrument: (definstrument simp (start duration frequency amplitude) (let* ((beg (floor (* start sampling-rate))) (end (+ beg (floor (* duration sampling-rate)))) (sinewave (make-oscil :frequency frequency))) (Run (loop for i from beg to end do (outa i (* amplitude (oscil sinewave))))))) This creates an oscillator (make-oscil), then calls it once on each sample between the start and end points, adding its output into the current output file via outa. Once compiled and loaded, we can call this instrument: (with-sound () (simp 0 1 440 .1)) to produce a 1 second sine wave at 440Hz. The normal structure of an instrument is (definstrument name (args) setup code (run (loop for i from beg to end do run-time code ))) The run macro is our dsp/C-compiler. Use only one call on run per instrument -- our run-time loader is not yet smart enough to thread its way through more than one dsp code vector. See sound-let and instrument-let (and expsrc.ins.lisp) for ways to get around this limitation. [DSP: If the instrument uses delay lines, it should end with (end-run) to free up the lines.] LISP FUNCTIONS that RUN can handle The following Lisp functions can occur inside the run loop: + / * - 1+ 1- incf decf setf setq psetf psetq = /= < > <= >= zerop plusp minusp oddp evenp max min abs mod rem gcd lcm floor ceiling round truncate signum sqrt random float ash log expt exp sin cos tan asin acos atan cosh sinh tanh asinh acosh atanh or and not null if unless when cond progn prog1 prog2 case tagbody go break error warn print princ terpri y-or-n-p yes-or-no-p block return return-from let let* loop do do* dotimes declare lambda apply funcall aref elt array-total-size array-in-bounds-p nth eref array-rank array-dimension logior logxor logand lognot logeqv lognand lognor logandc1 logandc2 logorc1 logorc2 logbit logtest Within the outer loop body, loop is expanded as a macro and anything in the loop syntax is ok if it expands into something else mentioned above (i.e. a lambda form with go's and so forth). The outermost loop, however, is special -- it is expected to be the first thing run sees, but cannot be anything but a simple loop (just one iteration variable, counting by 1 etc). Generally user variables are assumed to be either integer, short-float, one of the struct types defined in mus.lisp (see pages 35-37), or a user-defined struct that has been defined for run's benefit with def-clm-struct (pages 38-39). Variables are currently assumed to be of one type (that is, I haven't yet fully implemented Lisp's run-time typing). [DSP: The loop control variable is a long integer -- not a "normal" integer, and any arithmetic on it returns a long integer. It should only be used where you want a sample counter result.] Lisp's declare can be used to force a variable to be a particular type. Run currently recognizes the types fixnum, float, and bignum -- the latter should be used for long integers (see ins.lisp for an example). Very large arrays (or wave-tables) should be written out as sound files and read using ina or readin. [DSP: Funcall provides an escape to Lisp -- (funcall #'some-function some-args...) passes the arguments (dsp run-time numerical values) to the function "some-function" in the Lisp interpreter, and any numerical result is passed back to the dsp. This escape is 1000 times slower than a normal dsp function call. Break drops into both the DSP breakpoint handler and the lisp debugger; error halts the DSP and drops into the Lisp debugger; warn prints a warning; print and princ print a string or a variable's value. All but print and princ can take the usual optional formatting commands and arguments. If the optimize quality safety is set to be greater than 0 (the default), run inserts code to catch and report overflows -- these affect mainly those unit generators that require "fractional" input on the 56000]. In addition, the function clm-print stands in for Lisp's print+format -- we don't support all of format's options, but enough to be useful, I hope. Its syntax is (clm-print format-string &rest args). "UNIT GENERATORS" Nearly every computer music implementation, CLM included, provides highly optimized versions of the synthesis and processing functions that have proved most useful to date. We follow the ancients and call them "unit generators" -- weird inexplicable jargon, but at least we no longer talk about "slew rate" and "mag". Each generator consists of a pair of functions. Make- sets up the data structure associated with the generator at initialization time, and runs the generator, producing a new sample each time it is called. For example, (setf oscillator (make-oscil :frequency 330)) prepares a structure (of type osc in this case), ready to produce a sine wave when set in motion via (oscil oscillator) All generators follow this calling sequence. The initialization function normally takes a number of optional arguments, setting whatever state the given generator needs to operate on. The run- time function's first argument is always its associated structure. Its second argument is nearly always something like an FM input -- whatever run-time modulation might be desired. Amplitude envelopes are handled with a separate env generator, and attack and decay characteristics are handled by divseg at initialization time. Frequency sweeps of all kinds (vibrato, glissando, breath noise, FM proper) are all forms of run-time frequency modulation. So, in normal usage, our oscillator looks something like: (oscil oscillator (+ vibrato glissando frequency-modulation)) All these functions and structures are defined in mus.lisp. In the documentation below, we give the calling sequences and defaults, and mention any peculiarities that come to mind. Frequencies are always in cycles per second (also known as Hz), internal table size is (nearly) always two pi. The fm (or frequency change) argument is assumed to be a phase change in radians, applied on each sample. Normally composers would rather think in terms of Hertz, so the function In-hz can be used to convert from units of cycles per second to radians per sample (this conversion factor used to be called "mag"). Since all the generators agree that their internal period length is two-pi, you can always use In-hz to convert the frequency change (or fm) argument from a more easily interpreted value. OSCIL Oscil produces a sine wave with optional frequency change (i.e. fm). Its first argument is an osc structure, normally created by make-oscil. Oscil's second (optional) argument is the current (sample-wise) frequency change (defaults to 0). Its third argument is the (sample-wise) phase change (in addition to the carrier increment and so on). See fm.wn for a discussion of fm. make-oscil &key (frequency 440.0) ; Hz (initial-phase 0.0) ; radians oscil os &optional (fm-input 0.0) (pm-input 0.0) For example, a simple-FM instrument is: (definstrument fm (beg end freq amp mc-ratio index &optional (amp-env '(0 0 50 1 100 1)) (index-env '(0 1 100 1))) (let* ((cr (make-oscil :frequency freq)) (md (make-oscil :frequency (* freq mc-ratio))) (fm-index (in-Hz (* index mc-ratio freq))) (ampf (make-env :envelope amp-env :scaler amp :start beg :end end)) (indf (make-env :envelope index-env :scaler fm-index :start beg :end end))) (Run (loop for i from beg to end do (outa i (* (env ampf) (oscil cr (* (env indf) (oscil md))))))))) Oscil is essentially: returned-val = sin(current-phase+pm-input); current-phase += freq+fm-input; ENV In CLM, an envelope is a list of break point pairs: '(0 0 100 1) is a ramp from 0 to 1 over an x-axis excursion from 0 to 100. This list is passed to make-env along with the desired start and end times, the scaler applied to the y axis, and the offset added to every y value. The resulting object, when accessed with the env generator, creates an envelope of the same shape as the original, but stretched along the x-axis to fit the desired start time and duration, and transformed along the y-axis by offset + scaler * y-value. In normal use, the x-axis can be anything you like. Here at CCRMA, we use 0..100, but it might make more sense to use 0..1. The kind of interpolation used to get y-values between the break points is determined by the envelope's base. make-env &key envelope ; list of x,y break-point pairs start-time ; in seconds duration ; seconds start ; in samples (can be used instead of start-time) end ; in samples (can be used instead of duration) (offset 0.0) ; value added to every y value (scaler 1.0) ; scaler on every y value (before offset is added) base ; type of connecting line between break-points op ; function connecting break-points (Lisp only) env e restart-env e ; return to start of envelope Make-env produces a structure that env can handle at run-time. Each call on env gets the next value of the envelope, so unless you're being very clever, you should only use each envelope structure once within the run body (that is, make separate envelopes for each occurrence of env, even if everything is the same, or alternatively, use let or let* to hold the envelope value). With the arguments to make-env, you can apply a given shape to any situtation. Say we want a ramp moving from .3 to .5, starting at 1.0 seconds, and ending a second later. The corresponding make-env call would be (make-env :envelope '(0 0 100 1) :scaler .2 :offset .3 :start-time 1.0 :duration 1.0) The x and y axis limits of the envelope definition are arbitrary -- use whatever limits you find easy to visualize. When the envelope is actually applied to a specific situation, the x axis is stretched to fit the current duration and set to start at the start time. The y axis is scaled by scaler and offset by offset. Base determines how the break-points are connected. If it is 1.0 (the default), you get straight line segments. Base = 0.0 gives a step function (the envelope changes its value suddenly to the new one without any interpolation). Any other value becomes the exponent of the exponential curve connecting the points. Base < 1.0 gives convex curves (i.e. bowed out), and Base > 1.0 gives concave curves (i.e. sagging). [DSP: Extreme exponentials run out of bits on the dsp Ð create a line segment envelope as close as possible to the curve you want, then use exponential curves to connect those break-points.] Op is not implemented on the dsp, but in the lisp software version it is the name of an arbitrary function to call to provide any kind of connecting curve you want. Although they may appear otiose, Start-Time and Duration are not entirely useless -- an envelope can have any duration (and perhaps someday any start time). If its duration is shorter than the instrument's, the envelope sticks at its final value. If the envelope's duration is longer than the instrument's, you only march through the portion of the envelope that happens to fit within the instrument's duration. Restart-env causes an envelope to start all over again from the beginning. [DSP: To be able to restart an envelope on the dsp, set the argument restartable to env to t (all envelopes are automatically restartable in Lisp/C)]. divseg fn old-att new-att &optional old-dec new-dec divenv fn dur &optional p1 t1 p2 t2 p3 t3 p4 t4 p5 t5 Divseg provides attack and decay controls on an envelope. Very often, the envelope is considered to have two or three independent portions. The first, called the "attack", is very important, and its duration needs to be dealt with independently of the second ("steady state") and third ("decay") portions. Divseg takes the original envelope (Fn), the original x axis point at which the attack portion ends, the new x-axis point where it should end, and similar data for the decay portion if desired, and returns the new envelope that has been squeezed or stretched along the x-axis to produce the desired timings. (divseg '(0 0 100 1) 50 25) => '(0 0 25 .5 100 1) (divseg '(0 0 100 1) 50 25 75 90) => '(0 0 25 .5 90 .75 100 1) Divenv provides the same operations as divseg, but gives you up to five segments to push around, and instead of specifying the new x axis values, you supply the times (in seconds). Divenv can be extremely confusing -- it was a kludge in previous CCRMA software to get around severe memory limitations and to avoid inadequate envelope definition software. I urge you to make the envelopes from scratch. For example, say we want the basic shape '(0 0 1 1 2 1 3 0) to rise to 1 in the time att, and decay in the time dec, where the overall duration is dur. All it takes it (list 0 0 att 1 (- dur att dec) 1 dur 0) as the envelope argument to make-env. Surely this is clearer than the equivalent (divseg '(0 0 1 1 2 1 3 0) 1 (* 3 (/ att dur)) 2 (* 3 (/ (- dec att dec) dur)))? env.lisp has a variety of functions that operate on envelopes. The most useful are: env-reverse reverse an envelope env-repeat repeat an envelope env-concatenate concatenate any number of envelopes env+ add together any number of envelopes env* same but multiply env-simplify simplify an evelope (smoothing using graphics notions) fft-env-simplify same but use fft-filtering to smooth meld-envelopes return attempt to meld two envelopes together map-across-envelopes map a function across any number of envelopes mus.lisp has a few other useful, self-evident envelope functions: list-interp, scale-envelope, and normalize-envelope. OUTA OUTB OUTC OUTD outa loc data &optional (o-stream *current-output-file*) outb loc data &optional (o-stream *current-output-file*) outc loc data &optional (o-stream *current-output-file*) outd loc data &optional (o-stream *current-output-file*) Outa and friends merge Data into O-Stream at sample position Loc. O-Stream defaults to the current default output file. Data is a short-float, normally between -1.0 and 1.0 -- the sound file contains 2's complement integers interpreted as fractions between -1 and 1, but not including 1 itself. [DSP: clm's output functions truncate data, so if you (outa 0 1.0), the actual value written is 0]. Locsig is the recommended way to do multi-channel output. IN-A IN-B IN-C IN-D INA INB in-a loc &optional (i-stream *current-input-file*) in-b loc &optional (i-stream *current-input-file*) in-c loc &optional (i-stream *current-input-file*) in-d loc &optional (i-stream *current-input-file*) In-a and friends get the sample at position Loc in I-Stream as a short-float (normally between -1.0 and 1.0). I-Stream defaults to the current default input file. Ina and inb are the same as in- a and in-b -- "Ind" got too many loader errors, so I added the "-". REVIN REVOUT revin i revout i val Revin and revout read/write data from/to the reverb stream. In the clm software, reverb is normally handled as a third independent channel, and the actual reverberation is done after all other processing is complete. The current reverb stream is *reverb* READIN READIN-REVERSE make-readin &key file start-time start (channel :A) readin rd make-reverse &key file start-time start (channel :A) readin-reverse rd Readin provides a way to package up information related to a given input stream (File, Channel, and Start-Time (seconds) or Start (samples)). The field rdin-i is the sample counter (see under Structs below). Input-file-start-time and input-file-start are synonyms for start-time and start. Here is an instrument that applies an envelope to a sound file using readin and env: (definstrument env-sound (file beg &optional (amp 1.0) (amp-env '(0 1 100 1))) (let ((f (open-input file))) (unwind-protect (let* ((st (floor (* beg sampling-rate))) (dur (clm-get-duration f)) (rev-amount .01) (rdA (make-readin :file f :input-file-start-time 0.0)) (rdB (if (and (stereo f) (stereo *current-output-file*)) (make-readin :file f :input-file-start-time 0.0 :channel :B))) (ampf (make-env :envelope amp-env :scaler amp :start-time beg :duration dur)) (nd (+ st (floor (* sampling-rate dur))))) (Run (loop for i from st to nd do (let* ((amp-val (env ampf)) (outa-val (* amp-val (readin rdA)))) (outa i outa-val) (if rdB (outb i (* amp-val (readin rdB)))) (if *reverb* (revout i (* rev-amount outa-val))))))) (close-input f)))) To change direction in the run loop, you can either set rdin-inc (see page 36), or call read- forward or read-backward. The current readin sample number is returned by read-position. LOCSIG make-locsig &key (degree 0.0) (distance 1.0) (revscale 0.01) revin locsig loc i in-sig Locsig normally takes the place of outa and outb in an instrument. It tries to place a signal between outa and outb in an extremely dumb manner -- it just scales the respective amplitudes ("that old trick never works"). Revscale determines how much of the direct signal gets sent to the reverberator. Distance tries to imitate a distance cue by fooling with the relative amounts of direct and reverberated signal (independent of Revscale). Locsig is a kludge, but then so is any pretence of placement when you're piping the signal out a loudspeaker. It is my current belief that locsig does the "right" thing for all the wrong reasons -- a good concert hall provides "auditory spaciousness" by interfering with the ear's attempt to localize a sound -- that is, a diffuse sound source is the ideal! By sending an arbitrary mix of signal and reverberation to various speakers, locsig gives you a very diffuse "source" -- it does the opposite of what it claims to do, and by some perversity of Mother Nature, that is what you want. (See "Binaural Phenomena" by J Blauert). If you'd like to mess around with room simulations, see the function roomsig in mus.lisp with examples in room.ins. Locsig now handles quad output -- the speakers are assumed to be in a circle with channel A at 0, channel B at 90, and so on. TABLE-LOOKUP Table-lookup performs interpolating table lookup. Indices are first made to fit in the current table (fm input can produce negative indices), then linear interpolation returns the table value. Table-lookup scales its frequency change argument (fm-input) to fit whatever its table size is (that is, it assumes the caller is thinking in terms of a table size of two pi, and fixes it up). The wave table should be an array of floats (short-floats, preferably). The function make-table returns such an object, ready to be loaded. make-table-lookup &key (frequency 440.0) ; in Hz (initial-phase 0.0) ; in radians wave-table ; short-float array with wave table-lookup tl &optional (fm-input 0.0) make-table &optional (size default-table-size) There are several functions available that make it easier to load up various wave forms: load-synthesis-table synth-data table &optional (norm t) load-synthesis-table-with-phases synth-data table &optional (norm t) The synth-data argument is a list of (partial amp) pairs: '(1 .5 2 .25) gives a combination of a sine wave at the carrier (1) at amplitude .5, and another at the first harmonic (2) at amplitude .25. The partial amplitudes are normalized to sum to a total amplitude of 1.0 unless the argument norm is nil. If the initial phases matter (they almost never do), you can use load-synthesis-table-with-phases. The phases are in radians. [DSP: it takes a long time both to load the table, and to write the table contents to the 56000's private memory, so additive synthesis should be done with waveshaping, not table lookup.] /dist/lisp/clm/spectr.clm has a large number of steady state spectra of standard orchestral instruments, courtesy James A. Moorer. /dist/lisp/clm/bird.clm has about 50 North American bird songs. Very large tables should be handled as sound files and read with ina and friends. RANDH RANDI Randh and randi produce random numbers of various kinds. The Lisp function random is also available. make-randh &key (frequency 440.0) ;freq at which new random numbers occur (amplitude 1.0) ;numbers are between 0.0 and amplitude (increment 0.0) (type 'random) randh r &optional (sweep 0.0) make-randi &key (frequency 440.0) (amplitude 1.0) randi r &optional (sweep 0.0) Randh returns a sequence of random numbers between 0.0 and its amplitude (it produces a sort of step function -- David Mellinger suggests that the "h" stands for "hold"). Randi interpolates between successive random numbers. [DSP: If Type is 'Sambox-random, you get a simulation of the Samson box's random number generator.] The "central-limit theorem" says that you can get closer and closer to gaussian noise simply by adding randh's together. ARRAY-INTERP, TABLE-INTERP, DOT-PRODUCT These simple functions underlie some of the unit-generators, and can be called within Run, if you want to roll your own. See mus.lisp for details. SAWTOOTH-WAVE TRIANGLE-WAVE PULSE-TRAIN SQUARE-WAVE These generators produce some occasionally useful wave forms. Sawtooth-wave ramps from -1 to 1, then goes immediately back to -1. Triangle-wave ramps from -1 to 1, then ramps from 1 to -1. Pulse-train produces a single sample of 1.0, then zeros. Square-wave produces 1 for half a period, then 0. All have a "period" of two-pi, so the fm argument should have an effect comparable to the same fm applied to the same waveform in table-lookup. make-triangle-wave &key frequency (amplitude 1.0) (initial-phase pi) triangle-wave s &optional (fm 0.0) make-square-wave &key frequency (amplitude 1.0) (initial-phase 0) square-wave s &optional (fm 0.0) make-sawtooth-wave &key frequency (amplitude 1.0) (initial-phase pi) sawtooth-wave s &optional (fm 0.0) make-pulse-train &key frequency (amplitude 1.0) (initial-phase two-pi) pulse-train s &optional (fm 0.0) Triangle-wave appears almost exclusively in the vibrato computation (v.ins for example). One popular kind of vibrato is: (+ (triangle-wave pervib) (randi ranvib)) ONE-POLE ONE-ZERO TWO-POLE TWO-ZERO These are simple filters. make-one-pole a0 b1 ; b1 < 0.0 gives lowpass, b1 > 0.0 gives highpass one-pole f input make-one-zero a0 a1 ; a1 > 0.0 gives weak lowpass, a1 < 0.0 highpass one-zero f input make-two-pole a0 b1 b2 ; see ppolar below two-pole f input make-two-zero a0 a1 a2 ; see zpolar below two-zero f input The difference equations are: one-zero y(n) = a0 x(n) + a1 x(n-1) one-pole y(n) = a0 x(n) - b1 y(n-1) two-pole y(n) = a0 x(n) - b1 y(n-1) - b2 y(n-2) two-zero y(n) = a0 x(n) + a1 x(n-1) + a2 x(n-2) (This nomenclature is taken from Julius Smith's "An Introduction to Digital Filter Theory" in Strawn "Digital Audio Signal Processing", and is different from that used in more general filters given on page 15. It is also different from that used by the MusicKit. For example, the MusicKit's one-pole's A1 is our b1 and their B0 is our a0). The coefficients should be between -2.0 and 2.0. PPLOAR ZPOLAR FORMNT These generators provide a more intuitive handle on the simple filters given above. make-ppolar R freq ppolar f input ; bandpass, centered at freq, bandwidth set by R make-zpolar R freq zpolar f input ; bandstop, notch centered at freq, width set by R make-formnt R freq &optional G formnt f input ; resonator centered at freq, bandwidth set by R PPolar y(n) = x(n) + 2*R*cos(2*pi*Freq/Srate)*y(n-1) - R*R*y(n-2) ZPolar y(n) = x(n) - 2*R*cos(2*pi*Freq/Srate)*x(n-1) + R*R*x(n-2) Formnt y(n) = x(n) - R*x(n-2) + 2*R*cos(2*pi*Freq/Srate)*y(n-1) - R*R*y(n-2) PPolar ("poles polar") allows you to specify the radius and angle of a pole while two-pole requires actual filter coefficients. The filter provided by ppolar has two poles, one at (R,Freq), the other at (R,-Freq). R is between 0 and 1 (but less than 1), and Freq is between 0 and Srate/2. This is the standard resonator form with poles specified by the polar coordinates of one pole. Similar remarks apply to two-zero and zpolar. More than likely you want "polar-coordinate" calling parameters (R and Freq) when setting up a second order section. The "rectangular- coordinate" versions exist for generality (e.g. to make two real poles) and are rarely needed. The impulse response of a two-pole filter section is an exponentially decaying sinusoid: Rn * cos(2*pi*(Freq/Srate)*n+Phi) where (R,Freq) are as above, and Phi is a phase term. Therefore, the time constant of decay is 1/(1-R) samples which is 1/((1-R)*Srate)) seconds. Seven time constants is approximately the time it takes to decay 60dB. For example, if R=0.9999, then the time constant of decay is 10000 samples. If Srate=50000, then 10000 samples is one fifth of a second. Seven time-constants is then a little more than a second. Thus, the two-pole filter specified by ppolar with R=0.9999 and Freq=400, with Srate=50000, is a resonator which when pulsed will give an exponentially decaying tone at A440 which decays for about a second before becoming inaudible. Formnt is recommended for resonators (simple bandpass filters), vocal tract or instrument cavity simulations, etc. It provides robust bandpass filtering (simple resonance) using two poles and two zeroes. Only one coefficient need be changed in order to move the filter center frequency. The filter coefficients are set as a function of desired pole-radius R (set by desired bandwidth), center frequency F, and peak gain G. Here's an instrument that uses formnt to filter white noise: (definstrument simp (beg end R freq G) (let* ((amp .5) (f (make-formnt R freq G)) (noi (make-randh :frequency 5000 :amplitude .1))) (Run (loop for i from beg to end do (declare (optimize (safety 1))) ;catch and report overflows (outa i (* amp (formnt f (randh noi)))))))) (With-sound () (simp 0 10000 .99 1000 1.0)) FILTER DIRECT-FILTER LATTICE-FILTER LADDER-FILTER These are the general FIR/IIR filters of arbitrary order. [DSP: all coefficients must be between -1.0 and 1.0]. make-filter &key order m ; same as order a ; same as y-coeffs p ; same as x-coeffs x-coeffs y-coeffs (type direct-form) sn ; same as a, for ladder-filter rc ; for lattice-filter tap ; same as p, for ladder-filter or lattice-filter k ; same as rc, for lattice-filter cn ; for ladder-filter make-direct-filter &key a p m make-lattice-filter &key k rc tap p a m make-ladder-filter &key sn cn a p tap so m direct-filter fl inp lattice-filter fl inp ; Markel & Gray's "two multiplier lattice" ladder-filter fl inp ; M&G's "normalized ladder" filter fl inp Here we follow the nomenclature of Markel and Gray, "Linear Prediction of Speech". Many of the keyword arguments are synonyms -- you can use whichever name you're used to. If you give x-coeffs or y-coeffs, but ask for a lattice or ladder-filter, CLM automatically translates to the correct reflection coefficients. Use flt-a and flt-b as arrays if you want time-varying coefficients. We follow Markel and Gray in assuming that the first y-coeff is 1.0. That is, to implement the difference equation yn <= a0*x0 - b1*y1 we would set y-coeffs to (list 1.0 b1) and x-coeffs to (list a0). The possible types are direct-form, ladder-form, and lattice-form. See fltdes.lisp for functions compatible with these filters that provide the coefficients for various FIR and IIR filters. There are also functions to print the frequency response and so on -- this code could relatively easily be made more flexible, if anyone is interested. fltdes.lisp also contains examples and test cases. An example is given on page 32. COMB NOTCH make-comb mlt length comb cflt input make-notch mlt length notch cflt input Comb is a delay line with a scaler on the feedback term. Notch is a delay line with a scaler on the feedforward term. Mlt is the scaler (it should be less than 2.0 in magnitude). Length is the length in samples of the delay line. In filter parlance, comb is: y(n) <= x(n-length-1) + mlt * y(n-length) As a rule of thumb, the decay time of the feedback part is 7*length/(1-mlt) samples, so to get a decay of Dur seconds, mlt <= 1-7*length/(Dur*Srate). The peak gain is 1/(1-(abs mlt)). See Julius Smith's "An Introduction to Digital Filter Theory" in Strawn "Digital Audio Signal Processing", or Smith's "Music Applications of Digital Waveguides". ALL-PASS make-all-pass feedback-scaler feedforward-scaler length all-pass f input All-pass or "moving average comb" is just like comb but with added feed-forward term. If feedback-scaler = 0, we get a moving average comb filter. If both scale terms = 0, we get a pure delay line. In filter parlance, y(n) <= feedforward-scaler*x(n-1) + x(n-length-1) + feedback-scaler*y(n-length) [DSP: the samples in the delay are assumed to be fractional (between -1 and 1).] DELAY make-delay length &key initial-contents initial-element delay d input tap d &optional (offset 0) Delay is a straightforward delay line. Length is in samples. Input fed into a delay line reappears at the output Length samples later. Initial-element defaults to 0.0. If specified, initial-contents need not be the same length as the delay line. [DSP: input is assumed to be between -1 and 1.] Tap returns the current value of the delay generator. Offset is the distance of the tap from the current (default) delay line sample -- it is assumed to be positive or zero. ZDELAY make-zdelay (length &key initial-contents initial-element true-length) zdelay z input &optional (pm-input 0.0) ztap z &optional (pm-input 0.0) Zdelay is an "interpolating" delay line. Zdelay is just like delay but the current index into the delay line can have a fractional part. Pm-Input determines how far off from the normal index we are. Obviously there are limits to what you can get away with. Zdelay is intended for things like "flanging" where the delay line is relatively long, and the interpolation motion is relatively small. Zdelay uses "phase modulation" rather than FM, because it is closer to what we want to think about here -- the Pm-Input argument is difference between the nominal delay length (Length) and the current actual delay length (Length + Pm-Input) -- a positive Pm-Input corresponds to a longer delay line. If true-length is not set explicitly, you normally get the smaller of twice the length and the length + 512; a pm-input outside those bounds will cause trouble. To add a tap to a zdelay line, use ztap. (definstrument zcomb (time duration freq amp length1 length2 vib-env) (multiple-value-bind (beg end) (get-beg-end time duration) (let ((s (make-pulse-train :frequency freq)) (d0 (make-zdelay length1 :true-length (max length1 length2))) (venv (make-env :envelope vib-env :start-time time :duration duration :scaler (- length2 length1))) (feedback .5)) (run (loop for i from beg to end do (let ((dly-val (ztap d0))) (outa i (zdelay d0 (+ (* amp (pulse-train s)) (* feedback dly-val)) (env venv)))))) (end-run)))) ;;; (with-sound () (zcomb 0 1 10 .99 10 300 '(0 0 100 1))) WAVESHAPE make-waveshape &key (frequency 440.0) (partials '(1 1)) waveshape w &optional (index 1.0) (fm 0.0) signify Harm-amps make-waveshape-table harm-amps &optional (norm t) (tblsiz default-table-size) make-phase-quad-table harm-amps phases &optional (tblsiz default-table-size) get-chebychev-coefficients partials &optional (kind 1) Waveshape performs waveshaping. For a description of waveshaping, see "Digital Waveshaping Synthesis" by Marc Le Brun in JAES 1979 April, vol 27, no 4, p250. [DSP: it is expensive (that is, it takes a long time) to load the table and read it using waveshape. I recommend that you use polynomial instead.] (definstrument simp () (let ((wav (make-waveshape :frequency 440 :partials '(1 .5 2 .3 3 .2)))) (Run (loop for i from 0 to 1000 do (outa i (waveshape wav)))))) See bigbird below for an example of get-chebychev-coefficients, and pqw.ins for an example of phase quadrature waveshaping. Signify takes a list of harmonic amplitudes and puts a pattern of signs on that list that optimizes the behaviour of the waveshaper at low indices. Get- chebychev-coefficients takes a list of harmonic amplitudes and returns a list of Chebychev polynomial coefficients. The argument kind determines which kind of Chebychev polynomial we are interested in. POLYNOMIAL polynomial coeffs x Coeffs is an array of coefficients, x is the value to be plugged into the polynomial. Coeffs[0] is the constant term, and so on. For waveshaping, use the function get-chebychev-coefficients. [DSP: the coefficients in coeffs, the argument x and all intermediate values produced during polynomial evaluation are assumed to be "fractional".] Abramowitz and Stegun, "A Handbook of Mathematical Functions" is a treasure-trove of interesting polynomials. (definstrument BigBird (start duration frequency freqskew amplitude Freq-env Amp-env Partials) (multiple-value-bind (beg end) (get-beg-end start duration) (let* ((gls-env (make-env :envelope freq-env :scaler (In-Hz freqskew) :start-time start :duration duration)) (os (make-oscil :frequency frequency)) (fil (make-one-pole .1 .9)) (coeffs (get-chebychev-coefficients (normalize-partials partials))) (amp-env (make-env :envelope amp-env :scaler (* 8 amplitude) :start-time start :duration duration))) (loop for i from 0 below (length coeffs) do (setf (aref coeffs i) (* .125 (aref coeffs i)))) (Run (loop for i from beg to end do (outa i (one-pole fil (* (env amp-env) (polynomial coeffs (oscil os (env gls-env))))))))))) RESAMPLE make-resample &key file (srate 1.0) start-time start (channel :A) resample s &optional (sr-change 0.0) Resample performs sampling rate conversion by linear interpolation. File is the file of sound samples to be resampled, Start-Time is where to start in that file (in seconds), Start is the start point in samples, Channel is which channel to resample, and Srate is how much to move forward on each sample. Srate = 1.0 therefore causes no change at all, Srate = .5 interpolates a point between each of the originals, so the sound goes down an octave if played at the original sampling-rate. Similarly Srate = 2.0 moves it up an octave. Sr-Change is added to Srate to provide time varying sampling rate changes. Large downward shifts are often marred by the buzz or whine of quantization noise -- use src for extreme cases. Input-file-start-time and input-file-start are synonyms for start-time and start. SRC make-src &key file (srate 1.0) (channel :A) start-time start (width 5) src s &optional (sr-change 0.0) Src performs sampling rate conversion in a somewhat more sophisticated manner than resample -- it convolves the input with a sinc function, after filtering the input if Srate > 1.0. As in resample, File is the file to be converted, Srate is the ratio between the new sampling rate and the old, Start-Time is where to start in File (in seconds), Channel is which channel to convert, and Width is how many neighboring samples to convolve with sinc. Input-file-start-time and input-file-start are synonyms for start-time and start. In src, the Sr-Change argument is the amount to add to the current Srate on a sample by sample basis. Currently, the low pass filter is not changed in concert with Sr-Change, so large upward changes may not be engineer-approved. Here's an instrument that provides time-varying sampling rate conversion: (definstrument simp (start-time duration amp srt srt-env filename) (let* ((senv (make-env :envelope srt-env :scaler 1.0 :offset 0.0 :start-time start-time :duration duration)) (beg (floor (* start-time sampling-rate))) (end (+ beg (floor (* duration sampling-rate)))) (f (open-input filename)) (src-gen (make-src :file f :srate srt))) (Run (loop for i from beg to end do (outa i (* amp (src src-gen (env senv)))))) (close-input f))) CONTRAST-ENHANCEMENT contrast-enhancement in-samp &optional (fm-index 1.0) Contrast-enhancement phase-modulates a sound file. It's like audio MSG. The actual algorithm is sin(in-samp*pi/2 + (fm-index*sin(in-samp*2*pi))). The result is to brighten the sound, helping it cut through a huge mix and so on. WAVE-TRAIN make-wave-train &key wave (frequency 440.0) (initial-phase 0.0) wave-train w &optional (fm 0.0) Wave-train produces a wave train (an extension of Pulse-Train). Frequency is the repetition rate of the wave found in Wave. Successive waves can overlap. With some simple envelopes, or filters, you can use this for VOSIM and other related techniques. [DSP: the actual wave samples are assumed to be fractional.] See ins.lisp for a number of example instruments. RUN-BLOCK make-block &key size trigger run-block blk This unit generator is the main working portion of all the block processing generators, and is provided for those who want to build their own fancy generators. There are some examples in the ins.lisp file (phase-vocoders and so on). A block (rblk-buf) can be cleared quickly with clear- block. FFT-FILTER make-fft-filter &key filter file ;input file start-time ;start-time in input file in seconds start ;same but in samples (channel :A) (fft-size 512) fft-filter ff Fft-filter performs fft based filtering of File. Filter can be an envelope or an array. In either case, the values should be positive fractions (that is, numbers between 0.0 and 1.0, inclusive). The advantage of using an array is that you can change the filter on the fly in the instrument (changing an envelope is relatively difficult). Filter represents the frequency response of the filter for the frequencies below the Nyquist limit -- if an array is used, it should be 1/2 the Fft- Size. [DSP: fft-size should be small enough to fit in the available DSP memory]. Input-file- start-time and input-file-start are synonyms for start-time and start. Here's an example of using fft-filter with a table to do time-varying filtering. In this case, we are gradually notching out some of the lower frequencies. (definstrument fftins (beg dur spectrum file) (let* ((start (floor (* beg sampling-rate))) (end (+ start (floor (* dur sampling-rate)))) (fil (open-input file)) (val 1.0) (step (/ 1.0 (* (min dur 1.0) sampling-rate))) ;max 1 sec for ramp (notch-start 10) (notch-end 20) (ff (make-fft-filter :file fil :input-file-start-time 0.0 :fft-size 128 :filter spectrum))) (Run (loop for i from start to end do (when (>= val step) ;ramp down to 0, then leave it there (decf val step) (loop for k from notch-start to notch-end do (setf (aref (fftflt-env ff) k) val))) (outa i (fft-filter ff)))) (close-input fil))) In this example, we are assuming that "spectrum" is an array, not an envelope (we are accessing fftflt-env using aref inside run). Due to the stupidity of the foreign function interface the array passed must have elements of type 'short-float -- e.g. (make-array 256 :element-type 'short- float). (Of course, a "real" fftins would protect against interruptions and :reset and so on). If fftflt's file is explicitly nil, the input is white noise. This is a feature, not a bug -- an example can be found in ins.lisp. CONVOLVE make-convolve &key filter file start-time start (channel :A) (fft-size 512) convolve ff Closely related to fft-filter is the unit generator convolve. Its arguments are the same as in make-fft-filter and fft-filter, and it provides a very similar function. Fft-filter assumes that the imaginary part of the fft of the spectrum is 0, whereas convolve does not. [DSP: If filter is a file, convolve falls into C and can handle any size convolution; if both files to be convolved are bigger than will fit in memory, you'll spend a lot of time swapping]. ("Filter" = impulse response here). Input-file-start-time and input-file-start are synonyms for start-time and start. (definstrument convins (beg dur filter file &optional (size 128)) (let* ((start (floor (* beg sampling-rate))) (end (+ start (floor (* dur sampling-rate)))) (fil (open-input file)) (ff (make-convolve :file fil :input-file-start-time 0.0 :fft-size size :filter filter))) (Run (loop for i from start to end do (outa i (convolve ff)))) (close-input fil))) EXPAND make-expand file-name ;input file name &key start-time ;start-time (secs) in the input file start ;same, but in samples (channel :A) ;which channel this expand gen reads (segment-length . 15) ;length of file slices that are overlapped (segment-scaler .6) ;amplitude scaler on slices (to avoid overflows) (expansion-amount 1.0) ;how much to lengthen or compress the file (output-hop .05) ;speed at which slices are repeated in output (ramp-time .4) ;amount of slice-time spent ramping up/down expand e Expand "expands" the sound file File-name. It is the poor man's way to change the speed at which things happen in a recorded sound without changing the pitches. It works by slicing the input file into short pieces, then overlapping these slices to lengthen (or shorten) the result, sometimes known as granular synthesis, and similar to the "freeze" function. SUM-OF-COSINES make-sum-of-cosines &key (cosines 1) (frequency 440.0) (initial-phase 0.0) sum-of-cosines cs &optional (fm 0.0) Sum-of-cosines produces a band-limited pulse train containing Cosines cosines. It uses the formula: 1+2(cos(x)+cos(2x)+...cos(nx)) = sin((n+.5)x)/sin(x/2). RING-MODULATE AMPLITUDE-MODULATE ring-modulate in1 in2 amplitude-modulate am-carrier input1 input2 Ring-modulate returns (* in1 in2). Amplitude-modulate returns (* input1 (+ am-carrier input2)) Since neither needs any state information, there are no associated "make" functions. Both of these take advantage of the "Modulation Theorem" -- since multiplying a signal by a phasor (e ^ (j w t)) translates its spectrum by w / two-pi Hz, multiplying by a sinusoid splits its spectrum into two equal parts translated up and down by w/two-pi Hz. The simplest case is: cos f1 * cos f2 = (cos (f1 + f2) + cos (f1 - f2)) / 2. VARIOUS OTHER CLM FUNCTIONS stereo &optional (o-stream *current-output-file*) ;2 channels (these can occur in run) mono &optional (o-stream *current-output-file*) ;1 channel quad &optional (o-stream *current-output-file*) ;4 channels Stereo returns t if its argument is a stereo file (i.e. has exactly 2 channels). Mono returns t if its argument is a mono file. The default argument for both is the current output file, also known as *current-output-file*. (Quad and stereo can't both be true at the same time). get-beg-end start-time duration convert Start-Time and Duration from seconds into samples. set-srate new-srate Set the sampling-rate to New-Srate. clm-get-channels file Returns the number of channels in the file File (an IO struct as returned by open-input). clm-get-sampling-rate file Returns the sampling rate of File (IO struct). clm-get-max-amp file Returns the maximum amplitude of File (IO struct). clm-get-duration file Returns the duration (secs) of the file File (IO struct). clm-get-samples file Returns the number of full samples in the file File As an example of these, here's a function that returns the maximum amplitude in a sound file: (defun maxamp (file) (let ((hi (open-input (namestring (merge-pathnames file default-sound-file-name))))) (multiple-value-bind (a b) (clm-get-max-amp hi) (close-input hi) (list a b)))) clm-get-default-header Returns a string containing the current date and time, the current version of the clm software and so on. This is used as the default "comment" in every sound file header. in-Hz Freq-in-Hz Converts Freq-in-Hz to radians/sample (for any situation where a frequency is used as an amplitude -- glissando or FM, for example). It can be used within run. clu-reset Close all files, close the DSP, and clean up all internal state. fft, inverse-fft, and fft-window These provide run-time access to a standard fft routine. Both take as the only argument an fft- data structure, normally created by the function make-fft-data-arrays. Its only argument is the size of the ffts. (definstrument pvins (beg dur file amp shift &optional (fftsize 128)) ;;assume shift is a positive integer (let* ((start (floor (* beg sampling-rate))) (end (+ start (floor (* dur sampling-rate)))) (fil (open-input file)) (fftsize-1 (1- fftsize)) (fd (make-fft-data-arrays fftsize)) (wtb (make-block :size fftsize :trigger 0)) (window (make-block :size fftsize)) (freq-inc (/ fftsize 2)) (freq-inc-1 (1- freq-inc)) (rate (/ 1.0 (* freq-inc fftsize))) (filptr 0) (shift-top (- fftsize shift 1))) (loop for i from 0 below freq-inc and j from fftsize-1 by -1 and angle from 0.0 by rate do (setf (aref (rblk-buf window) i) angle) (setf (aref (rblk-buf window) j) angle)) (Run (loop for i from start to end do (when (zerop (rblk-loc wtb)) (dotimes (k fftsize) (setf (aref (fft-data-real fd) k) (* (aref (rblk-buf window) k) (ina filptr fil))) (incf filptr)) (clear-block (fft-data-imaginary fd)) (decf filptr freq-inc) (fft fd) ;; now shift the positive frequencies up and the negative frequencies down (loop for k from freq-inc-1 downto shift and j from freq-inc to shift-top do (setf (aref (fft-data-real fd) k) (aref (fft-data-real fd) (- k shift))) (setf (aref (fft-data-imaginary fd) k) (aref (fft-data-imaginary fd) (- k shift))) (setf (aref (fft-data-real fd) j) (aref (fft-data-real fd) (+ j shift))) (setf (aref (fft-data-imaginary fd) j) (aref (fft-data-imaginary fd) (+ j shift)))) (dotimes (k shift) (setf (aref (fft-data-real fd) k) 0.0) (setf (aref (fft-data-imaginary fd) k) 0.0) (setf (aref (fft-data-real fd) (- fftsize-1 k)) 0.0) (setf (aref (fft-data-imaginary fd) (- fftsize-1 k)) 0.0)) (inverse-fft fd) (dotimes (k fftsize) (incf (aref (rblk-buf wtb) k) (aref (fft-data-real fd) k))) (incf (rblk-ctr wtb) freq-inc)) (outa i (* amp (run-block wtb))))))) The arguments to fft-window are the fft-data structure and the array containing the window values. Many of the standard windows are available in mus.lisp and, for the enterprising, in lib56.lisp. See ins.lisp and san.ins for more examples (vocoder tricks and so on). Mix file begin &body body ;see also fasmix (page 39) With-mix options file begin &body body Mix is a macro, callable within with-sound or clm-load, that saves the computation in its body in a separate file named file (without the .snd extension), and can tell when that file's data is up to date and need not be recomputed. Mix is equivalent to open-input or mix-in (see below) with a separate file of clm instrument calls, but lets you keep all the notes in one file. Mix with no body is the same as mix-in. With-mix is the same as mix, but gives you a chance to specify local with-sound options (mainly for local reverb). For example, the following with-sound uses mix, with-mix, mix-in, and normal instrument calls: (with-sound () (fm-violin 0 .1 440 .1) (mix "sec1" .5 (fm-violin 0 .1 550 .1) (fm-violin .1 .1 660 .1)) (with-mix (:reverb jc-reverb) "sec2" 1.0 (fm-violin 0 .1 880 .1 :reverb-amount .2) (fm-violin .1 .1 1320 .1 :reverb-amount .2)) (fm-violin 2 .1 220 .1) (mix-in "/zap/slow.snd" 2)) Now, if we change just the first note in the mix call, the with-mix section will not be recomputed, but will be mixed in from the saved file "sec2.snd". By surrounding stable sections of a piece with calls on mix or with-mix, you can save a huge amount of time that would otherwise be spent waiting for these notes to be recomputed. This check-point or makefile capability is built on open-input. open-input &optional name &key verbose if-does-not-exist element-type mix-at mix-duration force-recomputation open-output &optional (name default-sound-file-name) close-input &optional (i-stream *current-input-file*) close-output &optional (o-stream *current-output-file*) reopen-output &optional (o-stream default-sound-file-name) mix-in file-name begin-time &optional duration mix-sound output-file output-start-samp input-file input-start-samp samples-to-merge sound-let (see page 30) clm-open-input name These functions open and close input and output sound files. Open-input and open-output take either strings or pathnames and return an IO object (see mus.lisp for a description of the innards of this structure). Various clm functions use that object as a handle on the file. The variable default-sound-file-name is "/zap/test.snd" at CCRMA. It is set in next-io.lisp. Open-input normally opens the sound file name and returns an IO structure that other clm functions can use to access the file. If you don't give a complete file name (name without the .snd extension), open-input checks to see if there's either no .snd file or a later .cm or .clm file, and in that case, suspends the current computation, makes the sound file from the sources, then resumes the old computation, opening the (newly computed) sound file. If you are working in sections, and keep the sections in separate files, the various layers of mixing can automatically notice when some section has changed, and update everything for you. Similarly, if all your sound files get deleted, the whole piece can still regenerate itself in one operation. You can avoid all this fanciness by calling clm-open-input. Open-input's &key parameters are patterned after Lisp's load function: Verbose (default is nil) turns on some informational printout. Element-type can be nil (the default), or :sound. In the latter case, the file passed to open-input is assumed to contain sound data, no matter what extension it has. This is a way to override the check for out of date sound files and so on. If-does-not-exist can be nil or :error (the default). In the latter case, if no sound file associated with name can be found or created, you get an error message. mix-at, mix-duration, force-recomputation -- see mix-in below The implicit load triggered by open-input with a non-specific file name sets *open-input- pathname* and *open-input-truename* and notices *open-input-verbose* (if t, print out informational messages). Finally, if all you want is from open-input is the "make" action, the function mix-in provides the simplest access. Its arguments are the file name and where to place the resulting sound file in the current output file (in seconds) and, optionally, how much of the mixed-in file to include. If you want to avoid the fancy checkpoint stuff in open-input, or start at some arbitrary place in the input file, the next lower level is mix-sound. Mix-sound takes sample numbers, not seconds. Force-recomputation, if t, forces the mix to be recomputed. DEFINSTRUMENT Definstrument is the most common way to define an instrument in CLM. Its syntax is almost the same as defun, but in addition it has a few options. These fulfill two roles: specifying instrument output language details, and requesting minor optimizations. The options are in a state of flux; currently they are: language :c, :lisp or :56k ; specify output language -- defcinstrument sets it to :c c-file nil ; [C: specify the instrument intermediate C file name] c-include-file nil ; [C: C code to be #include'd in the intermediate file] c-options "-c -O" ; [C: C compiler switches] exp-env t ; [DSP:should exponential envelopes support be loaded] save-labels nil ; [DSP: should debugging labels be saved] The output language is chosen based on the kind of CLM you load -- on any machine but the NeXT, it defaults to :c. On the NeXT the choice is made in all.lisp when you create CLM -- the C output easily matches the DSP in speed for most cases, so unless you have an Ariel QP board, the recommended output language is C. The :lisp option is mostly useful during debugging. The syntax for these options is somewhat similar to that of with-sound. For example, to specify that the instrument simp should use C with the "-c -O3" flags, (definstrument (simp :language :c :c-options "-c -O3") (beg dur ...) The following instruments are included as separate .ins files in the clm directory: complete-add add.ins additive synthesis addflts addflt.ins filters add-sound addsnd.ins mix in a sound file badd badd.ins fancier additive synthesis fm-bell bell.ins fm bell sounds bigbird bigbird.ins waveshaping bird.clm (also bird.ins) canter canter.ins fm bag.clm (bagpipes) cellon cellon.ins feedback fm clarinet clar.ins physical model(?) of a clarinet drone drone.ins additive synthesis bag.clm filter-noise fltnoi.ins filter with envelopes on coefficients filter-sound fltsnd.ins filter a sound file fm-insect insect.ins fm fusion fusion.ins spectral fusion using fm jc-reverb jcrev.ins an old reverberator fm-voice jcvoi.ins jc's fm voice kiprev kiprev.ins a fancier (temperamental) reverberator lbj-piano lbjPiano.ins additive synthesis piano mlb-voice mlbvoi.ins mlb's fm (originally waveshaping) voice fm-noise noise.ins noise maker nrev nrev.ins a popular reverberator pins san.ins spectral modelling pluck pluck.ins Karplus-Strong synthesis pqw pqw.ins waveshaping pqw-vox pqwvox.ins waveshaping voice resflt resflt.ins filters reson reson.ins fm reverberate revsnd.ins add reverb to sound file fm-trumpet trp.ins fm trumpet fm-violin v.ins fm violin fmviolin.clm, popi.clm vox vox.ins fm voice cream.clm The file makins.lisp exercises most of these instruments. See also ins.lisp for a large number of examples and some tutorial explanations. If you develop an interesting instrument that you're willing to share, please send it to me (bil@ccrma.stanford.edu). [DSP: If you load an uncompiled instrument, you should then call compile-ins on that instrument, not compile.] WITH-SOUND and CLM-LOAD with-sound &key ;; "With-sound: check it out!" -- Duane Kuiper, Giants broadcaster after Strawberry homer (output default-sound-file-name); name of output sound file sndfile ; same as output (channels 1) ; 1, 2, and 4 are supported (srate 22050) ; other playable rates are 44100 and 8012 sampling-rate ; same as srate continue-old-file ; open and continue old output file reverb ; name of the reverberator, if any -- the reverb ; is a normal clm instrument (see nrev.ins) reverb-data ; arguments passed to the reverberator -- ; passed here as an unquoted list (reverb-channels 1) ; chans in temp reverb stream revfile ; explicit reverb file name (normally nil) (play t) ; play new sound automatically? play-options ; args passed to user's DAC routine (cleanup-first t) ; if nil, don't try to get clean dsp state wait ; wait for previous computation to finish notehook ; form evaluated on each instrument call statistics ; print out various fascinating numbers (decay-time 1.0) ; ring time of reverb after end of piece output-buffer-size ; don't set unless you're a good sport comment ; comment placed in header (defaults to a time ; stamp (clm-get-default-header) commentary ; same as comment info ; non-comment header string type ; output sound file type save-body ; if t, copy the body (as a string) into the header verbose ; local *clm-verbose* value (force-recomputation nil) ; if t, force mix/with-mix calls to recompute With-sound is a macro that wraps an "unwind-protect" around its body to make sure that everything is cleaned up properly if you happen to interrupt computation. Here are some examples showing how to use the various options: (with-sound (:output "new.snd") (simp 0 1 440 .1)) (with-sound (:srate 44100 :channels 2) ...) (with-sound (:play 3) ...) ;play resultant sound 3 times (with-sound (:comment (format nil "This version is louder: ~A" (clm-get-default-header)))) (with-sound (:reverb jc-reverb) ...) (with-sound (:reverb nrev :reverb-data (:reverb-factor 1.2 :lp-coeff .95))...) Continue-Old-file: if t (default is nil), a previously existing file is reopened for further processing. The default (nil) clobbers any existing file of the same name as the output file (see Output above). By using Continue-Old-File, you can both add new stuff to an existing file, or (by subtracting) delete old stuff to any degree of selectivity. When you erase a previous note, remember that the subtraction has to be exact -- you have to create exactly the same note again, then subtract it. By the same token, you can make a selected portion louder or softer by adding or subtracting a scaled version of the original. Notehook: a form that is evaluated each time an instrument is called (defaults to nil). The following example prints a dot each time an instrument is called: (with-sound (:notehook '(princ ".")) (simp 0 110) (simp 1 2)) Statistics: currently just a flag -- if t (default is nil), clm keeps track of a variety of interesting things and prints them out at the end of the computation. If your favorite statistic isn't among the chosen few, send bil a complaint. Clm-load is the same as with-sound, but its argument is the name of a file containing clm instrument calls and so on (i.e. the body of with-sound). You can exit early from the note list with (throw :FINISH) -- (with-sound () (load "file")) is very similar. Instrument-let Instrument-let is like Lisp's flet -- it can be used to define local instruments. For example, (defun ins () (instrument-let ((simp (beg dur freq) (let* ((start (floor (* beg sampling-rate))) (end (+ start (floor (* dur sampling-rate)))) (osc (make-oscil :frequency freq))) (Run (loop for i from start to end do (outa i (* .25 (oscil osc))))))) (toot (beg dur) (let* ((start (floor (* beg sampling-rate))) (end (+ start (floor (* dur sampling-rate)))) (osc (make-oscil :frequency 220))) (Run (loop for i from start to end do (outa i (* .25 (oscil osc)))))))) (simp 0 1 660) (toot 3 1))) Now, when the function Ins is called within with-sound or clm-load, it will add a note from Simp and Toot. For a more useful example, see expsrc in ins.lisp. (Currently in CLM, these instrument names are not purely local; the DSP code vector is stored on the symbol's property list which means, much to my surprise, that you can't have a local and a global instrument with the same name). (Also, instrument-let does not work in KCL with C output). Don't use instrument-let within definstrument. Sound-Let Sound-let is a form of let* that creates temporary sound streams within with-sound. Its syntax is like that of let and with-sound: (sound-let ((temp-1 () (fm-violin 0 1 440 .1)) (temp-2 () (fm-violin 0 2 660 .1) (fm-violin .125 .5 880 .1))) (expand-sound temp-1 0 2 0 2);temp-1's value is the name of the temp file (expand-sound temp-2 1 1 0 2))) This creates two temporary files and passes them along to the subsequent calls on expand-sound. The first list after the sound file identifier (i.e. after "temp-1" in the example) is the list of with- sound options to be passed along when creating this temporary file. These default to :output with a unique name generated internally, and all other variables are taken from the overall (enclosing) output file. The rest of the list is the body of the associated with-sound. UNIT GENERATORS in CLM, not supported in RUN SINE-SUMMATION make-sine-summation &key (N 1) (a . 5) (B-ratio 1.0) (frequency 440.0) (initial-phase 0.0) sine-summation s &optional (fm 0.0) See J.A.Moorer, "Signal Processing Aspects of Computer Music" and "The Synthesis of Complex Audio Spectra by means of Discrete Summation Formulae" (Stan-M-5) -- the basic idea is very similar to that used in the sum-of- cosines generator. See also ins.lisp. ASYMMETRIC-FM make-asymmetric-fm &key (r 1.0) (ratio 1.0) (frequency 440.0) (initial-phase 0.0) asymmetric-fm af index fm See Palamin and Palamin, "A Method of Generating and Controlling Asymmetrical Spectra" JAES vol 36, no 9, Sept 88, p671-685: this is another extension of the sine-summation and sum-of-cosines approach. See also ins.lisp. Other Stuff of general interest sampling-rate current sampling rate frequency-mag scaler used to turn cycles per second into radians per sample *current-output-file* current default output stream (for outa and friends) *current-input-file* current default input stream (for ina and friends) *clm-instruments* list of the currently loaded clm instruments (see cream.clm) *clm-init* name of site-specific clm initializations, if any *sound-player* user-supplied DAC funtion (see sound.lisp for details) *clm-array-print-length* number of IO data buffer elements printed in trace *ignore-header-errors* whether to worry about illegal sound file headers *fix-header-errors* whether clm should try to make sense of illegal headers *grab-dac* whether clm should ask before interrupting the DAC default-sound-file-type what kind of output sound file to write read-header name write-header name &optional header make-header &key (channels 1) (format snd-16-linear) data-location (sampling-rate 22050) (info " ") (type default-sound-file-type) update-header name header new-data-size These header readers/writers are mostly for internal use. The useful one is: edit-header name &key channels datasize datalocation offset dataformat srate This function is an endless joy on a machine with a DAC that can play at a large number of sampling rates. Even on the NeXT, changing srate and channels can be a barrel of laughs. dac &optional (file default-sound-file-name) (report-errors t) (start 0.0) dac-n &key (file default-sound-file-name) (times 1) (worry t) (start 0.0) stop-dac wait-for-dac Dac is the simple way to hear a sound file. Its first optional argument is the file to be played (it defaults to the default sound file -- normally "/zap/test.snd"). Its second optional argument tells it whether to worry about illegal sound file formats and similar problems. Because NeXT's own sound file handling programs turn out files that are illegal according to NeXT's own software, we have to have a way to simply ignore errors and send arbitrary bytes to the DAC. So, for example, (dac "bad.snd" nil) explicitly overrides the error checking. Normally, the dac function starts playing in a background process and returns control to the lisp listener. You can stop the playing with stop-dac (synonyms). You can cause lisp to wait until the playing is finished with wait-for-dac. In the arguments to dac-n, File is the file name (a string), Times is the number of times to play the file, Worry is whether to worry about errors, and Start is where to start in the sound file. If you have Jean Laroche's Play program on the NeXT, the function jrdac can replace dac -- its arguments are the file name and optionally the new srate. end-run &optional phrase needed only on the DSP End-run cleans up memory allocation associated with delay lines. Any DSP instrument that calls delay lines (even implicitly as in all-pass) should call end-run after the run loop. See the section on Phrasing, below, for a discussion of the phrase argument. volume current volume. Can be used with setf. Values range between 0.0 and 1.0. dac-filter current DAC output filter setting (setf-able). The default "safety" setting for run (in the absence of any optimize declaration) is determined by *clm-safety* which defaults to 0. A value of 1 or higher causes overflow checks to be enabled. A value of 2 or higher causes array references to include bounds checks. A value of 3 causes some of the unit generators to use "reals" rather than "fractions". (Some of this is not yet implemented). *clm-speed* defaults to 1; a value of 2 or 3 causes some address calculations to be done at load time, and triggers other such optimizations. *clm-news* is a variable containing information on what has changed recently in the clm software. Similarly *clm-version* is the current version identifier (a date). If you want clm to print out instrument names and begin times as they are computed, set the variable *clm- verbose* to t. fltdes.lisp has a bunch of filter design functions. As an example of what is there, say we want to put a spectral envelope on a noise source. (definstrument filter-noise (beg dur amp &key x-coeffs) (let* ((st (floor (* beg sampling-rate))) (noi (make-randh :frequency (* .5 sampling-rate) :amplitude amp)) (flA (make-filter :x-coeffs x-coeffs)) (nd (+ st (floor (* sampling-rate dur))))) (Run (loop for i from st to nd do (outa i (filter flA (randh noi))))))) (with-sound () (filter-noise 0 1 .2 :x-coeffs (clm::make-filter-coefficients 12 (clm::design-FIR-from-env :order 12 :envelope '(0 0.0 .125 0.5 .2 0.0 .3 1.0 .5 0.0 1.0 0.0))))) The file files describes briefly each of the files in the clm directory; clm-example.lisp shows one way to write notelists; cm-clm.lisp is a brief example of using Rick Taube's Common Music to drive CLM. There are also examples on the cm/clm directory; the cm/220 directory examples use the MusicKit, not CLM. PHRASING Sometimes an instrument needs to know the final state of some preceding instrument to decide its initial state (in legato for example, it sometimes helps to match phases and so on). This state can be impossible to calculate outside the actual sound computation (it might depend on random numbers, or a phase might depend on FM), so the following functions are provided to package all this up and make it relatively easy to use. First, an instrument that uses phrase information: (definstrument pickup (phrase beg dur frq amp) ;; this instrument shows how to get the final state of ;; on-chip variables and pass them on to the next call. ;; The idea here is that by matching phases and so on ;; we might get a cleaner legato in some cases. (wait-for-phrase phrase) (let* ((s (or (phrase-value phrase 'S) (make-oscil :frequency frq))) (start (floor (* beg sampling-rate))) (end (+ start (floor (* dur sampling-rate))))) (Run (loop for i from start below end do (outa i (* amp (oscil s))))) (End-Run (phrase phrase 'S)))) and an example of running it: (setf phrase (make-phrase)) (with-sound () (pickup phrase 0 1 440 .1) (pickup phrase 1 1 440 .1)) The Loop in the run body uses below not to because we want to start the next instrument on the very next sample -- if we used "loop from start to end" in both, we would have written the end sample of the first instrument twice. The variable Phrase above is the identifier for a given phrase. It is created with: make-phrase &optional anything You can include anything you like in the phrase information being passed around. Just pass it as the optional argument to make-phrase. Once created, an instrument can wait for that phrase to be ready using wait-for-phrase &rest phrases Normally you'd only wait for one phrase to complete, but it is possible to wait on any number of phrases. You then get any saved variable's value with phrase-value phrase var Phrase-value returns nil if the given variable has not yet been set (i.e. normally at the start of the phrase). Phrase-value with a nil var argument is a reference to the "anything" that you want passed around with the phrase. To save state in the phrase structure, call phrase as the optional argument to end-run. The arguments to phrase are the phrase structure and the names of the variables you want saved. phrase phrase &rest vars Outside the dsp world (i.e. outside run and end-run) there's no complication because an instrument is just a lisp function and it can return anything it likes. Inside the dsp world, however, many dsp's can be running in parallel, simultaneous phrases may be interleaved, and we have no way in advance of knowing when or where the next portion of a given phrase will be needed, or which dsp will be free to handle it. In addition, the definstrument function returns immediately after setting up the dsp -- it returns long before any sound has been computed, so a subsequent definstrument call has to know how to wait its turn. Here's another similar example -- in this case we abut each instrument in the phrase, so none of the Pickup instruments gets a begin time. We use the nil option of phrase-value to store the end of the current instrument -- since the variable End does not explicitly appear within the run body (the Loop is a special case), we can't use (phrase phrase 'S 'END). If we had stared at (describe-dsp-state) long enough we might surmise that instead we could save *run-end*, but in general, user variables that don't make it into run need to be saved elsewhere (for example, the nil phrase-value slot might be an association list). Just for fun we'll play the same sort of game with the AMP variable. (definstrument pickup (phrase dur frq) (wait-for-phrase phrase) (let* ((s (or (phrase-value phrase 'S) (make-oscil :frequency frq))) (amp (* .9 (or (phrase-value phrase 'AMP) .5))) (start (or (phrase-value phrase nil) 0)) (end (+ start (floor (* dur sampling-rate))))) (setf (phrase-value phrase nil) end) (Run (loop for i from start below end do (outa i (* amp (oscil s))))) (End-Run (phrase phrase 'S 'AMP)))) (setf phrase (make-phrase)) (with-sound () (pickup phrase 1 440) (pickup phrase 1 440)) STRUCTS Several of the instruments in ins.lisp make explicit reference to the structs defined in mus.lisp -- the run macro knows about many of these, although a few are interpreted in somewhat odd ways. The following structure fields should work in run more or less as they do in lisp: OSC (oscil): Osc-freq ; in radians per sample Osc-phase ; in radians TBL (table-lookup): Tbl-phase ; in radians Tbl-freq ; radians per sample Tbl-table ; the location of the table Tbl-table-size Tbl-internal-mag ; table size scaler to make period two pi DLY (delay lines): Dly-size ; length of delay line Dly-pline ; location of delay line -- many possibilites here Dly-loc ; current pointer in delay line ZDLY (zdelay): Zdly-del ; location of zdelay delay generator Zdly-phase ; current location in delay line CMBFLT (comb and notch filters): Cmbflt-scaler Cmbflt-dly-unit ; location of delay line associated with comb filter ALLPASSFLT (all-pass filters): Allpassflt-feedback ; scaler on feedback term Allpassflt-feedforward ; scaler on feedforward term Allpassflt-dly-unit ; location of associated delay unit FLT (direct-form and associated filters): Flt-m ; the filter order (0 based) Flt-typ ; 0=direct, 1=lattice, 2=ladder Flt-so Flt-a ; a table of coefficients -- the "y" coeffs Flt-b ; ditto -- the "x" coeffs, also known as "p" Flt-c ; ditto -- used only by ladder filter Flt-d ; state of filter FRMNT (formnt): Frmnt-g ; gain Frmnt-tz ; location of two-zero filter Frmnt-tp ; location of two-pole filter COSP (sum-of-cosines): Cosp-phase ; radians Cosp-freq ; radians per sample SMPFLT (various simple filters -- one-pole et al): Smpflt-a0 -- these coefficients are divided by 2 on chip Smpflt-a1, Smpflt-a2, Smpflt-b1, Smpflt-b2, Smpflt-x1, Smpflt-y1, Smpflt-x2, Smpflt-y2 The following code fragment from addflt.ins shows one use of these fields to put envelopes on the filter coefficients: (dotimes (k numFilts) (incf outsig (ppolar (aref pps k) (* (aref scs k) insig))) (setf (smpflt-b1 (aref pps k)) (env (aref ffs k))) (setf (smpflt-b2 (aref pps k)) (env (aref rfs k)))) NOI (randh and randi): Noi-phase ; radians Noi-freq ; radians per sample SW (pulse-train, square-wave, triangle-wave, sawtooth-wave): Sw-phase ; radians Sw-freq ; radians per sample WS (waveshape): Ws-tab ; location of polynomial table Ws-os ; location of lookup oscillator Ws-offset ; midpoint of table LOCS (locsig): Locs-ascl ; outa scaler -- fractional (i.e. [-1.0,1.0)) Locs-bscl ; outb scaler Locs-cscl ; outc scaler Locs-dscl ; outd scaler Locs-rscl ; reverb signal scaler Locs-revname ; whether reverb is on or not (setf (locs-ascl loc) (env panning)) puts an envelope on the outa scaler. SMP (resample): Smp-sr ; current srate change Smp-lst ; last input value Smp-nxt ; next input value Smp-i ; current interpolation location SR (src): Sr-incr Sr-rd ; location of readin structure Sr-x ; current src location Sr-data ; input sequence for low pass filtering Sr-filt ; filter coefficients Sr-left Sr-right Sr-width ; sinc table size RDIN (readin and readin-reverse): Rdin-i ;current sample index Rdin-inc ;-1 (readin-reverse) or 1 (readin) RBLK (run-block): Rblk-siz Rblk-buf ; location of block data table Rblk-loc ; 0=>trigger run-block actions Rblk-ctr ; count down from freq samps to 0 WT (wave-train): Wt-wave ; location of table containing current wave Wt-wsiz Wt-freq ; radians per sample Wt-b ; location of run-block rblk structure Wt-phase ; radians Wt-internal-mag FFTFLT (fft-filter): Fftflt-env ; either the filter array or envelope Fftflt-siz ; true (power of 2) size of fft Fftflt-hop ; hop size of sliding fft's Fftflt-rd ; readin structure Fftflt-b ; run-block structure Fftflt-datar ; real data Fftflt-datai ; imaginary data Fftflt-half-siz FFT-DATA (fft and inverse-fft): Fft-Data-Real ;the real part of the fft data (or the sound input data) Fft-Data-Imaginary ;the imaginary part of the fft data Fft-Data-Size ;data size SPD (expand): Spd-rd, Spd-len, Spd-rmp, Spd-amp, Spd-in-spd, Spd-out-spd, Spd-cur-in, Spd-cur-out, Spd-s20, Spd-s50, Spd-ctr Spd's fields take more explanation than they're worth -- see the code in mus.lisp for details. In the C output case, a number of other fields are accessible, including those of the IO structure. def-clm-struct In addition, you can use your own structs in run if you first define them for run, as in the following example: (def-clm-struct hi ho (silver envelope) (away array integer)) ;; this replaces the lisp defstruct statement (definstrument simp () (let ((ha (make-hi :ho .1 :away 2 :silver (make-env :envelope '(0 0 100 1) :scaler .1 :start 0 :end 10)))) (run (loop for i from 0 to 10 do (outa i (+ (hi-ho ha) (* .1 (hi-away ha)) (env (hi-silver ha)))))))) Def-clm-struct is a macro whose first argument is the name of the struct and the rest of the arguments are the field names you intend to access in the run block. If the field type is not real, you should include the type as the second element of the (name type) list describing that field -- these are clm internal types like dly or envelope. [DSP: An array of fractional values can be passed as a table]. An array of any clm type can be passed as an array with the type of the element added as the third element of the field description: (envs array envelope): (def-clm-struct hi (ho array envelope) silver away) (definstrument simp () (let ((ha (make-hi :ho (make-array 1 :element-type 'envelope :initial-element (make-env :envelope '(0 0 100 1) :scaler .1 :start 0 :end 10))))) (run (loop for i from 0 to 10 do (outa i (env (aref (hi-ho ha) 0))))))) Bill Schottstaedt (bil@ccrma.stanford.edu) Appendix: How to use CLM outside with-sound or clm-load With-sound and clm-load are macros that expand into code similar to the following: (set-srate 44100) (open-output "/zap/test.snd" (make-header :channels 2 :sampling-rate 44100)) (setf *reverb* (open-output "/zap/reverb.snd" (make-header :channels 1 :sampling-rate 44100))) < insert instrument calls here > (close-output *reverb*) (open-input "/zap/reverb.snd") (jc-reverb 0 350 :double t) (close-input) (close-output) When a fatal error occurs (even within with-sound or clm-load), you can use this code in the interpreter to finish the current computation. Similarly, an instrument can be called outside with-sound and friends, or notice that it is being called in that way: (definstrument auto (dur) (let* ((outf (and (not *current-output-file*) (open-output "/zap/test.snd" (make-header :channels 1 :sampling-rate 22050)))) (os (make-oscil :frequency 440)) (end (floor (* dur 22050)))) (Run (loop for i from 0 to end do (outa i (* .1 (oscil os))))) (when outf (close-output) (clm-cleanup) (dac "/zap/test.snd")))) Now (auto) is the same as (with-sound () (auto)) Appendix: Fast sound file mixing The fastest way to mix sound files is with fasmix -- it is about 5 times faster than the equivalent clm instrument add-sound. Fasmix tries valiantly to be all things to all people, but it doesn't provide absolutely every possible mixing option. It assumes that the files to be mixed have the same sampling rate. The fasmix call is very similar to mix-in: fasmix infile &key (start-time 0.0) start ; start-time in output file (input-file-start-time 0.0) input-file-start duration amplitude ampA ampB ampAB ampBA ampC ampD amp-env) ; amp-env is the list of breakpoints Most of the options should be pretty obvious -- to extract channel A and scale it by .25, set ampA to .25. Here are a bunch of examples: ;; say we have these two sounds: (with-sound (:output "/zap/1.snd") (loop for i from 0 to 9 do (fm-violin i 1 (* (1+ i) 100) .1))) (with-sound (:output "/zap/2.snd" :channels 2) (loop for i from 0 to 9 do (fm-violin i 1 (* (- 10 i) 120) .1 :degree 60))) (with-sound () (fasmix "1") ;add "1.snd" to current output (fasmix "1" :duration 1.0 :amplitude .5) ;;scale first 1 sec of "1" by .5 and mix into current output (fasmix "1" :amplitude .5 :amp-env '(0 0 100 1));scale and envelope sound (fasmix "1" :duration 1.0 :start-time 1.5 :input-file-start-time 3.0)) ;;take section in "1.snd" from 3.0 to 4.0 and mix into output starting at 1.5 ;; the basic fasmix calls are the same if mixing stereo to stereo: (with-sound (:channels 2) (fasmix "2" :amp-env '(0 0 100 1))) ;; now mono-to-stereo: (with-sound (:channels 2) (fasmix "1") ;add "1" into channel 1 of output (fasmix "1" :ampA .5 :ampB .5) ;add .5 of "1" into each channel (fasmix "1" :ampB 1.0 :duration 2.0 :input-file-start-time 2.0 :amp-env '(0 0 1 1 2 1 3 0))) ;;put envelope portion (2.0:4.0) only into channel 2 (B) ;; and stereo-to-mono: (with-sound () (fasmix "2") ;channel 1 of "2" added into output (i.e. extract channel 1) (fasmix "2" :ampB .75) ;just channel 2 (scaled) of "2" into output (fasmix "2" :ampA .5 :ampB .75) ;mix chanA*.5+chanB*.75 into output (fasmix "2" :ampA 1.0 :duration 3.0 :input-file-start-time 2.0 :amp-env '(0 0 1 1 2 1 3 0))) ;chanA from 2.0 to 5.0, enveloped ;; and stereo to stereo where we want to mix the separate channels as well as scale them (with-sound (:channels 2) (fasmix "2" :ampB 1.0 :ampA 1.0 :ampBA .1 :ampAB .2)) ;outA<-(inA*1.0 + inB*.2), outB<-(inA*.1 + inB*1.0) ;; fasmix can perform header and data type translations: (with-sound (:srate 8012) (fasmix "/me/cl/mulaw.snd")) ;from mulaw to 16-bit linear (with-sound () (fasmix "/me/cl/aiff1.snd")) ;from 8-bit to 16-bit linear (AIFF->NeXT) (with-sound () (fasmix "/me/cl/esps.snd")) ;from ESPS to NeXT (16-bit linear) Appendix: Header and Data Types Supported by CLM CLM normally writes NeXT/Sun 16-bit linear sound files. It can also write AIFF or RIFF files, the data always being 16-bit linear. Read support is provided for the following headers and data types: * NeXT/Sun/DEC 8-bit mulaw, 8-bit linear, 24-bit linear, * 32-bit linear, 32-bit float, * 8-bit alaw, 16-bit emphasized, 64-bit double * AIFF 8-bit linear * 8SVX 8-bit linear * IRCAM 16-bit linear (BICSF), EBICSF * NIST-SPHERE 16-bit linear * INRS, ESPS, AVR 16-bit linear, AVR 8-bit linear and unsigned * RIFF (wav) 8-bit alaw, 8-bit mulaw, 8-bit unsigned, 32-bit linear * VOC 8-bit signed * no header (CLM will prompt for info it needs) * Sound Tools 8-bit unsigned * Turtle Beach SMP 16-bit linear * Sound Designer II (the data file, not the resource fork) I am willing to add almost anything to this list, except compressed data that requires block access. See the file headers.c for all the gory details. In with-sound, you can set the output header type with the keyword :type. Appendix: Debugging Aids The two main debugging aids are describe-dsp-state (for instruments running on the 56000) and describe-c-state (for instruments running in C). These try to show the names and values of all the variables local to the currently running instrument.. Describe-c-state works only if the variable c-debug is set to t. To play with the 56000, see the various debugging functions in lib56.lisp -- dsp-open, dsp-close, dsp-debug, and so on. Describe-c-state can only find the instrument data if the variable c-debug is t. You can look at the clm compiler's intermediate code by setting c56-debug to t. Appendix: Sources of Unwanted Noise The major source of unwanted noise in computer music is amplitude quantization. This means soft notes are buzzy. If these are later used as input to a reverberator, for example, the buzziness can easily be magnified. If the soft notes are split between channels (via locsig for example), you may end up increasing the overall noisiness. My experience has been that anything under .003 in amplitude is asking for trouble unless it is covered up in some way. Since reverb amounts are often less than .01, even a loud note can produce noisy input to the reverberator. The simplest way around this in CLM is to make one run and get the reverb stream max amp (reported if :statistics is t in with-sound). Then scale all the reverb signals up by the inverse of this (leaving some room for slop, of course). For example, if the reverb max amp is .01, you're throwing away about 7 bits of amplitude resolution in the reverb input. So, in this case, multiply all the locsig revscales by (say) 50, then in the reverberator, divide the reverb output by 50 before sending it out. This only works with reverberators that use C, not the DSP, but C is much faster than the DSP in this case anyway. See jcrev.ins for an example (the volume argument). Similarly, if your notes are soft, send them directly out channel A or B -- the spatial effects are going to add less to your piece than the noise will detract in this case. And if you're making repeated iterative passes over sound files, try to keep them scaled close to 1.0 in maxamp through all the passes. The exact maxamp is not important -- the difference between 1.0 and .5 is one bit of resolution. Next in importance, in my unhappy experience with headphones, speakers, and amplifiers has been that slow amplitude envelopes can cause annoying artifacts in less than perfect audio equipment. You may hear glissandos or chirps if a soft note is slowly ramping up or down. The function reduce-amplitude-quantization-noise in env.lisp makes the envelope jump through the initial or final section where there are too few bits, hopefully reducing the chirps (these are apparently caused by non-linearities in the audio equipment -- I don't hear them in very good equipment). A similar trick is to use exponential envelopes that are bowed out (a base greater than 1.0 in CLM) -- this is the default in the MusicKit for example. There may be some combination of problems here -- see Robert Maher, "On the Nature of Granulation Noise in Uniform Quantization Systems", JAES vol 40 no 1/2, 1992, p12 -- he says "The common assumption that the quantization noise is additive, white, and uncorrelated with the input signal is simply incorrect for the case of sinusoidal input". Since the ramping portions are often sinusoidal, or nearly so, these chirps might be analyzable as FM caused by quantization. Another source of "noise" is foldover. This is mostly a problem with FM when the sampling rate is low -- you'll hear inharmonic artifacts caused by components that are beyond half the sampling rate. Then there are the obvious problems like discontinuities in the wave becoming clicks at the speaker, and clipping -- these are easy to fix. Finally, avoid reverberating notes with sharp attacks. The major sources of noise in audio equipment are bad ground connections and "ground loops". In the former, either your ground connections aren't actually connected to a real ground (a common state of affairs in older U.S. houses), or the connections are flakey. In the latter, various pieces of equipment are at slightly different ground voltages due to tiny resistances in ground wires -- this sets up a sort of screen upon which all kinds of interference can be projected, so to speak. These loops are hard to avoid. In my house, for example, the grounds are real -- that is, the third plug on the power cords is actually connected to a solid ground, but I still had to connect all the drives, audio equipment, and NeXT itself to the same outlet (representing altogether about 1000 watts of worst case power consumption). The problem was that the ground wire meanders through the conduits alongside the power wires, so it becomes a serial connection of grounds along a given path -- it's a low gauge wire, but even so, there is a slight resistance along that path, causing equipment connected to different outlets to be at a slightly different ground voltage. Even after putting everything on the same outlet, the external drive was the source of a slight hum -- after unplugging it, I could turn all the volume knobs up all the way and hear only a small hiss which I attribute to the unavoidable randomness in any analog equipment (Johnson noise, for example). On the NeXT, the DACs are mounted near the monitor, so you can hear interference from that all the time. Similarly speaker cables can sometimes act as antennas -- I think the theory here is that radio frequency interference (which can be picked up by a short wire like a speaker connection) can occur in pulses that become clicks or hiss in the speakers (audio frequencies are not picked up directly; for example a 1000 Hz electromagnetic wave is 300,000 meters long). The simplest answer is to buy properly shielded computer equipment, and route the cables away from that equipment. If that's not an option, I'm told that it helps to use shielded cables.