Scheme, Ruby, and Forth Functions included with Snd

This file describes the Scheme, Ruby, and Forth code included with Snd. To use this code, load the relevant file:

Scheme: (load "dsp.scm") or (load-from-path "dsp.scm")
Ruby:   load "dsp.rb"
Forth:  "dsp.fs" file-eval

To start Snd with the file already loaded, snd -l v.scm, or put the load statement in your initialization file. For help with Forth and Snd/CLM, see the Forth documentation section "Snd, CLM, and Fth".

Contents
analog-filter standard IIR filters (Butterworth, Chebyshev, Bessel, Elliptic)
animals a bunch of animals
autosave auto-save (edit backup) support
bess FM demo
binary-io binary files
bird North-American birds
clean noise reduction
clm-ins, jcvoi various CLM instruments
dlocsig moving sounds (Michael Scholz)
draw graphics additions
dsp various DSP-related procedures
env envelope functions
enved envelope editor
examp many examples
extensions various generally useful Snd extensions
fade frequency-domain cross-fades
freeverb a reverb
generators.scm a bunch of generators
grani CLM's grani (Fernando Lopez-Lezcano) translated by Mike Scholz
heart use Snd with non-sound (arbitrary range) data
hooks functions related to hooks
index snd-help extension
inf-snd.el, DotEmacs Emacs subjob support (Michael Scholz, Fernando Lopez-Lezcano)
jcrev John Chowning's ancient reverb
lint A lint program for scheme code
maraca Perry Cook's maraca physical model
marks functions related to marks
maxf Max Mathews resonator
menus additional menus
mix functions related to mixes
moog Moog filter
musglyphs Music notation symbols (from CMN)
nb Popup File info etc
noise noise maker
numerics various numerical functions
peak-phases phases for the unpulse-train
piano piano physical model
play play-related functions
poly polynomial-related stuff
prc95 Perry Cook's physical model examples
pvoc phase-vocoder
rgb color names
rubber rubber-sound
s7test s7 regression tests
selection functions acting on the current selection
singer Perry Cook's vocal-tract physical model
snd15.scm Backwards compatibility
snddiff sound difference detection
snd-gl OpenGL examples (gl.c)
snd-motif, snd-xm Motif module (xm.c)
snd-test Snd regression tests
sndwarp Bret Battey's sndwarp instrument
spectr instrument steady state spectra
stochastic Bill Sack's dynamic stochastic synthesis
strad string physical model (from CLM)
tankrev Jon Dattorro's plate reverb (Anders Vinjar)
v fm-violin
ws with-sound
zip the zipper (the anti-cross-fader)
analog-filter
make-butterworth-lowpass order fcut
make-butterworth-highpass order fcut
make-butterworth-bandpass order flo fhi
make-butterworth-bandstop order flo fhi

make-chebyshev-lowpass order fcut (ripple-dB 1.0)
make-chebyshev-highpass order fcut (ripple-dB 1.0)
make-chebyshev-bandpass order flo fhi (ripple-dB 1.0)
make-chebyshev-bandstop order flo fhi (ripple-dB 1.0)

make-inverse-chebyshev-lowpass order fcut (loss-dB 60.0)
make-inverse-chebyshev-highpass order fcut (loss-dB 60.0)
make-inverse-chebyshev-bandpass order flo fhi (loss-dB 60.0)
make-inverse-chebyshev-bandstop order flo fhi (loss-dB 60.0)

make-bessel-lowpass order fcut
make-bessel-highpass order fcut
make-bessel-bandpass order flo fh
make-bessel-bandstop order flo fh

make-elliptic-lowpass order fcut (ripple-dB 1.0) (loss-dB 60.0)
make-elliptic-highpass order fcut (ripple-dB 1.0) (loss-dB 60.0)
make-elliptic-bandpass order flo fhi (ripple-dB 1.0) (loss-dB 60.0)
make-elliptic-bandstop order flo fhi (ripple-dB 1.0) (loss-dB 60.0)

;; fcut = cutoff frequency in terms of srate = 1.0, 
;; flo = low freq of band, fhi = high freq of band

analog-filter.scm has the usual IIR filters: Butterworth, Chebyshev, inverse Chebyshev, Bessel, and Elliptic filters in lowpass, highpass, bandpass, and bandstop versions. Each of the lowpass and highpass "make" functions returns a filter generator, whereas the bandstop and bandpass make functions return a function of one argument, the current input (the filter generators are built-in in these cases). The filter order should be an even number; very high orders can cause numerical disaster! The elliptic filters depend on GSL, so you'll also need GSL (Snd's configure script includes it by default, if possible).

(map-channel (make-elliptic-lowpass 8 .1)) ; 8th order elliptic with cutoff at .1 * srate

One quick way to see the frequency response of your filter is to create a sound that sweeps a sinewave upward in frequency, run it through the filter, then view the entire sound, treating the x axis as frequency in terms of srate = 1.0 (for convenience):

(define (filter-sweep flt chan)
  (let ((phase 0.0)
	(freq 0.0)
	(incr (/ (* 2 pi) 44100.0))
        (samps (seconds->samples 0.5)))
    (do ((i 0 (+ i 1)))
	((= i samps))
      (let ((sval (* .8 (sin phase))))
	(set! phase (+ phase freq)) 
	(set! freq (+ freq incr))
	(out-any i (flt sval) chan)))))

(with-sound (:channels 5 :output "test.snd")
  (filter-sweep (make-butterworth-lowpass 8 .1) 0)
  (filter-sweep (make-bessel-lowpass 8 .1) 1)
  (filter-sweep (make-chebyshev-lowpass 8 .1) 2)
  (filter-sweep (make-inverse-chebyshev-lowpass 8 .1) 3)
  (filter-sweep (make-elliptic-lowpass 8 .1) 4))
IIR filters, order=8, low cutoff at .1 (4410Hz), high cutoff at .3 (13230Hz)
iir filters
see also:   dsp   examp   moog   maxf   prc95   graphEq   clm
animals

People paint birds; why not bird songs? Check out a Hermit Thrush song down 2 or 3 octaves and slowed down as well (via granular synthesis, for example) — incredibly beautiful 2-part microtonal counterpoint. animals.scm contains several synthesized animal sounds: frogs, birds, insects, and one mammal. To hear them all, (calling-all-animals).

How to Paint a Bird Song

Back in 1980, I wanted some bird songs for "Colony", but my stabs at a fake bird song were completely unconvincing. So I went to my battered bird book (Robbins, Bruun, Zim, Singer "Birds of North America" Golden Press, NY 1966) which had sonograms of lots of bird songs. Unfortunately, the graphs were so tiny that I could barely read them:

Lincoln's Sparrow, approximately original size Lincoln's Sparrow approximately original size, but blurrier due to incredibly bad scanner software.

Graphs like this became bird.scm. It surprised me that the synthetic song could sound good even with just a sinewave and a couple sketchy envelopes. But squawks and screeches were harder. 27 years later, I tackled animal sounds again, but now using Snd and some extremely high quality recordings, mainly from Cornell. It's not that hard to match some animal sounds; perhaps someone would like to see the steps I took to match a Hairy Woodpecker call (hairy-woodpecker in animals.scm).

Open the Hairy Woodpecker. Find a squawk that seems within reach, and select it.

selecting a squawk

Zoom onto the first channel (the vertical slider on the right — we don't care about stereo here), and center the squawk in the time domain window (C-x v). Get the selection duration in seconds. Start the envelope editor dialog (under the Edit menu). Choose "selection" and "wave" in that dialog.

zoom in

Since the goal is a CLM instrument that synthesizes this sound, get a "blank bird", and fill in the duration.

(definstrument (hairy-woodpecker beg amp)
  (let* ((start (seconds->samples beg))
	 (dur 0.08)                                      ; filled in from the selection duration
	 (stop (+ start (seconds->samples dur)))
	 (ampf (make-env                                 ; left blank for the moment
			 :duration dur :scaler amp))
	 (gen1 (make-oscil))
	 (frqf (make-env                                 ; ditto
			 :duration dur :scaler (hz->radians 1.0))))
   (do ((i start (+ i 1)))
       ((= i stop))
     (outa i (* (env ampf)                           ; just a first guess at the synthesis algorithm
	        (oscil gen1 (env frqf)))))))

Now to get an amplitude envelope, set the y axis limits to be from 0.0 to the selection maxamp:

(set! (y-bounds (selected-sound) (selected-channel)) (list 0 (selection-maxamp)))

I have this action bound to the "m" key in my ~/.snd initialization file. The change is reflected in the envelope editor. We can define the amplitude envelope by approximating the shape. Call it "hairy-amp".

get amp env

Now go to the listener and get the value of hairy-amp as a list of breakpoints. I usually use this function to get the breakpoints:

> (define (clean-string e)
    (format #f "(~{~,3F~^ ~})" e))

> (clean-string hairy-amp)
"(0.000 0.000 0.099 0.188 0.152 0.148 0.211 0.558 0.242 0.267 0.278 0.519 0.434 0.472 0.527 0.543 0.612 0.479 
  0.792 0.941 0.831 0.523 0.854 1.000 0.913 0.422 0.927 0.200 0.946 0.430 0.971 0.304 1.000 0.000 )"

Plug this list into the bird instrument (ampf). Now switch to the FFT view (click "f" and "w"), open the FFT dialog (Options:Transform), choose Blackman10 window, sonogram, 512. In the color dialog, choose the "jet" colormap. In the envelope editor, switch from "amp" to "flt", and resize the dialog window so that its graph fits the displayed spectrum. For fast moving sounds, it's important to align the amp and freq envelopes exactly, but the FFT delays or blurs stuff, so mess with the graph placement until the amplitude envelope and the spectrum match (bright spots at loud spots and so on). I bind the arrow keys to precision movements for this reason (in my ~/.snd file, see move-one-pixel).

preapring to get freq env

Reset the envelope editor (to erase the amp envelope), and press "flt" and "wave" again. Now zoom in to the main spectral component (drag the FFT y axis up), and trace out the frequency curve in the envelope editor. Call it hairy-freq, and get the list of breakpoints as before in the listener. Plug that into the bird instrument. The "scaler" for the frequency envelope is the top of the FFT graph in Hz; it is 10KHz in this case.

get freq env
(definstrument (hairy-woodpecker beg amp)
  (let* ((start (seconds->samples beg))
	 (dur 0.08)
	 (stop (+ start (seconds->samples dur)))
	 (ampf (make-env '(0.000 0.000 0.099 0.188 0.152 0.148 0.211 0.558 0.242 0.267 0.278 0.519 0.434 0.472 
			   0.527 0.543 0.612 0.479 0.792 0.941 0.831 0.523 0.854 1.000 0.913 0.422 0.927 0.200 
			   0.946 0.430 0.971 0.304 1.000 0.000 )
			 :duration dur :scaler amp))
	 (frqf (make-env '(0.000 0.180 0.056 0.213 0.135 0.241 0.167 0.305 0.191 0.396 0.212 0.402 0.242 0.485 
			   0.288 0.506 0.390 0.524 0.509 0.530 0.637 0.537 0.732 0.530 0.770 0.503 0.808 0.503 
			   0.826 0.427 0.848 0.366 0.889 0.345 0.913 0.232 1.000 0.198)
			 :duration dur :scaler (hz->radians 10000.0)))
	 (gen1 (make-oscil)))
   (do ((i start (+ i 1)))
       ((= i stop))
     (outa i (* (env ampf)
	        (oscil gen1 (env frqf)))))))

This squawk has more than one component (it is not just a sine wave), and the components follow more or less the same amplitude envelope (so we can use polywave). Go to the "single transform" view (in the Transform dialog), make the FFT size bigger, move the time domain window into about the middle of the call, and get some estimate of the number of components and their relative amplitudes (concentrating for now on the steady state). Change the "make-oscil" to "make-polywave" and give some first stab at the steady-state spectrum:

...
(gen1 (make-polywave :partials (list 1 .9  2 .1  3 .01)))
  ...
(polywave gen1 (env frqf))
...

Load ws.scm, load the current woodpecker code, and listen to the squawk: (with-sound (:play #t) (hairy-woodpecker 0.0 0.5)). Not terrible. If it's cut off during playback, add a dummy silent call to the end: (with-sound (:play #t) (hairy-woodpecker 0.0 0.5) (hairy-woodpecker 0.5 0.0)). We're happy at this stage if it's in the right ballpark.

first take

The attack and decay sections need work. Returning to either FFT view, it's clear there's a set of components moving together at half the steady state frequency, so add another polywave with its own amplitude envelope:

(definstrument (hairy-woodpecker beg amp)
  (let* ((start (seconds->samples beg))
	 (dur 0.08)
	 (stop (+ start (seconds->samples dur)))
	 (ampf (make-env '(0.000 0.000 0.099 0.188 0.152 0.148 0.211 0.558 0.242 0.267 0.278 0.519 0.434 0.472 
			   0.527 0.543 0.612 0.479 0.792 0.941 0.831 0.523 0.854 1.000 0.913 0.422 0.927 0.200 
			   0.946 0.430 0.971 0.304 1.000 0.000 )
			 :duration dur :scaler amp))
	 (frqf (make-env '(0.000 0.180 0.056 0.213 0.135 0.241 0.167 0.305 0.191 0.396 0.212 0.402 0.242 0.485 
			   0.288 0.506 0.390 0.524 0.509 0.530 0.637 0.537 0.732 0.530 0.770 0.503 0.808 0.503 
			   0.826 0.427 0.848 0.366 0.889 0.345 0.913 0.232 1.000 0.198)
			 :duration dur :scaler (hz->radians 10000.0)))
	 (gen1 (make-polywave :partials (list 1 .9  2 .09  3 .01)))
	 (gen2 (make-polywave :partials (list 1 .2  2 .1  3 .1  4 .1  5 .1  6 .05  7 .01))) ; attack and decay
	 (ampf2 (make-env '(0 1  .3 1  .4 0 .75 0 .8 1  1 1) :duration dur :scaler 1.0)))      ; its amplitude
    (do ((i start (+ i 1)))
        ((= i stop))
      (let ((frq (env frqf)))
        (outa i (* (env ampf)
		   (+ (polywave gen1 frq)
		      (* (env ampf2)
			 (polywave gen2 (* 0.5 frq))))))))))

Now patience is the key. Use the speed control to slow playback down by an octave or two. (Perhaps the frequency envelope should end at a higher point?) Keep tweaking the envelopes and spectral amplitudes until it sounds right! Total elapsed time? Two or three hours probably.

the end

animals.scm has all the functions, key bindings, and dialog variable settings mentioned here. They can save you a ton of time.

see also:   birds
autosave
auto-save 
cancel-auto-save 

The auto-save code sets up a background process that checks periodically for unsaved edits, and if any are found it saves them in a temporary file (the name is the base file name enclosed in "#...#" and placed in the temp-dir directory). The time between checks is set by the variable auto-save-interval which defaults to 60.0 seconds. To start auto-saving, (load "autosave.scm"). Thereafter (cancel-auto-save) stops autosaving, and (auto-save) restarts it.

bess

bess.scm creates a dialog (named "FM Forever!"), puts up a bunch of scale widgets, and starts two CLM oscils doing frequency modulation. Michael Scholz has contributed a Ruby translation of this with many improvements: bess.rb.

fm dialog
;; bess plays the following:
(* amp 
   (oscil carosc 
     (+ (hz->radians frequency)
        (* index (oscil modosc 
                   (hz->radians (* ratio frequency)))))))

bess1.scm and bess1.rb give you real-time GUI-based control over the fm-violin while it cycles around in a simple compositional algorithm. Both were written by Michael Scholz, based on CLM's bess5.cl and rt.lisp.

see also:   fm
binary-io
read|write-l|bint16|32|64
read|write-l|bfloat32|64
read|write-chars|string
read|write-au-header

This file has functions to read and write numbers and strings to and from binary files. The function names are similar to those used for sample-type names, so for example, read-bint32 reads the next 4 bytes from the current input port, interpreting them as a big-endian 32-bit integer.

bird
bird start dur frequency freqskew amplitude freq-envelope amp-envelope
bigbird start dur frequency freqskew amplitude freq-envelope amp-envelope partials
one-bird beg maxdur func birdname
make-birds (output-file "test.snd")

bird.scm is a translation of the Sambox/CLM bird songs. The two instruments set up a sine wave (bird) and waveshaping synthesis (bigbird). Use a low-pass filter for distance effects (a bird song sounds really silly reverberated). All the real information is in the amplitude and frequency envelopes. These were transcribed from sonograms found in some bird guides and articles from the Cornell Ornithology Lab. Many of these birds were used in "Colony". To hear all the birds, (make-birds). This writes the sequence out as "test.snd" using with-sound. Waveshaping is described in Le Brun, "Digital Waveshaping Synthesis", JAES 1979 April, vol 27, no 4, p250. The lines

...
(coeffs (partials->polynomial (normalize-partials partials)))
...
   (polynomial coeffs
     (oscil os (env gls-env))))))
 

