CLM

This is work in progress -- the plan is to use Scheme/Ruby/Forth throughout this file, and leave the Common Lisp stuff to the CLM-3 version (clm.html).


CLM (originally an acronym for Common Lisp Music) is a sound synthesis package in the Music V family. This file describes CLM as implemented in Snd, aiming primarily at the Scheme version. Common Lisp users should check out clm.html in the CLM tarball. CLM is based on a set of functions known as "generators". These can be packaged into "instruments", and instrument calls can be packaged into "note lists". The main emphasis here is on the generators; note lists and instruments are described in sndscm.html.


Bill Schottstaedt (bil@ccrma.stanford.edu)

related documentation: snd.html extsnd.html grfsnd.html sndscm.html sndlib.html libxm.html fm.html index.html


Contents


Introduction
Generators
all-passall-pass filter
asymmetric-fmasymmetric fm
combcomb filter
convolveconvolution
delaydelay line
envline segment envelope
filterdirect form FIR/IIR filter
filtered-combcomb filter with filter on feedback
file->sampleinput sample from file
file->frameinput frame from file
fir-filterFIR filter
frame->fileoutput frame to file
formantresonance
granulategranular synthesis
iir-filterIIR filter
in-anysound file input
locsigstatic sound placement
move-soundsound motion
moving-averagemoving window average
notchnotch filter
one-poleone pole filter
one-zeroone zero filter
oscilsine wave and FM
out-anysound output
polyshapewaveshaping
phase-vocodervocoder analysis and resynthesis
sample->fileoutput sample to file
pulse-trainpulse train
rand,rand-interprandom numbers, noise
readinsound input
sawtooth-wavesawtooth
sine-summationsine summation synthesis
square-wavesquare wave
srcsampling rate conversion
ssb-amsingle sideband amplitude modulation
sum-of-cosinesband-limited pulse train
sum-of-sinessum of sines
table-lookupinterpolated table lookup
tapdelay line tap
triangle-wavetriangle wave
two-poletwo pole filter
two-zerotwo zero filter
wave-trainwave train
waveshapewaveshaping
Generic functions
Other functions
Instruments


Introduction

Start Snd, open the listener (choose "Show listener" in the View menu), and:

    >(load "v.scm")
    #<unspecified>
    >(with-sound () (fm-violin 0 1 440 .1))
    "test.snd"

Snd's printout is in blue here, and your typing is in red. The load function returns "#<unspecified>" in Guile to indicate that it is happy. If all went well, you should see a graph of the fm-violin's output. Click the "play" button to hear it; click "f" to see its spectrum.

What if this happened instead?

    >(load "v.scm")
    open-file: system-error: "No such file or directory": "v.scm" (2)

Snd is telling you that "open-file" (presumably part of the load sequence) can't find v.scm. I guess it's on some other directory, so try:

    >%load-path
    ("/usr/local/share/snd" "/usr/local/share/guile/1.9")

"%load-path" is a list of directorties that the "load" function looks at. Apparently these two directories don't have v.scm. So find out where v.scm is ("locate v.scm" is usually the quickest way), and add its directory to %load-path:

    >(set! %load-path (cons "/home/bil/cl" %load-path)) ; I'm adding my "cl" directory to the search list
    #<unspecified>
    >(load-from-path "v.scm")
    #<unspecified>

In Gauche, "load" returns #t if happy, #f if not, and Gauche's name for the directory search list is *load-path*. In Ruby, we'd do it this way:

    >load "v.rb"
    true
    >with_sound() do fm_violin_rb(0, 1.0, 440.0, 0.1) end
    #<With_CLM: output: "test.snd", channels: 1, srate: 22050>

and in Forth:

    snd> "clm-ins.fs" file-eval
    0
    snd> 0.0 1.0 440.0 0.1 ' fm-violin with-sound
    \ filename: test.snd

In most of this document, I'll stick with Scheme as implemented by Guile. extsnd.html and sndscm.html have numerous Ruby and Forth examples, and I'll toss some in here as I go along. You can save yourself a lot of typing by using two features of the listener. First, <TAB> (that is, the key marked TAB) tries to complete the current name, so if you type "fm-<TAB>" the listener completes the name as "fm-violin". And second, you can back up to a previous expression, edit it, move the cursor to the closing parenthesis, and type <RETURN>, and that expression will be evaluated as if you had typed all of it in from the start. Needless to say, you can paste code from this file into the Snd listener.

with-sound opens an output sound file, evaluates its body, closes the file, and then opens it in Snd. If the sound is already open, with-sound replaces it with the new version. The body of with-sound can be any size, and can include anything that you could put in a function body. For example, to get an arpeggio:

    (with-sound ()
      (do ((i 0 (1+ i)))
          ((= i 8))
        (fm-violin (* i .25) .5 (* 100 (1+ i)) .1)))

If that seemed to take awhile, make sure you've turned on optimization:

    >(set! (optimization) 6)
    6

The optimizer, a macro named "run", can usually speed up computations by about a factor of 10.

with-sound, instruments, CLM itself are all optional, of course. We could do everything by hand:

    (let ((sound (new-sound "test.snd" :size 22050))
          (increment (/ (* 440.0 2.0 pi) 22050.0))
          (current-phase 0.0))
      (map-channel (lambda (y)
	    	     (let ((val (* .1 (sin current-phase))))
		       (set! current-phase (+ current-phase increment))
		       val))))

This opens a sound file (via new-sound) and fills it with a .1 amplitude sine wave at 440 Hz. The "increment" calculation turns 440 Hz into a phase increment in radians (we could also use the function hz->radians). The "oscil" generator keeps track of the phase increment for us, so essentially the same thing using with-sound and oscil is:

    (with-sound ()
      (let ((osc (make-oscil 440.0)))
        (do ((i 0 (1+ i)))
	    ((= i 22050))
          (outa i (* .1 (oscil osc)) *output*))))

*output* is the file opened by with-sound, and outa is a function that adds its second argument (the sinusoid) into the current output at the sample given by its first argument ("i" in this case). oscil is our sinusoid generator, created by make-oscil. You don't need to worry about freeing the oscil; we can depend on the Scheme garbage collector to deal with that. All the generators are like oscil in that each is a function that on each call returns the next sample in an infinite stream of samples. An oscillator, for example, returns an endless sine wave, one sample at a time. Each generator consists of a set of functions: make-<gen> sets up the data structure associated with the generator; <gen> produces a new sample; <gen>? checks whether a variable is that kind of generator. Current generator state is accessible via various generic functions such as mus-frequency:

    (set! oscillator (make-oscil :frequency 330))

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

    (oscil oscillator)

The make-<gen> function takes a number of optional arguments, setting whatever state the given generator needs to operate on. The run-time function's first argument is always its associated structure. Its second argument is nearly always something like an FM input or whatever run-time modulation might be desired. Frequency sweeps of all kinds (vibrato, glissando, breath noise, FM proper) are all forms of frequency modulation. So, in normal usage, our oscillator looks something like:

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

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

    make-oscil :optional-key (frequency 440.0) (initial-phase 0.0)

This says that make-oscil has two optional arguments, frequency (in Hz), and initial-phase (in radians). The keywords associated with these values are :frequency and :initial-phase. When make-oscil is called, it scans its arguments; if a keyword is seen, that argument and all following arguments are passed unchanged, but if a value is seen, the corresponding keyword is prepended in the argument list:

    (make-oscil :frequency 440.0)
    (make-oscil :frequency 440.0 :initial-phase 0.0)
    (make-oscil 440.0)
    (make-oscil)
    (make-oscil 440.0 :initial-phase 0.0)
    (make-oscil 440.0 0.0)

are all equivalent, but

    (make-oscil :frequency 440.0 0.0)
    (make-oscil :initial-phase 0.0 440.0)