setup and run the waveshaping synthesis (in this case it's just a fast additive synthesis). partials->polynomial calculates the Chebyshev polynomial coefficients given the desired spectrum; the spectrum then results from driving that polynomial with an oscillator. Besides the bird guides, there are now numerous recordings of birds that can be turned into sonograms and transcribed as envelopes. sndclm.html has the code for the bird instrument in several languages.

see also:   animals
clean

This file provides a set of noise reduction functions packaged up in:

clean-channel snd chn
clean-sound snd

clean-channel tries to fix up clicks, pops, hum, DC offset, clipped portions, and hiss using a variety of functions from dsp.scm. The final low-pass filter is relatively conservative (that is, it's not a very intense filter), so you may want to run another filter over the data after calling clean-channel.

A Noisy Story

There is no built-in noise reduction function in Snd. I believe the most common such function is some variant of Perry Cook's Scrubber program (see anoi in clm-ins.scm or fft-squelch in examp.scm). Secondary tricks involve smoothing functions similar to smooth-channel, and enveloping to silence stuff between tracks, and so on. clean-channel came about when I blithely offered to clean up some recorded telephone conversations. The first step was to find the clipped locations (where the conversation was accidentally over-recorded). I did this first because there were places in the recordings where the DC offset was huge, causing clipping in a signal that would otherwise have been safe. I hoped to reconstruct the signal at the clipped points, but these would be hard to find once the DC was removed. A quick check:

(count-matches (lambda (y) (not (>= 0.9999 y -0.9999))))

returned 5437 (in 18 minutes of sound). That seemed high, and I thought "maybe those are just one sample clicks that can easily be smoothed over", so

(define* (count-clips snd chn)
  (let ((y0 0.0))
    (count-matches
     (lambda (y) (let ((val (not (or (>= 0.9999 y0 -0.9999) 
                                     (>= 0.9999 y -0.9999)))))
		   (set! y0 y)
		   val))
     0 snd chn)))

But this returned 4768! I made a list of clipped portions (this function has at least one bug, but I plowed past it — no time for perfection...):

(define* (list-clips snd chn)
  (let ((clip-data (make-vector (* 2 (count-clips snd chn)) 0))
	(clip-ctr 0)
	(clip-beg 0)
	(clip-end 0)
	(clip-max-len 0)
	(in-clip #f)
	(samp 0))
    (scan-channel
     (lambda (y)
       (if (not (<= -0.9999 y 0.9999))
           (begin
	     (unless in-clip
	       (set! in-clip #t)
	       (set! clip-beg samp))
	     (set! clip-end samp))
	   (if in-clip
	       (begin
		 (set! in-clip #f)
		 (set! (clip-data clip-ctr) clip-beg)
		 (set! (clip-data (+ 1 clip-ctr)) clip-end)
		 (set! clip-max-len (max clip-max-len (- (+ clip-end 1) clip-beg)))
		 (set! clip-ctr (+ clip-ctr 2)))))
       (set! samp (+ 1 samp))
       #f)) ; make sure scan doesn't quit prematurely
    (list clip-ctr clip-max-len clip-data)))

which returned a vector of 669 clipped portions, the worst being 42 samples long! I saved that data in a separate file, just in case of disaster:

(with-output-to-file "clips" (lambda () (display (list-clips))))

I decided to try to reconstruct the clipped portions before filtering them. This produced sample values outside -1.0 to 1.0, so I reset the graph y bounds:

(set! (y-bounds) (list -1.5 1.5))

Now to conjure up a plausible sine wave between the clip begin and end points. (This is also "just-good-enough" software).

(define (fix-clip clip-beg-1 clip-end-1)
  (and (> clip-end-1 clip-beg-1)
       (let* ((dur (- (+ clip-end-1 1) clip-beg-1))
              (samps (channel->float-vector (- clip-beg-1 4) (+ dur 9)))
              (clip-beg 3)
              (clip-end (+ dur 4)))
         (let ((samp0 (samps clip-beg))
               (samp1 (samps clip-end)))
           (unless (>= 0.99 samp0 -0.99)
             ;; weird!  some of the clipped passages have "knees"
             ;;   this looks nuts, but no time to scratch my head
             (set! clip-beg (- clip-beg 1))
             (set! samp0 (samps clip-beg))
             (unless (>= 0.99 samp0 -0.99)
               (set! clip-beg (- clip-beg 1))
               (set! samp0 (samps clip-beg))))
           (unless (>= 0.99 samp0 -0.99)
             (set! clip-end (+ 1 clip-end))
             (set! samp1 (samps clip-end))
             (unless (>= 0.99 samp0 -0.99)
               (set! clip-end (+ 1 clip-end))
               (set! samp1 (samps clip-end))))
           ;; now we have semi-plausible bounds
           ;; make sine dependent on rate of change of current 
           (let* ((samp00 (samps (- clip-beg 1)))
                  (samp11 (samps (+ 1 clip-end)))
                  (dist (- clip-end clip-beg))
                  (incr (/ pi dist))
                  (amp (* .125 (+ (abs (- samp0 samp00)) (abs (- samp1 samp11))) dist)))
             (if (> samp0 0.0)
                 ;; clipped at 1.0
                 (do ((i (+ 1 clip-beg) (+ i 1))
                      (angle incr (+ angle incr)))
                     ((= i clip-end))
                   (set! (samps i) (+ 1.0 (* amp (sin angle)))))
                 ;; clipped at -1.0
                 (do ((i (+ 1 clip-beg) (+ i 1))
                      (angle incr (+ angle incr)))
                     ((= i clip-end))
                   (set! (samps i) (- -1.0 (* amp (sin angle))))))
             (float-vector->channel samps (- clip-beg-1 4)))))))

(define (fix-it n)
  ;; turn off graphics and fix all the clipped sections
  (set! (squelch-update) #t)
  (do ((i 0 (+ i 1)))
      ((= i n))
      ;; "clips" here is a list form of the earlier vector of clip locations
    (fix-clip (clips (* i 2)) 
	      (clips (+ 1 (* i 2)))))
  (set! (squelch-update) #f))

(fix-it 669)

This produced 418 edits, with a maxamp of 2.26. So scale it back down: (scale-to .9). Next I ran some large ffts to see what sort of overall spectrum I had: (set! (transform-size) (expt 2 23)). This showed a massive DC component, and numerous harmonics of 60 Hz. I decided to get rid of the portions that were clearly noise. Since I was dealing with telephone recordings, I assumed anything under 40 Hz or above 4000 Hz was extraneous:

(define* (notch-out-rumble-and-hiss snd chn)
  (let ((cur-srate (exact->inexact (srate snd))))
    (filter-sound
     (list 0.0 0.0                    ; get rid of DC
	   (/ 80.0 cur-srate) 0.0     ; get rid of anything under 40 Hz (1.0=srate/2 here)
	   (/ 90.0 cur-srate) 1.0     ; now the passband
	   (/ 7000.0 cur-srate) 1.0 
	   (/ 8000.0 cur-srate) 0.0   ; end passband (40..4000)
	   1.0 0.0)                   ; get rid of some of the hiss
     ;; since I'm assuming the minimum band is 10 Hz here, 
     ;;   cur-srate/10 rounded up to next power of 2 seems a safe filter size
     ;;   filter-sound will actually use overlap-add convolution in this case
     (floor (expt 2 (ceiling (log (/ cur-srate 10.0) 2))))
     snd chn)))

(notch-out-rumble-and-hiss)

By now it was obvious I needed a simple way to play portions of the sound before and after an edit, sometimes with a tracking cursor. So I bound a few keys:

(define (play-from-cursor current)
  (play (cursor) #f #f #f #f (and (not current) (- (edit-position) 1))))

(define (play-from-cursor-with-tracking current)
  ;; patterned after pfc in extsnd.html
  (let ((old-tracking (with-tracking-cursor)))
    (set! (with-tracking-cursor) #t)
    (hook-push stop-playing-hook 
	       (lambda (hook)
		 (set! (with-tracking-cursor) old-tracking)))
    (play (cursor) #f #f #f #f (and (not current) (- (edit-position) 1)))))

(bind-key #\p 0 (lambda () 
                  "play from cursor" 
	  	  (play-from-cursor #t) keyboard-no-action))
(bind-key #\P 0 (lambda () 
                  "play previous from cursor" 
		  (play-from-cursor #f) keyboard-no-action))
(bind-key #\p 4 (lambda () 
                  "play from cursor with tracking" 
		  (play-from-cursor-with-tracking #t) keyboard-no-action))

In words, if the mouse is in the channel graph, 'p' plays from the cursor, 'P' plays the previous version from the cursor, and 'C-p' plays from the cursor with a "tracking cursor". In several of the sections (the overall sound consisted of a couple dozen separate conversations), there was a loud mid-range tone. To figure out what its component frequencies were, I FFT'd a portion containing only that noise and got this spectrum (plus a zillion other peaks that didn't look interesting):

((425 .05) (450 .01) (546 .02) (667 .01) (789 .034) (910 .032) (470 .01))

To hear that, I passed this list to play-sines:

(play-sines '((425 .05) (450 .01) (546 .02) (667 .01) (789 .034) (910 .032) (470 .01)))

And to my surprise, the result was close to the main portion of the hum. So now to notch out those frequencies, and see what is left: (notch-sound (list 425 450 470 546 667 789 910) #f 1 10). This erased most of the hum, but it also changed the timbre of the voices which wasn't acceptable. I goofed around with the notch-width and filter-size parameters, looking for something that would still do the trick without removing the personal side of the voices, but in only a few cases was the result usable. What was being said was not very important, but the individual characteristics of each voice were.

The next step was to take out noisy sections between snippets, mostly using (env-selection '(0 1 1 0 10 0 11 1)) and equalizing each snippet, more or less, with scale-selection-by. There were a few "you-are-being-recorded" beeps which I deleted (via the Edit menu delete selection option). In some of the conversations, between sections of speech the background hum would gradually increase, then the voice would abruptly start with a large peak amplitude. These were fixed mostly with small-section scale-by's and envelopes. In the female voice sections, it seemed to help to: (filter-selection '(0 0 .01 0 .02 1 1 1) 1024) which got rid of some of the rumble without noticeably affecting the vocal timbre.

see also:   smooth-channel   remove-clicks   fft-smoother   notch-out-rumble-and-hiss   fft-squelch   fft-cancel   notch-channel   anoi
clm-ins

These are instruments from CLM translated for use in Snd. All expect to be called within with-sound or some equivalent environment. This set of instruments is a bit of a grab-bag; some are just examples of synthesis techniques; a few others are historical, rather than useful. If I were using, for example, the fm trumpet, I'd remove all the attack and decay parameters, moving that up a level to Common Music or whoever calls the trumpet, and combine several other parameters to reflect the desired output, rather than the details of the algorithm; 30 parameters could be reduced to less than 10, and the resulting instrument would be much easier to use. But, it is an historical artifact, so I'm reluctant to change it.

To try out any of these instruments, start Snd, load ws.scm and clm-ins.scm, then paste the with-sound call into the listener. It will automatically write the new sound file and open it in Snd.

anoi file start dur (fftsize 128) (amp-scaler 1.0) (r 6.28)

anoi is a stab at noise reduction based on Perry Cook's Scrubber.m. It tracks an on-going average spectrum, then tries to squelch that, obviously aimed at reducing background noise in an intermittent signal.

(with-sound () (anoi "now.snd" 0 2))
attract beg dur amp c

attract is a translation to CLM of an instrument developed by James McCartney (CMJ vol 21 no 3 p 6), based on a "chaotic" equation. 'c' should be between 1 and 10 or thereabouts.

(with-sound () (attract 0 1 .1 1) (attract 1 1 .1 5))
bes-fm beg dur freq amp ratio index

bes-fm is J1(J1): (bes-j1 (* index (bes-j1 phase))); it uses the Bessel functions where FM uses sinusoids. J0 is also good in this context, and the few other Jn options that I've tried were ok.

Scheme:  (with-sound () (bes-fm 0 1 440 10.0 1.0 4.0))
Ruby:    with_sound() do bes_fm(0, 0.5, 440, 5, 1, 8) end

So why does this work? My "back-of-the-envelope" guess is that the Bessel functions are basically a bump at the start followed by a decaying sinusoid, so the bump gives us a percussive attack, and the damped sinusoid gives us a dynamic spectrum, mimicking FM more or less. The Bessel functions I0, Jn, and Yn are built-in; Kn and In are implemented in Scheme in snd-test.scm. See bess and friends for many more examples.

canter beg dur freq amp ...

canter is half of a bagpipe instrument developed by Peter Commons (the other portion is drone below). The (required) trailing parameters are:

deg dis pcrev ampfun ranfun skewfun skewpc ranpc ranfreq indexfun atdr dcdr
ampfun1 indfun1 fmtfun1 ampfun2 indfun2 fmtfun2 ampfun3 indfun3 fmtfun3 ampfun4 indfun4 fmtfun4

Here is a portion of a bagpipe tune:

(let ((fmt1 '(0 1200 100 1000))
      (fmt2 '(0 2250 100 1800))
      (fmt3 '(0 4500 100 4500))
      (fmt4 '(0 6750 100 8100))
      (amp1 '(0 .67 100 .7))
      (amp2 '(0 .95 100 .95))
      (amp3 '(0 .28 100 .33))
      (amp4 '(0 .14 100 .15))
      (ind1 '(0 .75 100 .65))
      (ind2 '(0 .75 100 .75))
      (ind3 '(0 1 100 1))
      (ind4 '(0 1 100 1))
      (skwf '(0 0 100 0))
      (ampf '(0 0 25 1 75 1 100 0))
      (ranf '(0 .5 100 .5))
      (index '(0 1 100 1))
      (solid '(0 0 5 1 95 1 100 0))
      (bassdr2 '(.5 .06 1 .62 1.5 .07 2.0 .6 2.5 .08 3.0 .56 4.0 .24 5 .98 6 .53 7 
                 .16 8 .33 9 .62 10 .12 12 .14 14 .86 16 .12 23 .14 24 .17))
      (tenordr '(.3 .04 1 .81 2 .27 3 .2 4 .21 5 .18 6 .35 7 .03 8 .07 9 .02 10 .025 11 .035)))
  (with-sound (:reverb nrev)
    (drone .000 4.000 115.000 (* .25 .500) solid bassdr2 .100 .500 .030 45.000 1 .010 10)
    (drone .000 4.000 229.000 (* .25 .500) solid tenordr .100 .500 .030 45.000 1 .010 11)
    (drone .000 4.000 229.500 (* .25 .500) solid tenordr .100 .500 .030 45.000 1 .010 9)
    (canter .000 2.100 918 (* .25 .700) 45.000 1 .050 ampf ranf skwf 
             .050 .010 10 index .005 .005 amp1 ind1 fmt1 amp2 ind2 fmt2 amp3 ind3 fmt3 amp4 ind4 fmt4)
    (canter  2.100  .300 688.5  (* .25 .700)  45.000 1  .050 ampf ranf skwf
	     .050  .010 10 index  .005  .005 amp1 ind1 fmt1 amp2 ind2 fmt2 amp3 ind3 fmt3 amp4 ind4 fmt4)
    (canter  2.400  .040 826.2  (* .25 .700)  45.000 1  .050 ampf ranf skwf
	     .050  .010 10 index  .005  .005 amp1 ind1 fmt1 amp2 ind2 fmt2 amp3 ind3 fmt3 amp4 ind4 fmt4)
    (canter  2.440  .560 459  (* .25 .700)  45.000 1  .050 ampf ranf skwf
	     .050  .010 10 index  .005  .005 amp1 ind1 fmt1 amp2 ind2 fmt2 amp3 ind3 fmt3 amp4 ind4 fmt4)
    (canter  3.000  .040 408  (* .25 .700)  45.000 1  .050 ampf ranf skwf
	     .050  .010 10 index  .005  .005 amp1 ind1 fmt1 amp2 ind2 fmt2 amp3 ind3 fmt3 amp4 ind4 fmt4)
    (canter  3.040  .040 619.65  (* .25 .700)  45.000 1  .050 ampf ranf skwf
	     .050  .010 10 index  .005  .005 amp1 ind1 fmt1 amp2 ind2 fmt2 amp3 ind3 fmt3 amp4 ind4 fmt4)
    (canter  3.080  .040 408  (* .25 .700)  45.000 1  .050 ampf ranf skwf
	     .050  .010 10 index  .005  .005 amp1 ind1 fmt1 amp2 ind2 fmt2 amp3 ind3 fmt3 amp4 ind4 fmt4)
    (canter  3.120  .040 688.5  (* .25 .700)  45.000 1  .050 ampf ranf skwf
	     .050  .010 10 index  .005  .005 amp1 ind1 fmt1 amp2 ind2 fmt2 amp3 ind3 fmt3 amp4 ind4 fmt4)
    (canter  3.160  .290 459  (* .25 .700)  45.000 1  .050 ampf ranf skwf
	     .050  .010 10 index  .005  .005 amp1 ind1 fmt1 amp2 ind2 fmt2 amp3 ind3 fmt3 amp4 ind4 fmt4)
    (canter  3.450  .150 516.375  (* .25 .700)  45.000 1  .050 ampf ranf skwf
	     .050  .010 10 index  .005  .005 amp1 ind1 fmt1 amp2 ind2 fmt2 amp3 ind3 fmt3 amp4 ind4 fmt4)
    (canter  3.600  .040 826.2  (* .25 .700)  45.000 1  .050 ampf ranf skwf
	     .050  .010 10 index  .005  .005 amp1 ind1 fmt1 amp2 ind2 fmt2 amp3 ind3 fmt3 amp4 ind4 fmt4)
    (canter  3.640  .040 573.75  (* .25 .700)  45.000 1  .050 ampf ranf skwf
	     .050  .010 10 index  .005  .005 amp1 ind1 fmt1 amp2 ind2 fmt2 amp3 ind3 fmt3 amp4 ind4 fmt4)
    (canter  3.680  .040 619.65  (* .25 .700)  45.000 1  .050 ampf ranf skwf
	     .050  .010 10 index  .005  .005 amp1 ind1 fmt1 amp2 ind2 fmt2 amp3 ind3 fmt3 amp4 ind4 fmt4)
    (canter  3.720  .180 573.75  (* .25 .700)  45.000 1  .050 ampf ranf skwf
	     .050  .010 10 index  .005  .005 amp1 ind1 fmt1 amp2 ind2 fmt2 amp3 ind3 fmt3 amp4 ind4 fmt4)
    (canter  3.900  .040 688.5  (* .25 .700)  45.000 1  .050 ampf ranf skwf
	     .050  .010 10 index  .005  .005 amp1 ind1 fmt1 amp2 ind2 fmt2 amp3 ind3 fmt3 amp4 ind4 fmt4)
    (canter  3.940  .260 459  (* .25 .700)  45.000 1  .050 ampf ranf skwf
	     .050  .010 10 index  .005  .005 amp1 ind1 fmt1 amp2 ind2 fmt2 amp3 ind3 fmt3 amp4 ind4 fmt4)))

It is not easy to keep track of all these arguments in a long note-list; hence the development of programs such as Score (Leland Smith), Pla (yers truly), and Common Music (Rick Taube). The full note list is bag.clm in the CLM tarball.

cellon beg dur freq amp ...

cellon, developed by Stanislaw Krupowiecz, uses feedback FM as in some old synthesizers. There's a brief discussion of it in fm.html. The trailing parameters are:

ampfun betafun beta0 beta1 betaat betadc ampat ampdc dis pcrev deg pitch1 glissfun glissat 
glissdc pvibfreq pvibpc pvibfun pvibat pvibdc rvibfreq rvibpc rvibfun

and I actually don't know what they all do. I think they're dealing with attack and decay portions of envelopes; in the old days we felt we had to store one envelope, then kludge around with attack and decay timings to bash that envelope into the correct shape; this made instruments needlessly messy. Here's a call:

(with-sound () 
  (cellon 0 2 220 .1 '(0 0 25 1 75 1 100 0) '(0 0 25 1 75 1 100 0) .75 1.0 0 0 0 0 1 0 0 220 
          '(0 0 25 1 75 1 100 0) 0 0 0 0 '(0 0 100 0) 0 0 0 0 '(0 0 100 0)))

The use of x axis values between 0 and 100, rather than 0.0 and 1.0 is a dead give-away that this is really ancient stuff.

clm-expsrc beg dur input-file exp-ratio src-ratio amp rev start-in-file

clm-expsrc can stretch or compress a sound (using granular synthesis) while optionally changing its sampling rate. 'exp-ratio' sets the expansion amount (greater than 1.0 makes the sound longer), and 'src-ratio' sets the sampling rate change (greater than 1.0 makes it higher in pitch). So to make a sound twice as long, but keep the pitch the same:

(with-sound () (clm-expsrc 0 4 "oboe.snd" 2.0 1.0 1.0))

'start-in-file' sets where we start reading the input file (in seconds); it defaults to 0.0.

drone beg dur freq amp ampfun synth ampat ampdc amtrev deg dis rvibamt rvibfreq

This is the other half of Peter Common's bagpipe — see canter above. 'synth' is a list of partials loaded into a table and read via table-lookup.

expandn time duration file amp ...

Here is the documentation from Rick Taube's granular synthesis page, edited slightly for the Scheme CLM.

The expandn instrument by Michael Klingbeil performs granular syntheisis by time-stretching (expanding/compressing) an input file. This effect is achieved by chopping up the input sound into very small segments (grains) that are then overlayed in the ouput stream. The larger the segments, the more the output sound is smeared, an effect approaching reverberation. The expandn instrument parameters are:

   time duration filename amplitude
	(expand 1.0)
	(matrix #f)
	(ramp 0.4)
	(seglen 0.15)
	(srate 1.0)
	(hop .05)
	(amp-env '(0 0 50 1 100 0))
	(input-start 0.0)
	(grain-amp 0.8)
	(reverb #f)

'time' is the start time of the sound in the output file. 'duration' is the duration of expanded sound. To expand an entire sound, set this to the expansion factor times the input sound's duration. 'filename' is the input file to expand. 'amplitude' is a scaler on the amplitude of the input file. Since the output is created by overlaying many copies of the intput this value is generally less than 1. 'hop' can be a number or an envelope. It is the average length in time between segments (grains) in the output. 'expand' can be a number or an envelope. It sets the amount of expansion to produce in the output file. 'seglen'can be a number or an envelope. It is the length in time of the sound segments (grains). 'srate' can be a number or an envelope. It sets the sampling rate change to apply to the output file. 'amp-env' is the amplitude envelope for the output sound. 'input-start' sets where to start reading in the input file. 'grain-amp' is a scaler on each grain's amplitude. 'matrix' is a list, a mixing matrix. 'reverb' is the reverb amount.

expfil start duration hopsecs rampsecs steadysecs file1 file2

expfile interleaves two granular synthesis processes (two readers pasting in tiny sections of their file, one after the other).

(with-sound () 
  (expfil 0 2 .2 .01 .1 "oboe.snd" "fyow.snd")
  (expfil 2 2 .01 .01 .02 "oboe.snd" "fyow.snd"))
exp-snd file beg dur amp (exp-amt 1.0) (ramp .4) (seglen .15) (sr 1.0) (hop .05) ampenv

exp-snd is a granular synthesis instrument with envelopes on the expansion amount ('exp-amt' as a list), segment ramp steepness ('ramp' as a list), segment length ('seglen' as a list), hop length ('hop' as a list), amplitude ('ampenv'), and resampling rate ('sr' as a list). In the next example, the expansion amount in both calls goes from 1 to 3 over the course of the note, the ramp time and segment lengths stay the same, the sampling rate changes from 2 to 0.5, and the hop stays the same (.05 in the first, and .2 in the second).

(with-sound ()
  (exp-snd "fyow.snd" 0 3 1 '(0 1 1 3) 0.4 .15 '(0 2 1 .5) 0.05)
  (exp-snd "oboe.snd" 1 3 1 '(0 1 1 3) 0.4 .15 '(0 2 1 .5) 0.2))
fm-bell beg dur frequency amplitude amp-env index-env index
fm-bell is an FM instrument developed by Michael McNabb in Mus10 in the late '70s. It is intended for low bell sounds (say middle C or so). The lines
  (mod1 (make-oscil (* frequency 2)))
  (mod2 (make-oscil (* frequency 1.41)))
  (mod3 (make-oscil (* frequency 2.82)))
  (mod4 (make-oscil (* frequency 2.4)))
  (car1 (make-oscil frequency))
  (car2 (make-oscil frequency))
  (car3 (make-oscil (* frequency 2.4)))
set up three FM pairs, car1 and mod1 handling the basic harmonic spectra, car2 and mod2 creating inharmonic spectra (using the square root of 2 more or less at random), and car3 and mod3 putting a sort of formant at the minor third (2.4 = a ratio of 12/5 = octave+6/5 = minor tenth).
(with-sound ()
  (fm-bell 0.0 2.0 220.0 .5
    '(0 0 .1000 1 10 .6000 25 .3000 50 .1500 90 .1000 100 0)
    '(0 1 2 1.1000 25 .7500 75 .5000 100 .5000)
    0.5))
fm-drum beg dur freq amp ind (high #f) (deg 0.0) (dist 1.0) (rev-amount 0.01)

The fm-drum uses "cascade FM" (see fm.html); it was developed by Jan Mattox.

(with-sound () (fm-drum 0 1.5 55 .3 5 #f) (fm-drum 1.5 1.5 66 .3 4 #t))
fm-insect beg dur freq amp ampenv modfreq modskew modenv index indexenv fmindex ratio deg dist rev

The fm-insect started as an attempt to get cicada sounds from FM (for the 5th movement of "Colony"), but ended with:

(with-sound (:srate 22050) 
  (let ((locust '(0 0 40 1 95 1 100 .5))
	(bug_hi '(0 1 25 .7 75 .78 100 1))
	(amp    '(0 0 25 1 75 .7 100 0)))
    (fm-insect 0      1.699  4142.627  .015 amp 60 -16.707 locust 500.866 bug_hi  .346  .500)
    (fm-insect 0.195   .233  4126.284  .030 amp 60 -12.142 locust 649.490 bug_hi  .407  .500)
    (fm-insect 0.217  2.057  3930.258  .045 amp 60 -3.011  locust 562.087 bug_hi  .591  .500)
    (fm-insect 2.100  1.500   900.627  .06  amp 40 -16.707 locust 300.866 bug_hi  .346  .500)
    (fm-insect 3.000  1.500   900.627  .06  amp 40 -16.707 locust 300.866 bug_hi  .046  .500)
    (fm-insect 3.450  1.500   900.627  .09  amp 40 -16.707 locust 300.866 bug_hi  .006  .500)
    (fm-insect 3.950  1.500   900.627  .12  amp 40 -10.707 locust 300.866 bug_hi  .346  .500)
    (fm-insect 4.300  1.500   900.627  .09  amp 40 -20.707 locust 300.866 bug_hi  .246  .500)))

See animals.scm for much more convincing insect calls.

fm-trumpet beg dur ...

This is Dexter Morrill's FM-trumpet; see CMJ feb 77 p51.

(with-sound () (fm-trumpet 0 .25))

As with many instruments from that era, it has a million parameters:

    beg dur (frq1 250.0) (frq2 1500.0) (amp1 0.5) (amp2 0.1)
    (ampatt1 0.03) (ampdec1 0.35) (ampatt2 0.03) (ampdec2 0.3)
    (modfrq1 250.0) (modind11 0.0) (modind12 2.66) 
    (modfrq2 250.0) (modind21 0.0) (modind22 1.8) 
    (rvibamp 0.007) (rvibfrq 125.0) (vibamp 0.007) (vibfrq 7.0) (vibatt 0.6) (vibdec 0.2)
    (frqskw 0.03) (frqatt 0.06) 
    (ampenv1 '(0 0  25 1  75 .9  100 0)) (ampenv2 '(0 0  25 1  75 .9  100 0)) 
    (indenv1 '(0 0  25 1  75 .9  100 0)) (indenv2 '(0 0  25 1  75 .9  100 0))
    (degree 0.0) (distance 1.0) (reverb-amount 0.005)

The pitch depends on the 'modfrq1' and 'modfrq2' parameters, as well as 'frq1' and 'frq2':

(with-sound () (fm-trumpet 0 1 :frq1 400 :frq2 1600 :modfrq1 400 :modfrq2 400))
fm-voice beg dur ...

This is John Chowning's FM voice instrument, used in "Phone" I think. It is in jcvoi.scm, not clm-ins.scm. Its parameters are:

beg dur pitch amp vowel-1 sex-1 ampfun1 ampfun2 ampfun3 
indxfun skewfun vibfun ranfun
dis pcrev deg vibscl pcran skewscl ranpower glissfun glissamt

Here's an example:

(let ((ampf '(0 0 1 1 2 1 3 0))) 
  (with-sound (:play #t) 
    (fm-voice 0 1 300 .8 3 1 ampf ampf ampf ampf ampf ampf ampf 1 0 0 .25 .01 0 ampf .01)))
fofins beg dur frq amp uvib f0 a0 f1 a1 f2 a2 (amp-env '(0 0 1 1 2 1 3 0))

fofins is an implementation of FOF synthesis, taken originally from fof.c of Perry Cook and the article "Synthesis of the Singing Voice" by Bennett and Rodet in "Current Directions in Computer Music Research" (MIT Press). FOF synthesis sets up a wave with the desired spectrum (to mimic vocal formats, for example), then calls wave-train to turn that into a tone. fofins just adds an amplitude envelope and vibrato. In the Scheme version, there is also an optional trailing vibrato envelope argument (this is slightly different from the CL version):

(with-sound ()  ; slowly ramp up the vibrato
  (fofins 0 4 270 .1 0.005 730 .6 1090 .3 2440 .1 
          '(0 0 .5 1 3 .5 10 .2 20 .1 50 .1 60 .2 85 1 100 0)
	  '(0 0 40 0 75 .2 100 1) )
  (fofins 0 4 648 .1 0.005 730 .6 1090 .3 2440 .1 ;648 (* 6/5 540)
          '(0 0 .5 .5 3 .25 6 .1 10 .1 50 .1 60 .2 85 1 100 0)
	  '(0 0 40 0 75 .2 100 1) )
  (fofins 0 4 135 .1 0.005 730 .6 1090 .3 2440 .1 
          '(0 0 1 3 3 1 6 .2 10 .1 50 .1 60 .2 85 1 100 0)
	  '(0 0 40 0 75 .2 100 1)))
fullmix infile beg outdur inbeg matrix srate reverb-amount

fullmix is a complicated way to mix stuff. It's built into the CL version of CLM, so there was clamor for some sort of replacement in other versions of CLM. fullmix provides a sound file mixer that can handle any number of channels of data in and out with scalers and envelopes on any path, sampling rate conversion, reverb — you name it! 'infile' is the file to be mixed:

(with-sound () (fullmix "pistol.snd")) ; this places pistol.snd at time 0

'beg' is the start time of the mix in the output sound; 'outdur' is the duration of the mixed-in portion in the output; 'inbeg' is where to start the mix in the input file:

(with-sound () (fullmix "pistol.snd" 1.0 2.0 0.25)) 
;; start at 0.25 in pistol.snd, include next 2 secs, put at time 1.0 in output

'srate' is the amount of sampling rate conversion to apply, and 'reverb' is the amount of the signal to send to the reverberator:

(with-sound (:reverb nrev) (fullmix "pistol.snd" 1.0 2.0 0.25 #f 2.0 0.1)) ; up an octave, lots of reverb!

The 'matrix' parameter is much harder to describe. It is either a number or a list of lists. In the first case, that number is the amplitude scaler on the output:

(with-sound (:reverb nrev) (fullmix "pistol.snd" 1.0 2.0 0.25 0.2 2.0 0.1)) ; same but much softer (0.2 amp)

If 'matrix' is a list of lists, each element of the inner lists can be either a number or list a breakpoints (an envelope). If a number, it is treated as an amplitude scaler for that input and output channel combination. Each inner list represents an input channel, so if we have a stereo input file going to a stereo output file and we want the channels to be mixed straight, but channel 0 at .5 amp and channel 1 at .75:

(with-sound (:channels 2) (fullmix "2a.snd" #f #f #f '((0.5 0.0) (0.0 0.75))))
;;                                                       ^   ^     ^   ^
;;                                                       |   |     |   |
;;                                                    0->0   |  1->0   |
;;                                                        0->1      1->1

So, 2a.snd's first channel gets mixed into the output's first channel, scaled by 0.5, and its second channel goes to the output second channel scaled by 0.75. If we have four channels in and are writing a mono file, and want to mix in only the second channel of the input:

(with-sound (:channels 1) (fullmix "4.aiff" #f #f #f '((0.0) (1.0) (0.0) (0.0))))

The next complication is that each entry in the inner lists can also be a list of envelope breakpoints. In that case, an envelope is applied to that portion of the mix, rather than just a scaler:

(with-sound (:channels 2) (fullmix "oboe.snd" #f #f #f (list (list (list 0 0 1 1 2 0) 0.5))))
;; mono input so one list, envelope output chan 0, scale output chan 1 (two copies of input) 

And finally(!) each inner list element can also be a CLM env generator:

(with-sound (:channels 2)
  (fullmix "oboe.snd" 1 2 0 (list (list .1 (make-env '(0 0 1 1) :duration 2 :scaler .5)))))

Here's a Ruby example:

with_sound(:channels, 2, :statistics, true) do
  fullmix("pistol.snd")
  fullmix("oboe.snd", 1, 2, 0, [[0.1, make_env([0, 0, 1, 1], :duration, 2, :scaler, 0.5)]])
end

"srate" can be negative (meaning read in reverse) or a list or breakpoints (an src envelope). Now we need filters!

gong beg dur freq amp (degree 0.0) (distance 1.0) (reverb-amount 0.005)

gong is an FM instrument developed by Paul Weineke.

Scheme:  (with-sound () (gong 0 3 261.61 .3))
Ruby:    with_sound() do gong(0, 3, 261.61, 0.6) end
gran-synth beg dur freq grain-dur grain-hop amp

gran-synth sets up a wave-train playing an enveloped sinusoid (the "grain" in this case). 'grain-dur' sets the grain's length (in seconds), 'grain-hop' sets the frequency of the wave-train generator (how quickly the grain is repeated), and 'freq' sets the grain sinusoid's frequency.

(with-sound () (gran-synth 0 1 300 .0189 .03 .4)) ; grain freq 300Hz, repetition rate 33Hz
graphEq file beg dur or-beg amp (amp-env '(0 1.0 0.8 1.0 1.0 0.0)) (amp-base 1.0) ...

graphEq is a sort of non-graphical graphical equalizer, developed by Marco Trevisani. It sets up a bank of formant generators with an optional envelope on each formant, then filters and envelopes the input file. Its trailing parameters are:

(offset-gain 0)  
(gain-freq-list '((0 1 1 0) 440 (0 0 1 1) 660))      
(filt-gain-scale 1)                   
(filt-gain-base 1)                    
(a1 .99)
(stats #t)

'a1' is the formant radius. 'gain-freq-list' is a list of gains and frequencies to filter The gains can be either numbers or envelopes (one or the other, not a mixture). 'offset-gain' is an offset (addition) to all the gains. 'filt-gain-scale' and 'filt-gain-base' are similar, but apply to the envelopes, if any. 'stats' prints encouraging numbers if #t.

(with-sound () (graphEq "oboe.snd")) ; accept all the defaults (Scheme is case sensitive)

If we want just steady bands:

(with-sound () (graphEq "oboe.snd" 0 0 0 1.0 '(0 1 1 0) 1.0 0 '(.1 440 .3 1500 .2 330)))
hammondoid beg dur freq amp

hammondoid is Perry Cook's additive-synthesis Hammond organ.

(with-sound () (hammondoid 0 1 440 .1))
jl-reverb (decay 3.0)

jl-reverb is a cavernous version of John Chowning's ancient reverberator. You can never get enough reverb!

(with-sound (:reverb jl-reverb) (fm-violin 0 .1 440 .1 :reverb-amount .1))

'decay' is the reverb decay time tacked onto the end of the output sound. To pass parameters to a reverberator, use the with-sound parameter :reverb-data. So, if we want 5 seconds of decay:

(with-sound (:reverb jl-reverb :reverb-data '(5.0)) (fm-violin 0 .1 440 .1 :reverb-amount .1))
;;                                            ^ this is passed as (jl-reverb 5.0)
lbj-piano beg dur freq amp (pfreq frequency) (degree 45) (reverb-amount 0) (distance 1)

lbj-piano, developed by Doug Fulton, uses James A Moorer's piano spectra and additive synthesis to mimic a piano.

(with-sound () (lbj-piano 0 2 110.0 .2))

Doug says, "The high notes sound pretty rotten" and thinks perhaps one major problem is the lack of mechanical noise. 'pfreq' sets which spectrum to use; it defaults to whatever matches 'freq'.

(with-sound () (lbj-piano 0 2 110.0 .2 :pfreq 550))
metal beg dur freq amp

metal is another Perry Cook creation (HeavyMtl); it's an FM instrument:

(with-sound () (metal 0 1 440 .2))
nrev (reverb-factor 1.09) (lp-coeff 0.7) (volume-1 1.0)

nrev, developed by Michael McNabb, is one of the more popular old-style reverbs. It is much cleaner than jc-reverb.

(with-sound (:reverb nrev) (fm-violin 0 .1 440 .1 :reverb-amount .1))

'reverb-factor' controls the length of the decay — it should not exceed 1.21 or so. 'lp-coeff' controls the strength of the low pass filter inserted in the feedback loop. 'volume-1' can be used to boost the reverb output.

(with-sound (:reverb nrev :reverb-data '(:lp-coeff 0.9 :volume-1 2.0)) 
  (fm-violin 0 .1 440 .1 :reverb-amount .1))
pins beg dur file amp ...

pins is a simple implementation of the spectral modeling synthesis of Xavier Serra and Julius Smith (sometimes known as "Sansy" or "SMS"). See Serra, X., J. O. Smith. 1990. "Spectral Modeling Synthesis:A Sound Analysis/Synthesis Based on a Deterministic plus Stochastic Decomposition". Computer Music Journal, vol. 14(4), 1990. The idea behind SMS is similar to the phase vocoder, but tracks spectral peaks so that its resynthesis options are much more sophisticated. The trailing parameters are:

(transposition 1.0) (time-scaler 1.0) (fft-size 256) 
     (highest-bin 128) (max-peaks 16) printit attack

'transposition' can be used to transpose a sound; 'time-scaler' changes the sound's duration; 'fft-size' may need to be larger if your sampling rate is 44100, or the input sound's fundamental is below 300 Hz; 'highest-bin' sets how many fft bins we search for spectral peaks; 'max-peaks' sets how many peaks we track (at a maximum) through the sound; 'printit', if set to #t, causes the peak envelopes to be printied; 'attack' is an optional float-vector containing the attack portion of the new sound.

Scheme:  (with-sound () (pins 0.0 1.0 "now.snd" 1.0 :time-scaler 2.0))
Ruby:    with_sound() do pins(0, 1, "now.snd", 1, :time_scaler, 2) end

Xavier has a website devoted to this system, but it seems to move; search for CLAM or SMS.

pluck beg dur freq amp (weighting .5) (lossfact .9)

pluck is based on the Karplus-Strong algorithm as extended by David Jaffe and Julius Smith — see Jaffe and Smith, "Extensions of the Karplus-Strong Plucked-String Algorithm" CMJ vol 7 no 2 Summer 1983, reprinted in "The Music Machine". The basic idea is to fill an array with noise, then filter the array values as it is played repeatedly, giving a sharp attack and a ringing decay, much like plucking a guitar. The CMJ article gives many variations, changing pick position and so on. Jaffe's "Silicon Valley Breakdown" makes great use of this instrument. 'weighting' is the ratio of the once-delayed to the twice-delayed samples. It defaults to .5 which gives a short decay; anything other than .5 produces a longer decay. It should be between 0.0 and 1.0. 'lossfact' can be used to shorten decays. The most useful values are between .8 and 1.0.

(with-sound () 
  (pluck 0 1 330 .3 .95 .95) 
  (pluck .3 2 330 .3 .9 .9999) 
  (pluck .7 2 330 .3 .8 .99))

In Ruby:

with_sound() do pluck(0.05, 0.1, 330, 0.1, 0.95, 0.95) end
pqw-vox beg dur freq spacing-freq amp ampfun freqfun freqscl phonemes formant-amps formant-shapes

pqw-vox is an extension of Marc LeBrun's instrument vox (described below) to use phase-quadrature (single-sideband) waveshaping. It uses both Chebyshev polynomial kinds to set up spectra-producing pairs of waveshapers that will add in such a way as to cancel either the upper or lower set of sidebands. These are then ganged together as in the vox instrument to mimic moving formants.

(with-sound () 
  (pqw-vox 0 1 300 300 .1 '(0 0 50 1 100 0) '(0 0 100 1) .3 '(0 L 100 L) '(.5 .25 .1) 
          '((1 1 2 .5) (1 .5 2 .5 3 1) (1 1 4 .5))))

(with-sound ()
  (pqw-vox 0 2 200 200 .1 '(0 0 50 1 100 0) '(0 0 100 1) .1 '(0 UH 100 ER) '(.8 .15 .05) 
           '((1 1 2 .5) (1 1 2 .5 3 .2 4 .1) (1 1 3 .1 4 .5)))
  (pqw-vox 2 2 200 314 .1 '(0 0 50 1 100 0) '(0 0 100 1) .01 '(0 UH 100 ER) '(.8 .15 .05) 
           '((1 1 2 .5) (1 1 4 .1) (1 1 2 .1 4 .05)))
  (pqw-vox 4 2 100 414 .2 '(0 0 50 1 100 0) '(0 0 100 1) .01 '(0 OW 50 E 100 ER) '(.8 .15 .05) 
           '((1 1 2 .5 3 .1 4 .01) (1 1 4 .1) (1 1 2 .1 4 .05))))
pqw beg dur freq spacing-freq carrier-freq amplitude ampfun indexfun partials ...

pqw is a phase-quadrature waveshaping instrument which produces asymmetric spectra. The trailing parameters just set the usual degree, distance, and reverb values.

(with-sound () (pqw 0 .5 200 1000 .2 '(0 0 25 1 100 0) '(0 1 100 0) '(2 .1 3 .3 6 .5)))

To see the asymmetric spectrum most clearly, set the index function above to '(0 1 100 1).

resflt beg dur driver ...

resflt, developed by Richard Karpen and Xavier Serra, sets up three resonators (two-pole filters), then drives them with either white noise or an ncos pulse train. Both can be used for vocal effects:

(with-sound () 
  (resflt 0 1.0 0 0 0 #f .1 200 230 10 '(0 0 50 1 100 0) '(0 0 100 1) 
          500 .995 .1 1000 .995 .1 2000 .995 .1)
  (resflt 1 1.0 1 10000 .01 '(0 0 50 1 100 0) 0 0 0 0 #f #f 
          500 .995 .1 1000 .995 .1 2000 .995 .1))

The trailing parameters are:

ranfreq noiamp noifun cosamp cosfreq1 cosfreq0 cosnum ampcosfun freqcosfun 
frq1 r1 g1 frq2 r2 g2 frq3 r3 g3
(degree 0.0) (distance 1.0)(reverb-amount 0.005)

Set 'driver' to 0 to get the pulse train, or to 1 to get white noise. In the latter case, 'ranfreq' is the random number generator frequency, 'noiamp' is its amplitude, and 'noifun' is an amplitude envelope on its output (filter input) In the pulse case, 'cosamp' is the pulse train amplitude, 'ampcosfun' the amplitude envelope, 'cosfreq0' and 'cosfreq1' set the frequency limits of 'freqcosfun', and 'cosnum' sets the number of cosines in the pulse. The three resonators are centered at 'frq1', 'frq2', 'frq3', with pole-radius 'r1', 'r2', and 'r3' respectively, and with gains of 'g1', 'g2', and 'g3'.

reson beg dur freq amp ...

reson is a vocal simulator developed by John Chowning. Its trailing parameters are:

numformants indxfun skewfun pcskew skewat skewdc vibfreq vibpc ranvibfreq ranvibpc 
degree distance reverb-amount data

'data' is a list of lists of form 
  '(ampf resonfrq resonamp ampat ampdc dev0 dev1 indxat indxdc)

Needless to say, no one has ever written out these parameters by hand, so here's an all-time first:

(with-sound ()
  (reson 0.0 1.0 440 .1 2 '(0 0 100 1) '(0 0 100 1) .1 .1 .1 5 .01 5 .01 0 1.0 0.01
     '(((0 0 100 1) 1200 .5 .1 .1 0 1.0 .1 .1) ((0 1 100 0) 2400 .5 .1 .1 0 1.0 .1 .1))))

But JC got very nice vocal sounds from this — I must have mistyped somewhere... Here's another stab at it:

(with-sound ()
      (reson 0.0 1.0 440 .1 2 '(0 1 100 0) '(0 0 100 1) .01 .1 .1 5 .01 5 .01 0 1.0 0.01
   	     '(((0 1 100 1) 1000 .65 .1 .1 0 1.0 .1 .1) ((0 0 100 1) 2400 .15 .1 .1 0 1.0 .1 .1))))

If you find a good example, please send me it!

rhodey beg dur freq amp (base .5)

rhodey is another of Perry Cook's instruments (an electric piano), based on a pair of FM generators.

(with-sound () (rhodey 0 1 440 .2))

One of the oscillators is set to a frequency 15 times the requested 'freq', so for higher notes, you'll need to set the srate higher:

(with-sound (:srate 44100) (rhodey 0 1 880 .2))
rms gen sig
balance gen sig comparison
gain gen sig rsmval
make-rmsgain (hp 10.0)

rms, balance, and gain are an implementation of the balance generators of CLM (based on CSound originals, Scheme versions originally provided by Fabio Furlanete). This section is a paraphrase of balance.html in the CLM tarball which was written by Sam Hiesz. balance, rms, and gain are used to track the RMS value of a signal and use that information to scale some other signal. rms returns the RMS value; gain takes a signal and an RMS value and modifies the signal to track the RMS value; balance packages gain and rms into one function call. make-rmsgain returns the generator used by rms, gain, and balance. The 'hp' parameter sets the speed with which the balance process tracks the RMS signal. An example is worth a zillion words:

  (with-sound (:channels 3)
    (let ((rg (make-rmsgain))
          (rg1 (make-rmsgain 40))
          (rg2 (make-rmsgain 2))
          (e (make-env '(0 0 1 1 2 0) :length 10000))
          (e1 (make-env '(0 0 1 1) :length 10000))
          (e2 (make-env '(0 0 1 1 2 0 10 0) :length 10000))
          (o (make-oscil 440.0)))
      (do ((i 0 (+ i 1)))
          ((= i 10000))
        (let ((sig (env e)))
          (outa i (balance rg sig (env e2)))
          (outb i (balance rg1 sig (env e1)))
          (outc i (balance rg2 (* .1 (oscil o)) (env e2)))))))
scratch beg file src-ratio turnlist

scratch moves back and forth in a sound file according to a list of turn times much like env-sound-interp. With voice input, we can create a "Remembrance of Bugs Bunny":

Scheme: (with-sound () (scratch 0.0 "now.snd" 1.5 '(0.0 .5 .25 1.0)))
Ruby:   with_sound() do scratch(0, "now.snd", 1.5, [0.0, 0.5, 0.25, 1.0]) end

I translate this as: "go forward from 0.0 to 0.5 secs, backwards to 0.25 secs, then forward to 1.0 secs".

spectra beg dur freq amp ...

spectra is an additive-synthesis instrument with vibrato and an amplitude envelope. It was intended originally to be used with the spectra in spectra.scm (information laboriously gathered at the dawn of the computer era by James A Moorer). One such spectrum is labelled "p-a4", so we can hear it via:

(load "spectr.scm")
(with-sound () 
  (spectra 0 1 440.0 .1 p-a4 '(0.0 0.0 1.0 1.0 5.0 0.9 12.0 0.5 25.0 0.25 100.0 0.0)))

The trailing parameters are:

 (partials '(1 1 2 0.5))
           (amp-envelope '(0 0 50 1 100 0))
           (vibrato-amplitude 0.005)
           (vibrato-speed 5.0)
           (degree 0.0)
           (distance 1.0)
           (reverb-amount 0.005)

We can pass our own partials:

(with-sound ()
  (spectra 0 1 440.0 .1 '(1.0 .4 2.0 .2 3.0 .2 4.0 .1 6.0 .1) 
           '(0.0 0.0 1.0 1.0 5.0 0.9 12.0 0.5 25.0 0.25 100.0 0.0)))
ssb-fm gen modsig
make-ssb-fm freq

These two functions implement a sort of asymmetric FM using ideas similar to those used in ssb-am.

stereo-flute beg dur freq flow ...

This is a physical model of a flute developed by Nicky Hind.

Scheme:
(with-sound (:channels 2) 
   (stereo-flute 0 1 440 0.55 :flow-envelope '(0 0 1 1 2 1 3 0))
   (stereo-flute 1 3 220 0.55 :flow-envelope '(0 0 1 1 2 1 3 0)))

Ruby:
with_sound() do stereo_flute(0, 2, 440, 0.55, :flow_envelope, [0, 0, 1, 1, 2, 1, 3, 0]) end

The trailing parameters are:

(flow-envelope '(0 1 100 1))
     (decay 0.01) 		; additional time for instrument to decay
     (noise 0.0356) 
     (embouchure-size 0.5)
     (fbk-scl1 0.5)		; these two are crucial for good results
     (fbk-scl2 0.55)
     (offset-pos 0.764264)      ; from 0.0 to 1.0 along the bore
     (out-scl 1.0)
     (a0 0.7) (b1 -0.3)	        ; filter coefficients
     (vib-rate 5) 
     (vib-amount 0.03)
     (ran-rate 5) 
     (ran-amount 0.03)

As with physical models in general, you may need to experiment a bit to find parameters that work.

touch-tone beg number

This instrument produces telephone tones:

Scheme:  (with-sound () (touch-tone 0.0 '(7 2 3 4 9 7 1)))
Ruby:    with_sound() do touch_tone(0, [7, 2, 3, 4, 9, 7, 1]) end

It is just two sine waves whose frequencies are chosen based on the number pressed.

  1     2     3   697 Hz
  4     5     6   770 Hz
  7     8     9   852 Hz
        0         941 Hz
 1209  1336  1477 Hz

For more than you really want to know about other such sounds, see Telephone Tone Frequencies.

tubebell beg dur freq amp (base 32.0)

Perry Cook's tubular bell:

(with-sound () 
  (tubebell 0 2 440 .1 32.0) 
  (tubebell 2 2 220 .1 64.0) 
  (tubebell 4 2 660 .1 .032))

'base' is the envelope base:

(with-sound () 
  (tubebell 0 2 440 .1 32.0) 
  (tubebell 2 2 220 .1 2048.0) 
  (tubebell 4 3 660 .1 .032))
two-tab beg dur freq amp ...

two-tab interpolates between two spectra.

(with-sound () (two-tab 0 2 440 .1 '(1.0 1.0) '(3.0 1.0)))
;; go from harmonic 1 to harmonic 3

The trailing parameters are:

(partial-1 '(1.0 1.0 2.0 0.5))
          (partial-2 '(1.0 0.0 3.0 1.0))
          (amp-envelope '(0 0 50 1 100 0))
          (interp-func '(0 1 100 0))
          (vibrato-amplitude 0.005)
          (vibrato-speed 5.0)
          (degree 0.0)
          (distance 1.0)
          (reverb-amount 0.005)

'interp-func' determines how we interpolate between the two spectra. When it is at 1.0, we get only the first, at 0.0 only the second.

(with-sound () (two-tab 0 2 440 .1 '(1.0 1.0) '(3.0 1.0) '(0 0 1 1 2 0) '(0 0 1 1)))

is the reverse of the earlier sound. To go out and back:

(with-sound () (two-tab 0 2 440 .1 '(1.0 1.0) '(3.0 1.0) '(0 0 1 1 2 0) '(0 0 1 1 2 0)))
vox beg dur freq amp ampfun freqfun freqscl voxfun index vibscl

vox is a translation of Marc LeBrun's MUS10 waveshaping voice instrument using FM in this case. The basic idea is that each of the three vocal formants is created by two sets of waveshapers (or oscils producing FM), one centered on the even multiple of the base frequency closest to the desired formant frequency, and the other on the nearest odd multiple. As the base frequency moves (due to vibrato or glissando), these center frequencies are recalculated on each sample, and the respective amplitudes set to reflect the distance of the current center frequency from the desired formant frequency. If a center frequency moves enough that the previous upper member of the pair has to become the lower member, the upper waveshaper (which has meanwhile ramped to zero amplitude), jumps down to its new center. The male-speaker formant table was provided by Robert Poor (see the code for the complete table of formants). For details on waveshaping, see Le Brun, "Digital Waveshaping Synthesis", JAES 1979 April, vol 27, no 4, p250. I used vox in the 5th movement of "Colony" and in "The New Music Liberation Army".

(with-sound ()
  (let ((amp-env '(0 0 25 1 75 1 100 0))
        (frq-env '(0 0 5 .5 10 0 100 1)))
  (vox 0 2 170 .4 amp-env frq-env .1 
    '(0 E 25 AE 35 ER 65 ER 75 I 100 UH) '(.8 .15 .05) '(.005 .0125 .025) .05 .1)
  (vox 2 2 110 .4 amp-env frq-env .5 
    '(0 UH 25 UH 35 ER 65 ER 75 UH 100 UH) '(.8 .15 .05) '(.005 .0125 .025))
  (vox 4 2 300 .4 amp-env frq-env .1 
    '(0 I 5 OW 10 I 50 AE 100 OO) '(.8 .15 .05) '(.05 .0125 .025) .02 .1)))

Or in Ruby:

with_sound() do
  amp_env = [0, 0, 25, 1, 75, 1, 100, 0]
  frq_env = [0, 0, 5, 0.5, 10, 0, 100, 1]
  vox(0, 2, 170, 0.4, amp_env, frq_env, 0.1, 
       [0, :E, 25, :AE, 35, :ER, 65, :ER, 75, :I, 100, :UH], 0.05, 0.1)
  vox(2, 2, 300, 0.4, amp_env, frq_env, 0.1, 
       [0, :I, 5, :OW, 10, :I, 50, :AE, 100, :OO], 0.02, 0.1)
  vox(4, 5, 600, 0.4, amp_env, frq_env, 0.1, 
       [0, :I, 5, :OW, 10, :I, 50, :AE, 100, :OO], 0.01, 0.1)
end

vox can also be use for less vocal effects:

(with-sound (:play #t :scaled-to .5)
  (vox 0 .25 500 .4 '(0 0 .1 1 1 1 2 .5 3 .25 10 0) '(0 0 5 .5 10 0 100 1) .1 
       '(0 E 25 OW 35 ER 105 ER) '(.13 .15 .15) '(.005 .005 .015) .05 .1))
wurley beg dur freq amp

Perry Cook's Wurlitzer (I assume).

(with-sound () (wurley 0 1 440 .1))
za time dur freq amp length1 length2 feedback feedforward
zc time dur freq amp length1 length2 feedback
zn time dur freq amp length1 length2 feedforward

The "z" instruments demonstrate "zdelay" effects — interpolating comb, notch, and all-pass filters.

(with-sound () (zn 0 1   100 .1 20 100 .995) 
  (zn 1.5 1 100 .1 100 20 .995)
  (zc 3 1   100 .1 20 100 .95) 
  (zc 4.5 1 100 .1 100 20 .95)
  (za 6 1   100 .1 20 100 .95 .95) 
  (za 7.5 1 100 .1 100 20 .95 .95))

snd-test.scm has examples of calling all these instruments.

see also:   bird   clm   dlocsig   examp   fade   fm   fmv   freeverb   graphEq   grani   jcrev   maraca   maxf   noise   piano   prc95   pvoc   singer   sndwarp   stochastic   strad   ws
dlocsig

dlocsig is a CLM generator developed by Fernando Lopez-Lezcano that can move sounds in two or three dimensions. Fernando's CLM/lisp-oriented documentation can be found in dlocsig.html. dlocsig.rb is Michael Scholz's translation of dlocsig to Ruby. It has lots of documentation and examples. If you load dlocsig.rb, a new menu is added named "Dlocsig". If you choose a path from this menu, you get a graphical user-interface to play with the various envelopes that drive dlocsig. Click the "With_Snd" button to apply the current path choices to the currently selected sound. Click "Gnuplot" to get a pretty picture of the path (in 3D!). An instrument that uses dlocsig is:

(define* (sinewave start-time duration freq amp (amp-env '(0 1 1 1))
		   (path (make-path :path '(-10 10 0 5 10 10))))
  (let ((vals (make-dlocsig :start-time start-time :duration duration :path path)))
    (let ((dloc (car vals))
	  (beg (cadr vals))
	  (end (caddr vals)))
      (let ((osc (make-oscil freq))
	    (aenv (make-env amp-env :scaler amp :duration duration)))
        (do ((i beg (+ i 1)))
            ((= i end))
	  (dlocsig dloc i (* (env aenv) (oscil osc))))))))

(with-sound (:channels 2) (sinewave 0 1.0 440 .5 :path (make-path '((-10 10) (0.5 0.5) (10 10)) :3d #f)))
draw

draw.scm has examples of graphics-oriented extensions.

color-samples color beg dur snd chn
uncolor-samples snd chn

color-samples displays the samples from sample 'beg' for 'dur' samples in 'color' whenever they're in the current time domain view. uncolor-samples cancels this action. To activate this, add it to after-graph-hook.

display-previous-edits snd chn

display-previous-edits displays all the edits of the current sound, with older edits gradually fading away. To activate this, add it to after-graph-hook:

(hook-push after-graph-hook display-previous-edits)
overlay-rms-env snd chn

overlay-rms-env displays the running rms value of the currently displayed data in red, overlayed upon the normal graph. To activate it, add it to the after-graph-hook:

(hook-push after-graph-hook overlay-rms-env)
overlay-sounds :rest sounds

overlay-sounds overlays onto its first argument (a sound) all subsequent arguments: (overlay-sounds 1 0 3).

samples-via-colormap snd chn

samples-via-colormap displays the time domain graph using the current colormap (it is really just an example of colormap-ref). To activate this, add it to after-graph-hook:

(hook-push after-graph-hook samples-via-colormap)
samples-via-colormap
dsp

dsp.scm is a DSP grabbag, mostly filters. There are more than 100 functions to describe here, so an alphabetical list is just a jumble of names. Instead, I've tried to divide them into several vague categories: FFTs, FIR filters, IIR filters, synthesis, sound effects, sampling rate conversion, linear algebra and stats, and scanned synthesis.

If you're new to DSP, I recommend Lyons' "Understanding Digital Signal Processing" and Steiglitz, "A Digital Signal Processing Primer"; there are many good books on advanced calculus — I especially liked Hildebrand, "Advanced Calculus for Applications", but it may be out of print (this was about 25 years ago, I think); a great book on complex analysis is Needham, "Visual Complex Analysis"; Poole's "Linear Algebra" is a very straightforward introduction; also Halmos, "Linear Algebra Problem Book"; the most enjoyable Fourier Analysis book is by Körner, but you don't want to start with it. For the ambitious, there is the encyclopedic set of books by Julius Smith. His "Mathematics of the DFT" and "Introduction to Digital Filters" are very clear.

FFTs
dht data

dht is the slow form of the Hartley transform, taken from Perry Cook's SignalProcessor.m. The Hartley transform is a kind of Fourier transform.

display-bark-fft off color1 color2 color3
undisplay-bark-fft 

display-bark-fft shows the current spectrum in the "lisp" graph in three different frequency scales: bark, mel, and erb, each in a different color. The default ticks follow the bark scale; click anywhere in the lisp graph to switch to a different tick scale choice. undisplay-bark-fft turns this graph off. Here we've used rgb.scm for some color names:

(display-bark-fft #f sea-green orange alice-blue)
(set! (selected-graph-color) gray30)
(set! (selected-data-color) light-green)
bark display
dolph n gamma

dolph is the Dolph-Chebyshev fft data window, taken from Richard Lyons, "Understanding DSP". The C version used by Snd/CLM is in clm.c. Another version of the same function, taken (with a few minor changes) from Julius Smith's "Spectral Audio", is named dolph-1.

down-oct n snd chn
stretch-sound-via-dft factor snd chn

down-oct tries to move a sound down by a factor of n (assumed to be a power of 2, 1 = no change) by goofing with the fft data, then inverse ffting. I think this is "stretch" in DSP jargon; to interpolate in the time domain we're squeezing the frequency domain. The power-of-2 limitation is based on the underlying fft function's insistence on power-of-2 data sizes. A more general version of this is stretch-sound-via-dft, but it's extremely slow.

goertzel freq beg dur snd
find-sine freq beg dur snd

goertzel and find-sine find the amplitude of a single component of a spectrum ('freq').

> (find-sine 550.0 0.0 (framples))
(0.00116420908413177 0.834196665512423)   ; car is amplitude, cadr is phase in radians
> (* (goertzel 550.0 0.0 (framples)) (/ 2.0 (framples)))
0.00116630805062827
periodogram N

periodogram (the "Bartlett" version, I think) runs over an entire file, piling up 'N' sized chunks of data, then displays the results in the "lisp graph" area; this needs a lot of work to be useful!

scentroid file (beg 0.0) dur (db-floor -40.0) (rfreq 100.0) (fftsize 4096)

scentroid is Brett Battey's CLM scentroid instrument, translated to Snd/Scheme. To paraphrase Brett: scentroid returns (in a float-vector) the continuous spectral centroid envelope of a sound. The spectral centroid is the "center of gravity" of the spectrum, and it has a rough correlation to our sense of "brightness" of a sound. 'db-floor' sets a lower limit on which framples are included in the analysis. 'rfreq' sets the number of measurements per second. 'fftsize' sets the fft window size (a power of 2). See also the moving-scentroid generator in generators.scm.

spot-freq samp snd chn

spot-freq is a first-pass at using autocorrelation for pitch tracking; it's easily fooled, but could probably be made relatively robust.

> (spot-freq 10000)  ; this is oboe.snd, in about .5 secs
555.262096862931    ; 555Hz is correct(!)

In the next example, we add spot-freq to the mouse-click-hook (in Ruby), so that each time we click somewhere in the graph, the pitch at that point is reported:

$mouse_click_hook.add_hook!("examp-cursor-hook") do |snd, chn, button, state, x, y, axis|
  if axis == Time_graph
    status_report(format("(freq: %.3f)", spot_freq(cursor(snd, chn))))
  end
end
rotate-phase func snd chn
zero-phase snd chn

These are fft phase manipulators taken from the phazor package of Scott McNab. zero-phase takes ffts, sets all phases to 0.0, then unffts. rotate-phase is similar, but applies 'func' to the phases.

(rotate-phase (lambda (x) 0.0))             ; same as (zero-phase)
(rotate-phase (lambda (x) (random 3.1415))) ; randomizes phases
(rotate-phase (lambda (x) x))               ; returns original
(rotate-phase (lambda (x) (- x)))           ; reverses original

or in Ruby:

rotate_phase(lambda {|x| random(PI) })      # randomizes phases

and Forth:

lambda: <{ x }> pi random ; #f #f rotate-phase \ randomizes phases
z-transform rl size z
fractional-fourier-transform rl im size angle

z-transform performs a z-transform returning a vector (to accommodate complex results):

> (define d0 (make-float-vector 8))
d0
;; and similarly for d1 and d2 ...
> (set! (d0 2) 1.0)
1.0
> (set! (d1 2) 1.0)
1.0
> (z-transform d0 8 (exp (make-rectangular 0.0 (* .25 pi))))
;; Ruby: z_transform(d0, 8, exp(Complex(0.0, (2.0 / 8) * PI)))
#(1.0  0.0+1.0i  -1.0  0.0-1.0i  1.0  0.0+1.0i  -1.0  0.0-1.0i)
> (mus-fft d1 d2 8)
#(1.0 0.0 -1.0 -0.0 1.0 0.0 -1.0 -0.0)
> d2
#(0.0 1.0 0.0 -1.0 0.0 1.0 0.0 -1.0)

which is a complicated way of showing that if 'z' is e^2*pi*i/n, you get a fourier transform. fractional-fourier-transform is the slow (DFT) version of the fractional Fourier Transform. If 'angle' is 1.0, you get a fourier transform.

FIR filters
make-highpass fc length, highpass f in
make-lowpass fc length, lowpass f in
make-bandpass flo fhi length, bandpass f in
make-bandstop flo fhi length, bandstop f in
make-differentiator length, differentiator f in

make-lowpass and lowpass provide FIR low pass filtering, and similarly for the other four choices. The order chosen is twice the 'length'; 'fc', 'flo', and 'fhi' are the edge frequencies in terms of srate = 2 * pi.

(let ((hp (make-bandpass (* .1 pi) (* .2 pi))))
  (map-channel (lambda (y)
    (bandpass hp y))))
make-hilbert-transform length
hilbert-transform f in
hilbert-transform-via-fft snd chn
sound->amp-env snd chn

These functions perform the hilbert transform using either an FIR filter (the first two) or an FFT. One example of its use is sound->amp-env (from R Lyons). Another is the ssb-am generator in CLM.

invert-filter coeffs

invert-filter inverts an FIR filter. Say we previously filtered a sound via

(filter-channel (float-vector .5 .25 .125))

and our mouse is broken so we can't use the Undo menu, and we've forgotten that we could type (undo). Nothing daunted, we use:

(filter-channel (invert-filter (float-vector .5 .25 .125)))

There are a million gotchas here. The primary one is that the inverse filter can "explode" — the coefficients can grow without bound. For example, any filter returned by spectrum->coeffs will be problematic.

make-spencer-filter

This returns a CLM fir-filter generator with the standard "Spencer Filter" coefficients.

notch-sound freqs order s c width
notch-channel freqs order beg dur s c e trunc width
notch-selection freqs order width

notch-channel, notch-selection, and notch-sound are aimed at noise reduction. Each takes a list of frequencies (in Hz), and an optional filter order, and notches out each frequency. The sharpness of the notch is settable explicitly via the 'width' argument, and implicitly via the filter 'order'. A common application cancels 60 Hz hum:

(notch-channel (do ((freqs ())
                    (i 60 (+ i 60))) 
                   ((= i 3000) 
                    (reverse freqs))
                 (set! freqs (cons i freqs))))

Here we've built a list of multiples of 60 and passed it to notch-channel. Its default notch width is 2 Hz, and its default order tries to maintain that width given the channel's sampling rate, so the default filter order can be very high (65536). The filtering is normally done via convolution (by CLM's convolve generator), so a high filter order is not a big deal. In ideal cases, this can reduce the hum and its harmonics by about 90%. But, if the hum is not absolutely stable, you'll probably want wider notches:

(notch-channel (do ((freqs ())
                    (i 60 (+ i 60))) 
                   ((= i 3000)
                    (reverse freqs))
                 (set! freqs (cons i freqs)))
                1024)

The order of 1024 means we get 20 Hz width minima (44100 Hz srate), so this notches out much bigger chunks of the spectrum. You get 98% cancellation, but also lose more of the original signal.

make-savitzky-golay-filter size (order 2)
savitzky-golay-filter f in

This the Savitzky-Golay filter, assuming symmetrical positioning. It is an FIR smoothing filter; perhaps it could be useful in noise reduction.

(define (unnoise order)
  (let ((flt (make-savitzky-golay-filter order 2)))
    (map-channel (lambda (y) (savitzky-golay-filter flt y)))))

For more info on this filter, See "Numerical Recipes in C".

spectrum->coeffs order spectrum
fltit-1 order spectr

spectrum->coeffs is a version of Snd's very simple spectrum->coefficients procedure ("frequency sampling"). It returns the FIR filter coefficients given the filter 'order' and desired 'spectrum' (a float-vector). An example of its use is fltit-1.

(map-channel (fltit-1 10 (float-vector 0 1.0 0 0 0 0 0 0 1.0 0)))
make-volterra-filter acoeffs bcoeffs
volterra-filter flt x

volterra-filter and make-volterra-filter implement one form of a common non-linear FIR filter. This version is taken from Monson Hayes "Statistical DSP and Modeling"; it is a slight specialization of the form mentioned by J O Smith and others. The 'acoeffs' apply to the linear terms, and the 'bcoeffs' to the quadratic.

(let ((flt (make-volterra-filter (float-vector .5 .1) (float-vector .3 .2 .1))))
  (map-channel (lambda (x) (volterra-filter flt x))))
IIR filters
make-biquad a0 a1 a2 b1 b2

make-biquad is a wrapper for make-filter to return a biquad filter section.

cascade->canonical coeffs

cascade->canonical converts cascade coefficients to canonical form (the form used by CLM's filter generator). 'coeffs' is a list of filter coefficients; the function returns a float-vector, ready for make-filter.

kalman-filter-channel (Q 1.0e-5)

This is an experimental function aimed at noise reduction using a Kalman filter.

make-butter-high-pass fq, make-butter-hp M fc
make-butter-low-pass fq, make-butter-lp M fc
make-butter-band-pass fq bw, make-butter-bp M f1 f2
make-butter-band-reject fq bw, make-butter-bs M f1 f2

These functions produce Butterworth filters, returning a CLM filter generator. The first named ones (make-butter-high-pass et al) are taken from Sam Heisz's CLM version of Paris Smaragdis's Csound version of Charles Dodge's code from "Computer Music: synthesis, composition, and performance". The second set (make-butter-lp et al) provide arbitrary order Butterworths. 'M' * 2 is the filter order, 'f1' and 'f2' are the band edges in Hz.

(clm-channel (make-butter-bp 3 1000 2000))
(filter-sound (make-butter-low-pass 500.0))

See also the notch filter in new-effects.scm, and of course analog-filter.scm: the latter renders this section obsolete.

make-iir-high-pass-2 fc din
make-iir-low-pass-2 fc din
make-iir-band-pass-2 f1 f2
make-iir-band-stop-2 f1 f2
make-eliminate-hum  (hum-freq 60.0) (hum-harmonics 5) (bandwidth 10)
make-peaking-2 f1 f2 m

More IIR filters.

(map-channel (make-eliminate-hum))

make-peaking (a bandpass filter) returns a function suitable for map-channel (it takes one argument, the current sample, and returns a sample):

(map-channel (make-peaking-2 500 1000 1.0))

In this case 'm' is the gain in the pass band. Use the functions in analog-filter.scm, rather than this group.

synthesis
cheby-hka k a coeffs

This returns the amplitude of the kth harmonic (0=DC) in the waveshaping output given the index 'a', and harmonic coefficients 'coeffs' (the 0th element is DC amplitude).

(with-sound ()
  (let ((gen (make-polyshape 1000.0 :partials (list 1 .5  2 .25  3 .125  4 .125))))
    (do ((i 0 (+ i 1)))
        ((= i 88200))
      (outa i (* .5 (polyshape gen 0.25))))))
(cheby-hka 1 0.25 (float-vector 0 .5 .25 .125 .125)) ; returns first partial (fundamental) amplitude
flatten-partials partials (tries 32)

flatten-partials takes a list or float-vector of partial numbers and amplitudes, as passed to make-polywave, and tries to find an equivalent set of amplitudes that produces a less spikey waveform. The difference is primarily one of loudness until you have a lot of partials.

fm-parallel-component freq-we-want wc wms inds ns bs using-sine
fm-cascade-component freq-we-want wc wm1 a wm2 b
fm-complex-component freq-we-want wc wm a b interp sine ; "sine" arg currently ignored

This returns the amplitude of "freq-we-want" in parallel (complex) FM, where "wc" is the carrier, "wms" is a list of modulator frequencies, "inds" is a list of the corresponding indices, "ns" and "bs" are null (used internally), and using-sine is #t if the modulators are set up to produce a spectrum of sines, as opposed to cosines (we need to know whether to add or subtract the components that foldunder 0.0).

(fm-parallel-component 200 2000.0 (list 2000.0 200.0) (list 0.5 1.0) () () #t)

To get the same information for FM with a complex index, use fm-compex-component: (fm-compex-component 1200 1000 100 1.0 3.0 0.0 #f). For cascade FM (two levels only), use fm-cascade-component.

ssb-bank old-freq new-freq pairs-1 (order 40) (bw 50.0) (beg 0) dur snd chn edpos
ssb-bank-env old-freq new-freq freq-env pairs-1 (order 40) (bw 50.0) (beg 0) dur snd chn edpos
shift-channel-pitch freq (order 40) (beg 0) dur snd chn edpos

The ssb-bank functions provide single-sideband amplitude modulation, and pitch/time changes based on the ssb-am generator. If you run ssb-am on some input signal, the signal is shifted in pitch by the 'freq' amount. The higher the 'order', the better the sideband cancellation (amplitude modulation creates symmetrical sidebands, one of which is cancelled by the ssb-am generator). ssb-bank uses a bank of ssb-am generators, each with its own bandpass filter to shift a sound's pitch without changing its duration; the ssb-am generators do the pitch shift, and the filters pick out successive harmonics, so each harmonic gets shifted individually (i.e. harmonic relations are maintained despite the pitch shift). For an oboe at 557 Hz, good values are: (ssb-bank 557 new-freq 6 40 50). For a person talking at ca. 150 Hz: (ssb-bank 150 300 30 100 30) or (ssb-bank 150 100 40 100 20). To get a duration change without a pitch change, use this function followed by sampling rate conversion back to the original pitch:

(define (stretch-oboe factor)
  (ssb-bank 557 (* factor 557) 7 40 40)
  (src-sound (/ 1.0 factor)))

ssb-bank-env is the same as ssb-bank, but includes a frequency envelope: (ssb-bank-env 557 880 '(0 0 1 100.0) 7). shift-channel-pitch applies an ssb-am generator to a sound's channel (this is a variant of amplitude modulation). 'freq' and 'order' are the corresponding arguments to make-ssb-am. There is a dialog that runs ssb-bank in snd-motif.scm: create-ssb-dialog.

any-random e
gaussian-distribution s
pareto-distribution a
gaussian-envelope s

any-random provides the same output as rand if the latter's envelope (distribution function) argument is used, but using a slightly different method to generate the numbers. gaussian-envelope makes a gaussian distribution envelope suitable for rand. Also included is inverse-integrate, a version of CLM's distribution-to-weighting function.

(map-channel (lambda (y) (any-random 1.0 '(0 1 1 1))))          ; uniform distribution
(map-channel (lambda (y) (any-random 1.0 '(0 0 0.95 0.1 1 1)))) ; mostly toward 1.0
(let ((g (gaussian-distribution 1.0))) (map-channel (lambda (y) (any-random 1.0 g))))
(let ((g (pareto-distribution 1.0))) (map-channel (lambda (y) (any-random 1.0 g))))

In Ruby:

map_channel(lambda do |y| any_random(1.0, [0, 1, 1, 1]))            # uniform distribution
map_channel(lambda do |y| any_random(1.0, [0, 0, 0.95, 0.1, 1, 1])) # mostly toward 1.0
let(gaussian-distribution(1.0)) do |g|  map_channel(lambda do |y| any_random(1.0, g)) end
let(pareto-distribution(1.0))   do |g| map_channel(lambda do |y| any_random(1.0, g)) end
Random Numbers in Snd/CLM
generators, arbitrary distributions, fractals, 1/f: rand and rand-interp
dithering: dither-channel, dither-sound
noise-making instrument: noise.scm, noise.rb
physical modeling of noisy instruments: maraca.scm, maraca.rb
arbitrary distribution via rejection method: any-random
s7: random, random-state: random number between 0 and arg
Ruby: kernel_rand (alias for Ruby's rand), srand: random integer between 0 and arg, or float between 0 and 1
mus-random, mus_random: random float between -arg and arg
mus-rand-seed (settable)
bounded brownian noise: green-noise
brown and pink noise: brown-noise
effects
adsat size beg dur snd chn
freqdiv n snd chn

These two functions come from a package of effects developed by sed_sed@my-dejanews.com. adsat is "adaptive saturation", and freqdiv is "frequency division". (freqdiv n) repeats each nth sample 'n' times, clobbering the intermediate samples: (freqdiv 8). It turns your sound into a bunch of square waves.

brighten-slightly amount snd chn

brighten-slightly is a slight simplification of contrast-enhancement.

chordalize 

chordalize uses harmonically-related comb-filters to bring out a chord in a sound. The comb filters are controled by chordalize-amount (the default is .95), chordalize-base (the default is 100 Hz), and chordalize-chord (the default is (list 1 3/4 5/4)). chordalize returns a function suitable for map-channel:

 (map-channel (chordalize))

chordalize seems to work best with vocal sounds.

chorus

chorus tries to produce the chorus sound effect, but it needs work. It is controlled by the following variables:

chorus-size (5)        ; number of flangers
chorus-time (.05)      ; scales delay line length (flanger)
chorus-amount (20.0)   ; amp of rand-interp (flanger)
chorus-speed (10.0)    ; freq of rand-interp (flanger)
harmonicizer freq coeffs pairs (order 40) (bw 50.0) (beg 0) dur snd chn edpos

harmonicizer splits a sound into separate sinusoids, then splits each resultant harmonic into a set of harmonics, then reassembles the sound. The basic idea is very similar to ssb-bank, but harmonicizer splits harmonics, rather than pitch-shifting them. The result can be a brighter or richer sound.

(harmonicizer 550.0 (list 1 .5 2 .3 3 .2) 10)

'coeffs' is a list of harmonic-number and amplitude pairs, describing the spectrum produced by each harmonic. 'pairs' controls how many bands are used to split the original sound. 'order' is the bandpass filter's order in each such pair, and 'bw' controls its bandwidth.

lpc-coeffs data n m

lpc-coeffs returns 'm' LPC coeffients (in a vector) given 'n' data points in the float-vector 'data'.

lpc-predict data n coeffs m nf clipped

lpc-predict takes the output of lpc-coeffs ('coeffs') and the length thereof ('m'), 'n' data points 'data', and produces 'nf' new data points (in a float-vector) as its prediction. If 'clipped' is #t, the new data is assumed to be outside -1.0 to 1.0.

> (lpc-predict (float-vector 0 1 2 3 4 5 6 7) 8 (lpc-coeffs (float-vector 0 1 2 3 4 5 6 7) 8 4) 4 2)
#(7.906 8.557)
spike snd chn

spike returns a product (rather than the more usual sum) of succesive samples, with the current sample's sign; this normally produces a more spikey output. The more successive samples we include in the product, the more we limit the output to pulses placed at (just after) wave peaks. In spike's case, just three samples are multiplied. See also the volterra filter.

unclip-channel snd chn

unclip-channel tries to reconstruct clipped portions of a sound by using LPC to predict (backwards and forwards) the lost samples.

unclip-sound snd

unclip-sound calls unclip-channel on each channel in the sound 'snd'.

sampling rate conversion
linear-src-channel srinc snd chn

linear-src-channel performs sampling rate conversion using linear interpolation; this can sometimes be a nice effect.

src-duration env

src-duration takes an envelope representing the input (src change) to src, and returns the resultant sound length.

(src-duration '(0 1 1 2)) ; -> 0.693147180559945

which means that if the original sound was 2 seconds long, and we apply the envelope '(0 1 1 2) (via src-channel, for example) to that sound, the result will be .693 * 2 seconds long. To scale an src envelope to return a given duration, see src-fit-envelope below.

src-fit-envelope env target-dur

src-fit-envelope returns a version of "env" scaled so that its duration as an src envelope is "target-dur".

> (src-duration (src-fit-envelope '(0 1 1 2) 2.0))
2.0
stats, linear algebra, etc
channel-mean snd chn                            ; <f, 1> / n
channel-total-energy snd chn                    ; <f, f>
channel-average-power snd chn                   ; <f, f> / n
channel-norm snd chn                            ; sqrt(<f, f>)
channel-rms snd chn                             ; sqrt(<f, f> / n)
channel-variance snd chn                        ; <f, f> - ((<f, 1> / n) ^ 2) with quibbles
channel-lp u-p snd chn              
channel-lp-inf snd chn                          ; max abs f
channel2-inner-product s1 c1 s2 c2              ; <f, g>
channel2-orthogonal? s1 c1 s2 c2                ; <f, g> == 0
channel2-angle s1 c1 s2 c2                      ; acos(<f, g> / (sqrt(<f, f>) * sqrt(<g, g>)))
channel2-coefficient-of-projection s1 c1 s2 c2  ; <f, g> / <f, f>
channel-distance (s1 0) (c1 0) (s2 1) (c2 0)    ; sqrt(<f - g, f - g>)

These functions are taken from (or at least inspired by) Julius Smith's "Mathematics of the DFT". Many are standard ways of describing a signal in statistics; others treat a signal as a vector (channel-distance, for example, returns the Euclidean distance between two sounds). The 's1' and 's2' parameters refer to sound objects, and the 'c1' and 'c2' parameters refer to channel numbers.

channel-polynomial coeffs snd chn
spectral-polynomial coeffs snd chn
float-vector-polynomial v coeffs

float-vector-polynomial returns the evaluation of the polynomial (given its coefficients) over an entire float-vector, each element being treated as "x". channel-polynomial performs the same operation over a sound channel. spectral-polynomial is similar, but operates in the frequency domain (each multiply being a convolution).

> (float-vector-polynomial (float-vector 0.0 2.0) (float-vector 1.0 2.0)) ; x*2 + 1
#(1.0 5.0)
> (channel-polynomial (float-vector 0.0 1.0 1.0 1.0)) ; x*x*x + x*x + x

The "constant" (0-th coefficient) term in spectral polynomial is treated as a dither amount (that is, it has the given magnitude, but its phase is randomized, rather than being simple DC). See also poly.scm. In channel-poynomial, if you have an nth-order polynomial, the resultant spectrum is n times as wide as the original, so aliasing is a possibility, and even powers create energy at 0Hz.

scanned synthesis
vibrating-uniform-circular-string size x0 x1 x2 mass xspring damp
vibrating-string size x0 x1 x2 masses xsprings esprings damps haptics

These functions implement scanned synthesis of Bill Verplank and Max Mathews. To watch the wave, open some sound (so Snd has some place to put the graph), turn off the time domain display (to give our graph all the window) then

(let ((size 128))
  (let ((x0 (make-float-vector size))	   
        (x1 (make-float-vector size))	   
        (x2 (make-float-vector size)))
    (do ((i 0 (+ i 1)))
        ((= i 12))
      (let ((val (sin (/ (* 2 pi i) 12.0))))
        (set! (x1 (- (+ i (/ size 4)) 6)) val)))
    (do ((i 0 (+ i 1)))
        ((= i 1024))
      (vibrating-uniform-circular-string size x0 x1 x2 1.0 0.1 0.0)
      (graph x0 "string" 0 1.0 -10.0 10.0))))
env

env.scm provides a variety envelope functions. An envelope in Snd/CLM is a list of breakpoint pairs. In the function names, I try to remember to use "envelope" to be a list of breakpoints, and "env" to be the result of make-env, a CLM env structure passed to the env generator. In an envelope, the x axis extent is arbitrary, though it's simplest to use 0.0 to 1.0. (In this file, envelopes are assumed to be flat lists, not float-vectors or lists of lists).

add-envelopes env1 env2

add-envelopes adds two envelopes together:

> (add-envelopes '(0 0 1 1) '(0 0 1 1 2 0))
(0 0 1/2 3/2 1 1)  ; i.e. (0 0 1 1.5 2 1) in the second env's terms
concatenate-envelopes :rest envs

concatenate-envelopes concatenates its arguments:

> (concatenate-envelopes '(0 1 1 0) '(0 0 1 1))
(0.0 1 1.0 0 2.0 1)
envelope-exp e (power 1.0) (xgrid 100)

envelope-exp interpolates segments into envelope to approximate exponential curves.

> (format #f "~{~,3F ~}" (envelope-exp '(0 0 1 1) 3.0 6))
"0.000 0.000 0.167 0.005 0.333 0.037 0.500 0.125 0.667 0.296 0.833 0.579 1.000 1.000 "
envelope-last-x env

envelope-last-x returns the maximum x value:

> (envelope-last-x '(0 1 1 0 2 0))
2
integrate-envelope env

integrate-envelope returns the area under the envelope.

> (integrate-envelope '(0 0 1 1))
0.5
> (integrate-envelope '(0 1 1 1))
1.0
> (integrate-envelope '(0 0 1 1 2 .5))
1.25
make-power-env e (scaler 1.0) (offset 0.0) duration
power-env e
power-env-channel pe (beg 0) snd chn edpos (edname "power-env-channel")
powenv-channel envelope (beg 0) dur snd chn edpos

make-power-env and power-env implement an extension of exponential envelopes; each segment has its own base. power-env-channel uses the same mechanism as an extension of env-channel.

(let ((pe (make-power-env '(0 0 32.0  1 1 0.0312  2 0 1) :duration 1.0)))
  (map-channel (lambda (y) (* y (power-env pe)))))

(let ((pe1 (make-power-env '(0 0 32.0  1 1 0.0312  2 0 1.0  3 .5 3.0  4 0 0) :duration 1.0)))
  (power-env-channel pe1))

powenv-channel is a simplification of power-env-channel; it takes a breakpoint list rather than a power-env structure:

(powenv-channel '(0 0 .325  1 1 32.0 2 0 32.0))
map-envelopes func env1 env2

map-envelopes applies 'func' to the breakpoints in the two envelope arguments, returning a new envelope.

> (map-envelopes + '(0 0 1 1 2 0) '(0 1 2 0))
(0 1 1/2 3/2 1 0)  ; i.e. '(0 1 1 1.5 2 0) in the original x-axis bounds
min-envelope env
max-envelope env

max-envelope returns the maximum y value in 'env', and min-envelope returns the minimum y value:

> (max-envelope '(0 0 1 1 2 3 4 0))
3.0
multiply-envelopes env1 env2

multiply-envelopes uses map-envelopes to multiply two envelopes:

Scheme:
> (multiply-envelopes '(0 0 1 1) '(0 0 1 1 2 0))
(0 0 0.5 0.5 1 0)

Ruby:
> multiply_envelopes([0, 0, 1, 1], [0, 0, 1, 1, 2, 0])
[0.0, 0.0, 0.5, 0.5, 1.0, 0.0]
 
Forth:
snd> '( 0e 0e 1.0 1.0 ) '( 0e 0e 1.0 1.0 2.0 0.0 ) multiply-envelopes
'( 0.0 0.0 0.5 0.5 1.0 0.0 )

The new envelope goes from 0.0 to 1.0 along the X axis; the multiplied envelopes are stretched or contracted to fit 0.0 to 1.0, and wherever one has a breakpoint, the corresponding point in the other envelope is interpolated, if necessary.

normalize-envelope env (new-max 1.0)

normalize-envelope returns a version of 'env' scaled so that its maximum y value is 'new-max'.

> (normalize-envelope '(0 0 1 1 2 3 4 0) .5)
(0 0.0 1 0.167 2 0.5 4 0.0)
repeat-envelope env repeats reflected normalized

repeat-envelope repeats an envelope (concatenates copies of itself). It's usually easier to use mus-reset to restart an envelope over and over (see pulsed-env).

> (repeat-envelope '(0 0 .1 .9 1 1 1.3 .2 2 0) 2)
(0 0 0.1 0.9 1.0 1 1.3 0.2 2.0 0 2.1 0.9 3.0 1 3.3 0.2 4.0 0)
repeated envelope

If the final y value is different from the first y value (as above), a quick ramp is inserted between repeats. 'normalized' causes the new envelope's x axis to have the same extent as the original's. 'reflected' causes every other repetition to be in reverse.

reverse-envelope env

reverse-envelope reverses an envelope.

> (reverse-envelope '(0 0 1 1 2 1))
(0 1 1 1 2 0)
rms-envelope file (beg 0.0) dur (rfreq 30.0) db

rms-envelope returns an rms envelope of a file; it is based on rmsenv.ins in the CLM package.

> (format #f "~{~,3F ~}" (rms-envelope "1a.snd"))
"0.000 0.049 0.033 0.069 0.067 0.049 0.100 0.000 0.133 0.000 0.167 0.000 0.200 0.000 "
scale-envelope env scl (offset 0.0)

scale-envelope scales the y values of an envelope by 'scl', and optionally adds 'offset'.

stretch-envelope env old-attack new-attack (old-decay #f) (new-decay #f)

stretch-envelope applies attack and optionally decay times to an envelope, much like divseg in clm-1.

> (stretch-envelope '(0 0 1 1) .1 .2)
(0 0 0.2 0.1 1.0 1)
> (stretch-envelope '(0 0 1 1 2 0) .1 .2 1.5 1.6)
(0 0 0.2 0.1 1.1 1 1.6 0.5 2.0 0)
window-envelope beg end env

window-envelope returns (as an envelope) the portion of its envelope argument that lies between the x axis values 'beg' and 'end'. This is useful when you're treating an envelope as a phrase-level control, applying successive portions of it to many underlying notes.

> (window-envelope 1.0 3.0 '(0.0 0.0 5.0 1.0))
(1.0 0.2 3.0 0.6)
see also:   make-env   env-channel   Enved   env-expt-channel
enved, xm-enved

enved.scm implements an independent envelope editor in each channel.

start-enveloping 
stop-enveloping 
channel-envelope snd chn
play-with-envs snd

(start-enveloping) opens an envelope editor for each subsequently opened sound. (stop-enveloping) turns this off. Each envelope can be read or written via (channel-envelope snd chn). An example use is play-with-envs which sets the channel's amplitude from its envelope during playback

channel enveds

Closely related to this is xm-enved.scm which implements a separate envelope editor widget.

xe-create-enved name parent args axis
xe-envelope xe-editor

xe-create-enved returns a new envelope editor whose x axis label is 'name', the x and y axis bounds are in the list 'axis', the editor's parent widget is 'parent', and the Xt-style resource argument list is 'args'. The editor's current envelope is accessible as 'xe-envelope'.

see also:   make-env   env-channel   Enved   functions   env-expt-channel
examp

examp.scm has become a bit of a mess; rather than get organized, I just appended new stuff as it came to mind. In this documentation, I'll divide the functions into the following non-orthogonal categories: ffts, filters, sound effects, marks, selections, graphics, and miscellaneous stuff

FFTs
display-correlation snd chn y0 y1

display-correlation graphs the correlation of the 2 channels of the sound 'snd'. To make this happen automatically as you move the time domain position slider: (hook-push graph-hook display-correlation). The last three parameters are unused; they are just for compatibility with graph-hook.

fft-cancel lo-freq hi-freq snd chn

fft-cancel ffts an entire channel, zeroes the bins between 'lo-freq' and 'hi-freq' (in Hz), then inverse ffts, giving a good notch filter.

(fft-cancel 500 1000)  ; squelch frequencies between 500 and 1000 Hz
fft-edit low-freq high-freq snd chn

fft-edit takes an fft of the entire sound, removes all energy below 'low-freq' and above 'high-freq' (in Hz), then inverse fft's. This is the complement of fft-cancel.

fft-env-edit env snd chn

fft-env-edit is similar to fft-edit, but applies an envelope to the spectral magnitudes.

(fft-env-edit '(0 0 .1 1 .2 1 .3 0 .5 1 1.0 0)) ; 1.0 = srate / 2 here
fft-env-interp env1 env2 interp snd chn

fft-env-interp performs fft-env-edit twice (using 'env1' and 'env2'), then mixes the two results following the interpolation envelope 'interp'.

fft-peak snd chn scale

fft-peak is an after-transform-hook function that reports the peak spectral magnitude in the status area.

Scheme: (hook-push after-transform-hook fft-peak)

Ruby:   $after_transform_hook.add_hook!(\"fft-peak\") do |snd, chn, scale|
          fft_peak(snd, chn, scale)
        end

This can be helpful if you're scanning a sound with the fft graph displayed; since it normalizes to 1.0 (to keep the graph from jumping around simply because the amplitude is changing), it's nice to know what the current peak actually represents. You can, of course, turn off the normalization:

(set! (transform-normalization) dont-normalize)
fft-smoother cutoff start samps snd chn

fft-smoother uses fft filtering to smooth a portion of a sound, returning a float-vector with the smoothed data. 'cutoff' sets where we starting zeroing out high frequencies.

Scheme: (float-vector->channel (fft-smoother .1 (cursor) 400 0 0) (cursor) 400)
Ruby:   vct2channel(fft_smoother(0.1, cursor, 400, 0, 0), cursor, 400)
fft-squelch squelch snd chn

fft-squelch is similar to fft-edit; any fft bin whose (normalized) magnitude is below 'squelch' is set to 0.0. This is sometimes useful for noise-reduction.

filter-fft func (normalize #t) snd chn

This a sort of generalization of the preceding functions. It gets the spectrum of all the data in the given channel, applies the function 'func' to each element of the spectrum, then inverse ffts. 'func' should take one argument, the current spectrum value.

(define brfft
  (let ((+documentation+ "(brfft lofrq hifrq) removes all frequencies between lofrq and hifrq: (brfft 1000.0 2000.0)"))
    (lambda (lofrq hifrq)
      (let* ((fsize (let ((len (framples)))
                      (expt 2 (ceiling (log len 2)))))
	     (ctr -1)
	     (lo (round (/ (* fsize lofrq) (srate))))
	     (hi (round (/ (* fsize hifrq) (srate)))))
        (filter-fft (lambda (y)
		      (set! ctr (+ 1 ctr))
		      (if (>= hi ctr lo)
		          0.0
		          y)))))))

Here are some sillier examples...

(filter-fft (make-one-zero .5 .5))
(filter-fft (make-one-pole .05 .95))
(filter-fft (lambda (y) (if (< y .1) 0.0 y))) ; like fft-squelch
(let ((rd (make-sampler 0 0 0 1 0))) 
  (scale-by 0) 
  (filter-fft (lambda (y) (rd))))             ; treat sound as spectrum
(filter-fft contrast-enhancement)
(filter-fft (lambda (y) (* y y y)))           ; extreme low pass
squelch-vowels snd chn

squelch-vowels uses fft data to try to distinguish a steady state portion (a vowel in speech) from noise (a consonant, sometimes), then tries to remove (set to 0.0) the vowel-like portions.

superimpose-ffts snd chn y0 y1

superimpose-ffts is a graph-hook function that superimposes the ffts of multiple (sync'd) sounds. (hook-push graph-hook superimpose-ffts) This function needs some work...

zoom-spectrum snd chn y0 y1

zoom-spectrum sets the transform size to correspond to the time-domain window size. (hook-push graph-hook zoom-spectrum).

filters
comb-filter scaler size
zcomb scaler size pm

comb-filter is an example of using the CLM comb generator.

Scheme:  (map-channel (comb-filter .8 32))
Ruby:    map_channel(comb_filter(0.8, 32))
Forth:   0.8 32 comb-filter-1 map-channel

it would be faster to use the comb filter directly:

(clm-channel (make-comb .8 32))

zcomb is a time-varying comb filter using the envelope 'pm' (the envelope is applied to the comb filter delay length).

(map-channel (zcomb .8 32 '(0 0 1 10)))
comb-chord scaler size amp

comb-chord uses comb filters at harmonically related sizes to create a chord (see also chordalize in dsp.scm). 'amp' is an overall amplitude scaler.

(map-channel (comb-chord .95 100 .3))
(map-channel (comb-chord .95 60 .3))
filtered-env e snd chn

filtered-env takes an amplitude envelope 'e' and creates a one-pole filter, and moves them in parallel over a sound; as the sound gets softer, the low-pass filter's cutoff frequency gets lower, a sort of poor-man's distance effect. When 'e' is at 1.0, no filtering takes place.

(filtered-env '(0 1 1 0)) ; fade out
formant-filter radius frequency

formant-filter applies a formant to its input.

(map-channel (formant-filter .99 2400))

It's probably faster to use the CLM filter directly:

(filter-sound (make-formant 2400 .99))
formants r1 f1 r2 f2 r3 f3

formants applies three formants in parallel.

(map-channel (formants .99 900 .98 1800 .99 2700))
moving-formant radius move-envelope

moving-formant moves a formant according to an envelope (the envelope y value is in Hz).

(map-channel (moving-formant .99 '(0 1200 1 2400)))
notch-filter scaler size

This is an example of calling the CLM notch filter.

(map-channel (notch-filter .8 32))
osc-formants radius bases amounts freqs

osc-formants sets up any number of independently oscillating formants, then calls map-channel.

Scheme: (osc-formants .99 (float-vector 400.0 800.0 1200.0) (float-vector 80.0 80.0 120.0) (float-vector 4.0 2.0 3.0))
Ruby:   osc_formants(0.99, vct(400, 800, 1200), vct(80, 80, 120), vct(4, 2, 3))

'bases' are the formant center frequencies; 'freqs' are the oscillator frequencies; 'amounts' are "deviations" — they scale the oscillator outputs which set the runtime formant frequencies (thereby setting the width of the warbling).

sound effects
add-notes notes snd chn

add-notes adds (mixes) 'notes' starting at the cursor in the currently selected channel. 'notes' is a list of lists of the form: (list file offset amp).

Scheme: (add-notes '(("oboe.snd") 
                     ("pistol.snd" 1.0 2.0)))

Ruby:   add_notes([["oboe.snd"], 
                   ["pistol.snd", 1.0, 2.0]])

This mixes "oboe.snd" at time 0.0, then "pistol.snd" at 1.0 (second) scaled by 2.0.

am freq
ring-mod freq gliss-env
ring-modulate-channel freq beg dur snd chn edpos
vibro speed depth

These functions perform amplitude modulation and ring-modulation. 'freq' is the modulation frequency.

(map-channel (am 440))
(map-channel (ring-mod 10 (list 0 0 1 (hz->radians 100))))
(ring-modulate-channel 100.0)
(map-channel (vibro 440 0.5))

am uses the CLM amplitude-modulate generator; the others are little more than oscil and a multiply. 'gliss-env' in ring-mod controls the frequency of the modulation. See also ssb-am.

chain-dsps beg dur :rest dsps

chain-dsps creates a patch of chained generators from its arguments. Someone wanted to set up generator patches in a note list:

(with-sound ()
  (chain-dsps 0 1.0 '(0 0 1 .25 2 0) (make-oscil 440))
  (chain-dsps 1.0 1.0 '(0 0 1 1 2 0) (make-one-zero .5) (make-readin "oboe.snd"))
  (chain-dsps 2.0 1.0 '(0 0 1 .125 2 0) (let ((osc1 (make-oscil 220)) 
	                                      (osc2 (make-oscil 440))) 
                                          (lambda (val) (+ (osc1 val) 
		                                           (osc2 (* 2 val)))))))

The 'dsps' is a sequence of breakpoint lists and generators; the breakpoint lists are treated as envelopes, and the generators are connected (previous) output to (current) input in the reverse of the order received. readin is an exception; since its input comes from a file, it is added to the current output. So, the first call is an oscil multiplied by an envelope. The second filters and envelopes readin input. The third sets up an additive synthesis patch. In Ruby, this example is:

with_sound() do
  chain_dsps(0, 1.0, [0, 0, 1, 1, 2, 0], make_oscil(:frequency, 440))
  chain_dsps(0, 1.0, [0, 0, 1, 1, 2, 0], make_one_pole(0.5), make_readin("oboe.snd"))
  chain_dsps(0, 1.0, [0, 0, 1, 1, 2, 0], 
   let(make_oscil(:frequency, 220),
       make_oscil(:frequency, 440)) 
     do |osc1, osc2|
       lambda do |val| 
         osc1.run(val) + osc2.run(2.0 * val) 
     end
   end)
end
compand 

These functions lookup a table value based on the current sample amplitude; the table is set up so that soft portions are slightly amplified. The companding curve is taken from Steiglitz "A DSP Primer".

cnvtest snd0 snd1 amp

This is an example of using convolution. It convolves 'snd0' and 'snd1' (using the CLM convolve generator), then scales by 'amp'. It returns the new maximum amplitude.

(cnvtest 0 1 .1)
cross-synthesis cross-snd amp fftsize radius

cross-synthesis performs cross-synthesis between 'cross-snd' (a sound) and the currently selected sound. 'cross-snd' is the sound that controls the spectra.

(map-channel (cross-synthesis 1 .5 128 6.0))
echo scaler secs
flecho scaler secs
zecho scaler secs frq amp

These are delay-based sound effects. echo returns an echo maker ('secs' is the delay in seconds between echos, 'scaler' is the amplitude ratio between successive echoes). zecho is similar, but also modulates the echoes. flecho is a low-pass filtered echo maker. See Snd with CLM for a discussion.

Scheme:
(map-channel (echo .5 .5) 0 44100)
(map-channel (zecho .5 .75 6 10.0) 0 65000)
(map-channel (flecho .5 .9) 0 75000)

Ruby:
map_channel(echo(0.5, 0.5), 0 44100)
map_channel(zecho(0.5, 0.75, 6, 10.0), 0, 65000)
map_channel(flecho(0.5, 0.9), 0, 75000)
expsrc rate snd chn

expsrc uses sampling rate conversion (the src generator) and granular synthesis (the granulate generator) to change the pitch of a sound without changing its duration.

(map-channel (expsrc 2.0)) ; up an octave

There are lots of other related examples: see for example clm-expsrc, expsnd below, ssb-bank, or the phase-vocoder.

expsnd rate-envelope snd chn

expsnd uses the same technique as expsrc, but uses it to change the tempo according to an envelope while maintaining the original pitch. expsnd needs dsp.scm (but doesn't check that it is loaded).

(expsnd '(0 1 2 .4))   ; speed up
(expsnd '(0 .5 2 2.0)) ; start fast, end slow
fp sr osamp osfrq snd chn

fp drives an src generator with an oscillator, modulating a sound. 'sr' is the base sampling rate; 'osamp' is the modulation depth; 'osfrq' is the modulation frequency. hello-dentist below is a randomized version of this. The name "fp" refers to "Forbidden Planet" which used this kind of sound effect a lot.

(fp 1.0 .3 20)
hello-dentist frq amp snd chn

hello-dentist drives n src generator with a rand-interp generator, producing a random quavering effect, hence the name.

(hello-dentist 40.0 .1)

'frq' is the random number frequency; 'amp' sets the depth of the modulation.

place-sound mono-snd stereo-snd panning-envelope-or-degree

place-sound mixes a mono sound ('mono-snd', an index) into a stereo sound ('stereo-snd') with panning determined by 'panning-envelope-or-degree'. If 'panning-envelope-or-degree' is a number (in degrees), the place-sound function has the same effect as using CLM's locsig generator; it mixes a mono sound into a stereo sound, splitting it into two copies whose amplitudes depend on the desired location. 0 degrees: all in channel 0, 90: all in channel 1.

(place-sound 0 1 45.0) 
;; 0=sound 0 (mono), 1=sound 1 (stereo), 45 deg, so outa * 0.5 and outb * 0.5

If 'panning-envelope-or-degree' is an envelope, the split depends on the panning envelope (0 = all in chan 0, etc).

(place-sound 0 1 '(0 0 1 1)) ; mix goes from all in outa to all in outb

This function could use at least a start time parameter.

Panning or Sound Placement
Place sound: place-sound above.
Place mix: mus-file-mix
CLM placement generator: locsig
CLM moving sound generator: dlocsig
Move sound via flanging: see flanging effect in new-effects.scm
Cross fade in frequency domain: fade.scm
pulse-voice cosines (freq 440.0) (amp 1.0) (fftsize 256) (r 2.0) snd chn

This function is a form of cross-synthesis which drives the resynthesis with a ncos pulse train. 'freq' is the ncos frequency; 'amp' is an overall amplitude scaler; 'cosines' is the number of cosines in the pulse (the more the spikier); 'fftsize' and 'r' (radius) control the formant bank used to get the current spectrum.

(pulse-voice 80 20.0 1.0 1024 0.01)
(pulse-voice 80 120.0 1.0 1024 0.2)
(pulse-voice 30 240.0 1.0 1024 0.1)
(pulse-voice 6 1000.0 1.0 512)

See also voice->unvoiced below.

make-ramp (size 128)
ramp gen up

ramp is a generator that produces a ramp of a given length, then sticks at 0.0 or 1.0 until the 'up' argument changes. The idea here is that we want to ramp in or out a portion of a sound based on some factor of the sound data; the ramp generator produces a ramp up when 'up' is #t, sticking at 1.0, and a ramp down when 'up' is #f, sticking at 0.0. 'size' sets the steepness of the ramp. A similar, less bumpy effect uses the moving-average generator. The following produces a very jittery, wandering amplitude envelope (brownian motion):

(let ((ramper (make-ramp 1000)))  ; ramp via increments of .001
  (map-channel (lambda (y) 
                 (* y (ramp ramper (< (random 1.0) .5))))))
reverse-by-blocks block-len snd chn
reverse-within-blocks block-len snd chn

reverse-by-blocks and reverse-within-blocks work best with speech. reverse-by-blocks divides a sound into blocks, then recombines those blocks in reverse order. reverse-within-blocks divides a sound into blocks, then recombines them in order, but with each block internally reversed. 'block-len' is the block length in seconds.

(reverse-by-blocks .1)
(reverse-within-blocks .1) ; .5 is also good
scramble-channels :rest new-order
scramble-channel silence

scramble-channels uses swap-channels to arbitrarily reorder the current sound's channels. The new channel order is 'new-order' so

(scramble-channels 3 2 0 1)

replaces chan0 with chan3, chan1 with chan2 and so on. scramble-channel searches for silences, sets up a list of segments based on those silences, and randomly re-orders the segments. 'silence' determines the background level that is treated as silence.

(scramble-channel .01)

This function needs cleaner splices between the sections.

sound-interp reader loc
make-sound-interp start snd chn
env-sound-interp envelope (time-scale 1.0) snd chn
granulated-sound-interp envelope (time-scale 1.0) (grain-length 0.10)
              (grain-envelope '(0 0 1 1 2 1 3 0)) (output-hop 0.05) snd chn

make-sound-interp returns an interpolating reader for the given channel. The interpolating reader reads a channel at an arbitary location, interpolating between samples if necessary. The corresponding generator is sound-interp. Here we use a sine wave to lookup the current sound:

 (let ((osc (make-oscil :frequency 0.5 :initial-phase (+ pi (/ pi 2))))
       (reader (make-sound-interp 0 0 0)) 
       (len (- (framples 0 0) 1)))
   (map-channel (lambda (val) 
     (sound-interp reader (* len (+ 0.5 (* 0.5 (oscil osc))))))))

This is effectively phase-modulation with an index of length-of-file-in-samples * 0.5 * hz->radians(oscil-frequency), or equivalently duration-in-seconds * frequency-in-Hz * pi. env-sound-interp reads the given channel (via a sound-interp generator) according to 'envelope' and 'time-scale', returning a new version of the data in the specified channel that follows that envelope; that is, when the envelope is 0.0 we get sample 0, when the envelope is 1.0 we get the last sample, when it is 0.5 we get the middle sample of the sound and so on.

Scheme: (env-sound-interp '(0 0 1 1))
Ruby:   env_sound_interp([0, 0, 1, 1])

returns an unchanged copy of the current sound. To get the entire sound in reverse:

Scheme: (env-sound-interp '(0 1 1 0))
Ruby:   env_sound_interp([0, 1, 1, 0])

And to go forward then backward, taking twice the original duration:

Scheme: (env-sound-interp '(0 0 1 1 2 0) 2.0)
Ruby:   env_sound_interp([0, 0, 1, 1, 2, 0], 2.0)

src-sound with an envelope could be used for this effect, but it is much more direct to apply the envelope to sound sample positions. A similar function is scratch in clm-ins.scm.

granulated-sound-interp is similar to env-sound-interp, but uses granular synthesis rather than sampling rate conversion to recreate the sound, so the effect is one of changing tempo rather than changing speed (pitch). Here we dawdle for awhile, then race at the end to get the whole sound in:

(granulated-sound-interp '(0 0 1 .1 2 1) 1.0 0.2 '(0 0 1 1 2 0))
voiced->unvoiced amp fftsize r tempo snd chn

This function is a form of cross-synthesis which drives the resynthesis with white noise (see also pulse-voice above).

 (voiced->unvoiced 1.0 256 2.0 2.0) ; whispered, twice as fast as original

'tempo' is the speed of the resynthesis.

marks
first-mark-in-window-at-left 

first-mark-in-window-at-left moves the (time domain) graph so that the leftmost visible mark is at the left edge. In large sounds it can be pain to get the left edge of the window aligned with a specific spot in the sound. In the following example, we assume the desired left edge has a mark, and the 'l' key (without control) will move the window left edge to that mark.

(bind-key #\l 0 first-mark-in-window-at-left)
mark-loops 

mark-loops places marks at any loop points found in the current sound's header. Only a few headers support loop points which are apparently used in synthesizers to mark portions of a waveform that can be looped without causing clicks, thereby lengthening a sound as a key is held down.

selections
region-play-list data
region-play-sequence data

region-play-list plays a list of regions. 'data' is list of lists: (list (list reg time)...); region 'reg' is played at time 'time' (in seconds).

(region-play-list (list (list reg0 0.0) (list reg1 0.5) (list reg2 1.0) (list reg0 1.0)))

which plays region reg0 at time 0.0 and 1.0, region reg1 at 0.5, and region reg2 at 1.0. Similarly, region-play-sequence plays a sequence of regions, one after the other:

(region-play-sequence (list reg0 reg1 reg2 reg0)) ; play in same order as before, but one after the other
region-rms reg

region-rms returns the rms value of the region's data (in chan 0).

selection-rms 

selection-rms returns the rms value of the selection's data (in chan 0).

graphics
auto-dot snd chn y0 y1

auto-dot sets the dot size (when you're using dots in the time domain) based on the current graph size.

(hook-push graph-hook auto-dot)
display-db snd chn

display-db is a lisp-graph-hook function to display the time domain data in dB.

(hook-push lisp-graph-hook display-db)

I just noticed that its y axis is labelled upside down.

display-energy snd chn

display-energy is a lisp-graph-hook function to display the time domain data squared. Here is a picture of it in action.

flash-selected-data time-interval

flash-selected-data causes the selected channel's graph to flash red and green. 'time-interval' is in milliseconds:

(flash-selected-data 100)

Not sure why anyone would want such a thing... examp.scm also has (commented out) functions to display colored text in rxvt:

(format #t "~Athis is red!~Abut this is not" red-text normal-text)
(format #t "~A~Ahiho~Ahiho" yellow-bg red-fg normal-text)

It's possible to use the same escape sequences in a normal shell script, of course:

echo '\e[41m This is red! \e[0m'
miscellaneous stuff
all-chans 

all-chans returns two parallel lists, the first a list of sound objects, the second of channel numbers. If we have two sounds open (indices 0 and 1 for example), and the second has two channels, (all-chans) returns '((#<sound 0> #<sound 1> #<sound 1>) (0 0 1)). The interpretation is: '((sound-with-index0 sound-with-index1 sound-with-index1) (chan0 chan0 chan1)), so if we're mapping some function with the usual snd chn parameters over all the current channels, we can get the sound and channel values from these lists.

channel-clipped? snd chn

channel-clipped? returns #t and a sample number if it finds clipping in the given channel. examp.scm also has commented out code that places a mark at the start of each clipped section in a sound, and adds a menu item ("Show Clipping") under the View menu.

do-chans func origin
do-all-chans func origin
do-sound-chans func origin

do-chans applies 'func' to all the sync'd channels using 'origin' as the edit history indication. do-all-chans is the same but applies 'func' to all channels of all sounds. do-sound-chans applies 'func' to all channels in the currently selected sound.

(do-all-chans (lambda (val) (* 2.0 val))) ; double all samples
every-sample? func

every-sample? applies 'func' to each sample in the current channel and returns #t if 'func' is not #f for all samples; otherwise it moves the cursor to the first offending sample.

> (every-sample? (lambda (y) (< (abs y) .1)))
#f
> (cursor)
4423
> (sample (cursor))
0.101104736328125
explode-sf2 

explode-sf2 turns a soundfont file (assuming it is the currently selected sound) into a bunch of files of the form sample-name.aif. It is based on soundfont-info; that documentation includes a function, mark-sf2, that places a named mark at start of each new member of the font and unnamed marks at the various loop points.

find-click loc

find-click finds the next click, starting its search at 'loc'. It returns #f if it can't find a click.

finfo filename

finfo returns a description of the file 'filename'.

> (finfo "oboe.snd")
"oboe.snd: chans: 1, srate: 22050, Sun/Next, big endian short (16 bits), len: 2.305"
find-pitch pitch
locate-zero limit
next-peak 
search-for-click 
zero+ 

locate-zero looks for the next sample where adjacent samples together are less than 'limit' and moves the cursor to that sample. The others are examples of searching procedures (to be used with C-s and friends): zero+ finds the next positive-going zero crossing (if searching forwards). next-peak finds the next maximum or minimum in the waveform. search-for-click looks for a click. find-pitch finds the next place where 'pitch' (in Hz) is predominate. For example, type C-s (in the graph), then in the status area: (find-pitch 600), and if the function finds some place in the sound where 600 Hz seems to be the basic pitch, it moves the cursor there and reports the time in the status area text window.

mpg mpgfile rawfile

mpg uses the "system" function to call mpg123 to translate an MPEG format sound file to a headerless ("raw") file containing 16-bit samples.

(mpg "mpeg.mpg" "mpeg.raw")

This is now built-in if the Snd configuration process can find mpg123.

open-next-file-in-directory 
click-middle-button-to-open-next-file-in-directory 

click-middle-button-to-open-next-file-in-directory sets up the mouse-click-hook and open-hook so that clicking the middle mouse button closes the current file and opens the next (alphabetical by filename) in the current directory. These are used in edit123.scm.

play-ac3 name

play-ac3 tries to play an AC3 encoded sound file by calling a52dec.

read-ascii file (out-filename "test.snd") (out-type mus-next) (out-format mus-bshort) (out-srate 44100)

read-ascii tries to turn a text file into a sound file. Octave or perhaps WaveLab produce these files; each line has one integer (as text), apparently a signed short. The read-ascii parameters describe the output file.

read-flac file
write-flac snd

read-flac and write-flac deal with FLAC files. This is now built into Snd if the flac program can be found at configuration time.

read-ogg file
write-ogg snd

read-ogg and write-ogg deal with OGG files. This is now built into Snd if the oggdec and offenc programs can be found at configuration time.

read-speex file
write-speex snd

read-speex and write-speex deal with SPEEX files. This is now built into Snd if speexdec and speexenc can be found at configuration time.

remove-clicks 

remove-clicks looks for obvious clicks and uses smooth-sound to remove them. See also remove-single-sample-clicks and remove-pops in clean.scm.

sounds->segment-data main-dir (output-file "sounds.data")

This function takes a directory name, and runs through all the sounds in the embedded directories, returning a text file with segment start and end times, and segment maxamps.

(sounds->segment-data "/home/bil/test/iowa/sounds/" "iowa.data")

It was written to find the note boundaries in the Iowa musical instrument sound library.

sort-samples bins

sort-samples provides a histogram of the samples (by amplitude) in 'bins' bins.

> (sort-samples 20)  ; bins go by 0.05
#(129017 90569 915 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
;; so 915 samples were > 0.1 in absolute value
sync-everything 

sync-everything sets the sync fields of all currently open sounds to the same unique value.

update-graphs 

update-graphs updates (redraws) all graphs.

window-rms 

window-rms returns the rms of the data in currently selected graph window.

window-samples snd chn

window-samples returns (in a float-vector) the samples displayed in the current window for the given channel. This is just a trivial wrapper for channel->float-vector.

xb-open snd
xb-close snd
switch-to-buf

These provide Emacs-like C-x b support where only one sound is visible at a time. To activate it:

(bind-key #\b 0 switch-to-buf #t)
(hook-push close-hook xb-close)
(hook-push after-open-hook xb-open)	    
extensions

These were originally scattered around examp.scm; I thought it would be more convenient if they were in one file.

channels-equal? snd1 chn1 snd2 chn2 (allowable-difference 0.0)
channels=? snd1 chn1 snd2 chn2 (allowable-difference 0.0)

channels=? returns #t if the two specified channels are the same within the given 'allowable-difference'. The 'allowable-difference' is checked on each sample, so any sample-wise difference larger than that causes the comparison to return #f. channels-equal? returns #t if channels=? and the channels are the same length. In channels=? one the other hand, the trailing (extra) samples in one channel are compared with 0.0 (that is, the shorter channel is padded out with zeros).

channel-sync snd chn

channel-sync uses the channel-properties list to implement a channel-local sync field. (This property is currently not used anywhere).

contrast-channel index beg dur snd chn edpos

contrast-channel applies the CLM contrast-enhancement function to a channel; this is largely equivalent to the control panel Contrast slider.

contrast-sound index (beg 0) dur snd

contrast-sound applies contrast-enhancement to every channel of the sound 'snd'. It is the multichannel version of contrast-channel.

dither-channel (amount .00006) beg dur snd chn edpos	

dither-channel adds "dithering" (noise) to a channel; some experts insist this makes everything copacetic. The noise consists of two white noise generators adding together.

dither-sound (amount .00006) (beg 0) dur snd

dither-sound adds dithering to every channel of the sound 'snd'. It is the multichannel version of dither-channel.

enveloped-mix filename beg env

enveloped-mix is like mix-sound, but includes an amplitude envelope over the mixed-in data.

(enveloped-mix "pistol.snd" 0 '(0 0 1 1 2 0))
env-expt-channel env exponent (symmetric #t) beg dur snd chn edpos
any-env-channel env func beg dur snd chn edpos
ramp-expt a0 a1 exponent (symmetric #t) beg dur snd chn edpos
sine-env-channel env beg dur snd chn edpos
sine-ramp a0 a1 beg dur snd chn edpos
blackman4-env-channel env beg dur snd chn edpos
blackman4-ramp a0 a1 beg dur snd chn edpos
env-squared-channel env (symmetric #t) beg dur snd chn edpos
ramp-squared a0 a1 (symmetric #t) beg dur snd chn edpos

These functions goof around with envelopes in various amusing ways. any-env-channel takes an envelope and a function to produce the connection between successive breakpoints, and applies the two to the current channel as an envelope. This packages up most of the "boilerplate" associated with applying an envelope to a sound. It is used by the other enveloping functions: sine-env-channel, blackman4-env-channel, and env-squared-channel. sine-ramp and sine-env-channel are the sinusoidal versions of ramp-channel and env-channel.

(sine-env-channel '(0 0 1 1 2 -.5 3 1))

applies the given envelope to the current channel, connecting the points with a sinusoidal curve. Similarly, blackman4-env-channel connects the dots with a sum of cosines, and env-squared-channel connects the dots with an x^2 curve. To get any other positive exponent, use env-expt-channel. The 'symmetric' argument determines whether the up and down moving ramps look symmetrical around a break point.

exponential envelopes
(env-channel '(0 0 1 1 2 -.75 3 0)) 
(env-sound '(0 0 1 1 2 -.75 3 0) 0 100 32.0)  
(env-sound '(0 0 1 1 2 -.75 3 0) 0 100 .032) 
(env-sound '(0 0 1 1 2 -.75 3 0) 0 100 0.0)
(sine-env-channel '(0 0 1 1 2 -.75 3 0))
(env-squared-channel '(0 0 1 1 2 -.75 3 0))
(blackman4-env-channel '(0 0 1 1 2 -.75 3 0))
(env-squared-channel '(0 0 1 1 2 -.75 3 0) #f)
(env-expt-channel '(0 0 1 1 2 -.75 3 0) 3.0)
(env-expt-channel '(0 0 1 1 2 -.75 3 0) 3.0 #f)
(env-expt-channel '(0 0 1 1 2 -.75 3 0) .3)
(env-expt-channel '(0 0 1 1 2 -.75 3 0) .3)
for-each-sound-file func dir
map-sound-files func dir
match-sound-files func dir

for-each-sound-file and map-sound-files apply 'func' to each sound file in 'dir'. The 'func' is passed one argument, the sound file name. map-sound-files returns a list of the results, if any, returned from 'func'. match-sound-files applies 'func' to each sound file in 'dir' and returns a list of files for which func does not return #f.

(for-each-sound-file
  (lambda (n) 
    (when (> (mus-sound-duration n) 10.0) 
      (snd-print (format #f "~%~A" n))))
  ".")
insert-channel filedat beg dur snd chn edpos

insert-channel inserts the specified data ('filedat') in the given channel at the given location. See mix-channel for a description of 'filedat'.

mix-channel filedat beg dur snd chn edpos

mix-channel is a "regularized" version of the file mixing functions (mix and mix-sound). Its first argument can be either a filename (a string), a sound, or a list containing the filename (or index), the start point in the file, and (optionally) the channel of the file to mix:

(mix-channel "pistol.snd")             ; mixing starts at sample 0, entire sound is mixed
(mix-channel "pistol.snd" 10000)       ; mixing starts at sample 10000 in current sound
(mix-channel (list "pistol.snd" 1000)) ; mixed data starts at sample 1000 in pistol.snd
(mix-channel (list "2.snd" 0 1))       ; mixed data reads channel 1 in 2.snd
mono->stereo new-name snd1 chn1 snd2 chn2
stereo->mono orig-snd chan1-name chan2-name
mono-files->stereo new-name chan1-file chan2-file

mono->stereo combines two mono sounds (currently open in Snd) into one (new) stereo file. mono-files->stereo is the same, but the source sounds are files, not necessarily already open in Snd. stereo->mono takes a stereo sound and produces two new mono sounds. (The corresponding stereo->mono-files can be based on the existing extract-channel function).

normalized-mix filename beg in-chan snd chn

normalized-mix is like mix but the mixed result has same peak amplitude as the original data.

normalize-sound amp (beg 0) dur snd

normalize-sound scales the sound 'snd' to peak amplitude 'amp'. It is the multichannel version of normalize-channel.

offset-channel amount beg dur snd chn edpos	

offset-channel adds a constant (DC offset) to a channel.

offset-sound off (beg 0) dur snd

offset-sound adds 'off' to every sample in the sound 'snd'. It is the multichannel version of offset-channel.

pad-sound beg dur snd

pad-sound places a block of 'dur' zeros in every channel of the sound 'snd' starting at 'beg'. It is the multichannel version of pad-channel.

redo-channel (edits 1) snd chn

redo-channel is a "regularized" version of redo.

scale-sound scl (beg 0) dur snd

scale-sound multiplies every sample in the sound 'snd' by 'scl'. It is the multichannel version of scale-channel.

undo-channel (edits 1) snd chn

undo-channel is a "regularized" version of undo.

fade

The two functions in fade.scm perform frequency-domain cross-fades, that is, the cross-fade is handled by a bank of bandpass filters (formant generators). The effect is sometimes only slightly different from a normal (time-domain) cross-fade, but there are some interesting possibilities ("sound evaporation", etc).

cross-fade beg dur amp file1 file2 ramp-beg ramp-dur ramp-type bank-dur fs fwidth

cross-fade stitches 'file1' to 'file2' using filtering to provide the join (rather than amplitude ramps). 'ramp-type' can be 0: sweep up, 1: sweep down, 2: sweep split from the middle; "sweep up" means that the low frequencies are filtered out first, etc. 'fs' is how many formants to set up; 'fwidth' is the formant resonance width control; 'ramp-beg' and 'ramp-dur' set the start point and length of the sweep; 'bank-dur' controls how much time is spent in the formant bank before starting or after ending the ramp.

(with-sound () (cross-fade 0 2 1.0 "oboe.snd" "trumpet.snd" 0.5 1.0 0 .1 256 2))
(float-vector->channel (cross-fade 0 1.5 1.0 0 1 0.5 .5 0 1.0 256 2))

These fades seem more successful to me when done relatively quickly (the opposite of the dissolve-fade below which is best if done as slowly as possible). With any luck the "sweep up" case can produce a sort of "evaporation" effect. A similar idea is behind dissolve-fade:

dissolve-fade beg dur amp file1 file2 fsize r lo hi

It ramps in and out frequency bands chosen at random. The original hope was to get something like a graphical dissolve, but it turns out to be better to let the random changes float along with no overall direction. If the current band amplitude is 1.0, we send it toward 0.0 and vice versa. Given patience and a suitably noisy original, strange pitches emerge and submerge. 'fsize' is the formant bank size; 'r' is the same as 'fwidth' in cross-fade (resonance width) modulo a factor of 2 (sigh...). 'lo' and 'hi' set the portion of the formant bank that is active during the dissolve.

(with-sound () (dissolve-fade 0 1 1.0 "oboe.snd" "trumpet.snd" 256 2 0 128))
(float-vector->channel (dissolve-fade 0 2 1 0 1 1024 2 2 #f))
freeverb

freeverb is Jezar Wakefield's reverberator, translated by Michael Scholz from CLM's freeverb.ins (written by Fernando Lopez-Lezcano), and documented in freeverb.html in the CLM tarball.

freeverb
    (room-decay 0.5)
    (damping 0.5)
    (global 0.3)
    (predelay 0.03)
    (output-gain 1.0)
    (output-mixer #f)
    (scale-room-decay 0.28)
    (offset-room-decay 0.7)
    (combtuning '(1116 1188 1277 1356 1422 1491 1557 1617))
    (allpasstuning '(556 441 341 225))
    (scale-damping 0.4)
    (stereo-spread 23)
    (verbose #f)

Here is a paraphrase of some of Fernando's documentation. 'room-decay' determines the decay time of the reverberation. 'damping' set the high frequency damping; this parameter can be a number, or an array or a list (with same number of elements as output channels). It is possible to control the damping for each output channel. 'global' controls how the outputs of all reverbs (one per channel) are mixed into the output stream. Specifying "0" will connect each reverberator directly to each output channel, "1" will mix all reverberated channels equally into all output channels. Intermediate values will allow for an arbitrary balance between local and global reverberation. The overall gain of the mixing matrix is kept constant. 'output-mixer' overrides this parameter. 'predelay' sets the predelay that is applied to the input streams. An array or list lets you specify the individual predelays for all chanenels. 'output-gain' is the overall gain multiplier for the output streams. 'output-mixer' sets the output mixing matrix directly (rather than through 'global').

(with-sound (:reverb freeverb :reverb-data '(:output-gain 3.0)) 
  (fm-violin 0 .1 440 .1 :reverb-amount .1))
see also:   jcrev   nrev convolution
grani

This is Fernando Lopez-Lezcano's CLM grani granular synthesis instrument translated to Scheme by Mike Scholz. The Ruby version is in clm-ins.rb.

grani start-time duration amplitude file
  (input-channel 0)                       ; input file channel from which samples are read
  (grains 0)                              ; if not 0, total number of grains to be generated
  (amp-envelope '(0 0 0.3 1 0.7 1 1 0))   ; overall amplitude envelope for note
  (grain-envelope '(0 0 0.3 1 0.7 1 1 0)) ; env for each individual grain
  (grain-envelope-end #f)                 ; if not #f, a second grain env
  (grain-envelope-transition '(0 0 1 1))  ; interp 0: use grain-envelope, 1: use grain-envelope-end
  (grain-envelope-array-size 512)         ; make-table-lookup table size
  (grain-duration 0.1)                    ; number or envelope setting grain duration (in seconds)
  (grain-duration-spread 0.0)             ; random spread around 'grain-duration'
  (grain-duration-limit 0.002)            ; minimum grain duration (in seconds)
  (srate 0.0)                             ; number or envelope setting sampling rate conversion
  (srate-spread 0.0)                      ; random spread of src around 'srate'
  (srate-linear #f)                       ; if #f, srate (envelope) is exponential
  (srate-base (expt 2 (/ 12)))            ; srate env base if exponential
  (srate-error 0.01)                      ; error bound for exponential conversion
  (grain-start '(0 0 1 1))                ; env that sets input file read point of current grain
  (grain-start-spread 0.0)                ; random spread around 'grain-start'
  (grain-start-in-seconds #f)             ; if #f, treat 'grain-start' as a percentage
  (grain-density 10.0)                    ; env on number of grains / second in output
  (grain-density-spread 0.0)              ; random spread around 'grain-density'
  (reverb-amount 0.01)
  (reverse #f)                            ; if #t, input is read backwards
  (where-to 0)                            ; locsig stuff — see the full documentation
  (where-bins ())
  (grain-distance 1.0)                    ; distance of sound source (locsig)
  (grain-distance-spread 0.0)             ; random spread around 'grain-distance'
  (grain-degree 45.0)
  (grain-degree-spread 0.0)
  (verbose #t)

(with-sound (:channels 2 :reverb jc-reverb :reverb-channels 1)
  (grani 0 1 .5 "oboe.snd" :grain-envelope '(0 0 0.2 0.2 0.5 1 0.8 0.2 1 0)))
see also:   granulate   expsrc   expand
heart

Snd can be used with non-sound data, and with-sound makes it easy to write such data to a sound file. An example is heart.scm. In this code, we search a file for blood pressure readings (they are scattered around with a bunch of other stuff), then write those numbers to a stereo sound file (the sphygmometer readings are between 70 and 150), then open that file in Snd with all the sound-related clipping features turned off. We also tell Snd to skip the data file in its start-up load process (since it is an uninterpretable text file) by incrementing script-arg.

heart picture
hooks

hooks.scm and hooks.rb have various hook-related functions.

describe-hook hook

describe-hook tries to decipher the functions on the hook list; this is almost identical to hook-functions.

hook-member func hook

hook-member returns #t if 'func' is already on the hook list, equivalent to (member value (hook-functions hook))

reset-all-hooks 

reset-all-hooks resets all of Snd's built-in hooks.

snd-hooks 

snd-hooks returns a list of all Snd built-in non-channel hooks.

with-local-hook hook local-hook-procs thunk

with-local-hook is a kind of "let" for hooks; it evaluates 'thunk' with 'hook' set to 'local-hook-procs' (a list which can be nil), then restores 'hook' to its previous state upon exit. The result returned by 'thunk' is returned by with-local-hook.

index
html obj

index.scm provides a connection between firefox and the Snd documentation. The index itself is built by make-index.scm, then accessed through the function html. (html arg), where 'arg' can be a string, a symbol, or a procedure sends the html reader to the corresponding url in the Snd documents.

see also:   html-program   snd-help   snd-urls
inf-snd.el, DotEmacs

These two files provide support for Snd as an Emacs subjob. inf-snd.el is by Michael Scholz, and DotEmacs is by Fernando Lopez-Lezcano. Both can be loaded in your ~/.emacs file (or ~/.xemacs/init.el if you're using xemacs).

DotEmacs sets up "dialects" for various versions of Common Lisp and for Snd, then binds C-x C-l to run ACL. This is intended for CCRMA'S 220 class, but it might be of interest to others. Much fancier is inf-snd.el. What follows is taken almost verbatim from Mike Scholz's comments in that file:

inf-snd.el defines a snd-in-a-buffer package for Emacs. It includes a Snd-Ruby mode (snd-ruby-mode), a Snd-Scheme mode (snd-scheme-mode), and a Snd-Forth mode (snd-forth-mode) for editing source files. The commands inf-snd-help and snd-help access the description which Snd provides for many functions. Using the prefix key C-u you get the HTML version of Snd's help. With tab-completion in the status area you can scan all functions at a glance. A menu "Snd/Ruby" is placed in the Emacs menu bar. Entries in this menu are disabled if no inferior Snd process exists. These variables may need to be customized to fit your system:

inf-snd-ruby-program-name   "snd-ruby"    Snd-Ruby program name
inf-snd-scheme-program-name "snd-s7"      Snd-Scheme program name using s7
inf-snd-forth-program-name  "snd-forth"   Snd-Forth program name
inf-snd-working-directory   "~/"          where Ruby or Scheme scripts reside
inf-snd-index-path          "~/"          path to snd-xref.c
inf-snd-prompt-char         ">"           listener prompt
snd-ruby-mode-hook          nil           to customize snd-ruby-mode
snd-scheme-mode-hook         nil           to customize snd-scheme-mode
snd-forth-mode-hook         nil           to customize inf-snd-forth-mode

You can start inf-snd-ruby-mode either with the prefix-key (C-u M-x run-snd-ruby) — you will be asked for program name and optional arguments — or directly via (M-x run-snd-ruby). In the latter case, the variable inf-snd-ruby-program-name needs to be set correctly. inf-snd-scheme-mode and inf-snd-forth-mode are handled in the same way. Here's an example for your ~/.emacs file:

(autoload 'run-snd-ruby     "inf-snd" "Start inferior Snd-Ruby process" t)
(autoload 'run-snd-scheme    "inf-snd" "Start inferior Snd-Scheme process" t)
(autoload 'run-snd-forth    "inf-snd" "Start inferior Snd-Forth process" t)
(autoload 'snd-ruby-mode    "inf-snd" "Load snd-ruby-mode." t)
(autoload 'snd-scheme-mode   "inf-snd" "Load snd-scheme-mode." t)
(autoload 'snd-forth-mode   "inf-snd" "Load snd-forth-mode." t)

(setq inf-snd-ruby-program-name "snd-ruby -notebook")
(setq inf-snd-scheme-program-name "snd-scheme -separate")
(setq inf-snd-forth-program-name "snd-forth")
(setq inf-snd-working-directory "~/Snd/")
(setq inf-snd-index-path "~/Snd/snd/")

See inf-snd.el for more info and examples of specializing these modes. You can change the mode while editing a Snd-Ruby, Snd-Scheme, or Snd-Forth source file with M-x snd-ruby-mode, M-x snd-scheme-mode, or M-x snd-forth-mode. To have Emacs determine automatically which mode to set, you can use special file-extensions. I use file-extension ".rbs" for Snd-Ruby source files, ".cms" for Snd-Scheme, and ".fth" for Snd-Forth.

(set-default 'auto-mode-alist
	     (append '(("\\.rbs$" . snd-ruby-mode)
                    ("\\.cms$" . snd-scheme-mode))
		     auto-mode-alist))

Or you can use the local mode variable in source files, e.g. by "-*- snd-ruby -*-" or "-*- snd-scheme -*-" in first line.

Key bindings for inf-* and snd-*-modes

\e\TAB        snd-completion    symbol completion at point
C-h m         describe-mode     describe current major mode

Key bindings of inf-snd-ruby|scheme|forth-mode:

C-c C-s   	 inf-snd-run-snd   (Snd-Ruby|Scheme|Forth from a dead Snd process buffer)
M-C-l		 inf-snd-load      load script in current working directory
C-c C-f   	 inf-snd-file      open view-files-dialog of Snd
M-C-p		 inf-snd-play      play current sound file
C-c C-t 	 inf-snd-stop      stop playing all sound files
C-c C-i   	 inf-snd-help      help on Snd-function (snd-help)
C-u C-c C-i	 inf-snd-help-html help on Snd-function (html)
C-c C-q   	 inf-snd-quit      send exit to Snd process
C-c C-k   	 inf-snd-kill      kill Snd process and buffer

Key bindings of snd-ruby|scheme|forth-mode editing source files:

C-c C-s   	 snd-run-snd
M-C-x     	 snd-send-definition
C-x C-e   	 snd-send-last-sexp
C-c M-e   	 snd-send-definition
C-c C-e   	 snd-send-definition-and-go
C-c M-r   	 snd-send-region
C-c C-r   	 snd-send-region-and-go
C-c M-o   	 snd-send-buffer
C-c C-o   	 snd-send-buffer-and-go
C-c M-b   	 snd-send-block          (Ruby only)
C-c C-b   	 snd-send-block-and-go   (Ruby only)
C-c C-z   	 snd-switch-to-snd
C-c C-l   	 snd-load-file
C-u C-c C-l 	 snd-load-file-protected (Ruby only)
C-c C-f   	 snd-file    	   open view-files-dialog of Snd
C-c C-p   	 snd-play    	   play current sound file
C-c C-t   	 snd-stop    	   stop playing all sound files
C-c C-i   	 snd-help    	   help on Snd-function (snd-help)
C-u C-c C-i	 snd-help-html	   help on Snd-function (html)
C-c C-q   	 snd-quit    	   send exit to Snd process
C-c C-k   	 snd-kill    	   kill Snd process and buffer

If xemacs complains that comint-snapshot-last-prompt is not defined, get the latest comint.el; I had to go to the xemacs CVS site since Fedora Core 5's xemacs (21.4) had an obsolete copy. Then scrounge around until you find xemacs-packages/xemacs-base/comint.el. Don't use the comint.el in the emacs package. It's not a tragedy if this variable isn't defined — you just don't get a prompt in the Snd Emacs window, but things still work. If either emacs or xemacs complains that it can't find gforth.el, you can find that file in the gforth package or site (or perhaps you can comment out the line (require 'forth-mode "gforth") in inf-snd.el). Finally, if temporary-file-directory is undefined, you can set it alongside the rest of the variables. So, for example, I (Bill S) have the following in my ~/.xemacs/init.el:

(setq load-path
      (append (list nil 
		    "/home/bil/cl"
		    "/home/bil/test/gforth-0.6.2" ; gforth.el
		    )
	      load-path))

(autoload 'run-snd-ruby     "inf-snd" "Start inferior Snd-Ruby process" t)
(autoload 'run-snd-scheme   "inf-snd" "Start inferior Snd-Scheme process" t)
(autoload 'run-snd-forth    "inf-snd" "Start inferior Snd-Forth process" t)
(autoload 'snd-ruby-mode    "inf-snd" "Load snd-ruby-mode." t)
(autoload 'snd-scheme-mode  "inf-snd" "Load snd-scheme-mode." t)
(autoload 'snd-forth-mode   "inf-snd" "Load snd-forth-mode." t)

(setq inf-snd-ruby-program-name "~/ruby-snd/snd") ; these are my local Snd's
(setq inf-snd-scheme-program-name "~/cl/snd")
(setq inf-snd-forth-program-name "~/forth-snd/snd")
(setq inf-snd-working-directory "~/cl/")
(setq inf-snd-index-path "~/cl/")
(setq inf-snd-working-directory   "~/cl/")
(setq inf-snd-index-path          "~/cl/")
(setq temporary-file-directory    "~/zap/")

If emacs complains about ruby-mode or something similar, you probably need to get ruby-mode.el and inf-ruby.el from ftp://ftp.ruby-lang.org/pub/ruby/ruby-*.tar.gz, or gforth.el from ftp://ftp.gnu.org/pub/gnu/gforth/gforth-0.[67].*.tar.gz.

jcrev

jc-reverb is a reverberator developed by John Chowning a long time ago (I can't actually remember when — before 1976 probably), based on ideas of Manfred Schroeder. It "colors" the sound much more than nrev, and has noticeable echoes, but I liked the effect a lot. new-effects.scm has a version of jc-reverb that runs as a normal snd editing function (via map-channel), whereas the jcrev.scm version assumes it is being called within with-sound:

Scheme:
    (with-sound (:reverb jc-reverb) (fm-violin 0 .1 440 .1 :reverb-amount .1))

Ruby:
    with_sound(:reverb, :jc_reverb) do fm_violin_rb(0, 0.1, 440, 0.1) end

Forth:
    0 1 440 0.2 ' fm-violin :reverb ' jc-reverb with-sound

jc-reverb has three parameters:

jc-reverb low-pass (volume 1.0) amp-env

if 'low-pass' if #t, a low pass filter is inserted before the output; 'volume' can be used to boost the output; 'amp-env' is an amplitude envelope that can be used to squelch the reverb ringing at the end of a piece.

(with-sound (:reverb jc-reverb :reverb-data '(#t 1.5 (0 0 1 1 2 1 3 0))) (fm-violin 0 .1 440 .1))
Reverbs in Snd
freeverb: freeverb.scm, freeverb.rb
jc-reverb: jcrev.scm
jl-reverb: clm-ins.scm
nrev: clm-ins.scm
control panel reverb: Reverb, control variables
convolution reverb: conrev
*reverb*: with-sound
lint

lint.scm is a lint program for Scheme. It tries to find errors or infelicities in your code. To try it:

(lint "some-code.scm")

lint tries to reduce false positives, so its default is somewhat laconic. There are several variables at the start of lint.scm to control additional output:

*report-unused-parameters*           ; if #t, report unused function/macro parameters
*report-unused-top-level-functions*  ; if #t, report unused functions
*report-undefined-variables*         ; if #t, report undefined identifiers
*report-shadowed-variables*          ; if #t, report function parameters that are shadowed
*report-minor-stuff*                 ; if #t, report all sorts of other stuff

lint is not real smart about functions defined outside the current file, so *report-undefined-variables* sometimes is confused. *report-minor-stuff* adds output about overly complicated boolean and numerical expressions, dangerous floating point operations, bad docstrings (this check is easily confused), and whatever else it finds that it thinks is odd.

maraca

The maracas are physical models developed by Perry Cook (CMJ, vol 21 no 3 Fall 97, p 44).

maraca beg dur 
       (amp .1) 
       (sound-decay 0.95) 
       (system-decay 0.999) 
       (probability .0625)
       (shell-freq 3200.0)
       (shell-reso 0.96)

maraca: (with-sound () (maraca 0 5 .5))
cabasa: (with-sound () (maraca 0 5 .5 0.95 0.997 0.5 3000.0 0.7))

big-maraca beg dur 
           (amp .1) 
           (sound-decay 0.95) 
           (system-decay 0.999) 
           (probability .0625)
           (shell-freqs '(3200.0))
           (shell-resos '(0.96))
           (randiff .01)
           (with-filters #t)

tambourine: 
    (with-sound () 
      (big-maraca 0 1 .25 0.95 0.9985 .03125 '(2300 5600 8100) '(0.96 0.995 0.995) .01))

sleighbells: 
    (with-sound () 
      (big-maraca 0 2 .15 0.97 0.9994 0.03125 '(2500 5300 6500 8300 9800) 
        '(0.999 0.999 0.999 0.999 0.999)))

sekere: 
    (with-sound ()
      (big-maraca 0 2 .5 0.96 0.999 .0625 '(5500) '(0.6)))

windchimes: 
    (with-sound () 
      (big-maraca 0 4 .5 0.99995 0.95 .001 '(2200 2800 3400) '(0.995 0.995 0.995) .01 #f))

big-maraca is like maraca, but takes a list of resonances and includes low-pass filter (or no filter).

see also:   noise   rand
marks

marks.scm/rb is a collection of mark-related functions.

define-selection-via-marks m1 m2

define-selection-via-marks selects the portion between the given marks, then returns the selection length. The marks defining the selection bounds must be in the same channel.

describe-mark mark

describe-mark returns a description of the movements of the mark over the channel's edit history:

> (define m (add-mark 1234))
m
> (describe-mark m)
((#<mark 1> sound: 0 "oboe.snd" channel: 0) 1234 478)

Here I placed a mark in oboe.snd at sample 1234, then deleted a few samples before it, causing it to move to sample 478.

fit-selection-between-marks m1 m2

fit-selection-between-marks tries to squeeze the current selection between two marks, using the granulate generator to fix up the selection duration (it currently does a less than perfect job).

mark-click-info mark

mark-click-info is a mark-click-hook function that describes a mark and its properties. It is used by with-marked-sound in ws.scm.

mark-explode (htype mus-next) (dformat mus-bfloat)

mark-explode splits a sound into a bunch of separate files based on mark placements. Each mark becomes the first sample of a separate sound.

mark-name->id name

mark-name->id is like find-mark, but searches all currently accessible channels. If a such a mark doesn't exist, it returns 'no-such-mark.

move-syncd-marks sync samples-to-move

move-syncd-marks moves any marks sharing the mark-sync value 'sync' by 'samples-to-move' samples.

pad-marks marks secs

pad-marks inserts 'secs' seconds of silence before each in a list of marks.

play-between-marks snd m1 m2

play-between-marks plays the portion in the sound 'snd' between the given marks.

play-syncd-marks sync

play-syncd-marks starts playing from all the marks sharing its 'sync' argument (see mark-sync).

report-mark-names 

report-mark-names causes a named mark to display its name in the status area when its sample happens to be played.

save-mark-properties 

save-mark-properties sets up an after-save-state-hook function to save any mark-properties.

snap-mark-to-beat 

snap-mark-to-beat forces a dragged mark to end up on a beat.

snap-marks 

snap-marks places marks at the start and end of the current selection in all its portions (i.e. in every channel that has selected data). It returns a list of all the marks it has added.

> (selection-position)
360
> (selection-framples)
259
> (snap-marks)
(#<mark 0> #<mark 1>)
> (mark-sample (integer->mark 0))
360
> (mark-sample (integer->mark 1))
619
syncup marks

syncup synchronizes a list of marks (positions them all at the same sample number) by inserting silences as needed.

marks.scm also has code that tries to make it simpler to sync marks together — you just click the marks that should share a mark-sync field, rather than laboriously setting each one in the listener; see start-sync and stop-sync. There is also some code (look for "eval-header" toward the end of the file) that saves mark info in a sound file header, and reads it when the file is subsequently reopened.

see also:   add-mark-pane   Marks   mark-loops   marks-menu
maxf

These files are translations (thanks to Michael Scholz!) of CLM's maxf.ins (thanks to Juan Reyes!). They implement a variant of the CLM formant generator developed by Max Mathews and Julius Smith (see their online paper). For a version of the filter closer to the paper, see the firmant generator. maxf.scm and maxf.rb provide a kind of demo instrument showing various ways to use the filter (banks tuned to different sets of frequencies, etc).

maxfilter file beg (att 1.0) (numf 1) (freqfactor 1.0) (amplitude 1.0)
		    (amp-env '(0 1 100 1)) (degree (random 90.0)) (distance 1.0) (reverb-amount 0.2)
(with-sound () (maxfilter "dog.snd" 0))
(with-sound (:srate 44100) (maxfilter "dog.snd" 0 :numf 12))
(with-sound (:srate 44100) (maxfilter "dog.snd" 0 :numf 13 :att 0.75))
(with-sound (:srate 44100) (maxfilter "dog.snd" 0 :numf 2 :att 0.25 :freqfactor 0.5))

The files described in this section either add new top-level menus to Snd, or modify existing ones. Most were written by Dave Phillips.

edit-menu.scm

edit-menu.scm adds some useful options to the Edit menu:

Selection->new      ; save selection in a new file, open that file
Cut selection->new  ; save selection in a new file, delete selection, open the file
Append selection    ; append selection to end of selected sound
Make stereofile     ; make a new 2-chan file, copy currently selected sound to it
Trim front          ; find first mark in each sync'd channel and remove all samples before it
Trim back           ; find last mark in each sync'd channel and remove all samples after it
Crop                ; find first and last mark in each sync'd channel and remove all samples outside them
fft-menu.scm

fft-menu.scm adds an "FFT Edits" top-level menu. It has entries for:

FFT notch filter   ; use FFT to notch out a portion of the spectrum
FFT squelch        ; use FFT to squelch low-level noise
Squelch vowels     ; use FFT to squelch vowel-like portions of speech
marks-menu.scm

marks-menu.scm adds a "Marks" top-level menu with entries:

Play between marks         ; play samples between marks
Loop play between marks    ; continuous play looping between marks
Trim before mark           ; remove samples before mark
Trim behind mark           ; remove samples after mark
Crop around marks          ; remove samples outside marks
Fit selection to marks     ; squeeze selection to fit between marks
Define selection by marks  ; define selection based on marks
Mark sync                  ; if on, click mark to sync with other marks
Mark sample loop points    ; place marks at header loop points, if any
Show loop editor           ; edit loop points; this dialog is not really functional yet
Delete all marks           ; delete all marks
Explode marks to files     ; writes a separate file for each set of marks
new-effects.scm, effects-utils.scm, effects.rb, effects.fs

new-effects.scm (effects.rb, effects.fs) implements an Effects menu. There are a ton of choices, most of them presented in separate dialogs. The gain dialog is illustrated below. Some of the outer menu items are:

Amplitude effects (gain, normalize)
Delay effects (various echos)
Filter effects (various filters)
Frequency effects (src, expsrc)
Modulation effects (AM)
Reverbs (nrev, jcrev, convolution)
Various (flange, locsig, etc)
Octave down
Remove clicks
Remove DC
Compand
Reverse
gain effect dialog

Most of these are either simple calls on Snd functions ("invert" is (scale-by -1)), or use functions in the other scm files. The actual operations follow the sync chain of the currently active channel.

special-menu.scm

special-menu.scm adds a "Special" menu with entries:

Append file
MIDI to WAV (using Timidity)
Record input channel 
Envelope new file (see start-enveloping)
Play panned 
Save as MP3 (using bladeenc)
Save as Ogg (using oggenc)
Explode SF2 (using the code explode-sf2 in examp.scm) 

misc.scm loads these menus and interface improvements, adds several sound file extensions, adds some hook functions for mpeg files, and includes a number of options such as show-disk-space. It then adds these menu items:

File:Delete                 ; delete the selected file
File:Rename                 ; rename the selected file
Edit:Append selection       ; append selection to end of current sound
Edit:Replace with selection ; put copy of selection at cursor

I think Dave expects you to customize this to suit yourself, perhaps even moving the stuff you want to your initialization file.

see also:   add-to-main-menu.
mix

mix.scm provides various mix utilities.

check-mix-tags snd chn
check-mix-tags looks at the current mix tags in the given channel, and if any are found that appear to be overlapping, it moves one of them down a ways.
color-mixes mix-list new-color
color-mixes sets the color of each mix in 'mix-list' to 'new-color'.
delay-channel-mixes beg dur snd chn
delay-channel-mixes adds dur (which can be negative) to the begin time of each mix that starts after beg in the given channel.
env-mixes mix-list envelope
env-mixes applies 'envelope' as an overall amplitude envelope to the mixes in 'mix-list'.
find-mix sample snd chn
find-mix returns the identifier of the mix at sample 'sample' (or anywhere in the given channel if 'sample' is not specified), or #f if no mix is found.
mix-click-info mix
mix-click-info is a mix-click-hook function that posts a description of the clicked mix in the help dialog.
mix-click-sets-amp 
mix-click-sets-amp adds a mix-click-hook function so that if you click a mix, it is removed (its amplitude is set to 0.0); a subsequent click resets it to its previous value. This is intended to make it easy to compare renditions with and without a given mix.
mix-maxamp mix
mix-maxamp returns the maxamp in the given mix.
mix-name->id name
mix-name->id returns the mix with the given name, or 'no-such-mix if none can be found.
mix-sound file start
mix-sound mixes 'file' (all chans) into the currently selected sound at 'start'.
mix->float-vector mix
mix->float-vector returns a mix's samples in a float-vector.
mixes-length mix-list
mixes-length returns the number of samples between the start of the first mix in 'mix-list' and the last end point.
mixes-maxamp mix-list
mixes-maxamp returns the maxamp of the mixes in 'mix-list'.
move-mixes mix-list samps
move-mixes moves each mix in 'mix-list' by 'samps' samples. To move all sync'd mixes together whenever one of them is dragged:
(hook-push mix-release-hook
	   (lambda (hook)
	     (let* ((id (hook 'id))
	            (samps-moved (hook 'samples))
                    (result (= (mix-sync id) 0)))
	       (if (not result)
		   (move-mixes (syncd-mixes (mix-sync id)) samps-moved))
	       (set! (hook 'result) result))))

See also snap-syncd-mixes-to-beat.

pan-mix file beg env snd auto-delete

pan-mix mixes 'file' into the current sound starting at 'beg' using the envelope 'env' to pan the mixed samples (0: all chan 1, 1: all chan 0). So,

(pan-mix "oboe.snd" 1000 '(0 0 1 1))

goes from all chan 0 to all chan 1. 'auto-delete' determines whether the in-coming file should be treated as a temporary file and deleted when the mix is no longer accessible.

pan-mix-region region beg env snd
pan-mix-region is similar to pan-mix above, but mixes a region, rather than a file.
pan-mix-selection beg env snd
pan-mix-selection is similar to pan-mix above, but mixes the current selection, rather than a file.
pan-mix-float-vector data beg env snd
pan-mix-float-vector is similar to pan-mix above, but mixes a float-vector, rather than a file. The argument 'data' represents one channel of sound. To sync all the panned mixes together:
(let ((new-sync (+ 1 (mix-sync-max))))
  (for-each
    (lambda (id)
     (set! (mix-sync id) new-sync))
   (pan-mix-float-vector (make-float-vector 10 0.5) 100 '(0 0 1 1))))
play-mixes mix-list
play-mixes plays the mixes in 'mix-list'.
scale-mixes mix-list scl
scale-mixes scales the amplitude of each mix in 'mix-list' by 'scl'.
scale-tempo mix-list scl
scale-tempo changes the positions of the mixes in 'mix-list' to reflect a tempo change of 'scl'. (scale-tempo (mixes 0 0) 2.0) makes the mixes in snd 0, chan 0 happen twice as slowly. To reverse the order of the mixes: (scale-tempo (mixes 0 0) -1).
set-mixes-tag-y mix-list new-y
set-mixes-tag-y sets the tag y location of each mix in 'mix-list' to 'new-y'.
silence-all-mixes 
This sets all mix amplitudes to 0.0.
silence-mixes mix-list
silence-mixes sets the amplitude of each mix in 'mix-list' to 0.0.
snap-mix-to-beat 
snap-mix-to-beat forces a dragged mix to end up on a beat (see x-axis-in-beats). To turn this off, (hook-remove mix-release-hook snap-mix-1). To snap the dragged mix, and every other mix syncd to it, use snap-syncd-mixes-to-beat.
src-mixes mix-list scl
src-mixes scales the speed (resampling ratio) of each mix in 'mix-list' by 'scl'.
sync-all-mixes (new-sync 1)
sync-all-mixes sets the mix-sync field of every active mix to 'new-sync'.
syncd-mixes sync
syncd-mixes returns a list (possibly null) of all mixes whose mix-sync field is set to 'sync'. Most of the functions in mix.scm take a 'mix-list'; to form that list based on a given mix-sync value, use syncd-mixes:
	
(scale-mixes (syncd-mixes 2) 2.0)
which scales the amplitude by 2.0 of any mix whose mix-sync field is 2.
transpose-mixes mix-list semitones
transpose-mixes sets the speed of mix in 'mix-list' to cause a transposition by the given number of semitones.
see also:   Mixes   View:Mix   mix-channel   dissolve-fade   mus-file-mix
moog
make-moog-filter frequency Q
moog-filter gen input

moog is a translation of CLM's moog.lisp (written by Fernando Lopez-Lezcano — http://ccrma.stanford.edu/~nando/clm/moog), itself a translation of Tim Stilson's original C code. The functions provide a kind of CLM generator view of the filter. Fernando describes it as a "Moog style four pole lowpass (24db/Oct) filter clm unit generator, variable resonance, warm, analog sound ;-)". In make-moog-filter, 'frequency' is the cutoff frequency in Hz (more or less) and 'Q' controls the resonance: 0.0 = no resonance, whereas 1.0 causes the filter to oscillate at 'frequency'.

(define (moog freq Q)
  (let ((gen (make-moog-filter freq Q)))
    (lambda (inval)
      (moog-filter gen inval))))

(map-channel (moog 1200.0 .7))

The Ruby version of this is in examp.rb, and the Forth version is in examp.fs.

musglyphs

musglyphs.scm provides Scheme/Snd wrappers to load CMN's cmn-glyphs.lisp (directly!), thereby defining most of the standard music notation symbols. Each of the original functions (e.g. draw-bass-clef) becomes a Snd/Scheme procedure of the form (name x y size style snd chn context). (draw-bass-clef 100 100 50) draws a bass clef in the current graph at position (100 100) of size 50; since the 'style' argument defaults to #f, the clef is displayed as a filled polygon; use #t to get an outline of the clef instead.

Snd with music symbols

A fancier example is included in musglyphs.scm. It takes a list of notes, each mixed as a virtual mix, and displays the note pitches as music notation on two staves at the top of the graph. The two main drawing functions are draw-staff and draw-a-note. The staves are drawn via an after-graph-hook function, and the notes are displayed via draw-mix-hook. mix-waveform-height sets the overall size of the music notation. The note list data is passed into these functions by setting various mix-properties: 'frequency and 'instrument (the first to give the pitch, the second the staff).

There's also an even fancier version of the same thing that treats the note heads as the mix tags, and changes the mix pitch if you drag the note up or down.

nb

nb.scm provides popup help for files in the View:Files dialog. As you move the mouse through the file list, the help dialog posts information about the file underneath the mouse. This uses a slightly fancier file information procedure than finfo in examp.scm.

nb file note
unb file
prune-db 

nb adds 'note' to the info associated with a file: (nb "test.snd" "test's info"). unb erases any info pertaining to a file: (unb "test.snd"). prune-db removes any info associated with defunct files. The Ruby version of nb (written by Mike Scholz) has several other features — see nb.rb for details.

noise

The noise files are translations (thanks to Michael Scholz) of CLM's noise.ins. noise.ins has a very long pedigree; I think it dates back to about 1978. It can produce those all-important whooshing sounds.

fm-noise startime dur freq0 amp ampfun ampat ampdc
   freq1 glissfun freqat freqdc rfreq0 rfreq1 rfreqfun rfreqat rfreqdc
   dev0 dev1 devfun devat devdc
   (degree 0.0) (distance 1.0) (reverb-amount 0.005)

This is an old instrument, so one must make allowances. 'ampat' and 'ampdc' are the amplitude envelope ('ampfun') attack and decay times, and similarly for the frequency envelope 'glissfun', the random number frequency envelope 'rfreqfun' and the index envelope 'devfun' (dev = "deviation", and old radio-style name for the FM index). Each envelope must go on the x axis from 0 to 100; the attack portion ends at 25, the decay portion starts at 75 (once upon a time there was a generator named LINEN; his full name was Line-Segment Envelope, but everyone just called him LINEN; they had to shout because he was a bit deaf). 'rfreq' is the frequency of the random number generator; if it is below about 25 Hz, you get automatic composition; above that you start to get noise. Well, you get a different kind of noise. 'dev' is the bandwidth of the noise; very narrow 'dev' gives a whistle, very broad more of a whoosh. This is simple FM where the modulating signal is white noise.

(with-sound ()
  (fm-noise 0 2.0 500 0.25 '(0 0 25 1 75 1 100 0) 0.1 0.1 1000 '(0 0 100 1) 0.1 0.1
            10 1000 '(0 0 100 1) 0 0 100 500 '(0 0 100 1) 0 0))

There is also a generator-like version of the instrument:

make-fm-noise len freq (amp 0.25) (ampfun '(0 0 25 1 75 1 100 0)) (ampat 0.1)
              (ampdc 0.1) (freq1 1000) (glissfun '(0 0 100 1)) (freqat 0.1) (freqdc 0.1)
              (rfreq0 10) (rfreq1 1000) (rfreqfun '(0 0 100 1)) (rfreqat 0) (rfreqdc 0)
              (dev0 100) (dev1 500) (devfun '(0 0 100 1)) (devat 0) (devdc 0) 
              (degree (random 90.0)) (distance 1.0) (reverb-amount 0.005)
see also:   maraca   rand
numerics

This file has a variety of functions oriented toward some experiments that so far haven't panned out.

factorial n
binomial n k
n-choose-k n k
plgndr l m x
legendre-polynomial a x
legendre n x
gegenbauer n x (alpha 0.0)
chebyshev-polynomial a x (kind 1)
chebyshev n x (kind 1)
hermite-polynomial a x
hermite n x
laguerre-polynomial a x (alpha 0.0)
laguerre n x (alpha 0.0)
Si x
Ci x
sin-m*pi/n m n
show-digits-of-pi-starting-at-digit start

In this case, "the code is the documentation" — these functions are informal, experimental, etc. One amusing function is sin-m*pi/n. It returns an expression giving the exact value of sin(m*pi/n), m and n integer, if we can handle n. Currently n can be anything of the form 2^a 3^b 5^c 7^d 11^e 13^f 17^g 257^h, so (sin-m*pi/n 1 60) returns an exact expression for sin(pi/60). The expression is not reduced much.

> (sin-m*pi/n 1 9)
(/ (- (expt (+ (sqrt 1/4) (* 0+1i (sqrt 3/4))) 1/3) (expt (- (sqrt 1/4) (* 0+1i (sqrt 3/4))) 1/3)) 0+2i)
> (eval (sin-m*pi/n 1 9))
0.34202014332567
> (sin (/ pi 9))
0.34202014332567
> (sin (/ pi (* 257 17)))
0.00071906440440859
> (eval (sin-m*pi/n 1 (* 17 257)))
0.00071906440440875

Another amusing function is show-digits-of-pi-starting-at-digit, translated from a C program written by David Bailey. It shows 10 (hex) digits of the expansion of pi starting from any point in that expansion.

peak-phases

peak-phases.scm contains the phases that produce a minimum peak amplitude ("low peak-factor") sum of sinusoids, the unpulse-train, so to speak. I started with the questions: given a sum of n equal amplitude harmonically related sinusoids, what set of initial phases minimizes the peak amplitude? What is that peak as a function of n? Can we find any pattern to the initial phases so that a tiresome search is unnecessary? For the second question, there are several simple cases. If all harmonics are cosines, the peak amplitude is n (they all are 1 at the start). If we have 2 harmonics, and vary the initial phase of the second from 0.0 to 2*pi, graphing the resulting peak amplitude, we get:

n=2 case n=2 case

The graph on the left is the second harmonic's initial phase vs the peak amplitude. Since 0.0 appears to be a minimum (we can show that it is via simultaneous non-linear equations; see peak-phases.scm), we can solve for the peak at that point using calculus: differentiate sin(x) + sin(2x) to get cos(x) + 2cos(2x) = 4cos^2(x) + cos(x) -2, a quadratic equation in cos(x). Let y=cos(x), solve for y: (sqrt(33)-1)/8. Plug that back into the original equation (x=acos(y) = 0.93592945566133), and get 1.7601725930461. Looking back at the peak-amplitude graph, it appears that the peak varies as approximately 1.76+0.24*abs(sin(initial-phase)). If we graph the peak location, we see that it is moving (nearly) linearly with the initial-phase from the 0.9359 business given above to the corresponding peak location when the initial-phase is pi (acos((1-sqrt(33))/8) = 2.20566), then the two peaks cross, and the other one predominates from pi to 2*pi. So the peak amplitude as a function of the initial-phase ("phi" below) is (very nearly):

(let ((a (acos (/ (- (sqrt 33) 1) 8)))
      (b (acos (/ (- 1 (sqrt 33)) 8))))
  (let ((ax (+ b (* (- phi pi) (/ (- a b) pi))))      ; same for peak 2 (the peak when phi is pi..2pi)
        (bx (let ((ap (- (* 2 pi) a))                 ; start location of peak 1 (the peak when phi is 0..pi)
	          (bp (- (* 2 pi) b)))                ; end location of peak 1 
              (+ ap (* phi (/ (- bp ap) pi))))))      ; the 2 peaks move in opposite directions
    (max (abs (+ (sin ax) (sin (+ (* 2 ax) phi))))    ; plug in the 2 locations and
         (abs (+ (sin bx) (sin (+ (* 2 bx) phi))))))) ;   return the max 

We can reduce the peak difference below .00000002 by using:

  (let ((waver (+ (* .002565 (sin (* 2 phi)))
                   (* .0003645 (sin (* 4 phi)))
                   (* .0001 (sin (* 6 phi)))
                   (* .00004 (sin (* 8 phi)))
                   (* .00002 (sin (* 10 phi)))
                   (* .00001 (sin (* 12 phi)))
                   (* .0000035 (sin (* 14 phi))))))
    (let ((ax (- (+ b (* (- phi pi) (/ (- a b) pi))) waver))
          (bx (- (+ ap (* phi (/ (- bp ap) pi))) waver)))
      ...))

Similarly sin(x)+sin(3x) differentiated is cos(x)+3cos(3x) = 12cos^3(x)-8cos(x). cos(x)=0 is a minimum of the original, but the other case is acos(sqrt(2/3)) = 0.61547970867039, and plugging that into the original gives 1.539600717839. If we vary the sin(3x) term's initial phase, we get approximately 1.5396 + 0.4604 * sin(initial-phase). As before, the location of the peak varies nearly linearly with the initial-phase, the end point now being acos(-(sqrt(2/3))):

  (let* ((a (acos (sqrt 2/3)))
	 (b (acos (- (sqrt 2/3))))
	 (ax (let ((ap (- (* 2 pi) a))                    ; start loc peak 1
	           (bp (- (* 2 pi) b)))                   ; end loc
               (+ ap (* phi (/ (- bp ap) 2 pi)))))        ; peak 1 
	 (bx (- ax pi)))                                  ; peak 2 (the two interleave)
    (max (abs (+ (sin ax) (sin (+ (* 3 ax) phi))))        ; plug in our 2 peak locations
	 (abs (+ (sin bx) (sin (+ (* 3 bx) phi))))))      ;   and return the max

sin(x)+sin(5x+a) becomes a quadratic in cos^2(x), so we can find the peak location as a function of the initial-phase:

  (let* ((a0 (* pi 1/2))
	 (a1 (acos (sqrt (/ (- 25 (sqrt 145)) 40))))
	 (ax (+ a0 (/ (* (- a1 a0) phi) pi)))
	 (bx (+ pi ax))
	 (cx (- a0 (/ (* (- a1 a0) (- (* 2 pi) phi)) pi)))
	 (dx (+ cx pi)))
   (max (abs (+ (sin ax) (sin (+ (* 5 ax) phi))))
	(abs (+ (sin bx) (sin (+ (* 5 bx) phi))))
	(abs (+ (sin cx) (sin (+ (* 5 cx) phi))))
	(abs (+ (sin dx) (sin (+ (* 5 dx) phi))))))

but now we have four peaks to track. The minimum peak is at initial-phase of pi, and is 1.81571610422. sin(x)+sin(4x+a) is much messier to handle in this manner when a=0 because it ends up in a quartic equation in cos(x). A glance at the derivative, cos(x)+4*cos(4x+a), shows there is a 0 at (x=0, a=acos(-1/4)), (x=pi, a=acos(1/4)), (x=pi/2, a=pi/2) and so on, but these points do not seem to be at maxima of the original. A brute force search finds that the minimum peak (which is at initial-phase of 0) is at 1.940859829001 and is 1.9282082241513. We could also use poly-roots in poly.scm:

  > (map (lambda (y) 
          (+ (sin y) (sin (* 4 y)))) 
        (map acos (poly-roots (float-vector 4 1 -32 0 32)))) ; 4 + cos(x) - 32cos^2(x) + 32cos^4(x)
  (... 1.928208224151313892413267491649096952858E0 ...)

I think in the sin(x)+sin(nx+a) case there's a minimum at a=pi, except when n=4k+3, and the peak itself (at either pi/2 or 3pi/2) approaches 2 as n increases. sin-nx-peak in numerics.scm searches for this peak, and for reasonable "n" it can be compared to the equivalent search using poly-roots in poly.scm:

  > (sin-nx-peak 6)
  (1.966832009581999989057660894590273760791E0 ...)
  (map (lambda (y) 
         (+ (sin y) (sin (* 6 y)))) 
       (map acos (poly-roots (float-vector -6 1 108 0 -288 0 192)))) ; n*Tn + cos(x)
  (1.966832009581999989057661205729776550611E0 ...)

Another case that is not too hard involves a sum of n sines all at 0 initial phase. This can be expressed as:

sum of sines

which is the nsin generator in clm. Since the waveform is a two-sided pulse with the first local maximum at the peak, we can easily search for that peak as n increases. We find that it is approaching (3*pi)/(4*n), and if we plug that into the original equation, we get that the peak amplitude approaches 8*n*(sin^2(3*pi/8))/(3*pi), about 0.7245 * n (using the right hand expression above, set x to (3*pi)/(4*n), let n be large, so (n+1)/n approaches 1 and sin(y) is close to y if it is very small). A sum of n odd harmonics behaves similarly (the peak comes half as far from the zero crossing, but has the same max). A sum of n sines of alternating sign also has the same peak amp, but now the peak is at pi-(3*pi)/(4*n). Those are the easy cases. The next case involves 3 harmonics, where we vary the second and third harmonic's initial phase, looking for the minimum peak amplitude. One view of this terrain has the second harmonic's initial phase on the Y axis, the third's on the X axis, and the color for the height of the corresponding peak:

3 harmonics in 3D
3 harmonics
4 harmonics in 3D
4 harmonics

I tilted the graph slightly to try to show how the colors match the peaks. If we set the second component's phase to (a+pi)/2 where "a" is the third one's initial phase, we travel along the minimum going diagonally through the middle of the graph (I think the graph got truncated slightly: the top should match the bottom). The graph on the right is an attempt to show the 4-dimensional 4-harmonic case by stacking 3-D slices. I forgot to "invert" the colors, so red in the n=4 case matches a minimum (blue in the n=3 case); I should redo these graphs! A different way to view these graphs that can be applied to any number of dimensions (until we run out of disk space and patience), is to move through the possibilities in much the way you'd count to 100; before each 10's digit increments, you'd count diligently through all the 1's. Similarly, in the next set of graphs, we go from 0 to 2pi completely on one component before incrementing the next lower component. So, we get the second component moving slowly from 0 to 2pi, and, in the n=3 case, at each step it takes, it waits until the third component has gone from 0 to 2pi. This way we get all possible initial phases graphed in a normal 2D picture. Here is the n=3 case. The top level looks a bit like the n=2 case, but zooming in shows more complexity (each graph on the right is the selected portion of the one on its left). (The complexity in this case is mostly due to the slice-at-a-time approach).

n=3 case

We're trying to pinpoint the minima (there appear to be 4 black areas in the 3D graph, corresponding (I hope!) to the four minima in the second graph). A multiprecision search finds these values:

 1.9798054823226 #(0.0 0.58972511242747172044431636095396243035  0.3166675693251937984129540382127743214) 
 1.9798054823226 #(0.0 1.58972511242745917492413809668505564332  0.3166675693251493894919690319511573761)
 1.9798054823222 #(0.0 0.41027488757208596670267297668033279478  1.68333243067326587816268101960304193198)  
 1.9798054823222 #(0.0 1.41027488757208596670267297668033279478  1.68333243067326587816268101960304193198)

which shows that the minima are essentially at (23/39 19/60), (16/39 101/60), (1 + 23/39, 19/60), and (1 + 16/39, 101/60), all numbers multiplied by pi of course. (Our labor was mostly wasted; once we find one such point, the symmetries of the sinusoids hand us the other three for free. See find-other-mins in peak-phases.scm).

Here is n=4 graphed in the same way:

n=4 case

One of those minima might be the one we found near 2.04. n=5:

n=5 case

Here is the corresponding 3-D graph of the 5 harmonic case. It is trying to show 8 4-D slices through the 5-D landscape, each 4-D case being 8 3-D slices as before (I forgot to invert the colors here also, so despite appearances, blue is a maximum, and we're looking for the reddest point, the global minimum):

all

n=6 is even more beautiful and complex, but the graph is too large to include here.

;;; use mix and with-temp-sound to create a draggable mix object for each component.

(load "peak-phases.scm")
(set! *with-mix-tags* #t)     ; drag the tag to change the harmonic's initial phase
(set! *show-mix-waveforms* #f)
(set! *with-inset-graph* #f)

(define (show choice n)                           ; (show :all 14) for example
  (definstrument (sine-wave start dur freq phase) ; make one harmonic
    (let* ((beg (seconds->samples start))
	   (end (+ beg (seconds->samples dur)))
	   (osc (make-oscil freq phase)))
     (do ((i beg (+ i 1))) 
	 ((= i end))
       (outa i (oscil osc)))))

  (if (null? (sounds))
      (new-sound))
  (let ((phases (cadr (get-best choice n))))
    (do ((i 0 (+ i 1)))
	((= i n))
      (let* ((freq (case choice
		     ((:all) (+ i 1))
		     ((:even) (max (* 2 i) 1))
		     ((:odd) (+ (* 2 i) 1))
		     ((:prime) (primes i))))
	     (snd (with-temp-sound (:ignore-output #t :clipped #f)
		   (sine-wave 0 2 (* 10 freq) (* pi (phases i))))))
	(let ((mx (car (mix snd 400)))) ; give some space after the axis
	  (set! (mix-tag-y mx) (+ 10 (* 40 i)))))))
  (let ((mx (+ 2.0 (maxamp))))
    (set! (y-bounds) (list (- mx) mx)))
  (set! (x-bounds) (list 0.0 0.2)))

(hook-push mix-drag-hook ; report the current maxamp as we drag a component
  (lambda (hook)
    (let ((beg 0)
	  (end (framples))
	  (mx 0.0))
      (for-each
       (lambda (sine)
	 (set! beg (max beg (mix-position sine)))
	 (set! end (min end (+ (mix-position sine) (mix-length sine)))))
       (caar (mixes)))
      (let ((rd (make-sampler beg)))
	(do ((i beg (+ i 1)))
	    ((> i end))
	  (set! mx (max mx (abs (rd))))))
      (status-report (format #f "maxamp: ~A" mx)))))

It's curious that the "min-peak-amplitude versus n" graphs look continuous; what happens to the minima as we slowly add the next higher harmonic? In the n=2 case, each minimum splits in two, then smoothly moves to its next minimum location (where the third harmonic has amplitude 1.0). Here's a graph of the moving minima, showing also the resultant peak amplitude:

moving minima moving minima

In the first graph, each dot is at the phase location of the minimum peak amplitude as the third harmonic is increased in amplitude by 0.025. The turning points are just before the third harmonic reaches an amplitude of 0.5. The n=2 minima are at (0, 0), (red and green, with the green x=2 rather than 0), and (0, 1), (black and blue). Each splits and wanders eventually to the n=3 global minima at (0.41 1.68), (1.41, 1.68), (1.59, 0.32), and (0.59, 0.32). Each of the n=2 global minima ends up at 2 of the 4 n=3 global minima! How lucky can we be? If this worked in general, we could use it to speed up our search by following a minimum of n harmonics as it meanders to a minimum of n+1 harmonics:

;; this starts at the current min and marches to an n+1 min
(do ((n 3)
     (phases (vector 0.0 0.0 1.0)))
     (x 0.1 (+ x .1)))
    ((>= x 1.0))
  (let ((p (fpsap x 0 n 1000 0.1 50 #f #t phases))) ; args may change without warning.
    (format () ";~A: ~A~%" x p)
    (do ((k 0 (+ k 1)))
        ((= k n))
      (set! (phases k) (modulo (p k) 2.0)))))

Since we can restrict our search to 0.1 (maybe less) in each direction (rather than 2.0), we get a reduction of 20^n in the size of the space we are searching. But, as usual, there's a problem. The search works for n=2 -> 3 -> 4 -> 5, but going from 5 to 6, I seem to fall into a non-optimal path.

The other short-cut that immediately comes to mind is to look for the zeros of the derivative, then plug those into the original to get the maxima. But it is just as hard to find those zeros as to find the peaks of the original. Or we could minimize the length of the curve. In the 57 harmonics case, for example, the cosine version (peak=57.0) has a length of 485.45, whereas the minimized peak version (peak=7.547) has a length of 909.52. But this also doesn't save us any time over the original search.

So we're resigned to a laborious search. The first thing we need is a fast way to produce a sum of sinusoids. Up to n=25 or 30, the Chebyshev polynomials are just as fast as an inverse FFT, but why stop at 30! Since we'll be doing an inverse FFT for every test case, we need to make the FFT size as small as possible while still giving a reasonably accurate peak (say within 0.001 of the true peak). According to N Higham in "Accuracy and Stability of Numerical Algorithms", the FFT is stable and very accurate. He has a graph showing accumulated numerical errors down in the 10^-15 range! But that is not where the inverse FFT loses. We get n points back from an n-point FFT, so effectively we're sampling the resultant waveform at those n points. This subsampling can easily miss the peak. Here are the errors for inverse FFT's of various sizes for the 8 and 128 all harmonics case (all initial phases = 0.0, multiply "mult" by the number of harmonics to get the FFT size):

              8 harmonics                     128 harmonics    
                 
mult    reported peak    error           reported peak     error

2        5.02733      1.11686e0           81.48324      11.62779
4        5.57658      5.67621e-1          81.98630      11.12473
8        6.10774      3.64636e-2          93.08931      0.021721
16       6.10774      3.64636e-2          93.08931      0.021721
32       6.14247      1.72736e-3          93.08931      0.021721
64       6.14247      1.72736e-3          93.08931      0.021721
128      6.14391      2.87163e-4          93.10728      0.003753
256      6.14405      1.50636e-4          93.10980      0.001232
512      6.14420      5.36694e-6          93.11143      0.000391
1024     6.14420      5.36694e-6          93.11143      0.000391
2048     6.14420      1.59697e-6          93.11156      0.000525
4096     6.14420      1.44227e-7          93.11156      0.000525
8192     6.14420      3.60112e-8          93.11156      0.000526

128 seems pretty good. Those are spikey cases. If we try the best minimum-peak case, the errors are much smaller. Here are graphs of both the 0.0 phase and minimum phase cases for 8 harmonics:

8 case 8 case

Ok, we have a fast way to make test cases. Off we go... When I started this search more than two years ago, I had no idea what a long and winding path I was headed down! My initial guess was that I could find minimum peaks close to the square root of n. This was based on nothing more than the breezy idea that the initial phases give you enough freedom that you're approaching the behavior of a sum of n random signals. I thought these minima could not be very hard to find; simply use a brute force grid. The first such grid used initial phases of 0 and pi, and I actually ran every possible such case, up to n=45 or so. Since each harmonic can be positive or negative, this is 2^44 cases to check, which is starting to be a pain. The results were discouraging; I did not get close to the square root of n. I also tried smaller grids (down to pi/32) for small n (say n < 8), without any success.

Next great idea: try random initial phases. This actually works better than it has any right to, but again the results are disappointing. You can run random phases until hell freezes over and only get to n^.6 or slightly less. And the long term trend of this process can dampen one's optimism. Here's a graph where we've taken 100 stabs at each case of N harmonics, N from 3 to 2000, using randomly chosen initial phases, and tracked the minimum, maximum, and average peaks. The graph is logarithmic (that is we show (log minimum N) and so on):

average peaks from n=3 to 2000

The trend continues upwards as N goes to 100000; it probably approaches 1 in the limit. But we can always do better than n^.6 by using phases 0.0938*i*i - 0.35*i where "i" is the harmonic number counting from 0. This little formula gives results as low as n^.541 (at n=2076), and it is always below n^.6 if n is large enough. Lots of such formulas get us down to n^.53 or, as n gets larger, .52: in the all harmonic case, if n=65536, 4.5547029e-05*i*i + 0.640075398*i gives a peak of 309.9, and -4.697190e-05*i*i + 1.357080536*i peaks at 303.6 (n^.515). If n=131072, 2.09440276*i*i + 1.4462367*i peaks at 438.7 (n^.516).

A good quadratic for each n, in the all harmonics case, is (pi/n)*i*i - i*pi/2. Except for the pi/n term, the rest just marches through the quadrants in order, so this is the same as (pi*(((mod(i,4)/2)+(i*i/n)))). This is similar to the formula suggested by M. Schroeder, but the addition of the "mod(i,4)/2" term improves its performance. If N=100, for example, Schroeder's peak is 13.49, whereas the mod peak is 11.90. There are better choices of quadrant than mod(i,4); if N=14, the mod(i,4) formula gives a peak of 4.89 (Schroeder's formula's peak is 5.1), but an exhaustive search of all quadrant choices finds #(0 0 0 1 3 3 0 1 2 3 1 3 2 3) with a peak of 4.28. Since the search involves approximately 4^n FFTs, there's not much hope of going above N=20 or thereabouts. I can't see any pattern in the lists of ideal quadrants.

The corresponding even harmonics version is (-pi/n)*(i+1)*(i+1) - (i+1)*pi/2. These sorts of formulas do better as n increases, but I don't think they reach n^.5. If n=4000000, the peak is at 2408.9 (n^.512). A linear equation in "i" here is simply phase offset in the sum of sines formula mentioned earlier, so given an initial phase of x*i, as x goes from 0 to pi/2, the peak goes from .7245*n to n. Another good variant is (pi*i*i)/n using cos rather than sin.

I haven't found any functions that get all the way to the square root. In the next graph, the y axis is the peak value with n=100, the x axis is the number of tests, and we've sorted the tests by peak. Each test is centered around a known excellent minimum peak, and the separate curves are showing the peaks when the initial phases can vary around that best value by pi/4, then pi/8 etc. It's hard to read at first, but take the black top curve. This is what you'd get if you randomly sampled a hypercube whose side length is pi/2 centered on that minimum. Nearly all the values are between 18 (100^.63) and 23 (100^.68). Each successive curve divides the space we sample by 2 in all 100 dimensions, so by the time we get to the bottom curve, we've reduced our search space by a factor of 2^800 (we're down to .006 on a side), and we still don't see the actual minimum even once in 50000 tries! Imagine trying to set up a grid to catch this point.

histogram of 100 reduced 8 times

What to do? There are a bunch of papers on this subject, but the best I found was: Horner and Beauchamp, "a genetic algorithm-based method for synthesis of low peak amplitude signals", J. Acoustic. Soc. Am Vol 99 No 1 Jan 96, online at ems.music.uiuc.edu/beaucham/papers/JASA.01.96.pdf, They report good results using the genetic algorithm, so it tried it. I started with 2000 randomly chosen initial points and a search radius of 1.0 (= pi). These are pretty good choices, but after a few months of searching, I reached a point of almost no returns. I tried variants of the basic algorithm and other search methods, but the results were not very good until I noticed that in the graphs of the peaks, the good values are more or less clustered together. So I tried centering the genetic search on the best phases I had found to that point, then repeating the search each time from the new best point, slowly reducing the search radius ("simulated annealing" is the jargon for this).

(define (iterated-peak choice n)
  (let ((phases (make-vector n 0.0))
	(cur-best n)
	(cur-incr 1.0))
    (do ((i 1 (+ i 1)))
	((= i n))
      (set! (phases i) (random 1.0)))
    (do ()
	((< cur-incr .001))
      (let ((vals (fpsap (case choice ((:all) 0) ((:odd) 1) ((:even) 2) (else 3))
                         n phases 5000 cur-incr)))
	(let ((pk (car vals))
	      (new-phases (cadr vals)))
	  (let ((down (- cur-best pk)))
	    (if (< down (/ cur-best 10))
		(set! cur-incr (* 0.5 cur-incr))))
	  (if (< pk cur-best)
	      (begin
		(set! cur-best pk)
		(set! phases (float-vector->vector new-phases)))))))
    (list cur-best phases)))

The "fpsap" function is the genetic algorithm mentioned earlier, written in C. Here is the GA code used to find the initial-phase polynomials mentioned above:

(define (piterate choice n)   ; (piterate :all 4096)
  (let* ((size 1000)
	 (pop (make-vector size))
	 (phases (make-vector n 0.0)))
    ;; initialize our set of choices		   
    (do ((i 0 (+ i 1)))
	((= i size))
      (let ((f1 (random 1.0)) ; or (- 1.0 (random 2.0)) and also below
	    (f2 (random 1.0)))
	(do ((k 0 (+ k 1)))
	    ((= k n))
	  (set! (phases k) (modulo (/ (* k (+ (* f2 k) f1)) pi) 2.0)))
	(set! (pop i) (list (get-peak choice n phases) f1 f2))))
    ;; now do the GA search with annealing
    (do ((try 0 (+ try 1))
	 (increment .3 (* increment .98)))
	((= try 1000))
      (set! pop (sort! pop (lambda (a b) (< (car a) (car b)))))
      (format () "~A ~D ~A ~A~%" choice n (pop 0) (log (car (pop 0)) n))
      (do ((i 0 (+ i 1))
	   (j (/ size 2) (+ j 1)))
	  ((= i (/ size 2)))
      (let ((f1 (+ (list-ref (pop i) 1) (random increment)))
	    (f2 (+ (list-ref (pop i) 2) (random increment))))
	(do ((k 0 (+ k 1)))
	    ((= k n))
	  (set! (phases k) (modulo (/ (* k (+ (* f2 k) f1)) pi) 2.0)))
	(set! (pop j) (list (get-peak choice n phases) f1 f2)))))))

Here are the results I have so far. In each set, the first number is the number of harmonics, then the minimum peak amplitude, then (log peak n).

=============================================================================================
        all                      odd                      even                    prime
=============================================================================================

20    4.288    0.4860  | 11    3.177   0.4820   | 120  11.314  0.5067   | 24    5.642   0.5444
14    3.612    0.4867  | 9     2.886   0.4824   | 115  11.111  0.5075   | 127   14.038  0.5453
23    4.604    0.4870  | 17    3.926   0.4827   | 88   9.718   0.5079   | 101   12.400  0.5455
11    3.218    0.4874  | 10    3.053   0.4848   | 113  11.042  0.5080   | 102   12.509  0.5463
17    3.980    0.4876  | 19    4.172   0.4851   | 114  11.098  0.5082   | 73    10.425  0.5464
16    3.874    0.4884  | 14    3.598   0.4852   | 111  10.962  0.5084   | 19    4.999   0.5465
24    4.728    0.4888  | 13    3.475   0.4856   | 126  11.703  0.5086   | 106   12.794  0.5466
19    4.218    0.4889  | 18    4.070   0.4856   | 124  11.614  0.5087   | 18    4.855   0.5467
22    4.540    0.4894  | 21    4.399   0.4866   | 122  11.530  0.5089   | 25    5.811   0.5467
21    4.443    0.4898  | 16    3.857   0.4869   | 117  11.296  0.5091   | 110   13.074  0.5469
15    3.768    0.4899  | 20    4.300   0.4869   | 123  11.589  0.5091   | 93    11.931  0.5470
25    4.853    0.4907  | 15    3.738   0.4869   | 128  11.830  0.5092   | 108   12.950  0.5470
13    3.524    0.4911  | 12    3.362   0.4879   | 102  10.539  0.5092   | 109   13.018  0.5470
12    3.389    0.4911  | 22    4.519   0.4880   | 121  11.499  0.5092   | 28    6.191   0.5471
18    4.140    0.4915  | 28    5.089   0.4883   | 127  11.790  0.5093   | 40    7.527   0.5472
10    3.102    0.4917  | 23    4.634   0.4891   | 125  11.697  0.5094   | 95    12.086  0.5472
29    5.241    0.4920  | 25    4.834   0.4895   | 99   10.388  0.5094   | 92    11.878  0.5473
27    5.064    0.4922  | 31    5.419   0.4921   | 100  10.442  0.5094   | 23    5.562   0.5473
28    5.157    0.4923  | 24    4.783   0.4925   | 93   10.066  0.5094   | 126   14.116  0.5474
37    5.918    0.4924  | 33    5.597   0.4925   | 104  10.656  0.5095   | 128   14.240  0.5474
35    5.762    0.4926  | 29    5.257   0.4929   | 94   10.123  0.5095   | 77    10.787  0.5475
26    4.982    0.4929  | 30    5.353   0.4932   | 116  11.278  0.5097   | 22    5.434   0.5476
33    5.608    0.4931  | 27    5.085   0.4935   | 96   10.241  0.5097   | 120   13.757  0.5476
59    7.469    0.4931  | 8     2.791   0.4935   | 105  10.724  0.5098   | 86    11.464  0.5476
32    5.526    0.4932  | 26    4.997   0.4938   | 256  16.896  0.5098   | 17    4.719   0.5476
30    5.361    0.4937  | 37    5.959   0.4943   | 83   9.527   0.5101   | 94    12.040  0.5477
51    6.972    0.4939  | 35    5.801   0.4945   | 103  10.638  0.5101   | 103   12.673  0.5479
31    5.453    0.4939  | 7     2.618   0.4946   | 108  10.902  0.5102   | 122   13.907  0.5479
36    5.872    0.4940  | 32    5.554   0.4947   | 70   8.738   0.5102   | 47    8.247   0.5480
9     2.962    0.4941  | 34    5.726   0.4948   | 109  10.958  0.5103   | 96    12.200  0.5480
8     2.795    0.4942  | 52    7.080   0.4954   | 119  11.463  0.5104   | 30    6.452   0.5481
34    5.715    0.4943  | 50    6.947   0.4955   | 97   10.332  0.5105   | 125   14.110  0.5482
70    8.177    0.4946  | 38    6.071   0.4958   | 106  10.813  0.5105   | 123   13.986  0.5482
39    6.124    0.4946  | 82    8.895   0.4960   | 112  11.128  0.5106   | 115   13.481  0.5482
93    9.413    0.4947  | 48    6.828   0.4962   | 107  10.872  0.5106   | 39    7.452   0.5482
41    6.278    0.4947  | 41    6.322   0.4966   | 118  11.430  0.5107   | 63    9.693   0.5482
82    8.850    0.4948  | 43    6.474   0.4966   | 1024 34.487  0.5108   | 89    11.717  0.5483
81    8.797    0.4948  | 39    6.168   0.4966   | 82   9.497   0.5108   | 119   13.746  0.5484
60    7.589    0.4950  | 72    8.366   0.4967   | 84   9.615   0.5108   | 97    12.294  0.5485
38    6.056    0.4951  | 45    6.625   0.4967   | 85   9.676   0.5109   | 121   13.881  0.5485
69    8.140    0.4952  | 42    6.403   0.4968   | 101  10.574  0.5110   | 87    11.587  0.5486
49    6.872    0.4952  | 74    8.488   0.4969   | 92   10.094  0.5113   | 100   12.508  0.5486
73    8.372    0.4952  | 78    8.715   0.4970   | 110  11.060  0.5113   | 98    12.373  0.5486
58    7.471    0.4953  | 46    6.709   0.4972   | 91   10.043  0.5114   | 113   13.379  0.5486
48    6.804    0.4953  | 105   10.116  0.4972   | 86   9.758   0.5114   | 107   12.988  0.5487
103   9.936    0.4954  | 47    6.785   0.4973   | 95   10.269  0.5115   | 79    11.000  0.5488
64    7.850    0.4955  | 40    6.265   0.4974   | 79   9.357   0.5118   | 51    8.652   0.5488
56    7.349    0.4955  | 89    9.332   0.4976   | 90   10.005  0.5118   | 76    10.773  0.5489
42    6.374    0.4956  | 111   10.417  0.4976   | 87   9.834   0.5118   | 56    9.112   0.5489
63    7.793    0.4956  | 56    7.419   0.4979   | 81   9.484   0.5119   | 114   13.476  0.5492
83    8.935    0.4956  | 36    5.956   0.4979   | 75   9.122   0.5120   | 124   14.115  0.5492
40    6.224    0.4956  | 106   10.198  0.4980   | 71   8.871   0.5121   | 116   13.609  0.5492
85    9.050    0.4958  | 59    7.618   0.4980   | 73   8.999   0.5121   | 104   12.817  0.5492
67    8.044    0.4959  | 57    7.489   0.4980   | 98   10.469  0.5122   | 20    5.183   0.5492
76    8.567    0.4960  | 91    9.457   0.4981   | 77   9.259   0.5124   | 21    5.324   0.5492
92    9.420    0.4960  | 51    7.088   0.4981   | 78   9.328   0.5126   | 117   13.680  0.5493
75    8.512    0.4960  | 80    8.870   0.4981   | 80   9.451   0.5126   | 57    9.217   0.5493
55    7.300    0.4961  | 81    8.926   0.4981   | 61   8.227   0.5126   | 74    10.646  0.5495
53    7.168    0.4961  | 101   9.965   0.4982   | 74   9.090   0.5128   | 111   13.304  0.5495
105   10.064   0.4961  | 119   10.815  0.4982   | 512  24.510  0.5128   | 71    10.407  0.5495
52    7.102    0.4961  | 77    8.707   0.4982   | 72   8.966   0.5129   | 75    10.729  0.5496
104   10.017   0.4962  | 76    8.651   0.4982   | 89   9.997   0.5129   | 29    6.365   0.5496
50    6.966    0.4962  | 62    7.817   0.4982   | 57   7.966   0.5133   | 72    10.497  0.5497
65    7.935    0.4962  | 55    7.364   0.4982   | 68   8.738   0.5137   | 105   12.917  0.5497
71    8.291    0.4962  | 67    8.128   0.4983   | 67   8.681   0.5140   | 80    11.125  0.5498
47    6.757    0.4962  | 110   10.408  0.4984   | 63   8.411   0.5140   | 78    10.979  0.5499
45    6.613    0.4962  | 90    9.422   0.4985   | 76   9.267   0.5141   | 59    9.418   0.5500
100   9.828    0.4962  | 60    7.700   0.4985   | 64   8.488   0.5143   | 38    7.396   0.5501
74    8.468    0.4964  | 86    9.213   0.4985   | 66   8.632   0.5145   | 61    9.597   0.5501
44    6.544    0.4964  | 108   10.325  0.4986   | 65   8.567   0.5146   | 37    7.291   0.5502
57    7.441    0.4964  | 44    6.599   0.4986   | 51   7.569   0.5148   | 33    6.846   0.5502
46    6.691    0.4965  | 88    9.324   0.4986   | 58   8.101   0.5152   | 31    6.616   0.5502
54    7.246    0.4965  | 64    7.957   0.4987   | 69   8.861   0.5153   | 118   13.810  0.5503
84    9.023    0.4965  | 83    9.061   0.4988   | 62   8.387   0.5153   | 41    7.719   0.5503
94    9.544    0.4965  | 68    8.204   0.4988   | 2048 50.887  0.5154   | 27    6.134   0.5503
95    9.595    0.4966  | 71    8.384   0.4988   | 55   7.888   0.5154   | 70    10.362  0.5504
43    6.475    0.4966  | 102   10.046  0.4988   | 53   7.748   0.5157   | 36    7.187   0.5504
87    9.188    0.4966  | 85    9.173   0.4989   | 44   7.039   0.5157   | 16    4.600   0.5504
66    8.012    0.4967  | 114   10.621  0.4989   | 59   8.192   0.5158   | 62    9.697   0.5505
68    8.131    0.4967  | 61    7.775   0.4989   | 56   7.975   0.5158   | 112   13.432  0.5505
88    9.243    0.4967  | 125   11.122  0.4989   | 47   7.290   0.5159   | 66    10.041  0.5506
72    8.368    0.4967  | 70    8.328   0.4989   | 38   6.536   0.5161   | 90    11.912  0.5506
114   10.518   0.4968  | 75    8.621   0.4989   | 54   7.843   0.5163   | 60    9.529   0.5506
77    8.656    0.4969  | 98    9.853   0.4990   | 50   7.547   0.5167   | 54    8.996   0.5507
86    9.145    0.4969  | 63    7.904   0.4990   | 60   8.295   0.5167   | 83    11.400  0.5507
79    8.767    0.4969  | 107   10.296  0.4990   | 48   7.392   0.5168   | 43    7.936   0.5507
91    9.407    0.4969  | 103   10.102  0.4990   | 52   7.705   0.5168   | 88    11.779  0.5508
78    8.713    0.4969  | 118   10.812  0.4990   | 45   7.164   0.5173   | 52    8.817   0.5509
98    9.767    0.4971  | 115   10.674  0.4990   | 40   6.748   0.5176   | 84    11.487  0.5510
80    8.832    0.4971  | 58    7.586   0.4990   | 46   7.274   0.5183   | 91    12.006  0.5510
61    7.718    0.4971  | 128   11.261  0.4990   | 42   6.940   0.5183   | 85    11.571  0.5511
89    9.316    0.4972  | 53    7.253   0.4990   | 39   6.680   0.5184   | 99    12.590  0.5512
101   9.922    0.4972  | 94    9.654   0.4991   | 34   6.223   0.5184   | 45    8.155   0.5513
90    9.369    0.4972  | 69    8.275   0.4991   | 49   7.529   0.5187   | 64    9.905   0.5514
99    9.827    0.4973  | 92    9.553   0.4991   | 43   7.052   0.5193   | 12    3.936   0.5514
97    9.734    0.4974  | 120   10.909  0.4991   | 41   6.880   0.5194   | 34    6.991   0.5515
109   10.316   0.4974  | 113   10.586  0.4991   | 36   6.432   0.5194   | 46    8.260   0.5515
62    7.792    0.4975  | 96    9.759   0.4991   | 37   6.530   0.5197   | 44    8.065   0.5517
112   10.460   0.4975  | 66    8.095   0.4992   | 32   6.061   0.5199   | 42    7.863   0.5517
106   10.180   0.4976  | 73    8.515   0.4992   | 33   6.162   0.5201   | 68    10.262  0.5518
96    9.699    0.4978  | 84    9.133   0.4992   | 29   5.766   0.5203   | 48    8.468   0.5518
102   10.000   0.4979  | 116   10.733  0.4993   | 35   6.362   0.5204   | 32    6.772   0.5519
110   10.385   0.4979  | 100   9.968   0.4993   | 26   5.452   0.5206   | 53    8.948   0.5520
116   10.667   0.4980  | 54    7.328   0.4993   | 31   5.988   0.5212   | 82    11.386  0.5520
115   10.622   0.4980  | 95    9.717   0.4993   | 24   5.253   0.5220   | 81    11.311  0.5520
107   10.251   0.4981  | 121   10.965  0.4993   | 30   5.907   0.5222   | 69    10.358  0.5521
113   10.533   0.4981  | 122   11.011  0.4993   | 23   5.148   0.5226   | 55    9.143   0.5522
128   11.210   0.4981  | 117   10.783  0.4994   | 21   4.920   0.5233   | 50    8.676   0.5523
111   10.443   0.4981  | 65    8.041   0.4994   | 27   5.620   0.5238   | 65    10.031  0.5523
122   10.950   0.4982  | 104   10.169  0.4994   | 28   5.732   0.5240   | 49    8.583   0.5524
127   11.176   0.4983  | 79    8.865   0.4994   | 25   5.403   0.5241   | 58    9.425   0.5525
108   10.313   0.4984  | 109   10.414  0.4995   | 22   5.055   0.5242   | 15    4.466   0.5526
117   10.740   0.4985  | 49    6.986   0.4995   | 18   4.569   0.5257   | 26    6.056   0.5528
126   11.145   0.4985  | 99    9.928   0.4995   | 20   4.839   0.5264   | 35    7.164   0.5538
120   10.878   0.4985  | 97    9.832   0.4996   | 17   4.463   0.5280   | 67    10.266  0.5539
121   10.925   0.4986  | 93    9.629   0.4997   | 16   4.325   0.5282   | 11    3.778   0.5544
118   10.790   0.4986  | 124   11.120  0.4997   | 19   4.741   0.5286   | 9     3.382   0.5546
124   11.060   0.4986  | 87    9.317   0.4998   | 15   4.192   0.5292   | 14    4.324   0.5548
119   10.836   0.4986  | 126   11.217  0.4999   | 14   4.097   0.5344   | 13    4.154   0.5553
123   11.016   0.4986  | 123   11.088  0.4999   | 12   3.787   0.5359   | 10    3.602   0.5565
125   11.105   0.4986  | 127   11.268  0.5000   | 13   3.973   0.5378   | 5     2.477   0.5635
7     2.639    0.4988  | 112   10.582  0.5000   | 11   3.656   0.5406   | 4     2.192   0.5662
256   15.997   0.5000  | 256   16.243  0.5027   | 10   3.559   0.5513   | 8     3.263   0.5687
512   23.194   0.5040  | 3     1.739   0.5035   | 8    3.198   0.5590   | 256   23.955  0.5728
1024  33.039   0.5046  | 512   23.517  0.5062   | 9    3.454   0.5641   | 7     3.062   0.5750
2048  48.913   0.5102  | 1024  33.732  0.5076   | 7    3.047   0.5726   | 6     2.805   0.5757
4     2.039    0.5139  | 2048  48.227  0.5083   | 6    2.837   0.5820   | 512   38.603  0.5856
6     2.549    0.5223  | 4     2.045   0.5161   | 5    2.605   0.5948   | 2048  95.904  0.5985
5     2.343    0.5292  | 6     2.523   0.5164   | 3    2.021   0.6406   | 1024  65.349  0.6030
3     1.980    0.6217  | 5     2.307   0.5195   | 4    2.431   0.6406   | 3     1.980   0.6217
2     1.760    0.8156  | 2     1.539   0.6220   | 2    1.760   0.8157   | 2     1.760   0.8156

Here is a graph of the peaks (as of February, 2015), followed by a graph of the exponent vs n (n^y = peak amp).

sqrt n n^y

The "even" cases are not independent of the "all" cases; each even-harmonics case can be at worst 1.0 above the corresponding (n-1) all-harmonics case (shift the current "all" choices right to multiply each by 2, then set the new fundamental phase to 0.0). If you then search around this set of phases, you'll find very good values. Using Snd's fpsap (a version of the genetic algorithm):

(let ((all (cadr (get-best :all (- n 1)))) ; get the best all-harmonic phases for n - 1
      (new-phases (make-vector n 0.0)))    ; place in new phase vector shifted up
  (do ((k 0 (+ k 1)))
      ((= k (- n 1)))
    (set! (new-phases (+ k 1)) (all k)))
  (set! (new-phases 0) 0.0)
  (fpsap 2 n new-phases))                   ; search that vicinity for a good set (2 = even harmonics)

Here is the time domain view of one of the n=5 cases when the minimum peak phases are chosen; the sum of the 5 components is in black.

n=5 case

The next graph compares the 100 harmonic minimum peak case in blue with the case where all the initial phases are 0.0 in black:

100 harmonics

And a few others:

57 harmonics 57 odd harmonics
99 harmonics

As N increases, the minimum peak amplitude waveform can approach white noise (in sound as well as appearance); here is a small portion of one period when n=65536 (the prescaling peak was 704):

65536 harmonics

but the waveforms generated from the initial-phase polynomials look more regular (this is with n=64):

64 harmonics using pi/n formula
piano

This instrument is a translation of CLM's piano.ins, a piano physical model by Scott van Duyne; see Julius O. Smith and Scott A. Van Duyne, "Commuted piano synthesis," in Proc. Int. Computer Music Conf., Banff, Canada, September 1995, pp. 335 - 342. To paraphrase, the model includes multiple coupled strings, a nonlinear hammer, and an arbitrarily large soundboard and enclosure. The actual instrument name is 'p':

(with-sound ()
  (do ((i 0 (+ i 1))) ((= i 7))
    (p (* i .5) :duration .5                    ; generate a sequence of 1/2 second tones
                :keyNum (+ 24 (* 12 i))         ; jump by octaves
                :strike-velocity .5             ; 0 to 1, 0 is softest played note, 1 is loud note
                :amp .4		                ; overall volume level
                :DryPedalResonanceFactor .25))) ; 0 no open string resonance
				                ; 1.0 is about full resonance of dampers raised
				                ; can be greater than 1.0

"p" has lots of parameters, and I really don't know what they do. The interested reader should goof around with them.

p (start 
   (duration 1.0)
   (keyNum 60.0)              ; middleC=60: can use fractional part to detune
   (strike-velocity 0.5)      ; corresponding normalized velocities (range: 0.0--1.0)
   (pedal-down #f)	      ; set to t for sustain pedal down...pedal-down-times not yet impl.
   (release-time-margin 0.75) ; extra compute time allowed beyond duration
   (amp .5)                   ; amp scale of noise inputs...
   (detuningFactor 1.0)
   (detuningFactor-table ())
   (stiffnessFactor 1.0)
   (stiffnessFactor-table ())
   (pedalPresenceFactor .3)
   (longitudinalMode 10.5)
   (StrikePositionInvFac -0.9)
   (singleStringDecayRateFactor 1.0)
   
   ;; parameter tables indexed by keyNum
   ;; you can override the loudPole-table by directly setting :loudPole to a value

   loudPole (loudPole-table default-loudPole-table)
   softPole (softPole-table default-softPole-table)
   loudGain (loudGain-table default-loudGain-table)
   softGain (softGain-table default-softGain-table)
   strikePosition (strikePosition-table default-strikePosition-table)
   detuning2 (detuning2-table default-detuning2-table)
   detuning3 (detuning3-table default-detuning3-table)
   stiffnessCoefficient (stiffnessCoefficient-table default-stiffnessCoefficient-table)
   singleStringDecayRate (singleStringDecayRate-table default-singleStringDecayRate-table)
   singleStringZero (singleStringZero-table default-singleStringZero-table)
   singleStringPole (singleStringPole-table default-singleStringPole-table)
   releaseLoopGain (releaseLoopGain-table default-releaseLoopGain-table)
   DryTapFiltCoeft60 (DryTapFiltCoeft60-table default-DryTapFiltCoeft60-table)
   DryTapFiltCoefTarget (DryTapFiltCoefTarget-table default-DryTapFiltCoefTarget-table)
   DryTapFiltCoefCurrent (DryTapFiltCoefCurrent-table default-DryTapFiltCoefCurrent-table)
   DryTapAmpt60 (DryTapAmpt60-table default-DryTapAmpt60-table)
   sustainPedalLevel (sustainPedalLevel-table default-sustainPedalLevel-table)
   pedalResonancePole (pedalResonancePole-table default-pedalResonancePole-table)
   pedalEnvelopet60 (pedalEnvelopet60-table default-pedalEnvelopet60-table)
   soundboardCutofft60 (soundboardCutofft60-table default-soundboardCutofft60-table)
   DryPedalResonanceFactor (DryPedalResonanceFactor-table default-DryPedalResonanceFactor-table)
   unaCordaGain (unaCordaGain-table default-unaCordaGain-table))

Here is another example; there are a couple other examples at the end of piano.scm:

(with-sound ()
  (do ((i 0 (+ i 1))) ((= i 8))
    (p (* i .5) :duration .5 :keyNum (+ 24 (* 12 i)) :strike-velocity .5 :amp .4 :DryPedalResonanceFactor .25
     :detuningFactor-table '(24 5 36 7.0 48 7.5 60 12.0 72 20 84 30 96 100 108 300)
		    ; scales the above detuning values so 1.0 is nominal detuning, 
                    ;  0.0 is exactly in tune,  > 1.0 is out of tune
     :stiffnessFactor-table '(21 1.5 24 1.5 36 1.5 48 1.5 60 1.4 72 1.3 84 1.2 96 1.0 108 1.0))))
		    ; 0.0 to 1.0 is less stiff, 1.0 to 2.0 is more stiff

In Ruby:

include Piano
with_sound(:clm, false, :channels, 1) do
  7.times do |i|
    p(i * 0.5,
      :duration, 0.5,
      :keyNum, 24 + 12.0 * i,
      :strike_velocity, 0.5,
      :amp, 0.4,
      :dryPedalResonanceFactor, 0.25)
  end
end
see also:   flute   maraca   pluck   prc95   singer   strad
play

This file has a variety of "real-time" audio output examples. It is almost entirely obsolete.

play-with-amps snd :rest amps

play-with-amps plays the sound 'snd' with each channel scaled by the corresponding amp: (play-with-amps 0 1.0 0.5) plays sound 0's channel 1 at full amplitude, and channel 2 at half amplitude.

play-often n
play-until-c-g 
play-region-forever reg

play-often plays the selected sound 'n' times. play-until-c-g plays the selected sound until you interrupt it via C-g. Similarly, play-region-forever plays region 'reg' until you interrupt it with C-g.

(bind-key #\p 0 
  (lambda (n) 
    "play often" 
    (play-often (max 1 n))))

(bind-key #\r 0 
  (lambda (n) 
    "play region forever" 
    (play-region-forever n)))

Now C-u 31 p plays the current sound 31 times; C-u 3 r plays region 3 until we type C-g.

play-sine freq amp
play-sines freqs-and-amps

play-sine plays a one-second sine wave at the given frequency and amplitude: (play-sine 440 .1). play-sines produces a spectrum given a list of lists of frequency and amplitude:

(play-sines '((425 .05) (450 .01) (470 .01) (546 .02) (667 .01) (789 .034) (910 .032)))
start-dac 
stop-dac 

start-dac opens the DAC ready for sound output, and stop-dac closes it.

poly

This file contains various functions related to the CLM polynomial function. A polynomial here is a vector (for complex coefficients) holding the polynomial coefficients from lowest to highest (i.e. the constant is (v 0), x+2 is (float-vector 2 1), etc).

poly+ p1 p2              ; new poly = p1 + p2
poly* p1 p2              ; new poly = p1 * p2
poly/ p1 p2              ; (list quotient-poly remainder-poly) = p1 / p2
poly-derivative p1       ; new poly = Dp1
poly-reduce p1           ; new poly = p1 without high zeros
poly-gcd p1 p2           ; new poly = gcd(p1, p2)
poly-roots p1            ; list of roots of p1
poly-resultant p1 p2     ; resultant of p1 and p2
poly-discriminant p1     ; discriminant of p1

poly+ adds two polynomials, and poly* multiplies two polynomials. poly/ divides two polynomials, with a few restrictions, and returns a list containing the quotient and remainder polynomials. poly-derivative returns the derivative of a polynomial. In all these cases, the resultant polynomials may have extra high-degree entries whose coefficients are zero. To remove these pointless coefficients, use poly-reduce. The last functions are just for fun.

You can treat a sound as a set of polynomial coefficients; then, for example, convolution the infinitely slow way is poly*:

(float-vector->channel (poly* (channel->float-vector 0 (framples)) (float-vector 2.0))) ; no, this is not serious
see also:   polynomial  
prc95

prc95.scm is a translation to Snd of Perry Cook's (1995) physical modelling toolkit; prc-toolkit95.lisp in CLM. One starting point for physical modelling is Smith, "Music Applications of Digital Waveguides", CCRMA, Stan-M-39, 1987, or Julius's home page, or any of several classic papers also by Julius Smith. Perry's own version of this code can be found in STK. The example instruments are:

plucky beg dur freq amplitude maxa  ; plucked string
bow beg dur frq amplitude maxa      ; bowed string
brass beg dur freq amplitude maxa   
clarinet beg dur freq amplitude maxa 
flute beg dur freq amplitude maxa

(with-sound ()
  (plucky 0 .3 440 .2 1.0)
  (bow .5 .3 220 .2 1.0)
  (brass 1 .3 440 .2 1.0)
  (clarinet 1.5 .3 440 .2 1.0)
  (flute 2 .3 440 .2 1.0))
see also:   maraca: maraca.scm, maraca.rb   piano: piano.scm, piano.rb   singer: singer.scm, singer.rb   bowed string: strad.scm, strad.rb   flute: clm-ins.scm   string: vibrating-string   plucked string: pluck in clm-ins.scm
pvoc

This is the same as the CLM phase-vocoder generator, but implemented in Scheme. If you're interested in how the thing works, I think the Scheme version is easiest to understand; the Common Lisp version is in mus.lisp, and the C version is in clm.c.

make-pvocoder fftsize overlap interp analyze edit synthesize
pvocoder gen input
pvoc (fftsize 512) (overlap 4) (time 1.0) (pitch 1.0) (gate 0.0) (hoffset 0.0) (snd 0) (chn 0)

The 'analyze', 'edit', and 'synthesize' arguments to make-pvocoder are functions that are applied as needed during pvocoder processing; similarly, the 'input' argument to pvocoder can be a function.

(begin
  (open-sound "oboe.snd")
  (let ((pv (make-pvocoder 256 4 64))
        (rd (make-sampler 0)))
    (map-channel (lambda (y) (pvocoder pv rd)))))

pvoc.scm also contains a few examples of using the CLM phase-vocoder generator:

(define test-pv-4
  (lambda (gate)
    (let ((pv (make-phase-vocoder
                (let ((reader (make-sampler 0)))
                  (lambda (dir) 
                    (reader)))
		512 4 128 1.0
		#f ;no change to analysis
		(lambda (v)
		  (do ((N (length v))
		       (i 0 (+ i 1)))
		      ((= i N) #t)
		    (if (< ((phase-vocoder-amp-increments v) i) gate)
		        (set! ((phase-vocoder-amp-increments v) i) 0.0))))
	        #f))) ;no change to synthesis
      (map-channel (lambda (val)
		  (phase-vocoder pv))))))

This sets up a phase-vocoder generator whose edit function is squelching soft partials. In this case, the input function is reading the currently selected channel.

pvoc is yet another (unoptimized) phase-vocoder; it applies the phase-vocoder to the current sound; 'pitch' specifies the pitch transposition ratio, 'time' specifies the time dilation ratio, 'gate' specifies a resynthesis gate in dB (partials with amplitudes lower than the gate value will not be synthesized), 'hoffset' is a pitch offset in Hz.

(pvoc :time 2.0)
see also:   phase-vocoder   pins
rgb

rgb.scm (rgb.rb) is a translation of the standard X11 color names into Snd color objects.

(define snow (make-color 1.00 0.98 0.98))

is taken from the line

255 250 250             snow

/usr/lib/X11/rgb.txt. The choice of a float between 0.0 and 1.0 (rather than an integer between 0 and 255) mimics PostScript; as video hardware has improved over the years, there's less and less need for these elaborate color names, and less reason (except perhaps psychophysical) to limit these numbers to bytes. There is one gotcha in this file — X11 defines a color named "tan" which is already used by Scheme, so (at the suggestion of Dave Phillips) this color is named "tawny" in rgb.scm. rgb.scm exports only *rgb* which is an environment holding all the color names and values.

rubber
rubber-sound stretch-factor snd chn

rubber-sound tries to stretch or contract a sound (in time); it scans the sound looking for stable (periodic) sections, then either deletes periods or interpolates new ones to shorten or lengthen the sound. It still needs a lot of robustification. The algorithm is 1) remove all frequencies below 16 Hz, 2) resample the file to be ten times longer (interpolating samples), 3) make a list of upward zero crossings, 4) using autocorrelation decide where the next fundamental zero crossing probably is and see how much difference there is between the current period and the next, 5) check intermediate crossing weights and if the autocorrelation weight is not the smallest, throw away this crossing, 6) sort the remaining crossings by least weight, 7) interpolate or delete periods until the sound has been sufficiently lengthened or shortened. rubber-sound is incredibly slow, and almost never works. The idea seems good however...

see also:   clm-expsrc   expsrc   pvoc   ssb-bank
s7test

s7test.scm is a regression test for s7. Any additional tests are most welcome!

selection
filter-selection-and-smooth ramp-dur flt order

filter-selection-and-smooth filters the current selection with flt, then mixes it back into the original using ramp-dur to set how long the cross-fade ramps are.

(filter-selection-and-smooth .01 (float-vector .25 .5 .5 .5 .25))
make-selection beg end snd chn

make-selection makes a selection, like make-region but without creating a region. make-selection follows snd's sync field, and applies to all snd's channels if chn is not specified. end defaults to end of channel, beg defaults to 0, and snd defaults to the currently selected sound.

(make-selection 1000 2000)
replace-with-selection 

replace-with-selection replaces any data at the cursor with the current selection.

selection-members 

selection-members returns a list of lists of '(snd chn) indicating the channels participating in the current selection. It is the selection-oriented version of all-chans.

swap-selection-channels

swap-selection-channels swaps the current selection's channels.

with-temporary-selection thunk beg dur snd chn

with-temporary selection saves the current selection placement, makes a new selection of the data from sample 'beg' to beg + dur in the given channel, calls 'thunk', then restores the previous selection (if any). It returns whatever 'thunk' returned.

singer

singer.scm is an implementation of Perry Cook's physical model of the vocal tract as described in:

  Cook, Perry R. "Synthesis of the Singing Voice Using a Physically Parameterized Model of the Human Vocal Tract"
     Published in the Proceedings of the International Computer Music Conference, Ohio 1989 
     and as Stanford University Department of Music Technical Report Stan-M-57, August 1989.
 
 ---- "Identification of Control Parameters in an Articulatory Vocal Tract Model, with Applications 
    to the Synthesis of Singing," Ph.D. Thesis, Stanford University Department of Music Technical Report 
    Stan-M-68, December 1990.

 ----  "SPASM, a Real-time Vocal Tract Physical Model Controller; and Singer, the Companion Software 
    Synthesis System", Computer Music Journal, vol 17 no 1 Spring 1993.

singer.scm is a translation of Perry's singer.c. I think that Perry's code assumes a sampling rate of 22050; you'll need to fix up lots of lengths in the code to run at 44100. The singer instrument looks deceptively simple:

singer beg amp data

but all the complexity is hidden in the 'data' parameter. 'data' is a list of lists; each imbedded list has the form: '(dur shape glot pitch glotamp noiseamps vibramt). The 'shape' and 'glot' entries are themselves lists; I think the 'glot' list describes the glottal pulse. I wish I could fully explain all these lists, but I translated this code a very long time ago, and can't remember any details. You'll have to read the code, or perhaps find something in Perry's publications. In any case, here's an example:

(with-sound () 
  (singer 0 .1 (list (list .4 ehh.shp test.glt 523.0 .8 0.0 .01) 
                     (list .6 oo.shp test.glt 523.0 .7 .1 .01))))

The *.shp and *.glt data is defined at the end of singer.scm. For example:

(define test.glt (list 10 .65 .65))
(define ee.shp (list 8 1.02 1.637 1.67 1.558 0.952 0.501 0.681 0.675 0.9 -0.4 1.0 0.0 0.0 0.0 0.0 0.0 0.0))

A more complex example is singer's attempt to say "requiem":

(with-sound ()
  (singer 0 .1 (list (list .05 ehh.shp test.glt 523.0 0.8 0.0 .01) 
       (list .15 ehh.shp test.glt 523.0 0.8 0.0 .01) 
       (list .05 kkk.shp test.glt 523.0 0.0 0.0 .01) 
       (list .05 kkk.shp test.glt 523.0 0.0 0.0 .01) 
       (list .02 kk+.shp test.glt 523.0 0.0 1.0 .01) 
       (list .08 kk+.shp test.glt 523.0 0.0 0.2 .01) 
       (list .05 ooo.shp test.glt 523.0 0.8 0.0 .01) 
       (list .15 ooo.shp test.glt 523.0 0.8 0.0 .01) 
       (list .05 eee.shp test.glt 523.0 0.8 0.0 .01) 
       (list .15 eee.shp test.glt 523.0 0.8 0.0 .01) 
       (list .05 ehh.shp test.glt 523.0 0.8 0.0 .01) 
       (list .15 ehh.shp test.glt 523.0 0.8 0.0 .01) 
       (list .05 mmm.shp test.glt 523.0 0.8 0.0 .01) 
       (list .15 mmm.shp test.glt 523.0 0.8 0.0 .01) 			      
       (list .10 mmm.shp test.glt 523.0 0.0 0.0 .01))))
see also:   fofins   reson   pqw-vox   vox
snd15

This file contains several procedures that were removed from or renamed in earlier versions of Snd (in Ruby, look in extensions.rb).

snddiff

The snddiff function tries to detect how one sound differs from another.

snddiff snd0 chn0 snd1 chn1

This could use about a lifetime's work, but it does find some differences:

                 ;; start with two identical sounds:
> (map short-file-name (sounds))
("oboe.snd" "oboe.snd")
> (snddiff 0 0 1 0)
no-difference
                 ;; snddiff can find individual sample differences:
> (set! (sample 1000 0 0) 0.5)
0.5
> (snddiff 0 0 1 0)
(differences ((1000 0.5 0.0328369140625)))
                 ;; and scaling changes (we reverted the previous change):
> (scale-channel 2.0)
2.0
> (snddiff 0 0 1 0)
(scale 2.0)
                 ;; and some initial delays:
> (pad-channel 0 200 0 0)
0
> (snddiff 0 0 1 0)
(lag 200 no-difference 0.0 #f #f #f)
snd-gl

snd-gl.scm has examples of using OpenGL. To try out these functions, build Snd with GL: configure --with-gl. You can tell if your current Snd has OpenGL loaded by checking the *features* list for 'gl: (provided? 'gl).

complexify 

complexify displays FFT data in the complex plane; each bin is rotated so that they all stack along the x axis, with a line drawn from the x axis to the current real/imaginary point (as (z, y)), so as you move (slowly) through a file, you'll see the phase info as well as the magnitude — the vectors whirl around in each slice of the complex plane. Use the View:Orientation dialog to change the viewing angle. To move one sample at a time through a sound, you could bind the arrow keys:

(bind-key "Left" 0 (lambda () 
                     (set! (left-sample) (max 0 (- (left-sample) 1))) 
                     keyboard-no-action))
(bind-key "Right" 0 (lambda () 
                     (set! (left-sample) (min (framples) (+ 1 (left-sample)))) 
                     keyboard-no-action))
gl-dump-state

gl-dump-state displays much of the current GL graphics state.

gl-info 

gl-info prints out information about the current GL system setup.

see also:   glSpectrogram   OpenGL
snd-motif, snd-xm

snd-motif.scm has a variety of user-interface extensions that rely on the Motif module (xm.c). In Ruby, see snd-xm.rb.

add-amp-controls

add-amp-controls adds amplitude sliders to the control panel for multichannel sounds so that each channel gets its own amplitude control slider. To make this the default, add (add-amp-controls) to your initialization file. Here is a 4-channel control panel after adding the channel-specific amp controls.

added amp controls
add-delete-option 

add-delete-option adds a "Delete" (file) option to the File menu.

add-find-to-listener 

add-find-to-listener causes C-s and C-r in the listener to start a separate "Find" dialog.

add-mark-pane 

add-mark-pane adds a pane to each channel giving the current mark locations (sample values). These can be edited to move the mark, or deleted to delete the mark. (If you add-mark-pane to a channel having marks, you need to make some change to them to force it to be displayed). Here's a picture (it also shows with-smpte-label, with-inset-graph, and show-disk-space).

mark pane SMPTE label SMPTE label SMPTE label SMPTE label SMPTE label SMPTE label SMPTE label
add-rename-option 

add-rename-option adds a "Rename" (file) option to the File menu.

add-text-to-status-area

add-text-to-status-area puts a text widget in the notebook's status area (the lower left portion of the main Snd window when using the -notebook invocation switch). It returns the widget; you can write to it via XmTextFieldSetString.

add-tooltip widget tip

add-tooltip adds a tooltip (also known as bubble-help) to a widget. Once added, set the variable with-tooltips to #f to turn it off.

(add-tooltip (cadr (channel-widgets)) "show the time domain waveform")
disable-control-panel snd

disable-control-panel does away with the control panel.

display-widget-tree widget

display-widget-tree displays the hierarchy of widgets beneath 'widget'.

equalize-panes snd

This equalizes multichannel sound panes (tries to make them the same size), It is specific to Motif. If the 'snd' argument is given, only that sound's panes are affected.

for-each-child w func
find-child w name

for-each-child applies 'func' to the widget 'w' and to each widget in the hierarchy of widgets below it. 'func' takes one argument, the child widget. for-each-child is used by find-child which searches for a widget named 'name' belonging to 'w'.

(for-each-child 
  ((sound-widgets) 2) ; control panel
  (lambda (w) 
    (snd-print (format #f "~%~A" (XtName w)))))
install-searcher-with-colors proc

install-searcher-with-colors places our own search procedure into the filter mechanism in the File:Open dialog. This has been superseded by the file-filter mechanism now built into Snd.

(install-searcher-with-colors (lambda (file) #t))
keep-file-dialog-open-upon-ok

keep-file-dialog-open-upon-ok changes File:Open so that clicking "ok" does not unmanage (dismiss) the dialog.

load-font font-name

load-font loads a font and returns a handle for it.

  (define new-font (load-font "-*-helvetica-bold-r-*-*-14-*-*-*-*-*-*-*"))

  (define* (show-greeting (snd 0) (chn 0)) ; works only in motif currently
  ;; show a red "hi!" in the helvetica bold font on a gray background
    (let ((ls (left-sample snd chn))
	  (rs (right-sample snd chn)))
      (if (and (< ls 1000)
	       (> rs 1000))
	  (let ((pos (x->position (/ 1000.0 (srate))))
		(old-color (foreground-color)))
	    (set! (foreground-color) (make-color .75 .75 .75))
	    (fill-rectangle pos 10 50 20 snd chn time-graph #f #f)
	    (set! (foreground-color) (make-color 1 0 0))
            (if new-font (set! (current-font) new-font))
	    (draw-string "hi!" (+ pos 5) 12 snd chn time-graph #f)
	    (set! (foreground-color) old-color)))))
make-channel-drop-site snd chn
set-channel-drop drop snd chn

make-channel-drop-site shows how to add a drop site panel to a channel. set-channel-drop changes the channel's graph's drop function to 'drop', a function of 3 arguments, the dropped filename (a string) and the current sound and channel number.

make-pixmap widget strs

make-pixmap turns an XPM-style description into pixmap. Briefly an XPM pixmap description is an array of strings; the first gives the size in pixels of the pixmap, and the number of colors; the next set give characters followed by the color desired for that character; then comes the pixmap itself using those characters. The following defines a 16 X 12 arrow using 6 colors:

(define arrow-strs (list
  "16 12 6 1"
  " 	c None s None"
  ".	c gray50"
  "X	c black"
  "o	c white"
  "O	c yellow"
  "-      c ivory2 s basiccolor"
  "--------X---------"
  "---------X--------"
  "----------X-------"
  "-----------X------"
  "------------X-----"
  "XXXXXXXXXXXXXX----"
  "------------X-----"
  "-----------X------"
  "----------X-------"
  "---------X--------"
  "--------X---------"
  "-------X----------"))

(make-pixmap (cadr (main-widgets)) arrow-strs) then creates the actual pixmap. The 'widget' argument is needed to give us access to the current colormap and so on. (cadr (main-widgets)) is just Snd's outer shell, which will do the trick in most cases.

make-variable-display page-name variable-name (type 'text) (range (list 0.0 1.0))
variable-display val widget

make-variable-display sets up a display point (a dialog) for an arbitrary expression which is updated via variable-display. The latter returns its argument, so it acts as a sort of probe, picking out any arbitrary point in an instrument and displaying it as the instrument is running. Display points can be organized as pages in a notebook widget:

(define wid (make-variable-display "do-loop" "i*2" 'text))
(define wid1 (make-variable-display "do-loop" "i" 'text))
(do ((i 0 (+ i 1)))
    ((= i 10))
  (variable-display (* (variable-display i wid1) 2) wid))

The 'type' argument to make-variable-display can be one of 'text 'scale, 'graph, 'spectrum, or 'meter. It determines the kind of widget(s) used to display that variable. The 'graph and 'spectrum cases create Snd channel displays, accessible via a sound (and channel 0); these respond to the various channel-related functions such as show-transform-peaks, although you have to give the sound explicitly:

(define wid2 (make-variable-display "do-loop" "x" 'spectrum))
(set! (show-transform-peaks (car wid2)) #t)

Each graph or spectrum display is placed in its own pane (this is a desperate kludge), whereas all the others are ordered vertically in a single pane. The 'scale choice has an additional argument that gives the range of the scale as a list (low high):

(define wid2 (make-variable-display "do-loop" "i*2" 'scale '(-1.0 1.0)))

You can watch a generator's state on a sample-by-sample basis by putting it in a text display:

(define wid1 (make-variable-display "simp" "beg" 'text))
(define wid2 (make-variable-display "simp" "oscil" 'text))
(define wid3 (make-variable-display "simp" "outa" 'graph))
(definstrument (simp)
  (let* ((beg 0)
	 (dur 1000)
	 (end (+ beg dur))
	 (osc (make-oscil 440.0)))
    (do ((i beg (+ i 1)))
	((= i end))
      (variable-display i wid1)
      (variable-display
        (oscil (variable-display osc wid2) 0.0)
       wid3))))
(simp)
variable display

To clear display state, there's also variable-display-reset.

mark-sync-color new-color

mark-sync-color uses the draw-mark-hook to set the color of sync'd marks.

menu-option menu-name

menu-option returns the widget associated with a given menu item name ("Print" for example). This is actually a bad idea since the menu names can change without warning.

select-file func title dir filter help

select-file starts a file selection dialog, running 'func' if a file is selected:

 (add-to-menu 0 "Insert File" 
   (lambda () 
     (select-file
       insert-sound
       "Insert File" "." "*" "file will be inserted at cursor")))
show-all-atoms 

show-all-atoms displays all current X atom names (there are several hundred of these atoms normally).

show-disk-space 

show-disk-space adds a label in the status area which shows the current amount of disk space available on the partition of the associated sound. There's a picture of it in action above (add-mark-pane).

make-sound-box name parent select-func peak-func sounds args
show-sounds-in-directory (dir ".")

make-sound-box makes a container of sound file icons, each icon containing a little sketch of the waveform, the length of the file, and the filename. What happens when an icon is selected is up to 'select-func'. However, if you drag (via button 2) the icon to the menubar, that sound is opened, and if you drag it to a channel graph, it is mixed at the mouse location in that channel. 'select-func' called when sound icon is selected; it is passed the sound file's name. 'peak-func' (if any) tells the soundbox code where to find any associated peak env files. 'sounds' is list of sound file names. 'args' is list of resource settings for each icon.

(make-sound-box "sounds"
		((main-widgets) 3)
                snd-print
		*peak-env-dir*
		(list "oboe.snd" "pistol.snd" "cardinal.snd" "storm.snd")
		())

show-sounds-in-directory calls make-sound-box, filling it with any sounds found in the directory passed as its argument (which defaults to the current directory).

show-sounds-in-directory
snd-clock-icon snd hour

snd-clock-icon replaces Snd's hourglass with a (very primitive) clock.

upon-save-yourself thunk

upon-save-yourself causes 'thunk' (a function of no arguments) to be called if the window manager sends a SAVE_YOURSELF message.

upon-take-focus thunk

upon-take-focus causes 'thunk' (a function of no arguments) to be called whenever Snd receives focus from the window manager.

with-minmax-button 

with-minmax-button adds an open/close button to each sound's pane. To activate it:

(hook-push after-open-hook with-minmax-button)
unzync 
zync 

The pair zync and unzync cause the y-axis zoom sliders of a multichannel file to move together (zync) or separately (unzync, the default).

see also:   motif   dialogs   graphics   menus   enved
snd-test

snd-test.scm and snd-test.rb are test suites for Snd. The simplest use is:

snd -l snd-test

which will run all the tests, assuming you have the various sound files it is expecting to find. You can run a particular test with:

snd -l snd-test 23

which runs test 23. snd-test is primarily useful to non-developers as a source of a huge number of examples.

sndwarp

This is a translation from CLM of Bret Battey's sndwarp instrument, itself based on Richard Karpen's sndwarp csound generator. It is similar to expsrc.

sndwarp beg dur file 
      (amp 1.0)
      (amp-env '(0 1 100 1))  ; amplitude envelope
      (stretch 1.0)           ; time stretch — 2.0 -> twice as long
      (srate 1.0)             ; src — 0.5 -> octave down
      (inputbeg 0.0)          ; source file start point
      (wsize 0.1)             ; size of windows in seconds
      (randw 0.02)            ; randomness of wsize
      (overlaps 15)           ; window overlaps per sec
      (time-ptr #f)           ; #f=stretch mode, #t=time-ptr mode
      (scale-time-ptr #f)     ; #f=absolute, #t=rescale
      (zero-start-time-ptr #f); #t=start at 0
      (window-offset #f)      ; #f=spread windows evenly
      (loc 0.5)               ; stereo loc, 0=left, 1=right
      (rev 0.1)               ; reverb amount
      (srcwidth 5)            ; src interpolation width

Many of the parameters can also be envelopes. The source has commentary which I'll slightly paraphrase here for convenience. 'time-ptr' is a flag that determines whether stretching or time-pointer mode is to be used in interpreting the 'stretch' parameter. In stretch mode, the value of 'stretch' scales the time of the sound. For example, a value of 2 will stretch the sound In time-ptr mode, the value(s) of 'stretch' are readin pointers into the soundfile. For example, to read through a file backwards from 2 seconds at half speed, use a stretch envelope such as '(0 2 1 0) with a 4 second note duration. 'scale-time-ptr' is a flag that determines whether the time-ptr envelope is interpreted in absolute seconds or rescaled to fit the duration of the input sound file. 'zero-start-time-ptr' is a flag that determines (in time-ptr mode) whether the first section of the windows start at time-ptr = 0. 'window-offset' is a flag that determines how the windows are offset in time.

(with-sound () (sndwarp 0 1 "oboe.snd"))
(with-sound () (sndwarp 0 4 "oboe.snd" :stretch 2.0 :srate 0.5))
see also:   clm-expsrc   expsrc   pvoc   rubber   ssb-bank
spectr

The spectr files were translated by Michael Scholz from CLM's spectr.clm. They contain a large set of instrument steady-state spectra, gathered many years ago (before 1976) by James A Moorer. The variable names are taken from the file names used by JAM, but by the time I got around to rescuing the data from mouldering magtapes, he had long since moved on, so I don't actually know what instrument some of the labels refer to. The data is in the form of a bunch of lists, each given a name:

(define  trp-gs5 '(  1.02 .0114  2.02 .0346  3.02 .0045  4.04 .0013  5.06 .0002))

which (I think) refers to a trumpet playing the note gs5. The first number is the harmonic, the second its amplitude, the third the next harmonic, then its amplitude, and so on. These spectra can be used directly in the instrument spectra in clm-ins.scm. spectr.scm exports only *spectr* which is an environment that holds the spectral names and values.

see also:   two-tab
stochastic

stochastic is Bill Sack's implementation of Xenakis' Dynamic Stochastic Synthesis as heard in his GENDY3, S.709, Legende d'Eer, etc.

stochastic start dur
           (amp .9)       ; overall amplitude
           (bits 16)      ; resolution of the wave's amplitude dimension
           (xmin 1)       ; minimum number of samples between time breakpoints, must be >= 1
           (xmax 20)      ; maximum number of samples between time breakpoints
           (xwig 0)       ; amplitude applied to random walk function in time dimension
           (xstep 1)      ; quantization of freedom in time dimension, in samples, minimum: 1
           (ywig 0)       ; amplitude applied to random walk function in amplitude dimension, as %amp
           (xfb 0)        ; FIR filter
           (init-array '((10 0) (10 1) (10 0) (10 -.7) (10 0) (10 .5) 
                         (10 0) (10 -.3) (10 0) (10 .2) (10 0) (10 -.1)))
                          ; initial x and y breakpoints for wave,
                          ;    x values must be integers >= 1, y values between -1.0 and 1.0

stochastic.ins in the CLM tarball has an elaborate Common Music-based example. Here is one that is much simpler, but very loud:

(with-sound () (stochastic 0 10 :xwig .25 :ywig 10.0))
strad

strad.scm is a translation (by Michael Scholz) of CLM's strad.ins (by Juan Reyes). It implements a physical model of a bowed string with stiffness.

tank-rev

tankrev.scm has Anders Vinjar's implementation of Jon Dattorro's plate reverb.

v and fmv

The fm violin was my favorite instrument while working in the 70's and 80's, primarily on the Samson box. It was developed in Mus10 (ca 1977) based on ideas of John Chowning.

fm-violin startime dur frequency amplitude
	    (fm-index 1.0)                        ; scales all indices
	    (amp-env '(0 0  25 1  75 1  100 0))   ; amplitude envelope
	    (periodic-vibrato-rate 5.0) 
	    (random-vibrato-rate 16.0)            ; jitter added to vibrato
	    (periodic-vibrato-amplitude 0.0025) 
	    (random-vibrato-amplitude 0.005)
	    (noise-amount 0.0)                    ; noise added to modulation
	    (noise-freq 1000.0)
	    (ind-noise-freq 10.0)                 ; index envelope jitter
	    (ind-noise-amount 0.0)
	    (amp-noise-freq 20.0)                 ; amplitude envelope jitter
	    (amp-noise-amount 0.0)
	    (gliss-env '(0 0  100 0))             ; frequency envelope
	    (glissando-amount 0.0) 
	    (fm1-env '(0 1  25 .4  75 .6  100 0)) ; 1:1 modulator amp (fm index) env
	    (fm2-env '(0 1  25 .4  75 .6  100 0)) ; 3:1 mod env
	    (fm3-env '(0 1  25 .4  75 .6  100 0)) ; 4:1 mod env
	    (fm1-rat 1.0)                         ; 1:1 actual mod:carrier freq ratio
	    (fm2-rat 3.0)	                  ; 3:1 same
	    (fm3-rat 4.0)                         ; 4:1 same
	    (fm1-index #f)                        ; 1:1 mod local index scaler
	    (fm2-index #f)                        ; 3:1 same
	    (fm3-index #f)                        ; 4:1 same
	    (degree 0)
	    (distance 1.0)
	    (reverb-amount 0.01)
	    (base 1.0)                            ; amp env base (1.0 = line segments)

Most of these parameters are for special cases; normally you need only:

Scheme:    (with-sound () (fm-violin 0 1 440 .1))
Ruby:      with_sound() do fm_violin_rb(0, 1, 440, .1, [[:fm_index, 2.0]]) end

fm-violin sets up several parallel modulators of one carrier (see fm.html for details, or (ah nostalgia...) Schottstaedt, "The Simulation of Natural Instrument Tones Using Frequency Modulation with a Complex Modulating Wave", CMJ vol 1 no 4 1977 p46-50). The modulators themselves are modulated (vibrato, noise, etc). The FM indices were chosen to try to mimic violin or cello sounds over a wide range of frequencies. The various envelope "jitter" parameters set up slow moving random changes in the associated envelopes; in some case this can produce a much richer sound. There's no limit on what this instrument can do; nearly all my compositions in the 80's used it. To hear some of the effects, load fmviolin.clm (it is a CLM notelist, but it is completely compatible with Snd/Scheme).

fmv.scm (or v.rb in Ruby) implements the fm-violin as a CLM-style generator, making it possible to call the violin anywhere a generator could be called; since each call on the fm-violin function produces the next sample of the given violin, this form of the fm-violin is easy to call in "real-time" situations. Any other CLM-style instrument could be rewritten in the same form.

make-fm-violin
    frequency amplitude (fm-index 1.0) (amp-env #f) 
    (periodic-vibrato-rate 5.0) (random-vibrato-rate 16.0)
    (periodic-vibrato-amplitude 0.0025) (random-vibrato-amplitude 0.005) 
    (noise-amount 0.0) (noise-freq 1000.0)
    (ind-noise-freq 10.0) (ind-noise-amount 0.0) 
    (amp-noise-freq 20.0) (amp-noise-amount 0.0) (gliss-env #f)
    (fm1-env #f) (fm2-env #f) (fm3-env #f) 
    (fm1-rat 1.0) (fm2-rat 3.0) (fm3-rat 4.0) 
    (fm1-index #f) (fm2-index #f) (fm3-index #f) (base 1.0)

fm-violin gen
fm-violin-ins [same args as original violin in v.scm]

fm-violin-ins shows how this generator can be fitted into the original fm-violin code. The plethora of arguments is an historical artifact; normally only a few of them are used at a time. There are two examples of calling this generator in fmv.scm, the simpler one being:

(define test-v 
  (lambda (beg dur freq amp amp-env)
    (let ((v (make-fm-violin
	      freq amp 
	      :amp-env (let ((e (make-env (or amp-env '(0 0 1 1 2 0)) 
					  :scaler amp 
					  :length dur)))
			 (lambda () (env e)))))
	  (data (channel->float-vector beg dur)))
      (do ((i 0 (+ i 1)))
	  ((= i dur))
	(set! (data i) (+ (data i)
                          (v))))
      (set-samples beg dur data))))

Here we are setting up an fm-violin generator (via make-fm-violin), then calling it 'dur' times, mixing its output into the current data (this could also use mix-float-vector and so on). The generator is called via (v). As can be seen here, each envelope is treated as a function called on each sample very much like the "as-needed" input in src or granulate; the envelopes could actually be any arbitrary function you like (see test-v1 in fmv.scm which uses an oscillator as one of the fm index envelopes). One complication in some "real-time" situations is that you don't know in advance how long a note will be; in this case, the envelope generating functions should have attack and decay ramps, triggered by note-on and note-off; once the ramp has reached its end point, the end value should be held; the note itself should be called until it has had time to ramp off.

I can't resist including an historical digression. Here is a Mus10 version of fm-violin (in this code ":=" is used in place of the original SAIL left arrow character, and so on):

ARRAY GlissFunc, DecayFunc, AttackFunc, SineWave, AmpFunc(512);
SYNTH(Sinewave); 1,1 999;
SEG(AmpFunc); 0,0 1,25 1,50 0,75 0,100;
SEG(GlissFunc);0,1 1,50, 0,100;
SEG(AttackFunc);0,0 1,100;
SEG(DecayFunc);1,1 .6,5 .3,10 .15,25 .07,50 0,100;
	
INSTRUMENT VN1;
VARIABLE Reset1,Noise,/NewMag,OtherFreq,/Gliss,Distance,Stereo,
	Freq,Amp1,Amp2,Duration,AttackTime,DecayTime,Memory1,
	Index1,Index2,Index3,scFreq,DecayLength,Switch1,Switch2,
	/Mod1,/Mod2,/Mod3,/Env,/Att,/Vibrato,IMult,/Snd,
	/Flutter,VibRate,VibAmp,/Ramp,/Decay,VibSwitch,LogFreq,
	GlissLength,Bowing,DecayCall,VibCall,GlissCall,RampCall;
	
Memory1:=1;
	
I_ONLY BEGIN
  Duration:=P2;
  Freq:=P3;
  Amp1:=P4;
  Amp2:=P5;
  OtherFreq:=P6;
  IF Freq>=C THEN Freq:=Freq+Freq/100;
  IF Freq<C THEN Freq:=Freq-20/Freq;
	
  Switch1:=P14;
  Switch2:=1-Switch1;
  IMult:=P7-(Switch2/4);
  VibSwitch:=P8;
  Bowing:=P9;
	
  Distance:=P10;
  Stereo:=P11;
  Noise:=P12;
  GlissLength:=P13;
  LogFreq:=ALOG(Freq);
  
  DecayCall:=VibCall:=RampCall:=GlissCall:=20;
  IF Amp1=Amp2 THEN RampCall:=SRATE;
  IF Freq=OtherFreq THEN GlissCall:=SRATE;
  IF VibSwitch=0 THEN VibCall:=SRATE;
  IF Switch1=1 THEN DecayCall:=SRATE;
	
  Vibrate:=5.25+RAND*.75;
  VibAmp:=.006+RAND*.001;
	
  IF Bowing=0
    THEN
      IF Memory1>.08
	THEN
	  BEGIN
	  DecayTime:=.7;
	  AttackTime:=.2;
	  END
	ELSE
	  BEGIN
	  DecayTime:=.7;
	  AttackTime:=.05;
	  Noise:=0;
	  END
    ELSE
      IF Memory1>.05
	THEN
	  BEGIN
	  DecayTime:=.05;
	  AttackTime:=.2;
	  END
	ELSE
	  BEGIN
	  DecayTime:=.05;
	  AttackTime:=.05;
	  Noise:=0;
	  END;
	
  Memory1:=DecayTime;
	
  IF AttackTime+DecayTime>=Duration
    THEN
      BEGIN
      AttackTime:=Duration*AttackTime;
      DecayTime:=DecayTime*Duration;
      IF AttackTime<=.05 THEN AttackTime:=Duration-DecayTime-.01;
      END;
	
  ScFreq:=Freq*MAG;
  DecayLength:=1000/Freq;
  IF Switch1=0 THEN Noise:=.1;
  Index1:=7.5*IMult/LogFreq;
  Index2:=5/SQRT(Freq);
  Index3:=IMult*30*(8.5-LogFreq)/Freq;
END;
	
Decay:=Switch1+EXPEN[DecayCall](Switch2,MAG*20/DecayLength,DecayFunc);
ENV:=Switch2+LINEN[20](Switch1,AttackTime/20,DecayTime/20,Duration/20,AmpFunc,Reset1:=0);
Ramp:=Amp1+NOSCIL[RampCall](Amp2-Amp1,20*MAG/Duration,AttackFunc);
Gliss:=Freq+EXPEN[GlissCall](OtherFreq-Freq,20*MAG/GlissLength,GlissFunc);
FLutter:=RANDI[VibCall](1,200*Mag);
Vibrato:=NOSCIL[VibCall](ENV,Vibrate*MAG*20,SineWave);
Att:=1-EXPEN[20](1,MAG*640,AttackFunc);
	
NewMag:=(1+Flutter*.005)*(1+Vibrato*VibAmp)*(1+RANDI(Noise*Att,2000*Mag))*Gliss*Mag;
	
Mod1:=NOSCIL(Decay*ScFreq*(Att+Index1),NewMag,Sinewave);
Mod2:=NOSCIL(Decay*ScFreq*(Att+Index2),4*NewMag,Sinewave);
Mod3:=NOSCIL(Decay*ScFreq*(Att+Index3),3*NewMag,Sinewave);
Snd:=ZOSCIL(Decay*ENV*Ramp,NewMag+Mod1+Mod2+Mod3,Sinewave);
OUTA:=OUTA+Snd*0.5;
END;
at the park, copyright Patte Wood at home
thennow

This instrument required about 60 seconds of computing on a PDP-10 (a $250,000 minicomputer) for 1 second of sound (our normal sampling rate was 12800). Since the PDP was massively time-shared, 60 seconds of computing could involve many minutes of sitting around watching AI scientists play Space War. Mus10 was an extension of Music V for the PDP-10 family of computers. To give a feel for how one worked in those days, here's a brief quote from the Mus10 manual (by Tovar and Leland Smith, May 1977):

The following generates  1 second of a  440 Hz sine wave  followed by
1/2 sec. of a  660Hz sine wave. The output goes to a file, MUSIC.MSB,
which is written on DSKM.  

COMMENT Fill array with sine wave;
ARRAY SINETABLE[511];
FOR I:=0 STEP 1 UNTIL 511 DO SINETABLE[I]:=SIN(2*PI/512);

INSTRUMENT SINE;
  COMMENT Generate simple sine wave.  P4 = Amplitude, P3 = frequency;
  OUTA:=OUTA+OSCIL(P4,P3*MAG,SINETABLE);
  END;

COMMENT Now, generate the sound;
PLAY ;
  SIMP 0, 1, 440, 1000;
  SIMP 1, 1/2, 660, 1000;
  FINISH;

The computation involved was considered so burdensome, that the names of the main users were posted in the AI lab halls, apparently to try to get us to go away. I was normally the primary user (in terms of computrons) for the entire lab, and I had no intention of going away. In the Samson box world, this (in its initial "chorus" version) was:

Instrument(Violin);
RECORD_POINTER(seg) nullfunc;
INTEGER ARRAY gens[1:4],indgens[1:6], GensA[1:4],AmpGens[1:2];
					! synthesizer addresses;
REAL ARRAY ratsA[1:4],Indrats[1:6],ratsB[1:4],AmpRats[1:2];
					! envelope data;
INTEGER ModGens1Sum,i,FuncOffSet,k,GenOutLoc,GenInLoc,ModGens2Sum,x1,x2;

Pars(<(InsName,Beg,Dur,Freq,Amp,Function AmpFunc,Function IndFunc,IndMult,
	SkewMult,Nothing,PcRev,No11,No12,No13,Function SkewFunc)>);
					! the parameters of this instrument;

Dbugit(Pns);				! debugging aid;
GenOutLoc:=CASE (Pn[1] MOD 4) OF (Outma,Outmb,Outmc,Outmd);
					! OUTMA is channel 1, OUTMB channel 2, etc;
if freq>srate/3 then return;		! note too high, so leave it out;
x1:=3;					! modulating frequency checks;
x2:=4;					! (we want them less than srate/2);
If x1*freq>srate/2 Then x1:=1;
If x2*freq>srate/2 then x2:=1;
amp:=Amp/2;				! two carriers, so halve the amplitude;

waiter(Beg);				! wait for the beginning of the note;

indRats[1]:=(x1*Freq*IndMult*((8.5-log(freq))/(3+(freq/1000)))*4/srate) MIN .999;
indRats[2]:=(x2*Freq*IndMult*(1/(freq^.5))*4/srate) MIN .999;
indRats[3]:=(freq*IndMult*(5/log(freq))*4/srate) MIN .999;
indrats[4]:=indrats[1]; indrats[5]:=indrats[2]; indrats[6]:=indrats[3];

ratsA[1]:=x1; ratsA[2]:=x2;     ratsA[3]:=1;     ratsA[4]:=1;	
ratsB[1]:=x1+.002; ratsB[2]:=x2+.003;     ratsB[3]:=1.002;     ratsB[4]:=1;	
					! this is the skewing for the chorus effect;
Gens[1]:=Osc(Pns,ModGens1Sum);		! now set up the oscillators;
Gens[2]:=Osc(Pns,ModGens1Sum);
Gens[3]:=Osc(Pns,ModGens1Sum);
Gens[4]:=Osc(Pns,genInLoc,ModGens1Sum);	! carrier 1;

GensA[1]:=Osc(Pns,ModGens2Sum);
GensA[2]:=Osc(Pns,ModGens2Sum);
GensA[3]:=Osc(Pns,ModGens2Sum);
GensA[4]:=Osc(Pns,genInLoc,ModGens2Sum);! carrier 2;

indgens[1]:=gens[1];   indgens[2]:=gens[2];  indgens[3]:=gens[3];
indgens[4]:=gensA[1];   indgens[5]:=gensA[2];  indgens[6]:=gensA[3];
					! set up envelope addressing;

ModSig(Pns,GenOutLoc,GenInLoc,1-pcRev);	! send signal to DACs;
ModSig(Pns,RevIn,GenInLoc,pcRev);	! and signal to reverberator;

AmpGens[1]:=Gens[4]; AmpGens[2]:=GensA[4]; AmpRats[1]:=1; AmpRats[2]:=1;
					! now add the envelopes;
AddArrEnv(Pns,AmpGens,2,"A",0,Amp/2,AmpFunc,AmpRats);
AddArrEnv(Pns,IndGens,6,"A",0,1,IndFunc,Indrats);
AddArrEnv(Pns,Gens,4,"F",freq,Freq*skewmult,skewfunc,ratsA,
	5,.011,.011,nullfunc,6,.017,.017,nullfunc,0,0);
AddArrEnv(Pns,GensA,4,"F",freq,Freq*skewmult,skewfunc,ratsA,
	6,.010,.010,nullfunc,5,.017,.017,nullfunc,1,0);
End!Instrument(Pns);			! deallocation;

The Sambox version eventually became incredibly complicated, mainly to try to handle note list problems in the instrument. The Samson box could run about 5 or 6 of these in "real-time", similar to a modern-day 500 MHz Pentium running CLM. The parallel in the Sambox world to the SIMP example above is (this is taken from SAMBOX.BIL, November 1984):

    Instrument(Simp);
    Integer Gen1;
    Gen1:=Osc(Pns,OutA,Zero,SineMode,0,0,Pn[3]);
    AddEnv(Pns,Gen1,"A",0,Pn[4],Pf[5]);
    End_Instrument(Pns);

The Common Lisp version of this is:

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

In Common Lisp, the fm-violin became (fm.html, 1989):

(definstrument violin (beg end frequency amplitude fm-index)
  (let* ((frq-scl (hz->radians frequency))
         (maxdev (* frq-scl fm-index))
         (index1 (* maxdev (/ 5.0 (log frequency))))
         (index2 (* maxdev 3.0 (/ (- 8.5 (log frequency)) (+ 3.0 (/ frequency 1000)))))
         (index3 (* maxdev (/ 4.0 (sqrt frequency))))
         (carrier (make-oscil frequency))
         (fmosc1 (make-oscil frequency))
         (fmosc2 (make-oscil (* 3 frequency)))
         (fmosc3 (make-oscil (* 4 frequency)))
         (ampf  (make-env '(0 0 25 1 75 1 100 0) :scaler amplitude))
         (indf1 (make-env '(0 1 25 .4 75 .6 100 0) :scaler index1))
         (indf2 (make-env '(0 1 25 .4 75 .6 100 0) :scaler index2))
         (indf3 (make-env '(0 1 25 .4 75 .6 100 0) :scaler index3))
         (pervib (make-triangle-wave :frequency 5 :amplitude (* .0025 frq-scl)))
         (ranvib (make-randi :frequency 16 :amplitude (* .005 frq-scl)))
         (vib 0.0))
    (run
     (loop for i from beg to end do
       (setf vib (+ (triangle-wave pervib) (randi ranvib)))
       (outa i (* (env ampf)
                  (oscil carrier
                         (+ vib 
                            (* (env indf1) (oscil fmosc1 vib))
                            (* (env indf2) (oscil fmosc2 (* 3.0 vib)))
                            (* (env indf3) (oscil fmosc3 (* 4.0 vib)))))))))))

or in its actual (non-simplified) form:

(defun bit20 (x)			;Samson box modifier got 20 bit int interpreted as fraction 
  (if (>= x (expt 2 19))                ;(keep fm-violin compatible with old note lists)
      (float (/ (- x (expt 2 20)) (expt 2 19)))
    (float (/ x (expt 2 19)))))

(defun make-frobber-function (beg end frobl)
  (let ((result (list beg))
	(val (bit20 (cadr frobl))))
    (loop for x in frobl by #'cddr and 
              y in (cdr frobl) by #'cddr do
      (when (and (>= x beg)
		 (<= x end))
	(push val result)
	(push x result)
	(setf val (bit20 y))))
    (push val result)
    (push end result)
    (push val result)
    (nreverse result)))

(definstrument fm-violin 
  (startime dur frequency amplitude &key
	    (fm-index 1.0)
	    (amp-env '(0 0  25 1  75 1  100 0))
	    (periodic-vibrato-rate 5.0) 
            (random-vibrato-rate 16.0)
	    (periodic-vibrato-amplitude 0.0025) 
            (random-vibrato-amplitude 0.005)
	    (noise-amount 0.0) (noise-freq 1000.0)
	    (ind-noise-freq 10.0) (ind-noise-amount 0.0)
	    (amp-noise-freq 20.0) (amp-noise-amount 0.0)
	    (gliss-env '(0 0  100 0)) (glissando-amount 0.0) 
	    (fm1-env '(0 1  25 .4  75 .6  100 0)) 
            (fm2-env '(0 1  25 .4  75 .6  100 0)) 
            (fm3-env '(0 1  25 .4  75 .6  100 0))
	    (fm1-rat 1.0) (fm2-rat 3.0)	 (fm3-rat 4.0)                    
	    (fm1-index nil) (fm2-index nil) (fm3-index nil)
	    (base nil) (frobber nil)
	    (reverb-amount 0.01)
	    (index-type :violin)
	    (degree nil) (distance 1.0) (degrees nil)
	    (no-waveshaping nil) (denoise nil)
	    (denoise-dur .1) (denoise-amp .005)
	    &allow-other-keys)
  (if (> (abs amplitude) 1.0) 
      (setf amplitude (clm-cerror ".1?" .1 #'numberp "amplitude = ~A?" amplitude)))
  (if (<= (abs frequency) 1.0) 
      (setf frequency (clm-cerror "440.0?" 440.0 #'numberp "frequency = ~A?" frequency)))
  (let* ((beg (floor (* startime *srate*)))
	 (end (+ beg (floor (* dur *srate*))))
	 (frq-scl (hz->radians frequency))
	 (modulate (not (zerop fm-index)))
	 (maxdev (* frq-scl fm-index))
	 (vln (not (eq index-type :cello)))
	 (logfreq (log frequency))
	 (sqrtfreq (sqrt frequency))
	 (index1 (or fm1-index (min pi (* maxdev (/ (if vln 5.0 7.5) logfreq)))))
	 (index2 (or fm2-index (min pi (* maxdev 3.0 (if vln 
							     (/ (- 8.5 logfreq) (+ 3.0 (* frequency .001)))
							   (/ 15.0 sqrtfreq))))))
	 (index3 (or fm3-index (min pi (* maxdev (/ (if vln 4.0 8.0) sqrtfreq)))))

	 (easy-case (and (not no-waveshaping)
			 (zerop noise-amount)
			 (eq fm1-env fm2-env)
			 (eq fm1-env fm3-env)
			 (zerop (- fm1-rat (floor fm1-rat)))
			 (zerop (- fm2-rat (floor fm2-rat)))
			 (zerop (- fm3-rat (floor fm3-rat)))
			 (zerop (nth-value 1 (floor fm2-rat fm1-rat)))
			 (zerop (nth-value 1 (floor fm3-rat fm1-rat)))))
	 (coeffs (and easy-case modulate
	 	      (partials->polynomial
	 	       (list fm1-rat index1
	 		     (floor fm2-rat fm1-rat) index2
	 		     (floor fm3-rat fm1-rat) index3))))
	 ;; that is, we're doing the polynomial evaluation using fm1osc running at fm1-rat * frequency
	 ;; so everything in the polynomial table should be in terms of harmonics of fm1-rat
	 
	 (norm (or (and easy-case modulate 1.0) index1))
	 
	 (carrier (make-oscil frequency))
	 (fmosc1  (and modulate (make-oscil (* fm1-rat frequency))))
	 (fmosc2  (and modulate (or easy-case (make-oscil (* fm2-rat frequency)))))
	 (fmosc3  (and modulate (or easy-case (make-oscil (* fm3-rat frequency)))))
	 (ampf  (make-env 
                  (if denoise
                       (reduce-amplitude-quantization-noise amp-env dur amplitude denoise-dur denoise-amp) 
                     amp-env)
	          amplitude :base base :duration dur))
	 (indf1 (and modulate (make-env fm1-env norm :duration dur)))
	 (indf2 (and modulate (or easy-case (make-env fm2-env index2 :duration dur))))
	 (indf3 (and modulate (or easy-case (make-env fm3-env index3 :duration dur))))
	 (frqf (make-env gliss-env (* glissando-amount frq-scl) :duration dur))
	 (pervib (make-triangle-wave periodic-vibrato-rate (* periodic-vibrato-amplitude frq-scl)))
	 (ranvib (make-rand-interp random-vibrato-rate (* random-vibrato-amplitude frq-scl)))
	 (fm-noi (if (and (/= 0.0 noise-amount)
			  (null frobber))
		     (make-rand noise-freq (* pi noise-amount))))
	 (ind-noi (if (and (/= 0.0 ind-noise-amount) (/= 0.0 ind-noise-freq))
		      (make-rand-interp ind-noise-freq ind-noise-amount)))
	 (amp-noi (if (and (/= 0.0 amp-noise-amount) (/= 0.0 amp-noise-freq))
		      (make-rand-interp amp-noise-freq amp-noise-amount)))
	 (frb-env (if (and (/= 0.0 noise-amount) frobber)
		      (make-env (make-frobber-function startime (+ startime dur) frobber) :duration dur
				:base 0	:scaler (* two-pi noise-amount))))
	 (vib 0.0) 
	 (modulation 0.0)
	 (loc (make-locsig :degree (or degree degrees (random 90.0)) 
                :reverb reverb-amount :distance distance))
	 (fuzz 0.0)
	 (ind-fuzz 1.0)
	 (amp-fuzz 1.0))
    (run
     (loop for i from beg to end do
       (if (/= 0.0 noise-amount)
	   (if (null frobber)
	       (setf fuzz (rand fm-noi))
	     (setf fuzz (env frb-env))))
       (setf vib (+ (env frqf) (triangle-wave pervib) (rand_interp ranvib)))
       (if ind-noi (setf ind-fuzz (+ 1.0 (rand-interp ind-noi))))
       (if amp-noi (setf amp-fuzz (+ 1.0 (rand-interp amp-noi))))
       (if modulate
	   (if easy-case
	       (setf modulation
		 (* (env indf1) 
		    (polynomial coeffs (oscil fmosc1 vib)))) ;(* vib fm1-rat)??
	     (setf modulation
	       (+ (* (env indf1) (oscil fmosc1 (+ (* fm1-rat vib) fuzz)))
		  (* (env indf2) (oscil fmosc2 (+ (* fm2-rat vib) fuzz)))
		  (* (env indf3) (oscil fmosc3 (+ (* fm3-rat vib) fuzz)))))))
       (locsig loc i
	     (* (env ampf) amp-fuzz
		(oscil carrier (+ vib (* ind-fuzz modulation)))))))))

which is very similar to the Scheme version (v.scm). And I just found this out on the net; I'm no csound expert, so I merely quote what I find:

;ORC
; edited by R. Pinkston, modified for use with MIDI2CS by R. Borrmann
;
;==========================================================================;
;                Schottstaedt FM String Instrument from Dodge              ;
;                                                                          ;
;p4 = amp p5 = pch p6 = rise p7 = dec p8 = vibdel p9 = vibwth p10 = vibrte ;
;==========================================================================;
;        sr      =       44100
;        kr      =       4410
;        ksmps   =       10
;        nchnls  =       1
;
;                instr   1

par
  p_maxamplitude 32000
  p_cps
endpar

        iamp    =       p4

        irise   = .2    ;p6
        idec    = .2    ;p7
        ivibdel = .75   ;p8
        ivibwth = .03   ;p9
        ivibrte = 5.5   ;p10

        ifc     =       p5
        ifm1    =       ifc
        ifm2    =       ifc*3
        ifm3    =       ifc*4
        indx1   =       7.5/log(ifc)    ;range from ca 2 to 1
        indx2   =       15/sqrt(ifc)    ;range from ca 2.6 to .5
        indx3   =       1.25/sqrt(ifc)  ;range from ca .2 to .038
        kvib    init    0 

                timout  0,ivibdel,transient  ;delays vibrato for p8 seconds
        kvbctl  linen   1,.5,p3-ivibdel,.1   ;vibrato control envelope
        krnd    randi   .0075,15        ;random deviation in vib width 
        kvib    oscili  kvbctl*ivibwth+krnd,ivibrte*kvbctl,1 ;vibrato generator
               
transient:
        timout  .2,p3,continue          ;execute for .2 secs only
        ktrans  linseg  1,.2,0,1,0      ;transient envelope 
        anoise  randi   ktrans,.2*ifc   ;noise... 
        attack  oscil   anoise,2000,1   ;...centered around 2kHz

continue: 
        amod1   oscili  ifm1*(indx1+ktrans),ifm1,1
        amod2   oscili  ifm2*(indx2+ktrans),ifm2,1
        amod3   oscili  ifm3*(indx3+ktrans),ifm3,1
        asig    oscili  iamp,(ifc+amod1+amod2+amod3)*(1+kvib),1
        asig    linen   asig+attack,irise,p3,idec
;                out     asig
; 
;                endin
        aright  = asig
        aleft   = asig

There's a C/CLM version of this instrument in sndlib.html. The body of the fm-violin in C/CLM is:

      if (noise_amount != 0.0) fuzz = mus_rand(fmnoi,0.0);
      if (frqf) vib = mus_env(frqf); else vib = 0.0;
      vib += mus_triangle_wave(pervib, 0.0) + 
             mus_rand_interp(ranvib, 0.0);
      if (easy_case)
        modulation = mus_env(indf1) * 
                     mus_polynomial(coeffs, mus_oscil(fmosc1, vib, 0.0), npartials);
      else
        modulation = mus_env(indf1) * mus_oscil(fmosc1, (fuzz + fm1_rat * vib), 0.0) +
                     mus_env(indf2) * mus_oscil(fmosc2, (fuzz + fm2_rat * vib), 0.0) +
                     mus_env(indf3) * mus_oscil(fmosc3, (fuzz + fm3_rat * vib), 0.0);
      mus_locsig(loc, i, mus_env(ampf) *
                         mus_oscil(carrier, vib + indfuzz * modulation, 0.0));

And here is the Ruby version, written by Michael Scholz (see examp.rb):

#
# fm_violin([start=0.0[, dur=1.0[, freq=440.0[, amp=0.3[, *args]]]]])
#

def fm_violin(start = 0.0, dur = 1.0, freq = 440.0, amp = 0.3, *args)
  include Math;			# PI

  usage = "fm_violin([start=0.0[, dur=1.0[, freq=440.0[, amp=0.3[, *args]]]]])

	[:fm_index, 1.0]
	[:amp_env, [0, 0, 25, 1, 75, 1, 100, 0]]
	[:periodic_vibrato_rate, 5.0]
	[:random_vibrato_rate, 16.0]
	[:periodic_vibrato_amp, 0.0025]
	[:random_vibrato_amp, 0.005]
	[:noise_amount, 0.0]
	[:noise_freq, 1000.0]
	[:ind_noise_freq, 10.0]
	[:ind_noise_amount, 0.0]
	[:amp_noise_freq, 20.0]
	[:amp_noise_amount, 0.0]
	[:gliss_env, [0, 0,  100, 0]]
	[:gliss_amount, 0.0]
	[:fm1_env, [0, 1, 25, 0.4, 75, 0.6, 100, 0]]
	[:fm2_env, [0, 1, 25, 0.4, 75, 0.6, 100, 0]]
	[:fm3_env, [0, 1, 25, 0.4, 75, 0.6, 100, 0]]
	[:fm1_rat, 1.0]
	[:fm2_rat, 3.0]
	[:fm3_rat, 4.0]
	[:fm1_index, false]
	[:fm2_index, false]
	[:fm3_index, false]
	[:base, 1.0]
	[:reverb_amount, 0.01]
	[:index_type, :violin]
	[:degree, false]
	[:distance, 1.0]
	[:degrees, false]

  Ruby: fm_violin(0, 1, 440, .1, [[:fm_index, 2.0]])
 Scheme: (fm-violin 0 1 440 .1 :fm-index 2.0)\n\n";

  fm_index = (args.assoc(:fm_index)[1] rescue 1.0);
  amp_env = (args.assoc(:amp_env)[1] rescue [0, 0, 25, 1, 75, 1, 100, 0]);
  periodic_vibrato_rate = (args.assoc(:periodic_vibrato_rate)[1] rescue 5.0);
  random_vibrato_rate = (args.assoc(:random_vibrato_rate)[1] rescue 16.0);
  periodic_vibrato_amp = (args.assoc(:periodic_vibrato_amp)[1] rescue 0.0025);
  random_vibrato_amp = (args.assoc(:random_vibrato_amp)[1] rescue 0.005);
  noise_amount = (args.assoc(:noise_amount)[1] rescue 0.0);
  noise_freq = (args.assoc(:noise_freq)[1] rescue 1000.0);
  ind_noise_freq = (args.assoc(:ind_noise_freq)[1] rescue 10.0);
  ind_noise_amount = (args.assoc(:ind_noise_amount)[1] rescue 0.0);
  amp_noise_freq = (args.assoc(:amp_noise_freq)[1] rescue 20.0);
  amp_noise_amount = (args.assoc(:amp_noise_amount)[1] rescue 0.0);
  gliss_env = (args.assoc(:gliss_env)[1] rescue [0, 0,  100, 0]);
  gliss_amount = (args.assoc(:gliss_amount)[1] rescue 0.0);
  fm1_env = (args.assoc(:fm1_env)[1] rescue [0, 1, 25, 0.4, 75, 0.6, 100, 0]);
  fm2_env = (args.assoc(:fm2_env)[1] rescue [0, 1, 25, 0.4, 75, 0.6, 100, 0]);
  fm3_env = (args.assoc(:fm3_env)[1] rescue [0, 1, 25, 0.4, 75, 0.6, 100, 0]);
  fm1_rat = (args.assoc(:fm1_rat)[1] rescue 1.0);
  fm2_rat = (args.assoc(:fm2_rat)[1] rescue 3.0);
  fm3_rat = (args.assoc(:fm3_rat)[1] rescue 4.0);
  fm1_index = (args.assoc(:fm1_index)[1] rescue false);
  fm2_index = (args.assoc(:fm2_index)[1] rescue false);
  fm3_index = (args.assoc(:fm3_index)[1] rescue false);
  base = (args.assoc(:base)[1] rescue 1.0);
  reverb_amount = (args.assoc(:reverb_amount)[1] rescue 0.01);
  index_type = (args.assoc(:index_type)[1] rescue :violin);
  degree = (args.assoc(:degree)[1] rescue false);
  distance = (args.assoc(:distance)[1] rescue 1.0);
  degrees = (args.assoc(:degrees)[1] rescue false);

  srate = (srate() rescue $rbm_srate);
  chans = (channels() rescue $rbm_channels);
  beg = (srate * start).round;
  len = (srate * dur).round;
  frq_scl = hz2radians(freq);
  modulate = fm_index.nonzero?;
  maxdev = frq_scl * fm_index;
  vln = (not (index_type == :cello))
  logfreq = log(freq);
  sqrtfreq = sqrt(freq);
  index1 = (fm1_index or [PI, maxdev * (vln ? 5.0 : 7.5) / logfreq].min);
  index2 = (fm2_index or [PI, maxdev * 3.0 * 
	      (vln ? ((8.5 - logfreq) / (3.0 + freq * 0.001)) : (15.0 / sqrtfreq))].min);
  index3 = (fm3_index or [PI, maxdev * (vln ? 4.0 : 8.0) / sqrtfreq].min);
  easy_case = (noise_amount.zero? and
	       (fm1_env == fm2_env) and 
	       (fm1_env == fm3_env) and 
	       (fm1_rat - fm1_rat.floor).zero? and 
	       (fm2_rat - fm2_rat.floor).zero? and 
	       (fm3_rat - fm3_rat.floor).zero?);
  coeffs = (easy_case and modulate and 
	    partials2polynomial([fm1_rat, index1, 
				  (fm2_rat / fm1_rat).floor, index2,
				  (fm3_rat / fm1_rat).floor, index3]));
  norm = ((easy_case and modulate and 1.0) or index1);
  carrier = make_oscil(freq);
  fmosc1 = (modulate and make_oscil(fm1_rat * freq));
  fmosc2 = (modulate and (easy_case or make_oscil(fm2_rat * freq)));
  fmosc3 = (modulate and (easy_case or make_oscil(fm3_rat * freq)));
  ampf = make_env(amp_env, amp, dur, 0.0, base);
  indf1 = (modulate and make_env(fm1_env, norm, dur));
  indf2 = (modulate and (easy_case or make_env(fm2_env, index2, dur)));
  indf3 = (modulate and (easy_case or make_env(fm3_env, index3, dur)));
  frqf = make_env(gliss_env, gliss_amount * frq_scl, dur);
  pervib = make_triangle_wave(periodic_vibrato_rate, periodic_vibrato_amp *  frq_scl);
  ranvib = make_rand_interp(random_vibrato_rate, random_vibrato_amp * frq_scl);
  fm_noi = (noise_amount.nonzero? and make_rand(noise_freq, PI * noise_amount));
  ind_noi = ((ind_noise_amount.nonzero? and ind_noise_freq.nonzero?) and 
	     make_rand_interp(ind_noise_freq, ind_noise_amount));
  amp_noi = ((amp_noise_amount.nonzero? and amp_noise_freq.nonzero?) and
	     make_rand_interp(amp_noise_freq, amp_noise_amount));
  vib = 0.0;
  modulation = 0.0;
  # make_locsig(degree=0.0, distance=1.0, reverb=0.0, output, revout, chans=1, type=Mus_linear)
  # Ruby's rand() is shadowed by CLM's rand(), that's why mus_random().abs.
  loc = make_locsig((degree or degrees or mus_random(90.0).abs), 
		    distance, reverb_amount, false, false, chans);
  fuzz = 0.0;
  ind_fuzz = 1.0;
  amp_fuzz = 1.0;
  out_data = make_vct(len);

  vct_map!(out_data,
	   lambda { | |
	     fuzz = rand(fm_noi) if noise_amount.nonzero?;
	     vib = env(frqf) + triangle_wave(pervib) + rand_interp(ranvib);
	     ind_fuzz = 1.0 + rand_interp(ind_noi) if ind_noi;
	     amp_fuzz = 1.0 + rand_interp(amp_noi) if amp_noi;

	     if(modulate)
	       if(easy_case)
		 modulation = env(indf1) * polynomial(coeffs, oscil(fmosc1, vib));
	       else
		 modulation = env(indf1) * oscil(fmosc1, fm1_rat * vib + fuzz) +
		   env(indf2) * oscil(fmosc2, fm2_rat * vib + fuzz) +
		   env(indf3) * oscil(fmosc3, fm3_rat * vib + fuzz);
	       end
	     end

	     env(ampf) * amp_fuzz * oscil(carrier, vib + ind_fuzz * modulation);
	   });

  if(chans == 2)
    mix_vct(vct_scale!(vct_copy(out_data), locsig_ref(loc, 1)), beg, $rbm_snd, 1, false);
    mix_vct(vct_scale!(out_data, locsig_ref(loc, 0)), beg, $rbm_snd, 0, false);
  else
    mix_vct(out_data, beg, $rbm_snd, 0, false);
  end
rescue
  die(usage + "fm_violin()");
end

ws

with-sound provides a way to package up a bunch of instrument calls into a new sound file, and open that file in Snd when the computation is complete. To hear (and see) the fm-violin, for example, we first load with-sound and the instrument:

;; Scheme:
(load "ws.scm")
(load "v.scm")
# Ruby:
load("ws.rb")
load("v.rb")
\ Forth:
"clm.fs" file-eval
"clm-ins.fs" file-eval

Then call with-sound, accepting the default sound file settings, with one fm-violin note at A4 (440 Hz):

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

with_sound() do 
  fm_violin_rb(0, 1, 440, 0.1) 
end
0 1 440 0.1 ' fm-violin with-sound


The body of with-sound can hold any number of notes, or any arbitrary code. For example, say we want to hear an arpeggio from the fm-violin:

(with-sound ()
  (do ((i 0 (+ i 1)))
      ((= i 4))                ; 4 notes in all
    (fm-violin (* i 0.25)      ; notes 1/4 secs apart
               0.25            ; each note 1/4 sec long
               (* 220.0 (+ i 1)); go up by 220 Hz on each note
               .1)))           ; all notes .1 amp

with-sound opens an output object, either a sound file, or a vector: (*output*), and optionally a reverb output object: *reverb*. Each instrument uses out-any to add its sounds to the *output* results. with-sound next sets up a variety of variables describing the current output, and establishes an environment where various problems can be handled nicely (in Scheme, a dynamic-wind with various debugging hooks). Then the with-sound body is evaluated, presumably producing sound. Once evaluated, the outputs are closed, and if reverb is requested, the reverberator is run. Once complete, with-sound prints out statistics (if :statistics is #t), scales the result (if :scale-to), and plays it (if :play is #t). Then, if the output is a sound file (and :to-snd is #t), with-sound opens it in Snd, first closing any previous sound with the same name (this makes it easier to call with-sound over and over while trying out some patch). with-sound returns its :output argument.

  with-sound
          (output *clm-file-name*)               ; output file name ("test.snd")
	  (channels *clm-channels*)              ; channels in output (1)
          (srate *clm-srate*)                    ; output sampling rate (44100)
	  (sample-type *clm-sample-type*)        ; output sample data type (mus-bfloat or mus-lshort)
	  (header-type *clm-header-type*)        ; output header type (mus-next or mus-aifc)
	  (comment #f)                           ; any comment to store in the header (a string)
	  (verbose *clm-verbose*)                ; if #t, print out some info
	  (reverb *clm-reverb*)                  ; reverb instrument (jc-reverb)
	  (reverb-data *clm-reverb-data*)        ; arguments passed to the reverb
	  (reverb-channels *clm-reverb-channels*); chans in the reverb intermediate file
          (revfile *clm-reverb-file-name*)       ; reverb intermediate output file name ("test.rev")
	  (continue-old-file #f)                 ; if #t, continue a previous computation
	  (statistics *clm-statistics*)          ; if #t, print info at end of with-sound (compile time, maxamps)
	  (scaled-by #f)                         ; is a number, scale output by that amp
	  (scaled-to #f)                         ; if a number, scale the output to peak at that amp
	  (play *clm-play*)                      ; if #t, play the sound automatically
	  (to-snd *to-snd*)                      ; if #t, open the output file in Snd

The with-sound syntax may look sightly odd; we include the arguments in the first list, then everything after that is evaluated as a note list.

(with-sound (:srate 44100 :channels 2 :output "test.snd")
  (fm-violin 0 1 440 .1)
  (fm-violin 1 1 660 .1))

produces a sound file with two fm-violin notes; the sound file is named "test.snd", is stero, and has a sampling rate of 44100.

(with-sound (:reverb jc-reverb :statistics #t :play #t) 
  (fm-violin 0 1 440 .1 :reverb-amount .3))

produces one fm-violin note, heavily reverberated, and plays it, printing this info:

> (with-sound (:reverb jc-reverb :statistics #t :play #t) 
    (fm-violin 0 1 440 .1 :reverb-amount .3))
test.snd:
maxamp: 0.3038
rev max: 0.0300
compute time: 0.030

It's often hard to predict how loud a set of notes is going to be, so we can use "scaled-to" to set its final amplitude:

(with-sound (:scale-to .5) 
  (do ((i 0 (+ i 1))) ((= i 10)) (fm-violin 0 i 440.0 (random 1.0))))

Here are examples in Ruby and Forth:

:with_sound(:channels, 2, :play, false, :statistics, true) do 
  fm_violin_rb(0, 1, 440, 0.1); 
  fm_violin_rb(1, 1, 660, 0.1);
  end
# filename: "test.snd"
#    chans: 2, srate: 22050
#   length: 2.000 (44100 framples)
#   format: big endian short (16 bits) [Sun/Next]
#     real: 2.248  (utime 2.240, stime 0.000)
#    ratio: 1.12  (uratio 1.12)
#  max out: [0.098, 0.024]
#<With_Snd: output: "test.snd", channels: 2, srate: 22050>

and in Forth:

snd> 0.0 1.0 330.0 0.5 ' simp :play #f :channels 2 with-sound
\ filename: test.snd
\    chans: 2, srate: 22050
\   format: little endian float (32 bits) [Sun/Next]
\   length: 1.000  (22050 framples)
\     real: 0.162  (utime 0.267, stime 0.000)
\    ratio: 0.16  (uratio 0.27)
\ maxamp A: 0.500 (near 0.680 secs)
\ maxamp B: 0.000 (near 0.000 secs)
\  comment: Written on Fri Jul 14 07:41:47 PDT 2006 by bil at cat using clm (fth) of 30-Jun-06

The default values listed above (*clm-srate* and friends) are set in ws.scm:

(define *clm-file-name*          "test.snd")
(define *clm-srate*              *default-output-srate*)       ; 44100
(define *clm-channels*           *default-output-chans*)       ; 1
(define *clm-sample-type*        *default-output-sample-type*) ; mus-lfloat
(define *clm-header-type*        *default-output-header-type*) ; mus-next
(define *clm-verbose*            #f)
(define *clm-play*               #f)
(define *clm-statistics*         #f)
(define *clm-reverb*             #f)
(define *clm-reverb-channels*    1)
(define *clm-reverb-data*        ())
(define *clm-reverb-file-name*   "test.rev")
(define *clm-table-size*         512)
(define *clm-file-buffer-size*   65536)
(define *clm-locsig-type*        mus-interp-linear)
(define *clm-clipped*            #t)
(define *clm-array-print-length* *print-length*)  ; 12
(define *clm-player*             #f)
(define *clm-notehook*           #f)
(define *to-snd*                 #t)
(define *reverb*                 #f)
(define *output*                 #f)
(define *clm-delete-reverb*      #f)

You can set any of these to permanently change with-sound's defaults

> (set! *clm-file-name* "test.aif")
#<unspecified>
> (set! *clm-srate* 44100)
#<unspecified>
> (set! *clm-channels* 2)
#<unspecified>
> (set! *clm-header-type* mus-aifc)
#<unspecified>
> (set! *clm-sample-type* mus-bfloat)
#<unspecified>
> (with-sound ()  (fm-violin 0 1 440 .1))test.aif:
"test.aif"
> (srate "test.aif")
44100
> (channels "test.aif")
2

To display the entire sound automatically (independent of after-open-hook), use with-full-sound:

(define-macro (with-full-sound args . body)
  `(let ((snd (with-sound-helper (lambda () ,@body) ,@args)))
     (set! (x-bounds *snd-opened-sound*) (list 0.0 (/ (framples *snd-opened-sound*) (srate *snd-opened-sound*))))
     snd))

Since with-sound returns the new sound's file name, we save that, get the new sound's index (*snd-opened-sound*), and set the x-bounds to display the full sound, then return the file name. You could obviously customize this any way you like. To continue adding notes to an existing file, set 'continue-old-file':

(with-sound (:continue-old-file #t) (fm-violin 0 1 440 .1))

The "notehook" argument is a function called each time an instrument is called:

> (with-sound (:notehook (lambda (name . args) 
                          (snd-print (format #f "~%;~A: ~A" name (caddr args)))))
   (fm-violin 0 1 440 .1) 
   (fm-violin 1 1 660 .1))
;fm-violin: 440
;fm-violin: 660
"test.snd"

The arguments passed to the notehook function are the current instrument name (a string) and all its arguments. definstrument implements the notehook feature. The "output" argument can be a vector as well as a filename:

(with-sound (:output (make-float-vector 44100)) (fm-violin 0 1 440 .1))

See fade.scm, snd-test.scm.

definstrument

definstrument is very much like define*, but with added code to support notehook and (for Common Music) *definstrument-hook*. It uses old CL-style documentation strings. An instrument that wants to cooperate fully with with-sound and Common Music has the form:

(definstrument (ins args)
  (let ...
    (do ((i start (+ i 1)))
        ((= i end))
      (outa i ...))))

definstrument is an extension of define*, so its arguments are handled as optional keyword arguments:

(definstrument (simp beg dur (frequency 440.0) (amplitude 0.1))
  (let ((os (make-oscil frequency)))
     (do ((i 0 (+ i 1))) ((= i dur))
       (outa (+ i beg) (* amplitude (oscil os))))))

(with-sound () 
  (simp 0 10000) 
  (simp 10000 10000 550.0 :amplitude 0.1) 
  (simp 20000 10000 :amplitude 0.2))

You don't have to use definstrument; in the next example we make a Shepard tone by calling the oscils and whatnot directly in the with-sound body:

(define (shepard-tone)
  (let ((x 0.0)
	(incr .000001)               ; sets speed of glissandoes
	(oscs (make-vector 12)))
    (do ((i 0 (+ i 1)))
	((= i 12))
      (set! (oscs i) (make-oscil :frequency 0.0)))
    (with-sound (:srate 44100)
     (do ((samp 0 (+ 1 samp))
          (sum 0.0 0.0))
         ((= samp 300000))
       (do ((i 0 (+ i 1)))
           ((= i 12))
         (let ((loc (+ x (/ i 12.0))))  ; location of current oscil in overall trajectory
           (if (> loc 1.0) (set! loc (- loc 1.0)))
           (set! sum (+ sum (* (let ((y (- 4.0 (* 8.0 loc))))
                                 (exp (* -0.5 y y)))  ; Gaussian normal curve as amplitude envelope
                               (oscil (oscs i) 
                                      (hz->radians (expt 2.0 (+ 2 (* loc 12.0))))))))))
                                      ;; (- 1.0 loc) to go down
       (set! x (+ x incr))
       (outa samp (* .1 sum))))))
shepard tone spectrum

There are several other versions of with-sound: with-temp-sound, with-mixed-sound, sound-let, clm-load, and the Common Music handles, init-with-sound and finish-with-sound. with-temp-sound and sound-let set up temporary bindings for embedded with-sounds.

sound-let

sound-let is a form of let* that creates temporary sound files within with-sound. Its syntax is a combination of let* and with-sound: with-sound:

(sound-let ((temp-1 () (fm-violin 0 1 440 .1))
            (temp-2 () (fm-violin 0 2 660 .1)
                       (fm-violin .125 .5 880 .1)))
  (granulate-sound temp-1 0 2 0 2)     ;temp-1's value is the name of the temporary file
  (granulate-sound temp-2 1 1 0 2))

This creates two temporary files and passes them along to the subsequent calls on granulate-sound. The first list after the sound file identifier (i.e. after "temp-1" in the example) is the list of with-sound options to be passed along when creating this temporary file. These default to :output with a unique name generated internally, and all other variables are taken from the overall (enclosing) with-sound. The rest of the list is the body of the associated with-sound. The difference between sound-let and an embedded with-sound is primarily that sound-let names and later deletes the temporary files it creates, whereas with-sound leaves its explicitly named output intact (and tries to open it in Snd, which can be confusing in this context). Here's another example:

  (with-sound ()
    (sound-let ((temp-sound () (fm-violin 0 1 440 .1))) ; create temp-sound with an fm-violin note
       (pins 0.0 2.0 temp-sound 1.0 :time-scaler 2.0))  ; stretch it with the pins instrument (clm-ins.scm)
    (fm-violin 1 1 550 .1))                             ; add another fm-violin note
with-temp-sound

with-temp-sound is like sound-let, but does not delete its output file:

(with-sound ()
  (clm-expsrc 0 2 (with-temp-sound () (fm-violin 0 1 440 .1)) 2.0 1.0 1.0))

Here are Ruby examples:

with_sound() do
  clm_mix(with_sound(:output, "hiho.snd") do
            fm_violin_rb(0, 1, 440, 0.1)
          end.output, :scale, 0.5)
end

with_sound() do
  with_mix "s1", %Q{
  sound_let(lambda do fm_violin_rb(0, 1, 440, 0.1) end) do |tmp|
    clm_mix(tmp)
  end
  }
end
with-mixed-sound

with-mixed-sound is a variant of with-sound that creates a "mix" for each note in the notelist. If you move the mixes around, you can write out the new note list via with-mixed-sound->notelist. In multichannel files, all the channels associated with a note are sync'd together, so if you drag one, the others follow. Also, if you click a mix tag, the corresponding note in the notelist is displayed in the status area.

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

(with-mixed-sound (:channels 2) 
  (fm-violin 0 .1 440 .1 :degree 0) 
  (fm-violin 1 .1 660 .1 :degree 45))

There's also a quick sound file mixer named mus-file-mix:

mus-file-mix outfile infile (outloc 0) (framples) (inloc 0) mixer envs

This function mixes 'infile' into 'outfile' starting at 'outloc' in 'outfile' and 'inloc' in 'infile', mixing 'framples' framples into 'outfile'. 'framples' defaults to the length of 'infile'. If 'mixer', use it to scale the various channels; if 'envs' (an array of envelope generators), use it in conjunction with mixer to scale and envelope all the various ins and outs. 'outfile' can also be a frample->file generator, and 'infile' can be a file->frample generator.

(with-sound () 
  (fm-violin 0 .1 440 .1) 
  (mus-file-mix *output* "oboe.snd") 
  (fm-violin .1 .1 660 .1))
with-marked-sound

with-marked-sound is yet another version of with-sound that adds a mark at the start of each note.

clm-load

clm-load provides a slightly different way to load a notelist. Its first argument is a filename, assumed to be a text file containing notes (equivalent to the body of with-sound). The rest of the arguments to clm-load are the usual with-sound arguments, if any. For example, if we have a file named clm-load-test.clm with these contents:

(fm-violin 0 1 440 .1)
(fm-violin 1 1 660 .1)

then (clm-load "clm-load-test.clm") is the same as (with-sound () (fm-violin 0 1 440 .1) (fm-violin 1 1 660 .1)). Similarly for, (clm-load "clm-load-test.clm" :srate 44100 :channels 2) and so on.

init-with-sound

init-with-sound and finish-with-sound split with-sound into two pieces, primarily for Common Music's benefit.

(define w (init-with-sound :scaled-to .5))
(fm-violin 0 1 440 .1)
(finish-with-sound w)

is equivalent to

(with-sound (:scaled-to .5)
  (fm-violin 0 1 440 .1))
other stuff associated with with-sound

The *clm-* variables are saved in the save-state file by ws-save-state, which may not be a good idea — feedback welcome! Two more convenience functions are ->frequency and ->sample. ->frequency takes either a number or a common-music pitch symbol ('c4 is middle C), and returns either the number or the frequency associated with that pitch:

> (->frequency 'cs5)
554.365261953744

It's optional second argument can be #t to get integer ratios, rather than the default equal temperment. ->sample returns a sample number given a time in seconds:

> (->sample 1.0)
44100

mix-notelists takes any number of notelist arguments, and returns a new notelist with all the input notes sorted by begin time.

(mix-notelists '((fm-violin 0 1 440 .1)
		 (fm-violin 1 1 550 .1))
	       '((bird 0 .1 )
		 (bird .2 .1)
		 (bird 1.2 .3)
		 (bird .5 .5)))

((bird 0 0.1) (fm-violin 0 1 440 0.1) (bird 0.2 0.1) (bird 0.5 0.5) (fm-violin 1 1 550 0.1) (bird 1.2 0.3))
zip
make-zipper ramp-env frame-size frame-env
zipper gen in1 in2
zip-sound beg dur file1 file2 ramp size

The zipper generator performs a kind of cross fade, but not one that tries to be smooth! It marches through the two sounds taking equal short portions of each, then abutting them while resampling so that as one takes less overall frame space, the other takes more. The 'frame-size' argument is the maximum length of each twosome in seconds (for initial array allocation), the 'frame-env' argument determines the current such length as new frames are needed, and the 'ramp-env' argument determines which of the files gets more space in the frame (0: all first, 1: all second). The following function sets up two sounds, an upward ramp and a downward ramp, then zips them together:

(define (ramp-test)
  (let ((data (make-float-vector 10000)))
    (new-sound "new-0.snd")
    (do ((i 0 (+ i 1))) ((= i 10000)) 
      (set! (data i) (* i .0001)))
    (float-vector->channel data 0 10000 0)
    (new-sound "new-1.snd")
    (do ((i 0 (+ i 1))) ((= i 10000)) 
      (set! (data i) (- 1.0 (* i .0001))))
    (float-vector->channel data 0 10000 1)
    (let ((zp (let ((dur (framples)))
                (make-zipper 
                  (make-env '(0 0 1 1) :length dur)
		  0.05
		  (make-env (list 0 (* (srate) 0.05)) :length dur))))
	  (reader0 (make-sampler 0 0 0))
	  (reader1 (make-sampler 0 1 0)))
      (map-channel (lambda (val) (zipper zp reader0 reader1))))))
zipper ramp output

Needless to say, this is not intended to be a suave, romantic gesture!

zip-sound applies the zipper to a pair of sounds:

(zip-sound 0 1 "fyow.snd" "now.snd" '(0 0 1 1) .05)
(zip-sound 0 3 "mb.snd" "fyow.snd" '(0 0 1.0 0 1.5 1.0 3.0 1.0) .025)