are in error, because once we see any keyword, all the rest of the arguments have to use keywords too (we can't reliably make any assumptions after that point about argument ordering).

Since we often want to use a given sound-producing algorithm many times (in a note list, for example), it is convenient to package up that code into a function. Our sinewave could be rewritten:

(define (simp start end freq amp)
  (let ((os (make-oscil freq)))
    (do ((i start (1+ i))) 
        ((= i end))
      (outa i (* amp (oscil os)) *output*))))

Now to hear our sine wave:

    (with-sound () (simp 0 22050 330 .1))

This version of "simp" forces you to think in terms of sample numbers ("start" and "end") which are dependent on the overall sampling rate changes. Our first enhancement is to use seconds:

(define (simp beg dur freq amp)
  (let* ((os (make-oscil freq))
	 (start (inexact->exact (floor (* beg (mus-srate)))))
	 (end (+ start (inexact->exact (floor (* dur (mus-srate)))))))
    (do ((i start (1+ i))) 
        ((= i end))
      (outa i (* amp (oscil os)) *output*))))

Now we can use any sampling rate, and call "simp" using seconds:

    (with-sound (:srate 44100) (simp 0 1.0 440.0 0.1))

Our next improvement adds the "run" macro to speed up processing by about a factor of 10:

(define (simp beg dur freq amp)
  (let* ((os (make-oscil freq))
	 (start (inexact->exact (floor (* beg (mus-srate)))))
	 (end (+ start (inexact->exact (floor (* dur (mus-srate)))))))
    (run
      (lambda ()
        (do ((i start (1+ i))) 
            ((= i end))
          (outa i (* amp (oscil os)) *output*))))))

Next we turn the "simp" function into an "instrument". An instrument is a function that has a variety of built-in actions within with-sound. The only change is the word "definstrument":

(definstrument (simp beg dur freq amp)
  (let* ((os (make-oscil freq))
	 (start (inexact->exact (floor (* beg (mus-srate)))))
	 (end (+ start (inexact->exact (floor (* dur (mus-srate)))))))
    (run
      (lambda ()
        (do ((i start (1+ i))) 
            ((= i end))
          (outa i (* amp (oscil os)) *output*))))))

Now we can simulate a telephone:

(define (telephone start telephone-number)
  (let ((touch-tab-1 '(0 697 697 697 770 770 770 852 852 852 941 941 941))
	(touch-tab-2 '(0 1209 1336 1477 1209 1336 1477 1209 1336 1477 1209 1336 1477)))
    (do ((i 0 (1+ i)))
	((= i (length telephone-number)))
      (let* ((num (list-ref telephone-number i))
	     (frq1 (list-ref touch-tab-1 num))
	     (frq2 (list-ref touch-tab-2 num)))
        (simp (+ start (* i .4)) .3 frq1 .1)
        (simp (+ start (* i .4)) .3 frq2 .1)))))

(with-sound () (telephone 0.0 '(7 2 3 4 9 7 1)))

As a last change, let's add an amplitude envelope:

(definstrument (simp beg dur freq amp envelope)
  (let* ((os (make-oscil freq))
         (amp-env (make-env envelope :duration dur :scaler amp))
	 (start (inexact->exact (floor (* beg (mus-srate)))))
         (end (+ start (inexact->exact (floor (* dur (mus-srate)))))))
    (run
      (lambda ()
        (do ((i start (1+ i))) 
            ((= i end))
          (outa i (* (env amp-env) (oscil os)) *output*))))))

A CLM envelope is a list of (x y) break-point pairs. The x-axis bounds are arbitrary, but it is conventional (here at ccrma) to go from 0 to 1.0. The y-axis values are normally between -1.0 and 1.0, to make it easier to figure out how to apply the envelope in various different situations.

    (with-sound () (simp 0 2 440 .1 '(0 0  0.1 1.0  1.0 0.0)))

Add a few more oscils and envs, and you've got the fm-violin.



Generators



oscil


  make-oscil :optional-key (frequency 440.0) (initial-phase 0.0)
  oscil os :optional (fm-input 0.0) (pm-input 0.0)
  oscil? os
  sine-bank amps phases

oscil produces a sine wave (using sin) with optional frequency change (FM). Its first argument is an oscil created by make-oscil. Oscil's second argument is the frequency change (frequency modulation), and the third argument is the phase change (phase modulation). The initial-phase argument to make-oscil is in radians. You can use degrees->radians to convert from degrees to radians. To get a cosine (as opposed to sine), set the initial-phase to (/ pi 2).

sine-bank simply loops through its arrays of amps and phases, summing (* amp (sin phase)) -- it is mostly a convenience function for additive synthesis (the phase-vocoder in particular).

oscil methods
mus-frequencyfrequency in Hz
mus-phasephase in radians
mus-cosines1 (no set!)
mus-incrementfrequency in radians per sample

  (let ((result (sin (+ phase pm-input))))
    (set! phase (+ phase (hz->radians frequency) fm-input))
    result)

One slightly confusing aspect of oscil is that glissando has to be turned into a phase-increment envelope. This means that the envelope y values should be passed through hz->radians:

(define (simp start end freq amp frq-env)
  (let ((os (make-oscil freq)) 
        (frqe (make-env frq-env :dur (- end start) :scaler (hz->radians freq))))
    (do ((i start (1+ i))) 
        ((= i end))
      (outa i (* amp (oscil os (env frqe))) *output*))))

(with-sound () (simp 0 10000 440 .1 '(0 0 1 1))) ; sweep up an octave

Here is an example of FM (here the hz->radians business is folded into the FM index):

(definstrument (simple-fm beg dur freq amp mc-ratio index :optional amp-env index-env)
  (let* ((start (inexact->exact (floor (* beg (mus-srate)))))
	 (end (+ start (inexact->exact (floor (* dur (mus-srate))))))
	 (cr (make-oscil freq))                     ; carrier
         (md (make-oscil (* freq mc-ratio)))        ; modulator
         (fm-index (hz->radians (* index mc-ratio freq)))
         (ampf (make-env (or amp-env '(0 0  .5 1  1 0)) :scaler amp :duration dur))
         (indf (make-env (or index-env '(0 0  .5 1  1 0)) :scaler fm-index :duration dur)))
    (run
      (lambda ()
        (do ((i start (1+ i)))
            ((= i end))
          (outa i (* (env ampf) (oscil cr (* (env indf) (oscil md)))) *output*))))))

;;; (with-sound () (simple-fm 0 1 440 .1 2 1.0))

See fm.html for a discussion of FM (using the Common Lisp version of CLM). The standard additive synthesis instruments use an array of oscillators to create the individual spectral components:

(define (simple-add beg dur freq amp)
  (let* ((start (inexact->exact (floor (* beg (mus-srate)))))
	 (end (+ start (inexact->exact (floor (* dur (mus-srate))))))
	 (arr (make-vector 20)))     ; we'll create a tone with 20 equal amplitude harmonics
    (do ((i 0 (1+ i)))               ;   use the 'f' button to check out the spectrum
	((= i 20))
      (vector-set! arr i (make-oscil (* (1+ i) freq))))
    (run
     (lambda ()
       (do ((i start (1+ i))) 
           ((= i end))
	 (let ((sum 0.0))
	   (do ((k 0 (1+ k)))
	       ((= k 20))
	     (set! sum (+ sum (oscil (vector-ref arr k)))))
	   (out-any i (* amp .05 sum) 0 *output*)))))))

;;; (with-sound () (simple-add 0 1 220 .3))

To compare the Scheme, Ruby, Forth, and C versions of a CLM instrument (not to mention the Common Lisp version in the CLM tarball), here are versions of the bird instrument; it produces a sinusoid with (usually very elaborate) amplitude and frequency envelopes.

(define (scheme-bird start dur frequency freqskew amplitude freq-envelope amp-envelope)
  (let* ((gls-env (make-env freq-envelope (hz->radians freqskew) dur))
         (os (make-oscil :frequency frequency))
         (amp-env (make-env amp-envelope amplitude dur))
	 (len (inexact->exact (round (* (mus-srate) dur))))
	 (beg (inexact->exact (round (* (mus-srate) start))))
	 (end (+ beg len)))
    (run
     (lambda ()
       (do ((i beg (1+ i)))
	   ((= i end))
	 (outa i (* (env amp-env) 
                    (oscil os (env gls-env)))
	       *output*))))))
def ruby_bird(start, dur, freq, freqskew, amp, freq_envelope, amp_envelope)
  gls_env = make_env(:envelope, freq_envelope, :scaler, hz2radians(freqskew), :duration, dur)
  os = make_oscil(:frequency, freq)
  amp_env = make_env(:envelope, amp_envelope, :scaler, amp, :duration, dur)
  run_instrument(start, dur) do
    env(amp_env) * oscil(os, env(gls_env))
  end
end
instrument: forth-bird { f: start f: dur f: freq f: freq-skew f: amp freqenv ampenv -- }
    :frequency freq make-oscil { os }
    :envelope ampenv :scaler amp :duration dur make-env { ampf }
    :envelope freqenv :scaler freq-skew hz>radians :duration dur make-env { gls-env }
    90e random :locsig-degree
    start dur run-instrument  ampf env  gls-env env os oscil-1  f*  end-run
    os gen-free
    ampf gen-free
    gls-env gen-free
;instrument
void c_bird(double start, double dur, double frequency, double freqskew, double amplitude, 
	    double *freqdata, int freqpts, double *ampdata, int amppts, mus_any *output)
{
  off_t beg, end, i;
  mus_any *amp_env, *freq_env, *osc;
  beg = start * mus_srate();
  end = start + dur * mus_srate();
  osc = mus_make_oscil(frequency, 0.0);
  amp_env = mus_make_env(ampdata, amppts, amplitude, 0.0, 1.0, dur, 0, NULL);
  freq_env = mus_make_env(freqdata, freqpts, mus_hz_to_radians(freqskew), 0.0, 1.0, dur, 0, NULL);
  for (i = beg; i < end; i++)
    mus_sample_to_file(output, i, 0, 
		       mus_env(amp_env) * 
		         mus_oscil(osc, mus_env(freq_env), 0.0));
  mus_free(osc);
  mus_free(amp_env);
  mus_free(freq_env);
}

Related generators are sum-of-cosines, sum-of-sines, asymmetric-fm, sine-summation, and waveshape. Some instruments that use oscil are bird and bigbird, fm-violin (v.ins), lbj-piano (clm-ins.scm), vox (clm-ins.scm), and fm-bell (clm-ins.scm). Interesting extensions of oscil include the various summation formulas in dsp.scm (sum-of-n-sines et al). For a sine-bank example, see pvoc.scm. To goof around with FM from a graphical interface, see bess.scm and bess1.scm.



env


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

  env e
  env? e

  env-interp x env :optional (base 1.0)
  envelope-interp x envelope :optional (base 1.0)
env methods
mus-locationnumber of calls so far on this env
mus-incrementbase
mus-databreakpoint list
mus-scalerscaler
mus-offsetoffset
mus-lengthduration in samples
an envelope

An envelope is a list of break point pairs: '(0 0 100 1) is a ramp from 0 to 1 over an x-axis excursion from 0 to 100. This list is passed to make-env along with the scaler applied to the y axis, the offset added to every y value, and the time in samples or seconds that the x axis represents. make-env returns an env generator. env then returns the next sample of the envelope each time it is called. Say we want a ramp moving from .3 to .5 during 1 second.

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

I find the second version easier to read. The first is handy if you have a bunch of stored envelopes.

The base argument determines how the break-points are connected. If it is 1.0 (the default), you get straight line segments. If base is 0.0, you get a step function (the envelope changes its value suddenly to the new one without any interpolation). Any other positive value affects the exponent of the exponential curve connecting the points. A base less than 1.0 gives convex curves (i.e. bowed out), and a base greater than 1.0 gives concave curves (i.e. sagging). If you'd rather think in terms of e^-kt, set the base to (exp k). There are more pictures of these choices in sndscm.html.

base .03 choice base 32 choice

To get arbitrary connecting curves between the break points, treat the output of env as the input to the connecting function. Here's an instrument that maps the line segments into sin x^3:

(definstrument (mapenv beg dur frq amp en)
  (let* ((start (inexact->exact (floor (* beg (mus-srate)))))
	 (end (+ start (inexact->exact (floor (* dur (mus-srate))))))
	 (osc (make-oscil frq))
         (half-pi (* pi 0.5))
	 (zv (make-env en 1.0 dur)))
    (run
     (lambda ()
       (do ((i start (1+ i)))
           ((= i end))
         (let ((zval (env zv))) 
           ;; zval^3 is [0.0..1.0], as is sin between 0 and half-pi.
	   (outa i 
             (* amp (sin (* half-pi zval zval zval)) (oscil osc)) 
             *output*)))))))

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

Or write a function that traces out the curve you want. J.C.Risset's bell curve could be:

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

There are many more such functions in the Snd package. See extensions.scm for examples and pictures.

mus-reset of an env causes it to start all over again from the beginning. To jump to any position in an env, use mus-location. Here's a function that uses these methods to apply an envelope over and over:

(define (strum e)
  (map-channel (lambda (y)
		 (if (> (mus-location e) (mus-length e)) ; mus-length = dur
		     (mus-reset e))     ; start env again (default is to stick at the last value)
		 (* y (env e)))))

;;; (strum (make-env (list 0 0 1 1 10 .6 25 .3 100 0) :end 2000))

To copy an env while changing one aspect (say duration), it's simplest to use make-env:

(defun change-env-dur (e dur)
  (make-env (mus-data e) :scaler (mus-scaler e) :offset (mus-offset e) :base (mus-increment e)
	    :duration dur))

env-interp and envelope-interp return the value of the envelope at some point on the x axis; env-interp operates on an env (the output of make-env), whereas envelope-interp operates on an envelope (a list of breakpoints). There are many more functions that operate on envelopes; see in particular env.scm.

Envelopes

envelope sound: env-channel, env-sound
other enveloping functions: ramp-channel, xramp-channel, smooth-channel
Various operations on envelopes: env.scm
The envelope editor: Edit or View and Envelope
Panning: place-sound in examp.scm
Read sound indexed through envelope: env-sound-interp
Cosine as envelope: cosine-channel, cosine-channel-via-ptree, bell-curve
envelope with sinusoidal connections between points: sine-env-channel
envelope with separate base for each segment: powenv-channel
envelope with x^2 connections: env-squared-channel
envelope with x^n connections: env-expt-channel
envelope with sum-of-cosines connections: blackman4-env-channel



table-lookup


  make-table-lookup :optional-key 
        (frequency 440.0)   ; in Hz
        (initial-phase 0.0) ; in radians 
        wave                ; a vct
        size                ; table size if wave not specified
        type                ; interpolation type (mus-interp-linear)

  table-lookup tl :optional (fm-input 0.0)
  table-lookup? tl

table-lookup performs interpolating table lookup with a lookup index that moves through the table at a speed set by make-table-lookup's "frequency" argument and table-lookup's "fm-input" argument. That is, the waveform in the table is produced repeatedly, the repetition rate set by the frequency arguments. Table-lookup scales its fm-input argument to make its table size appear to be two pi. The intention here is that table-lookup with a sinusoid in the table and a given FM signal produces the same output as oscil with that FM signal. The "type" argument sets the type of interpolation used: mus-interp-none, mus-interp-linear, mus-interp-lagrange, mus-interp-bezier, or mus-interp-hermite.

table-lookup methods
mus-frequencyfrequency in Hz
mus-phasephase in radians
mus-datawave vct
mus-lengthwave size (no set!)
mus-interp-typeinterpolation choice (no set!)
mus-incrementtable increment per sample

(let ((result (array-interp wave phase))) (set! phase (+ phase (hz->radians frequency) (* fm-input (/ (length wave) (* 2 pi))))) result)

In the past, table-lookup was often used for additive synthesis, so there are two functions that make it easier to load up various such waveforms:

 partials->wave synth-data :optional wave-vct (norm #t)
 phase-partials->wave synth-data :optional wave-vct (norm #t)

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

(definstrument (simple-table dur)
  (let ((tab (make-table-lookup :wave (partials->wave '(1 .5  2 .5)))))
    (do ((i 0 (1+ i))) ((= i dur))
      (outa i (* .3 (table-lookup tab)) *output*))))

spectr.clm has a steady state spectra of several standard orchestral instruments, courtesy of James A. Moorer. The drone instrument in clm-ins.scm uses table-lookup for the bagpipe drone. two-tab in the same file interpolates between two tables. See also grani and display-scanned-synthesis.



waveshape


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

  make-polyshape :optional-key 
     (frequency 440.0) (initial-phase 0.0) coeffs 
     (partials '(1 1)) 
     (kind mus-chebyshev-first-kind)
  polyshape w :optional (index 1.0) (fm 0.0)
  polyshape? w

  partials->waveshape :optional-key partials (size *clm-table-size*)
  partials->polynomial partials :optional (kind mus-chebyshev-first-kind)

waveshape and polyshape perform waveshaping; waveshaping drives a sum of Chebyshev polynomials with a sinusoid, creating a sort of cross between additive synthesis and FM; see "Digital Waveshaping Synthesis" by Marc Le Brun in JAES 1979 April, vol 27, no 4, p250. waveshape uses an internal table-lookup generator, whereas polyshape uses the polynomial function. The "kind" argument determines which kind of Chebyshev polynomial is used internally: mus-chebyshev-first-kind or mus-chebyshev-second-kind.

waveshape methods
mus-frequencyfrequency in Hz
mus-phasephase in radians
mus-datawave vct (no set!)
mus-lengthwave size (no set!)
mus-incrementfrequency in radians per sample

(let ((result (array-interp wave (* (length wave) (+ 0.5 (* index 0.5 (sin phase))))))) (set! phase (+ phase (hz->radians frequency) fm)) result) (let ((result (polynomial wave (sin phase)))) (set! phase (+ phase (hz->radians frequency) fm)) result)

In its simplest use, waveshaping is additive synthesis:

(definstrument (simp)
  (let ((wav (make-waveshape :frequency 440 :partials '(1 .5 2 .3 3 .2))))
    (do ((i 0 (1+ i))) ((= i 1000))
      (outa i (waveshape wav) *output*))))

we can also use partials->polynomial with polyshape; bigbird is an example:

(definstrument (bigbird start duration frequency freqskew amplitude freq-env amp-env partials)
  (let* ((beg (inexact->exact (floor (* start (mus-srate)))))
         (end (+ beg (inexact->exact (floor (* duration (mus-srate))))))
         (gls-env (make-env freq-env (hz->radians freqskew) duration))
         (polyos (make-polyshape frequency :coeffs (partials->polynomial partials)))
         (fil (make-one-pole .1 .9))
         (amp-env (make-env amp-env amplitude duration)))
    (run
      (lambda ()
        (do ((i beg (1+ i)))
            ((= i end))
          (outa i 
            (one-pole fil   ; for distance effects
              (* (env amp-env) 
                 (polyshape polyos 1.0 (env gls-env))))
            *output*))))))

(with-sound ()
  (bigbird 0 .05 1800 1800 .2
           '(.00 .00 .40 1.00 .60 1.00 1.00 .0)         ; freq env
           '(.00 .00 .25 1.00 .60 .70 .75 1.00 1.00 .0) ; amp env
           '(1 .5 2 1 3 .5 4 .1 5 .01)))                ; bird song spectrum

partials->waveshape with waveshape produces the same output as partials->polynomial with polyshape. See also the pqw instrument for phase quadrature waveshaping (single-sideband tricks). The fm-violin uses polyshape for the multiple FM section in some cases.



sawtooth-wave, triangle-wave, pulse-train, square-wave


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

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

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

  make-pulse-train :optional-key (frequency 440.0) (amplitude 1.0) (initial-phase (* 2 pi))
  pulse-train s :optional (fm 0.0)
  pulse-train? s
saw-tooth and friends' methods
mus-frequencyfrequency in Hz
mus-phasephase in radians
mus-scaleramplitude arg used in make-<gen>
mus-widthwidth of square-wave pulse (0.0 to 1.0)
mus-incrementfrequency in radians per sample

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

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

(define (cossq c theta)   ; as c -> 1.0+, more of a square wave (try 1.00001)
  (let* ((cs (cos theta)) ; (+ theta pi) if matching sin case (or (- ...))
	 (cp1 (+ c 1.0))
	 (cm1 (- c 1.0))
	 (cm1c (expt cm1 cs))
	 (cp1c (expt cp1 cs)))
    (/ (- cp1c cm1c)
       (+ cp1c cm1c))))  ; from "From Squares to Circles..." Lasters and Sharpe, Math Spectrum 38:2

(define (sinsq c theta) (cossq c (- theta (* 0.5 pi))))
(define (sqsq c theta) (sinsq c (- (sinsq c theta)))) ; a sharper square wave

(let ((angle 0.0))
  (map-channel (lambda (y)
                 (let ((val (* 0.5 (+ 1.0 (sqsq 1.001 angle))))) 
                   (set! angle (+ angle .02)) 
                   val))))

Just for completeness, here's an example:

(define (simple-saw beg dur amp)
  (let* ((os (make-sawtooth-wave 440.0))
	 (start (inexact->exact (floor (* beg (mus-srate)))))
	 (end (+ start (inexact->exact (floor (* dur (mus-srate)))))))
    (run
     (lambda ()
       (do ((i start (1+ i))) 
           ((= i end))
         (outa i (* amp (sawtooth-wave os)) *output*))))))



sum-of-cosines


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

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

sum-of-cosines methods
mus-frequencyfrequency in Hz
mus-phasephase in radians
mus-scaler(/ 1.0 cosines)
mus-cosinescosines arg used in make-<gen>
mus-lengthsame as mus-cosines
mus-incrementfrequency in radians per sample

based on: cos(x) + cos(2x) + ... cos(nx) = (sin((n + .5)x) / (2 * sin(x / 2))) - 1/2 known as the Dirichlet kernel see also cosine-summation, fejer-sum, etc in dsp.scm

(define (simple-soc beg dur freq amp)
  (let* ((os (make-sum-of-cosines 10 freq 0.0))
	 (start (inexact->exact (floor (* beg (mus-srate)))))
	 (end (+ start (inexact->exact (floor (* dur (mus-srate)))))))
    (run
     (lambda ()
       (do ((i start (1+ i))) ((= i end))
	 (out-any i (* amp (sum-of-cosines os)) 0 *output*))))))
sum of cosines example

Almost identical is the following (Scheme) sinc-train generator:

(define* (make-sinc-train #:optional (frequency 440.0) (width #f))
  (let ((range (or width (* pi (- (* 2 (inexact->exact (floor (/ (mus-srate) (* 2.2 frequency))))) 1)))))
    ;; 2.2 leaves a bit of space before srate/2, (* 3 pi) is the minimum width, normally
    (list (- (* range 0.5))
	  range
	  (/ (* range frequency) (mus-srate)))))
	
(define* (sinc-train gen #:optional (fm 0.0))
  (let* ((ang (car gen))
	 (range (cadr gen))
	 (top (* 0.5 range))
	 (frq (caddr gen))
	 (val (if (= ang 0.0) 1.0 (/ (sin ang) ang)))
	 (new-ang (+ ang frq fm)))
    (if (> new-ang top)
	(list-set! gen 0 (- new-ang range))
	(list-set! gen 0 new-ang))
    val))

If you sweep sum-of-cosines upwards in frequency, you'll eventually get foldover; the generator produces its preset number of cosines no matter what. It is possible to vary the spectrum smoothly:

(let ((os (make-sum-of-cosines 4 100.0))
      (pow (make-env '(0 1.0 1 30.0) :end 10000))) ; our "index" envelope in FM jargon
   (map-channel (lambda (y) 
		  (let ((val (sum-of-cosines os 0.0))) 
		    (* (signum val) 
		       (expt (abs val) (env pow)))))))

This trick works on all the pulse-train functions in dsp.scm (or an oscil for that matter!), but perhaps a filter is a simpler approach.



sum-of-sines


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

sum-of-sines produces a sum of sines. It is very similar (good and bad) to sum-of-cosines.

sum-of-sines methods
mus-frequencyfrequency in Hz
mus-phasephase in radians
mus-scalerdependent on number of sines
mus-cosinessines arg used in make-<gen>
mus-lengthsame as mus-cosines
mus-incrementfrequency in radians per sample

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

See also the sine-summation generator, and dsp.scm which has a collection of neat sum-of-sinusoids formulas.



sine-summation


  make-sine-summation :optional-key (frequency 440.0) (initial-phase 0.0) (n 1) (a .5) (ratio 1.0)
  sine-summation s :optional (fm 0.0)
  sine-summation? s
sine-summation methods
mus-frequencyfrequency in Hz
mus-phasephase in radians
mus-scaler"a" parameter; sideband scaler
mus-cosines"n" parameter
mus-incrementfrequency in radians per sample
mus-offset"ratio" parameter

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

sine-summation produces a kind of additive synthesis. See J.A.Moorer, "Signal Processing Aspects of Computer Music" and "The Synthesis of Complex Audio Spectra by means of Discrete Summation Formulae" (Stan-M-5). "n" is the number of sidebands, "a" is the amplitude ratio between successive sidebands, and "ratio" is the ratio between the carrier frequency and the spacing between successive sidebands. A "ratio" of 2 would give odd-numbered harmonics for a (vaguely) clarinet-like sound. The basic idea is very similar to that used in the sum-of-cosines generator, but you have control of the fall-off of the spectrum and the spacing of the partials. The output amplitude of this generator is hard to predict; see Moorer's paper for some normalization functions (and it is numerically a disaster -- don't set "a" to 1.0!).

(definstrument (ss beg dur freq amp :optional (N 1) (a .5) (B-ratio 1.0))
  (let* ((st (inexact->exact (floor (* (mus-srate) beg))))
         (nd (+ st (inexact->exact (floor (* (mus-srate) dur)))))
         (sgen (make-sine-summation :n N :a a :ratio B-ratio :frequency freq)))
    (do ((i st (1+ i))) ((= i nd))
      (outa i (* amp (sine-summation sgen)) *output*))))

There are some surprising sounds lurking in this generator; a sonata for moped, anyone?



ssb-am


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

ssb-am provides single sideband suppressed carrier amplitude modulation, normally used for frequency shifting. The basic notion is to shift a spectrum up or down while cancelling either the upper or lower half of the spectrum. See dsp.scm for a number of curious possibilities (time stretch without pitch shift for example). When this works, which it does more often than I expected, it is much better than the equivalent phase-vocoder or granular synthesis kludges.

ssb-am methods
mus-frequencyfrequency in Hz
mus-phasephase (of embedded sin osc) in radians
mus-orderembedded delay line size
mus-cosines1
mus-lengthsame as mus-order
mus-interp-typemus-interp-none
mus-xcoeffFIR filter coeff
mus-xcoeffsembedded Hilbert transform FIR filter coeffs
mus-dataembedded filter state
mus-incrementfrequency in radians per sample

(define* (ssb-am freq #:optional (order 40)) 
  ;; higher order = better cancellation
  (let* ((car-freq (abs freq))
	 (cos-car (make-oscil car-freq (* .5 pi)))
	 (sin-car (make-oscil car-freq))
	 (dly (make-delay order))
	 (hlb (make-hilbert-transform order)))
    (map-channel 
      (lambda (y)
        (let ((ccos (oscil cos-car))
	      (csin (oscil sin-car))
	      (yh (hilbert-transform hlb y))
  	      (yd (delay dly y)))
          (if (> freq 0.0)
	      (- (* ccos yd) ; shift up
	         (* csin yh))
	      (+ (* ccos yd) ; shift down
	         (* csin yh))))))))




wave-train


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

wave-train produces a wave train (an extension of pulse-train and table-lookup). Frequency is the rate at which full copies of the wave-train's wave are added into the output. Unlike in table-lookup, successive copies of the wave can overlap. With some simple envelopes or filters, you can use this for VOSIM and other related techniques.

wave-train methods
mus-frequencyfrequency in Hz
mus-phasephase in radians
mus-datawave array (no set!)
mus-lengthlength of wave array (no set!)
mus-interp-typeinterpolation choice (no set!)

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

(definstrument (fofins beg dur frq amp vib f0 a0 f1 a1 f2 a2 :optional ve ae)
  (let* ((start (inexact->exact (floor (* beg (mus-srate)))))
         (end (+ start (inexact->exact (floor (* dur (mus-srate))))))
         (ampf (make-env :envelope (or ae (list 0 0 25 1 75 1 100 0)) :scaler amp :duration dur))
         (frq0 (hz->radians f0))
         (frq1 (hz->radians f1))
         (frq2 (hz->radians f2))
         (foflen (if (= (mus-srate) 22050) 100 200))
         (vibr (make-oscil :frequency 6))
	 (vibenv (make-env :envelope (or ve (list 0 1 100 1)) :scaler vib :duration dur))
         (win-freq (/ (* 2 pi) foflen))
         (foftab (make-vct foflen))
         (wt0 (make-wave-train :wave foftab :frequency frq)))
    (do ((i 0 (1+ i)))
        ((= i foflen))
      (set! (vct-ref foftab i) ;; this is not the pulse shape used by B&R
            (* (+ (* a0 (sin (* i frq0))) 
                  (* a1 (sin (* i frq1))) 
                  (* a2 (sin (* i frq2)))) 
               .5 (- 1.0 (cos (* i win-freq))))))
    (run
     (lambda ()
       (do ((i start (1+ i)))
           ((= i end))
         (outa i (* (env ampf) (wave-train wt0 (* (env vibenv) (oscil vibr)))) *output*))))))

(with-sound () (fofins 0 1 270 .2 .001 730 .6 1090 .3 2440 .1)) ; "Ahh"

(with-sound () ; one of JC's favorite demos
  (fofins 0 4 270 .2 0.005 730 .6 1090 .3 2440 .1 '(0 0 40 0 75 .2 100 1) 
          '(0 0 .5 1 3 .5 10 .2 20 .1 50 .1 60 .2 85 1 100 0))
  (fofins 0 4 (* 6/5 540) .2 0.005 730 .6 1090 .3 2440 .1 '(0 0 40 0 75 .2 100 1) 
          '(0 0 .5 .5 3 .25 6 .1 10 .1 50 .1 60 .2 85 1 100 0))
  (fofins 0 4 135 .2 0.005 730 .6 1090 .3 2440 .1 '(0 0 40 0 75 .2 100 1) 
          '(0 0 1 3 3 1 6 .2 10 .1 50 .1 60 .2 85 1 100 0)))


rand, rand-interp

  make-rand :optional-key 
        (frequency 440.0)          ; frequency at which new random numbers occur
        (amplitude 1.0)            ; numbers are between -amplitude and amplitude
        (envelope '(-1 1 1 1))     ; distribution envelope (uniform distribution is the default)
        distribution               ; pre-computed distribution
  rand r :optional (sweep 0.0)
  rand? r

  make-rand-interp :optional-key 
        (frequency 440.0) 
        (amplitude 1.0)
        (envelope '(-1 1 1 1)
        distribution)
  rand-interp r :optional (sweep 0.0)
  rand-interp? r

  mus-random amp
  mus-rand-seed

rand produces a sequence of random numbers between -amplitude and amplitude (a sort of step function). rand-interp interpolates between successive random numbers. rand-interp could be defined as (moving-average agen (rand rgen)) where the averager has the same period (length) as the rand. In both cases, the "envelope" argument or the "distribution" argument determines the random number distribution. mus-random returns a random number between -amplitude and amplitude. mus-rand-seed provides access to the seed for mus-random's random number generator.

rand and rand-interp methods
mus-frequencyfrequency in Hz
mus-phasephase in radians
mus-scaleramplitude arg used in make-<gen>
mus-lengthdistribution table (vct) length
mus-datadistribution table (vct), if any
mus-incrementfrequency in radians per sample

rand: (if (>= phase (* 2 pi)) (set! output (mus-random amplitude))) (set! phase (+ phase (hz->radians frequency) sweep))

There are a variety of ways to get a non-uniform random number distribution: (random (random 1.0)) or (sin (mus-random pi)) are simple examples. Exponential distribution could be:

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

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

    (define (gaussian-noise)
      (let ((val 0.0))
        (do ((i 0 (1+ i))) 
            ((= i 12) (/ val 12.0) )
          (set! val (+ val (random 1.0))))))

You can watch this (or any other distribution) in action via:

(define (add-rands n)
  (let ((bins (make-vector 201 0))
	(rands (make-vector n #f)))
    (do ((i 0 (1+ i)))
	((= i n))
      (vector-set! rands i (make-rand :frequency (mus-srate) :amplitude (/ 100 n)))
      (rand (vector-ref rands i)))
    (do ((i 0 (1+ i)))
	((= i 100000))
      (let ((sum 0.0))
	(do ((k 0 (1+ k)))
	    ((= k n))
	  (set! sum (+ sum (rand (vector-ref rands k)))))
	(let ((bin (inexact->exact (+ 100 (round sum)))))
	  (vector-set! bins bin (+ (vector-ref bins bin) 1)))))
    bins))

(let ((ind (new-sound "test.snd")))
  (do ((n 1 (+ n 1)))
      ((or (c-g?) (= n 12)))
    (let* ((bins (vector->vct (add-rands n)))
	   (pk (vct-peak bins)))
      (vct->channel (vct-scale! bins (/ 1.0 pk)))
      (set! (x-axis-label) (format #f "n: ~D" n))
      (update-time-graph))))

Another way to get different distributions is the "rejection method" in which we generate random number pairs until we get a pair that falls within the desired distribution; see any-random in dsp.scm. The rand and rand-interp generators, however, use the "transformation method". The make-rand and make-rand-interp "envelope" arguments specify the desired distribution function; the generator takes the inverse of the integral of this envelope, loads that into an array, and uses (array-interp (random array-size)). This gives random numbers of any arbitrary distribution at a computational cost equivalent to the waveshape generator (which is very similar). The x axis of the envelope sets the output range (before scaling by the "amplitude" argument), and the y axis sets the relative weight of the corresponding x axis value. So, the default is '(-1 1 1 1) which says "output numbers between -1 and 1, each number having the same chance of being chosen". An envelope of '(0 1 1 0) outputs values between 0 and 1, denser toward 0. If you already have the distribution table (a vct, the result of (inverse-integrate envelope) for example), you can pass it through the "distribution" argument. Here is gaussian noise using the "distribution" argument:

(define (gaussian-envelope s)
  (let ((e '())
	(den (* 2.0 s s)))
    (do ((i 0 (1+ i))
	 (x -1.0 (+ x .1))
	 (y -4.0 (+ y .4)))
	((= i 21))
      (set! e (cons x e))
      (set! e (cons (exp (- (/ (* y y) den))) e)))
    (reverse e)))

(make-rand :envelope (gaussian-envelope 1.0))

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

(define (make-1f-noise n)
  ;; returns an array of rand's ready for the 1f-noise generator
  (let ((rans (make-vector n)))
    (do ((i 0 (1+ i))) 
        ((= i n) rans)
      (vector-set! rans i (make-rand :frequency (/ (mus-srate) (expt 2 i)))))))

(define (1f-noise rans)
  (let ((val 0.0) 
        (len (vector-length rans)))
    (do ((i 0 (1+ i)))
        ((= i len) (/ val len))
      (set! val (+ val (rand (vector-ref rans i)))))))

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

(definstrument (fractal start duration m x amp)
  ;; use formula of M J Feigenbaum
  (let* ((beg (inexact->exact (floor (* (mus-srate) start))))
	 (end (+ beg (inexact->exact (floor (* (mus-srate) duration))))))
    (run
     (lambda ()
       (do ((i beg (1+ i)))
           ((= i end))
         (outa i (* amp x) *output*)
         (set! x (- 1.0 (* m x x))))))))

;;; this quickly reaches a stable point for any m in[0,.75], so:
(with-sound () (fractal 0 1 .5 0 .5)) 
;;; is just a short "ftt"
(with-sound () (fractal 0 1 1.5 .20 .2))

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

(definstrument (attract beg dur amp c) ; c from 1 to 10 or so
  ;; by James McCartney, from CMJ vol 21 no 3 p 6
  (let* ((st (inexact->exact (floor (* beg (mus-srate)))))
	 (nd (+ st (inexact->exact (floor (* dur (mus-srate))))))
	 (a .2) (b .2) (dt .04)
	 (scale (/ (* .5 amp) c))
	 (x1 0.0) (x -1.0) (y 0.0) (z 0.0))
    (run
     (lambda ()
      (do ((i st (1+ i)))
          ((= i nd))
       (set! x1 (- x (* dt (+ y z))))
       (set! y (+ y (* dt (+ x (* a y)))))
       (set! z (+ z (* dt (- (+ b (* x z)) (* c z)))))
       (set! x x1)
       (outa i (* scale x) *output*))))))

which gives brass-like sounds! We can also get all the period doublings and so on from sin:

  (let ((x 0.5)) 
    (map-channel 
      (lambda (y) 
        (let ((val x)) 
          (set! x (* 4 (sin (* pi x)))) 
          val))))

See also dither-channel (dithering), maraca.scm (physical modelling), noise.scm, noise.rb (a truly ancient noise-maker), any-random (arbitrary distribution via the rejection method), and green.scm (bounded Brownian noise).



one-pole, one-zero, two-pole, two-zero

   make-one-pole :optional-key a0 b1    ; b1 < 0.0 gives lowpass, b1 > 0.0 gives highpass
   one-pole f input 
   one-pole? f

   make-one-zero :optional-key a0 a1    ; a1 > 0.0 gives weak lowpass, a1 < 0.0 highpass
   one-zero f input 
   one-zero? f

   make-two-pole :optional-key a0 b1 b2 frequency radius
   two-pole f input 
   two-pole? f

   make-two-zero :optional-key a0 a1 a2 frequency radius
   two-zero f input 
   two-zero? f

These are the simplest of filters.

simple filter methods
mus-xcoeffa0, a1, a2 in equations
mus-ycoeffb1, b2 in equations
mus-order1 or 2 (no set!)
mus-scalertwo-pole and two-zero radius
mus-frequencytwo-pole and two-zero center frequency
one-zero  y(n) = a0 x(n) + a1 x(n-1)
one-pole  y(n) = a0 x(n) - b1 y(n-1)
two-pole  y(n) = a0 x(n) - b1 y(n-1) - b2 y(n-2)
two-zero  y(n) = a0 x(n) + a1 x(n-1) + a2 x(n-2)

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

We can use a one-pole filter as an "exponentially weighted moving average":

    (make-one-pole (/ 1.0 order) (/ (- order) (+ 1.0 order)))

where "order" is more or less how long an input affects the output. The mus-xcoeff and mus-ycoeff functions give access to the filter coefficients. prc95.scm uses them to make "run time" alterations to the filters:

    (set! (mus-ycoeff p 1) (- val))     ; "p" is a one-pole filter, this is setting "b1"
    (set! (mus-xcoeff p 0) (- 1.0 val)) ; this is setting "a0"

We can also use mus-frequency and mus-scaler (the pole "radius") as a more intuitive handle on these coefficients:

    >(define p (make-two-pole :radius .9 :frequency 1000.0))
    #<unspecified>
    >p
    #<two-pole: a0: 1.000, b1: -1.727, b2: 0.810, y1: 0.000, y2: 0.000>
    >(mus-frequency p)
    1000.00025329731
    >(mus-scaler p)
    0.899999968210856
    >(set! (mus-frequency p) 2000.0)
    2000.0
    >p
    #<two-pole: a0: 1.000, b1: -1.516, b2: 0.810, y1: 0.000, y2: 0.000>


formant

  make-formant :optional-key radius frequency (gain 1.0)
  formant f input
  formant? f

formant is a resonator (a two-pole, two-zero bandpass filter) centered at "frequency", with its bandwidth set by "radius".

formant methods
mus-xcoeffa0, a1, a2 in equations
mus-ycoeffb1, b2 in equations
mus-formant-radiusformant radius
mus-frequencyformant center frequency
mus-order2 (no set!)
mus-scalergain
    y(n) = x(n) - 
           r * x(n-2) + 
           2 * r * cos(frq-in-rads) * y(n-1) - 
           r * r * y(n-2)
various formant cases

The formant generator is described in "A Constant-gain Digital Resonator Tuned By a Single Coefficient" by Julius O. Smith and James B. Angell in Computer Music Journal Vol. 6 No. 4 (winter 1982). The filter coefficients are set as a function of the given "radius", "frequency", and "gain". The bandwidth of the resonance is (* 2 (- 1.0 radius)), so as the radius approaches 1.0 (our unit circle), the resonance gets narrower. Use mus-frequency to change the center frequency, and mus-formant-radius to change the radius. In the paper mentioned above, radius ~= e^(-pi*Bw*T) where Bw is the bandwidth in Hz and T is the sampling period (1/sampling-rate); see also "A note on Constant-Gain Digital Resonators" by Ken Steiglitz, CMJ vol 18 No. 4 pp.8-10 (winter 1994). The bandwidth can be specified in Hz:

    (define (compute-radius bw) ; bw in Hz
      (exp (/ (* (- pi) bw) (mus-srate))))

The "gain" argument to make-formant is not used directly; it becomes gain * (1 - radius) or some variant thereof (see mus_make_formant in clm.c). When you set mus-formant-radius, the gain is also adjusted.

formant generators are commonly used in a bank of filters to provide a sort of sample-by-sample spectrum. An example is fade.scm which has various functions for frequency domain mixing. See also grapheq (a non-graphic equalizer), and cross-synthesis. Here's a simpler example that processes a sound by moving a formant around in it:

(define (moving-formant radius move)
  (let ((frm (make-formant radius (cadr move)))
	(menv (make-env move :end (frames))))  ; frequency envelope
    (lambda (x)
      (let ((val (formant frm x)))             ; apply the formant filter
	(set! (mus-frequency frm) (env menv))  ; move its center frequency
	val))))

(map-channel (moving-formant .99 '(0 1200 1 2400)))



filter, iir-filter, fir-filter

   make-filter :optional-key order xcoeffs ycoeffs
   filter fl inp 
   filter? fl

   make-fir-filter :optional-key order xcoeffs
   fir-filter fl inp 
   fir-filter? fl

   make-iir-filter :optional-key order ycoeffs
   iir-filter fl inp 
   iir-filter? fl

   envelope->coeffs :key order envelope dc

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

general filter methods
mus-orderfilter order
mus-xcoeffx (input) coeff
mus-xcoeffsx (input) coeffs
mus-ycoeffy (output) coeff
mus-ycoeffsy (output) coeffs
mus-datacurrent state (input values)
mus-lengthsame as mus-order

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

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

   (define (make-biquad a0 a1 a2 b1 b2) 
      (make-filter 3 (vct a0 a1 a2) (vct 0.0 b1 b2)))

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

(define* (make-hilbert-transform :optional (len 30))
  (let* ((arrlen (1+ (* 2 len)))
	 (arr (make-vct arrlen))
	 (lim (if (even? len) len (1+ len))))
    (do ((i (- len) (1+ i)))
	((= i lim))
      (let* ((k (+ i len))
	     (denom (* pi i))
	     (num (- 1.0 (cos (* pi i)))))
	(if (or (= num 0.0) (= i 0))
	    (vct-set! arr k 0.0)
	    (vct-set! arr k (* (/ num denom) 
			       (+ .54 (* .46 (cos (/ (* i pi) len)))))))))
    (make-fir-filter arrlen arr)))

(define hilbert-transform fir-filter)

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


Filters

filter a sound: filter-sound, filter-channel, and clm-channel
lowpass filter: make-lowpass in dsp.scm
highpass filter: make-highpass in dsp.scm
bandpass filter: make-bandpass in dsp.scm
bandstop filter: make-bandstop in dsp.scm
the usual analog filters (Butterworth, Chebyshev, Bessel, Elliptic): analog-filter.scm
Butterworth filters: make-butter-high-pass, make-butter-low etc in dsp.scm, used in new-effects.scm
IIR filters of various orders/kinds: dsp.scm
Hilbert transform: make-hilbert-transform in dsp.scm
differentiator: make-differentiator in dsp.scm
block DC: see example above, dc-block in prc95.scm, or stereo-flute in clm-ins.scm
hum elimination: see eliminate-hum and notch-channel in dsp.scm
hiss elimination: notch-out-rumble-and-hiss
smoothing filters: smoothing-filter, weighted-moving-average, exponentially-weighted-moving-average
notch-filters: notch-channel and notch-selection
arbitrary spectrum via FIR filter: spectrum->coeffs in dsp.scm
invert an FIR filter: invert-filter in dsp.scm
filtered echo sound effect: flecho in examp.scm
time varying filter: fltit in examp.scm
draw frequency response: use envelope editor or filter control in control panel
Moog filter: moog.scm
Click reduction: remove-clicks, clean-channel
LADSPA-based filter effects: see ladspa.scm
Max Mathews resonator: maxf.scm, maxf.rb, mfilter
graphical equalizer filter bank: graphEq
nonlinear (Volterra) filter: volterra-filter
Kalman filter: kalman-filter-channel
see also convolution, physical modeling, reverb, and fft-based filtering




delay, tap

  make-delay :optional-key size initial-contents (initial-element 0.0) max-size type
  delay d input :optional (pm 0.0)
  delay? d

  tap d :optional (offset 0)
  delay-tick d input

The delay generator is a delay line. The make-delay "size" argument sets the delay line length (in samples). Input fed into a delay line reappears at the output size samples later. If "max-size" is specified in make-delay, and it is larger than "size", the delay line can provide varying-length delays (including fractional amounts). The delay generator's "pm" argument determines how far from the original "size" we are; that is, it is difference between the length set by make-delay and the current actual delay length, size + pm. So, a positive "pm" corresponds to a longer delay line. See zecho in examp.scm for an example. The make-delay "type" argument sets the interpolation type in the fractional delay case: mus-interp-none, mus-interp-linear, mus-interp-all-pass, mus-interp-lagrange, mus-interp-bezier, or mus-interp-hermite.

delay methods
mus-lengthlength of delay (no set!)
mus-ordersame as mus-length
mus-datadelay line itself (no set!)
mus-interp-typeinterpolation choice (no set!)
mus-scalerunused internally, but available for delay specializations

(let ((result (array-interp line (- loc pm))))
  (set! (vct-ref line loc) input)
  (set! loc (1+ loc))
  (if (<= size loc) (set! loc 0))
  result)


The tap function taps a delay line at a given offset from the output point. delay-tick is a function that just puts a sample in the delay line, 'ticks' the delay forward, and returns its "input" argument. See prc95.scm for examples of both of these functions.

(definstrument (echo beg dur scaler secs file)
  (let ((del (make-delay (inexact->exact (round (* secs (mus-srate))))))
	(rd (make-sample-reader 0 file)))
    (run
     (lambda ()
       (do ((i beg (1+ i)))
           ((= i (+ beg dur)))
         (let ((inval (rd)))
  	   (outa i (+ inval (delay del (* scaler (+ (tap del) inval)))) *output*)))))))

(with-sound () (echo 0 60000 .5 1.0 "pistol.snd"))

The mus-scaler field is available for simple extensions of the delay. For example, the following "moving-max" generator uses it to track the current maximum sample value in the delay line (the result is an envelope that tracks the peak amplitude in the last "size" samples).

(define* (make-moving-max #:optional (size 128))
  (let ((gen (make-delay size)))
    (set! (mus-scaler gen) 0.0)
    gen))

(define (moving-max gen y)
  (let* ((absy (abs y))
         (mx (delay gen absy)))
    (if (>= absy (mus-scaler gen))
	(set! (mus-scaler gen) absy)
	(if (>= mx (mus-scaler gen))
	    (set! (mus-scaler gen) (vct-peak (mus-data gen)))))
    (mus-scaler gen)))

The delay generator is used in some reverbs (nrev), many physical models (stereo-flute), dlocsig, chorus effects (chorus in dsp.scm), and flanging (new-effects), and is the basis for about a dozen extensions (comb and friends below).



comb, notch

  make-comb :optional-key (scaler size 1.0) initial-contents (initial-element 0.0) max-size
  comb cflt input :optional (pm 0.0)
  comb? cflt

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

  make-notch :optional-key (scaler 1.0) size initial-contents (initial-element 0.0) max-size
  notch cflt input :optional (pm 0.0)
  notch? cflt

The comb generator is a delay line with a scaler on the feedback. notch is a delay line with a scaler on the current input. filtered-comb is a comb filter with a filter on the feedback. Although normally this is a one-zero filter, it can be any CLM generator. The make-<gen> "size" argument sets the length in samples of the delay line, and the other arguments are also handled as in delay.

comb, filtered-comb, and notch methods
mus-lengthlength of delay (no set!)
mus-ordersame as mus-length
mus-datadelay line itself (no set!)
mus-feedbackscaler (comb only)
mus-feedforwardscaler (notch only)
mus-interp-typeinterpolation choice (no set!)
 comb:           y(n) = x(n - size) + scaler * y(n - size)
 notch:          y(n) = x(n) * scaler  + x(n - size)
 filtered-comb:  y(n) = x(n - size) + scaler * filter(y(n - size))
sonogram of comb

As a rule of thumb, the decay time of the feedback is 7.0 * size / (1.0 - scaler) samples, so to get a decay of feedback-dur seconds,

    (make-delay :size size :scaler (- 1.0 (/ (* 7.0 size) (* feedback-dur (mus-srate)))))

The peak gain is 1.0 / (1.0 - (abs scaler)). The peaks (or valleys in notch's case) are evenly spaced at (mus-srate) / size. The height (or depth) thereof is determined by scaler -- the closer to 1.0 it is, the more pronounced the dips or peaks. See Julius Smith's "An Introduction to Digital Filter Theory" in Strawn "Digital Audio Signal Processing", or Smith's "Music Applications of Digital Waveguides". The following instrument sweeps the comb filter using the pm argument:

(definstrument (zc time dur freq amp length1 length2 feedback)
  (let* ((beg (inexact->exact (floor (* time (mus-srate)))))
         (end (+ beg (inexact->exact (floor (* dur (mus-srate))))))
         (s (make-pulse-train :frequency freq))  ; some raspy input so we can hear the effect easily
         (d0 (make-comb :size length1 :max-size (max length1 length2) :scaler feedback))
         (aenv (make-env '(0 0 .1 1 .9 1 1 0) :scaler amp :duration dur))
         (zenv (make-env '(0 0 1 1) :scaler (- length2 length1) :base 12.0 :duration dur)))
    (run (lambda ()
      (do ((i beg (1+ i))) ((= i end))
        (outa i (* (env aenv) (comb d0 (pulse-train s) (env zenv))) *output*))))))

(with-sound () 
  (zc 0 3 100 .1 20 100 .5) 
  (zc 3.5 3 100 .1 90 100 .95))

The comb filter can produce some nice effects; here's one that treats the comb filter's delay line as the coefficients for an FIR filter:

(define (fir+comb beg dur freq amp size)
  (let* ((start (inexact->exact (floor (* (mus-srate) beg)))) 
	 (end (+ start (inexact->exact (floor (* (mus-srate) dur)))))
	 (dly (make-comb :scaler .9 :size size)) 
	 (flt (make-fir-filter :order size :xcoeffs (mus-data dly))) ; use comb delay line as FIR coefficients
	 (r (make-rand freq)))                                       ; feed comb with white noise
    (run 
     (lambda () 
       (do ((i start (1+ i))) 
	   ((= i end)) 
	 (outa i (* amp (fir-filter flt (comb dly (rand r)))) *output*))))))

(with-sound () 
  (fir+comb 0 2 10000 .001 200)
  (fir+comb 2 2 1000 .0005 400)
  (fir+comb 4 2 3000 .001 300)
  (fir+comb 6 2 3000 .0005 1000))

For comb filter examples, see also chordalize in dsp.scm, any of the standard reverbs (nrev). filtered-comb is used in freeverb. In that case, a one-zero filter is placed in the feedback loop:

    (make-filtered-comb :size len :scaler room-decay-val :filter (make-one-zero :a0 (- 1.0 dmp) :a1 dmp))


all-pass

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

The all-pass or moving average comb generator is just like comb but with an added scaler on the input ("feedforward" is Julius Smith's suggested name for it). If feedback is 0.0, we get a moving average comb filter. If both scale terms are 0.0, we get a pure delay line.

all-pass methods
mus-lengthlength of delay (no set!)
mus-ordersame as mus-length
mus-datadelay line itself (no set!)
mus-feedbackfeedback scaler
mus-feedforwardfeedforward scaler
mus-interp-typeinterpolation choice (no set!)

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

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



moving-average

  make-moving-average :optional-key size initial-contents (initial-element 0.0)
  moving-average f input
  moving-average? f

The moving-average or moving window average generator returns the average of the last "size" values input to it.

moving-average methods
mus-lengthlength of table
mus-ordersame as mus-length
mus-datatable of last 'size' values

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

moving-average is used both to track rms values and to generate ramps between 0 and 1 in a "gate" effect in new-effects.scm and in rms-envelope in env.scm. It can also be viewed as a low-pass filter. And in sounds->segment-data in examp.scm, it is used to segment a sound library. Here is an example (from new-effects.scm) that implements a "squelch" effect, throwing away any samples below a threshhold, and ramping between portions that are squelched and those that are unchanged (to avoid clicks):

(define (squelch-channel amount snd chn gate-size)  ; gate-size = ramp length and rms window length
  (let ((gate (make-moving-average gate-size))
        (ramp (make-moving-average gate-size :initial-element 1.0)))
    (map-channel (lambda (y) 
                   (* y (moving-average ramp                           ; ramp between 0 and 1
                          (if (< (moving-average gate (* y y)) amount) ; local (r)ms value
                              0.0                               ; below amount so squelch
                            1.0))))
                 0 #f snd chn)))

See also dsp.scm for several related functions: moving-rms, moving-sum, moving-length, weighted-moving-average, and exponentially-weighted-moving-average (the latter being just a one-pole filter).



src

  make-src :optional-key input (srate 1.0) (width 5)
  src s :optional (sr-change 0.0) input-function
  src? s
src methods
mus-incrementsrate arg to make-src

The src generator performs sampling rate conversion by convolving its input with a sinc function. make-src's "srate" argument is the ratio between the old sampling rate and the new; an srate of 2 causes the sound to be half as long, transposed up an octave. A negative "srate" reads the sound backwards, if possible.

The "width" argument sets how many neighboring samples to convolve with the sinc function. If you hear high-frequency artifacts in the conversion, try increasing this number; Perry Cook's default value is 40, and I've seen cases where it needs to be 100. It can also be set as low as 2 in some cases. The greater the width, the slower the src generator runs.

The src generator's "sr-change" argument is the amount to add to the current srate on a sample by sample basis (if it's 0.0 and the original make-src srate argument was also 0.0, you get a constant output because the generator is not moving at all). Here's an instrument that provides time-varying sampling rate conversion:

(definstrument (simple-src start-time duration amp srt srt-env filename)
  (let* ((senv (make-env :envelope srt-env :duration duration))
         (beg (inexact->exact (floor (* start-time (mus-srate)))))
         (end (+ beg (inexact->exact (floor (* duration (mus-srate))))))
         (src-gen (make-src :input (make-readin filename) :srate srt)))
    (run
      (lambda ()
        (do ((i beg (1+ i)))
            ((= i end))
          (outa i (* amp (src src-gen (env senv))) *output*))))))

(with-sound () (simple-src 0 4 1.0 0.5 '(0 1 1 2) "oboe.snd"))

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

(definstrument (srcer start-time duration amp srt fmamp fmfreq filename)
  (let* ((os (make-oscil :frequency fmfreq))
         (beg (inexact->exact (floor (* start-time (mus-srate)))))
         (end (+ beg (inexact->exact (floor (* duration (mus-srate))))))
         (src-gen (make-src :input (make-readin filename) :srate srt)))
    (run
      (lambda ()
        (do ((i beg (1+ i)))
            ((= i end))
          (outa i (* amp (src src-gen (* fmamp (oscil os)))) *output*))))))

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

The "input" argument to make-src and the "input-function" argument to src provide the generator with input as it is needed. The input function is a function of one argument (the desired read direction, if the reader can support it), that is called each time src needs another sample of input.

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

There are several other ways to resample a sound. Some of the more interesting ones are in dsp.scm (down-oct, sound-interp, linear-src, etc). To calculate a sound's new duration after a time-varying src is applied, use src-duration.



convolve

  make-convolve :optional-key input filter fft-size filter-size
   convolve ff :optional input-function
   convolve? ff

   convolve-files file1 file2 :optional (maxamp 1.0) (output-file "tmp.snd")
convolve methods
mus-lengthfft size used in the convolution

The convolve generator convolves its input with the impulse response "filter" (a vct). "input" and "input-function" are functions of one argument that are called whenever convolve needs input.

(definstrument (convins beg dur filter file :optional (size 128))
  (let* ((start (inexact->exact (floor (* beg (mus-srate)))))
         (end (+ start (inexact->exact (floor (* dur (mus-srate))))))
         (ff (make-convolve :input (make-readin file) :fft-size size :filter filter)))
    (run
      (lambda ()
        (do ((i start (1+ i)))
            ((= i end))
          (outa i (convolve ff) *output*))))))

(with-sound () 
  (convins 0 2 (vct 1.0 0.5 0.25 0.125) "oboe.snd")) ; same as fir-filter with those coeffs

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



granulate

  make-granulate :optional-key   
        input
        (expansion 1.0)   ; how much to lengthen or compress the file
        (length .15)      ; length of file slices that are overlapped
        (scaler .6)       ; amplitude scaler on slices (to avoid overflows)
        (hop .05)         ; speed at which slices are repeated in output
        (ramp .4)         ; amount of slice-time spent ramping up/down
        (jitter 1.0)      ; affects spacing of successive grains
        max-size          ; internal buffer size
        edit              ; grain editing function
  granulate e :optional input-function edit-function
  granulate? e
granulate methods
mus-frequencytime (seconds) between output grains (hop)
mus-ramplength (samples) of grain envelope ramp segment
mus-hoptime (samples) between output grains (hop)
mus-scalergrain amp (scaler)
mus-incrementexpansion
mus-lengthgrain length (samples)
mus-datagrain samples (a vct)
mus-locationgranulate's local random number seed

result = overlap add many tiny slices from input

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

The duration of each slice is "length" -- the longer the slice, the more the effect resembles reverb. The portion of the length (on a scale from 0 to 1.0) spent on each ramp (up or down) is set by the "ramp" argument. It can control the smoothness of the result of the overlaps.

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

The overall amplitude scaler on each segment is set by the "scaler" argument; this is used to try to avoid overflows as we add all these zillions of segments together. "expansion" determines the input hop in relation to the output hop; an expansion-amount of 2.0 should more or less double the length of the original, whereas an expansion-amount of 1.0 should return something close to the original tempo. "input" and "input-function" are the same as in src and convolve (functions of one argument that return a new input sample whenever they are called by granulate).

(definstrument (granulate-sound file beg :optional dur (orig-beg 0.0) (exp-amt 1.0))
  (let* ((f-srate (mus-sound-srate file))
	 (f-start (inexact->exact (round (* f-srate orig-beg))))
         (f (make-readin file :start f-start))
	 (st (inexact->exact (floor (* beg (mus-srate)))))
	 (new-dur (or dur (- (mus-sound-duration file) orig-beg)))
	 (exA (make-granulate :input f :expansion exp-amt))
	 (nd (+ st (inexact->exact (floor (* (mus-srate) new-dur))))))
    (run
     (lambda ()
       (do ((i st (1+ i)))
           ((= i nd))
         (outa i (granulate exA) *output*))))))

(with-sound () (granulate-sound "now.snd" 0 3.0 0 2.0))

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

(definstrument (grev beg dur exp-amt file file-beg)
  (let* ((exA (make-granulate :expansion exp-amt))
	 (fil (make-file->sample file))
	 (ctr file-beg))
    (run
      (lambda ()
       (do ((i beg (1+ i)))
           ((= i (+ beg dur)))
         (outa i (granulate exA
		   (lambda (dir)
		     (let ((inval (file->sample fil ctr 0)))
		       (if (> ctr 0) (set! ctr (1- ctr)))
		       inval)))
	        *output*))))))

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

The "edit" argument can be a function of one argument, the current granulate generator. It is called just before a grain is added into the output buffer. The current grain is accessible via mus-data. The edit function, if any, should return the length in samples of the grain, or 0. In the following example, we use the edit function to reverse every other grain:

(let ((forward #t))
  (let ((grn (make-granulate :expansion 2.0
			     :edit (lambda (g)
				     (let ((grain (mus-data g))  ; current grain
					   (len (mus-length g))) ; current grain length
				       (if forward ; no change to data
				           (set! forward #f)
					   (begin
					     (set! forward #t)
					     (vct-reverse! grain len)))
				       len))))
	(rd (make-sample-reader 0)))
    (map-channel (lambda (y) (granulate grn (lambda (dir) (rd)))))))


phase-vocoder

  make-phase-vocoder :optional-key 
        input (fft-size 512) (overlap 4) interp (pitch 1.0) analyze edit synthesize
  phase-vocoder pv input-function analyze-function edit-function synthesize-function
  phase-vocoder? pv
phase-vocoder methods
mus-frequencypitch shift
mus-lengthfft-size
mus-incrementinterp
mus-hopfft-size / overlap

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

(definstrument (simple-pvoc beg dur amp size file)
  (let* ((start (inexact->exact (floor (* beg (mus-srate)))))
	 (end (+ start (inexact->exact (floor (* dur (mus-srate))))))
	 (sr (make-phase-vocoder (make-readin file) :fft-size size)))
    (run
      (lambda ()
        (do ((i start (1+ i)))
            ((= i end))
          (outa i (* amp (phase-vocoder sr)) *output*))))))

(with-sound () (simple-pvoc 0 2.0 1.0 512 "oboe.snd"))

clm23.scm has a variety of instruments calling the phase-vocoder generator, including pvoc-e that specifies all of the functions with their default values (that is, it explicitly passes in functions that do what the phase-vocoder would have done without any function arguments). pvoc.scm implements the phase-vocoder directly in Scheme (rather than going through the CLM generator).



asymmetric-fm

  make-asymmetric-fm :optional-key (frequency 440.0) (initial-phase 0.0) (r 1.0) (ratio 1.0)
  asymmetric-fm af index :optional (fm 0.0)
  asymmetric-fm? af
asymmetric-fm methods
mus-frequencyfrequency in Hz
mus-phasephase in radians
mus-scaler"r" parameter; sideband scaler
mus-offset"ratio" parameter
mus-incrementfrequency in radians per sample

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

The asymmetric-fm generator provides a way around the symmetric spectra normally produced by FM. See Palamin and Palamin, "A Method of Generating and Controlling Asymmetrical Spectra" JAES vol 36, no 9, Sept 88, p671-685: this is another extension of the sine-summation and sum-of-cosines approach.

r is the ratio between successive sideband amplitudes, r > 1.0 pushes energy above the carrier, r < 1.0 pushes it below. (r = 1.0 gives normal FM). ratio is the ratio between the carrier and modulator (i.e. sideband spacing). It's somewhat inconsistent that asymmetric-fm takes index (the fm-index) as its second argument, but otherwise it would be tricky to get time-varying indices. The generator's output amplitude is not always easy to predict.

(definstrument (asy beg dur freq amp index :optional (r 1.0) (ratio 1.0))
  (let* ((st (inexact->exact (floor (* beg (mus-srate)))))
         (nd (+ st (inexact->exact (floor (* dur (mus-srate))))))
         (asyf (make-asymmetric-fm :r r :ratio ratio :frequency freq)))
    (do ((i st (1+ i))) 
        ((= i nd))
      (outa i (* amp (asymmetric-fm asyf index 0.0)) *output*))))

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



frames and mixers

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

make-frame chans :rest argscreate frame and load it with args
frame? objis obj a frame
frame-ref f1 chanreturn f1[chan]
frame-set! f1 chan valf1[chan] = val (also set! with frame-ref)
frame+ f1 f2 :optional outfadd f1 and f2 element-wise, return new frame (or outf)
frame* f1 f2 :optional outfmultiply f1 and f2 element-size, return new frame (or outf)

make-mixer chans :rest argscreate a mixer and load it with args
make-scalar-mixer chans sclcreate a mixer with scl on the diagonal
mixer? objis obj a mixer
mixer-ref m1 in outm1[in,out] (use set! to change)
mixer-set! m1 in out valm1[in,out] = val (also set! with mixer-ref)
mixer* m1 m2 :optional outmmatrix multiply of m1 and m2, return new mixer (or outm)
mixer+ m1 m2 :optional outmmatrix add of m1 and m2, return new mixer (or outm)

frame->frame mf mf :optional outfpass frame through mixer, return new frame (or outf)
frame->list framereturn list of frame's contents
sample->frame mf sample :optional outfpass sample through mf (a frame or mixer), return new frame (or outf)
frame->sample mf framepass frame through mf (a frame or mixer), return sample
frame and mixer methods
mus-channelsnumber of channels accommodated
mus-lengthsame as mus-channels
mus-dataframe data (vct)

The arguments to frame*, frame+, mixer*, and mixer+ can be floats as well as mixers and frames. In that case, the mixer or frame is either scaled by the float, or the float is added to each element. In matrix terminology, a mixer is a square matrix, a frame is a column (or row) vector, mixer* is a matrix multiply, and so on. The form (frame->frame frame mixer frame) multiplies a row vector (the first frame) by a matrix (the mixer), whereas (frame->frame mixer frame frame) multiplies a matrix by a column vector. See frame.scm for many more frame-related functions, and mixer.scm for mixer functions. In Ruby, frame* is frame_multiply, frame+ is frame_add, and mixer* is mixer_multiply.

    >(define f1 (make-frame 3 1.0 0.5 0.1))
    #<unspecified>
    >f1
    #<frame[3]: [1.000 0.500 0.100]>
    >(frame-ref f1 2)
    0.100000001490116
    >(frame* f1 2.0)
    #<frame[3]: [2.000 1.000 0.200]>
    >(define f2 (make-frame 3 0.0 1.0 0.0))
    #<unspecified>
    >(frame+ f1 f2)
    #<frame[3]: [1.000 1.500 0.100]>
    >(frame->sample f1 f2) ; dot-product in this case
    0.5
    >(define m1 (make-mixer 3 1 0 0  0 1 0  0 0 2))
    #<unspecified>
    >m1
    #<mixer: chans: 3, [
     1.000 0.000 0.000
     0.000 1.000 0.000
     0.000 0.000 2.000
    ]>
    >(mixer-ref m1 2 2)
    2.0
    >(frame->frame m1 f1)
    #<frame[3]: [1.000 0.500 0.200]>
    >(mus-length m1)
    3
    >(mus-data f1)
    #<vct[len=3]: 1.000 0.500 0.100>


sound IO

Sound file IO is based on a set of file readers and writers that deal either in samples, frames, or vcts. The six functions are file->sample, sample->file, file->frame, frame->file, array->file, and file->array. The name "array" is used here, rather than "vct" for historical reasons (the CL version of CLM predates Snd by many years). These functions are then packaged up in more convenient forms as in-any, out-any, locsig, readin, etc. Within with-sound, the variable *output* is bound to the with-sound output file via a sample->file object.

  make-file->sample name :optional (buffer-size 8192)
  make-sample->file name :optional (chans 1) (format mus-lfloat) (type mus-next) comment
  file->sample? obj
  sample->file? obj
  file->sample obj samp :optional chan
  sample->file obj samp chan val
  continue-sample->file file

  make-file->frame name :optional (buffer-size 8192)
  make-frame->file name :optional (chans 1) (format mus-lfloat) (type mus-next) comment
  frame->file? obj
  file->frame? obj
  file->frame obj samp :optional outf
  frame->file obj samp val
  continue-frame->file file

  file->array file channel beg dur array
  array->file file data len srate channels

  mus-input? obj
  mus-output? obj
  mus-close obj
  *output*
  *reverb*
  mus-file-buffer-size

file->sample writes a sample to a file, frame->file writes a frame, file->sample reads a sample from a file, and file->frame reads a frame. continue-frame->file and continue-sample->file reopen an existing file to continue adding sound data to it. mus-output? returns #t is its argument is some sort of file writing generator, and mus-input? returns #t if its argument is a file reader. file->frame returns a new frame unless you pass it an "outf" argument (a frame). In make-file->sample and make-file->frame, the buffer-size defaults to mus-file-buffer-size. There are many examples of these functions in snd-test.scm, clm-ins.scm, and clm23.scm. Here is one that uses file->sample to mix in a sound file (there are a zillion other ways to do this):

(define (simple-f2s beg dur amp file)
  (let* ((start (inexact->exact (floor (* beg (mus-srate)))))
	 (end (+ start (inexact->exact (floor (* dur (mus-srate))))))
	 (fil (make-file->sample file))
	 (ctr 0))
    (run
     (lambda ()
       (do ((i start (1+ i))) ((= i end))
	 (out-any i (* amp (file->sample fil ctr 0)) 0 *output*)
	 (set! ctr (1+ ctr)))))))

If you forget to call mus-close, the GC will eventually do it for you.



in-any, out-any

  out-any loc data channel output
  outa loc data output
  outb loc data output
  outc loc data output
  outd loc data output

  in-any loc channel input
  ina loc input
  inb loc input

out-any adds its "data" argument (a sound sample) into the "output" object at sample position "loc". In with-sound, the current output is *output* and the reverb output is *reverb*. outa is the same as out-any with a channel of 0. The "output" argument can be a vct or a sound-data object, as well as the more usual frame->file object.

in-any returns the sample at position "loc" in "input". ina is the same as in-any with a channel of 0.

(definstrument (simple-ina beg dur amp file)
  (let* ((start (inexact->exact (floor (* beg (mus-srate)))))
	 (end (+ start (inexact->exact (floor (* dur (mus-srate))))))
	 (fil (make-file->sample file)))
    (run
      (lambda ()
        (do ((i start (1+ i)))
            ((= i end))
          (outa i 
             (* amp (in-any i 0 fil)) ; same as (ina i fil)
             *output*))))))

(with-sound () (simple-ina 0 1 .5 "oboe.snd"))


readin

 make-readin :optional-key file (channel 0) (start 0) (direction 1) size
 readin rd
 readin? rd
readin methods
mus-channelchannel arg to make-readin (no set!)
mus-locationcurrent location in file
mus-incrementsample increment (direction arg to make-readin)
mus-file-namename of file associated with gen
mus-lengthnumber of frames in file associated with gen

readin returns successive samples from a file. Its "file" argument is the input file's name. "start" is the frame at which to start reading the input file. "channel" is which channel to read (0-based). "size" is the read buffer size in samples. It defaults to mus-file-buffer-size. Here is an instrument that applies an envelope to a sound file using readin and env:

(definstrument (env-sound file beg :optional (amp 1.0) (amp-env '(0 1 100 1)))
  (let* ((st (inexact->exact (floor (* beg (mus-srate)))))
         (dur (mus-sound-duration file))
         (rev-amount .01)
         (rdA (make-readin file))
         (ampf (make-env amp-env amp dur))
         (nd (+ st (inexact->exact (floor (* (mus-srate) dur))))))
    (run
      (lambda ()
        (do ((i st (1+ i)))
	    ((= i nd))
          (let ((outval (* (env ampf) (readin rdA))))
  	    (outa i outval *output*)
	    (if *reverb* 
              (outa i (* outval rev-amount) *reverb*))))))))

(with-sound () (env-sound "oboe.snd" 0 1.0 '(0 0 1 1 2 1 3 0)))


locsig

 make-locsig :optional-key 
        (degree 0.0) (distance 1.0) (reverb 0.0) 
        (channels 1) output revout 
        (type mus-interp-linear)
 locsig loc i in-sig
 locsig? loc

 locsig-ref loc chan
 locsig-set! loc chan val
 locsig-reverb-ref loc chan
 locsig-reverb-set! loc chan val

 move-locsig loc degree distance
 locsig-type ()
locsig methods
mus-dataoutput scalers (a vct)
mus-xcoeffreverb scaler
mus-xcoeffsreverb scalers (a vct)
mus-channelsoutput channels
mus-lengthoutput channels

locsig normally takes the place of out-any in an instrument. It tries to place a signal in a N-channel circle by scaling the respective amplitudes ("that old trick never works"). "reverb" determines how much of the direct signal gets sent to the reverberator. "distance" tries to imitate a distance cue by fooling with the relative amounts of direct and reverberated signal (independent of the "reverb" argument). The distance should be greater than or equal to 1.0. "type" (returned by the function locsig-type) can be mus-interp-linear (the default) or mus-interp-sinusoidal. The mus-interp-sinusoidal case uses sin and cos to set the respective channel amplitudes (this is reported to help with the "hole-in-the-middle" problem). The output argument can be a vct or a sound-data object, as well as a frame->file generator.

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

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

Here is an instrument that has envelopes on the distance and degrees, and optionally reverberates a file:

(definstrument (space file onset duration :key (distance-env '(0 1 100 10)) (amplitude-env '(0 1 100 1))
		     (degree-env '(0 45 50 0 100 90)) (reverb-amount .05))
  (let* ((beg (inexact->exact (floor (* onset (mus-srate)))))
	 (end (+ beg (inexact->exact (floor (* (mus-srate) duration)))))
         (loc (make-locsig :degree 0 :distance 1 :reverb reverb-amount :output *output* :revout *reverb*))
         (rdA (make-readin :file file))
         (dist-env (make-env distance-env :duration duration))
         (amp-env (make-env amplitude-env :duration duration))
         (deg-env (make-env degree-env :scaler (/ 1.0 90.0) :duration duration))
         (dist-scaler 0.0))
    (run
      (lambda ()
        (do ((i beg (1+ i)))
            ((= i end))
          (let ((rdval (* (readin rdA) (env amp-env)))
	        (degval (env deg-env))
	        (distval (env dist-env)))
            (set! dist-scaler (/ 1.0 distval))
            (set! (locsig-ref loc 0) (* (- 1.0 degval) dist-scaler))
            (if (> (mus-channels *output*) 1)
                (set! (locsig-ref loc 1) (* degval dist-scaler)))
            (if *reverb* 
                (set! (locsig-reverb-ref loc 0) (* reverb-amount (sqrt dist-scaler))))
            (locsig loc i rdval)))))))

(with-sound (:reverb jc-reverb :channels 2) (space "pistol.snd" 0 3 :distance-env '(0 1 1 2) :degree-env '(0 0 1 90)))

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

(definstrument (move-osc start dur freq amp :key (degree 0) (dist 1.0) (reverb 0))
  (let* ((beg (inexact->exact (floor (* start (mus-srate)))))
         (end (+ beg (inexact->exact (floor (* dur (mus-srate))))))
         (car (make-oscil freq))
         (loc (make-locsig :degree degree :distance dist :channels 2 :output *output*))
	 (pan-env (make-env '(0 0 1 90) :duration dur)))
    (run
      (lambda ()
        (do ((i beg (1+ i)))
            ((= i end))
         (let ((ut (* amp (oscil car))))
	   (move-locsig loc (env pan-env) dist)
           (locsig loc i ut)))))))



move-sound


 make-move-sound dlocs-list output revout
 move-sound dloc i in-sig
 move-sound? dloc

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

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

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

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


Functions

Generic Functions

The generators respond to a set of "generic" functions: mus-frequency, for example, tries to return (or set) a generator's frequency, where that makes sense. The generic functions are:

mus-channelchannel being read/written
mus-channelschannels open
mus-cosinessinusoids in output
mus-dataarray of data
mus-describedescription of current state
mus-feedbackfeedback coefficient
mus-feedforwardfeedforward coefficient
mus-file-namefile being read/written
mus-formant-radiuswidth of formant
mus-frequencyfrequency (Hz)
mus-hophop size for block processing
mus-incrementvarious increments
mus-interp-typeinterpolation type (mus-interp-linear, etc)
mus-lengthdata array length
mus-locationsample location for reads/writes
mus-namegenerator name ("oscil")
mus-offsetenvelope offset
mus-orderfilter order
mus-phasephase (radians)
mus-rampgranulate grain envelope ramp setting
mus-resetset gen to default starting state
mus-runrun any generator
mus-scalerscaler, normally on an amplitude
mus-widthwidth of interpolation tables, etc
mus-xcoeffx (input) coefficient
mus-xcoeffsarray of x (input) coefficients
mus-ycoeffy (output, feedback) coefficient
mus-ycoeffsarray of y (feedback) coefficients
  mus-float-equal-fudge-factor     how far apart values can be and still be considered equal
  mus-array-print-length ()        how many array (vct) elements to print in mus-describe

Many of these are settable: (set! (mus-frequency osc1) 440.0) sets osc1's current frequency to (hz->radians 440.0). In Scheme, Ruby and Forth, mus-generator? returns #t if its argument is a CLM generator.

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

;;; (with-sound () (backandforth 0 10 "pistol.snd" 2.0))

Useful functions

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

hz->radians freqconvert freq to radians per sample
radians->hz radsconvert rads to Hz
db->linear dBconvert dB to linear value
linear->db valconvert val to dB
times->samples start durationconvert start and duration from seconds to samples (beg+dur in latter case)
samples->seconds sampsconvert samples to seconds
seconds->samples secsconvert secondss to samples
degrees->radians degsconvert degrees to radians
radians->degrees radsconvert radians to degrees
clear-array arrset all values in arr to 0.0
mus-sratesrate in with-sound

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

  freq-in-hz * 2 * pi / (mus-srate).  

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


polynomial

   polynomial coeffs x

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


array-interp, dot-product, sine-bank

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

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

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

dot-product is the usual "inner product" or "scalar product". We could define our own FIR filter using dot-product:

(define (make-fr-filter coeffs)
  (list coeffs (make-vct (vct-length coeffs))))

(define (fr-filter flt x)
  (let* ((coeffs (car flt))
	 (xs (cadr flt))
	 (xlen (vct-length xs)))
    (vct-move! xs (- xlen 1) (- xlen 2) #t)
    (vct-set! xs 0 x)
    (dot-product coeffs xs xlen)))

edot-product returns a dot product, the sum of e^(freq*i) with data[i], i going from 0 to (1 less than) data's size. freq and data can be complex, as can the return value. (This is intended for DFT applications).

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


contrast-enhancement

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

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

Waveshaping can provide a similar effect:

(definstrument brighten (start duration file file-maxamp partials)
  (multiple-value-bind (beg end) (times->samples start duration)
  (let ((coeffs (partials->polynomial (normalize-partials partials)))
        (rd (make-readin file)))
	    (run (loop for i from beg below end do
		   (outa i (* file-maxamp (polynomial coeffs (/ (readin rd) file-maxamp)))))))))

(with-sound () (brighten 0 3 "oboe" .15 '(1 1 3 .5 7 .1)))

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

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

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

A similar (slightly simpler) effect (in Snd/Scheme) is:

(let ((mx (maxamp))) 
  (map-channel 
    (lambda (y) 
      (* mx (sin (/ (* pi y) mx))))))

This modulates the sound but keeps the output maxamp the same as the input. There is a similar function in sndscm.html that does this kind of scaling throughout the sound, resulting in a steady modulation, rather than an intensification of just the peaks -- see moving-max.


ring-modulate, amplitude-modulate

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

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

  (defvar carrier (make-oscil carrier-freq (* .5 pi)))
  ...
  (amplitude-modulate 1.0 (oscil carrier) signal)

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

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

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

We can use these to shift all the components of a signal by the same amount up or down ("single-sideband modulation"). But this is exactly what the ssb-am generator provides.


FFT (fourier transform)

  fft rdat idat fftsize :optional sign
  make-fft-window :optional-key type size (beta 0.0) (alpha 0.0)
  multiply-arrays rdat window
  rectangular->polar rdat idat
  polar->rectangular rdat idat
  spectrum rdat idat window norm-type
  convolution rdat idat size

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

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

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

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


def-clm-struct

def-clm-struct is syntactically like def-struct, but sets up the struct field names for the run macro. There are several examples in prc-toolkit95.lisp, and other instruments. In CL, the fields can only be of a numerical type (no generators, for example). Elsewhere, use :type clm for any such fields (see snd-test.scm for examples).


Definstrument

It's hard to decide what's an "instrument" in this context, but I think I'll treat it as something that can be called as a note in a notelist (say in with-sound) and generate its own sound. Test 23 in snd-test.scm has a with-sound that includes most of these instruments.

instrumentfunctionCLSchemeRubyForth
complete-add additive synthesis add.ins
addflts filters addflt.ins dsp.scm dsp.rb
add-sound mix in a sound file addsnd.ins
anoi noise reduction anoi.ins clm-ins.scm clm-ins.rb clm-ins.fs
autoc autocorrelation-based pitch estimation (Bret Battey) autoc.ins
badd fancier additive synthesis (Doug Fulton) badd.ins
fm-bell fm bell sounds (Michael McNabb) bell.ins clm-ins.scm clm-ins.rb clm-ins.fs
bigbird waveshaping (bird.clm and bird.ins) bigbird.ins bird.scm bird.rb clm-ins.fs, bird.fs
canter fm (bag.clm -- bagpipes) (Peter Commons) canter.ins clm-ins.scm clm-ins.rb clm-ins.fs
cellon feedback fm (Stanislaw Krupowicz) cellon.ins clm-ins.scm clm-ins.rb clm-ins.fs
cnvrev convolution (aimed at reverb) cnv.ins
moving sounds quad sound movement (Fernando Lopez-Lezcano) dlocsig.lisp dlocsig.scm dlocsig.rb
drone additive synthesis (bag.clm) (Peter Commons) drone.ins clm-ins.scm clm-ins.rb clm-ins.fs
granulate-sound examples of the granulate generator (granular synthesis) expsrc.ins clm-ins.scm clm-ins.rb clm-ins.fs
cross-fade cross-fades and dissolves in the frequency domain fade.ins fade.scm
filter-sound filter a sound file fltsnd.ins dsp.scm
stereo-flute physical model of a flute (Nicky Hind) flute.ins clm-ins.scm clm-ins.rb clm-ins.fs
fm examples fm bell, gong, drum (Paul Weineke, Jan Mattox) fmex.ins clm-ins.scm clm-ins.rb clm-ins.fs
Jezar's reverb fancy reverb (Jezar Wakefield) freeverb/freeverb.ins freeverb.scm freeverb.rb clm-ins.fs
fofins FOF synthesis clm.html clm-ins.scm clm-ins.rb clm-ins.fs
fullmix a mixer fullmix.ins clm-ins.scm clm-ins.rb clm-ins.fs
grani granular synthesis (Fernando Lopez-Lezcano) grani.ins grani.scm
grapheq graphic equalizer (Marco Trevisani) grapheq.ins clm-ins.scm clm-ins.rb clm-ins.fs
fm-insect fm insect.ins clm-ins.scm clm-ins.rb
jc-reverb an old reverberator (jlrev is a cavernous version) jcrev.ins jcrev.scm clm-ins.rb clm-ins.fs
fm-voice fm voice (John Chowning) jcvoi.ins
kiprev a fancier (temperamental) reverberator (Kip Sheeline) kiprev.ins
lbj-piano additive synthesis piano (Doug Fulton) lbjPiano.ins clm-ins.scm clm-ins.rb clm-ins.fs
maraca Perry Cook's maraca physical models maraca.ins maraca.scm maraca.rb
maxfilter Juan Reyes modular synthesis maxf.ins maxf.scm maxf.rb
mlb-voice fm (originally waveshaping) voice (Marc LeBrun) mlbvoi.ins clm-ins.scm clm-ins.rb clm-ins.fs
moog filters Moog filters (Fernando Lopez-Lezcano) moog.lisp moog.scm
fm-noise noise maker noise.ins noise.scm noise.rb clm-ins.fs
nrev a popular reverberator (Michael McNabb) nrev.ins clm-ins.scm clm-ins.rb clm-ins.fs
one-cut a "cut and paste" instrument (Fernando Lopez-Lezcano) one-cut.ins
p Scott van Duyne's piano physical model piano.ins piano.scm piano.rb
pluck Karplus-Strong synthesis (David Jaffe) pluck.ins clm-ins.scm clm-ins.rb clm-ins.fs
pqw waveshaping pqw.ins clm-ins.scm clm-ins.rb clm-ins.fs
pqw-vox waveshaping voice pqwvox.ins clm-ins.scm clm-ins.rb clm-ins.fs
physical models physical modelling (Perry Cook) prc-toolkit95.lisp prc95.scm prc95.rb clm-ins.fs
various ins from Perry Cook's Synthesis Toolkit prc96.ins clm-ins.scm clm-ins.rb clm-ins.fs
pvoc phase vocoder (Michael Klingbeil) pvoc.ins pvoc.scm pvoc.rb
resflt filters (3 resonators) (Xavier Serra, Richard Karpen) resflt.ins clm-ins.scm clm-ins.rb clm-ins.fs
reson fm formants (John Chowning) reson.ins clm-ins.scm clm-ins.rb clm-ins.fs
ring-modulate ring-modulation of sounds (Craig Sapp) ring-modulate.ins examp.scm examp.rb
rmsenv rms envelope of sound (Bret Battey) rmsenv.ins
track-rms rms envelope of sound file (Michael Edwards) rmsp.ins
pins spectral modelling san.ins clm-ins.scm clm-ins.rb clm-ins.fs
scanned Juan Reyes scanned synthesis instrument scanned.ins dsp.scm
scentroid spectral scentroid envelope (Bret Battey) scentroid.ins dsp.scm
shepard Shepard tones (Juan Reyes) shepard.ins sndscm.html
singer Perry Cook's vocal tract physical model singer.ins singer.scm singer.rb
sndwarp Csound-like sndwarp generator (Bret Battey) sndwarp.ins sndwarp.scm
stochastic Bill Sack's stochastic synthesis implementation stochastic.ins
bow Juan Reyes bowed string physical model strad.ins strad.scm strad.rb
fm-trumpet fm trumpet (Dexter Morrill) trp.ins clm-ins.scm clm-ins.rb clm-ins.fs
various ins granular synthesis, formants, etc ugex.ins clm-ins.scm clm-ins.rb
test ins CLM regression tests -- see clm-test.lisp ug(1,2,3,4).ins clm23.scm
fm-violin fm violin (fmviolin.clm, popi.clm) v.ins v.scm v.rb clm-ins.fs
vowel vowels via pulse-train and formant (Michelle Daniels) vowel.ins
vox fm voice (cream.clm) vox.ins clm-ins.scm clm-ins.rb clm-ins.fs
zc, zn interpolating delays zd.ins clm-ins.scm clm-ins.rb clm-ins.fs
zipper The 'digital zipper' effect. zipper.ins zip.scm zip.rb

If you develop an interesting instrument that you're willing to share, please send it to me (bil@ccrma.stanford.edu).

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

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

(definstrument no-loop-2 (beg dur)
  (let ((o (make-oscil 440)))
    (run
     (dotimes (k dur)
       (outa (+ k beg) (* .1 (oscil o)))))))

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

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


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

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


related documentation: snd.html extsnd.html grfsnd.html sndscm.html sndlib.html libxm.html fm.html index.html