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.
related documentation: | snd.html | extsnd.html | grfsnd.html | sndscm.html | fm.html | sndlib.html | libxm.html | index.html |
Contents |
|
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Introduction |
I'm not sure how to motivate this set of functions. If you try to make new sounds, or recreate and alter existing sounds, you'll find that there are some functions that seem to pop up everywhere. The basic building block of sound is the sinusoid, so we have things like oscil and polywave. Another basic thing is noise, so we have rand and rand-interp. Sounds get louder and softer, or go up and down in pitch, so we have envelopes (env). We need a way to get at them (in-any, readin), and play them (out-any, locsig). We need tons of reverb (delay, convolve). I love this stuff. |
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)) ; add the "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 44100)) (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 *clm-default-frequency*) (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 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). This style of argument passing is very similar to the "Optional Positional and Named Parameters" extension of scheme: SRFI-89.
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)))))) ; outa output defaults to *output* so we can omit it |
Now to hear our sine wave:
(with-sound (:play #t) (simp 0 44100 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 (seconds->samples beg)) (end (+ start (seconds->samples dur)))) (do ((i start (1+ i))) ((= i end)) (outa i (* amp (oscil os)))))) |
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 (seconds->samples beg)) (end (+ start (seconds->samples dur)))) (run (lambda () (do ((i start (1+ i))) ((= i end)) (outa i (* amp (oscil os)))))))) |
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 (seconds->samples beg)) (end (+ start (seconds->samples dur)))) (run (lambda () (do ((i start (1+ i))) ((= i end)) (outa i (* amp (oscil os)))))))) |
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 (seconds->samples beg)) (end (+ start (seconds->samples dur)))) (run (lambda () (do ((i start (1+ i))) ((= i end)) (outa i (* (env amp-env) (oscil os)))))))) |
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. You can try out a generator or a patch of generators quickly by plugging it into the following with-sound call:
(with-sound () (let ((sqr (make-square-wave 100))) ; test a square-wave generator (do ((i 0 (1+ i))) ((= i 10000)) (outa i (square-wave sqr))))) |
By the way, there's nothing special about a generator in CLM: it is a function, or perhaps more accurately, a closure. If such a function happens to restrict itself to functions that the "run" macro can handle (and this includes most of Scheme), then it will run nearly as fast as any built-in function. If it needs to keep on-going state around, it is simplest to use a vct as the generator object:
(define (make-my-oscil frequency) ; we want our own oscil! (vct 0.0 (hz->radians frequency))) ; current phase and frequency-based phase increment (define (my-oscil gen fm) ; the corresponding generator (let ((result (sin (vct-ref gen 0)))) ; return sin(current-phase) (vct-set! gen 0 (+ (vct-ref gen 0) ; increment current phase (vct-ref gen 1) ; by frequency fm)) ; and FM result)) ; return sine wave (with-sound () (run (lambda () (let ((osc (make-my-oscil 440.0))) (do ((i 0 (1+ i))) ((= i 44100)) (outa i (my-oscil osc 0.0))))))) |
There are many more such generators scattered around the Snd package. For more sophisticated situations, you can use def-clm-struct, and defgenerator.
Generators |
oscil |
make-oscil :optional-key (frequency *clm-default-frequency*) (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).
mus-frequency | frequency in Hz |
mus-phase | phase in radians |
mus-length | 1 (no set!) |
mus-increment | frequency 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 frequency 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 :length (1+ (- end start)) :scaler (hz->radians freq)))) (do ((i start (1+ i))) ((= i end)) (outa i (* amp (oscil os (env frqe))))))) (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 (seconds->samples beg)) (end (+ start (seconds->samples dur))) (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)))))))))) ;;; (with-sound () (simple-fm 0 1 440 .1 2 1.0)) |
See fm.html for a discussion of FM. 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 (seconds->samples beg)) (end (+ start (seconds->samples dur))) (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))))))) ;;; (with-sound () (simple-add 0 1 220 .3)) |
To show CLM in its various embodiments, here are the Scheme, Common Lisp, Ruby, Forth, and C versions of the bird instrument; it produces a sinusoid with (usually very elaborate) amplitude and frequency envelopes. | |
| |
| |
| |
| |
|
Related generators are ncos, nsin, asymmetric-fm, nrxysin, and waveshape. Some instruments that use oscil are bird and bigbird, fm-violin (v), 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 generators.scm. For a sine-bank example, see pvoc.scm. To goof around with FM from a graphical interface, see bess.scm and bess1.scm.
When oscil's frequency is high relative to the sampling rate, the waveform you'll see may not look very sinusoidal. Here, for example, is oscil at 440 Hz when the srate is 1000, 4000, and 16000: ![]() |
env |
make-env :optional-key envelope ; list or vct of x,y break-point pairs (scaler 1.0) ; scaler on every y value (before offset is added) duration ; duration in seconds (offset 0.0) ; value added to every y value base ; type of connecting line between break-points end ; end sample number (obsolete -- use length) length ; duration in samples env e env? e env-interp x env :optional (base 1.0) ;value of env at x env-any e connecting-function
|
![]() |
An envelope is a list or vct of break point pairs: '(0 0 100 1)
is
a ramp from 0 to 1 over an x-axis excursion from 0 to 100, as is (vct 0 0 100 1)
.
This data is passed
to make-env along with the scaler (multiplier)
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. To specify the breakpoints, you can also use the form '((0 0) (100 1)).
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 |
![]() |
![]() |
There are several ways to get arbitrary connecting curves between the break points. The simplest method is to 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:
|
![]() |
Another method is to write a function that traces out the curve you want. J.C.Risset's bell curve is:
(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)))) |
But the most flexible method is to use env-any. env-any takes the env generator that produces the underlying envelope, and a function to "connect the dots", and returns the new envelope applying that connecting function between the break points. For example, say we want to square each envelope value:
(with-sound () (let ((e (make-env '(0 0 1 1 2 .25 3 1 4 0) :duration 0.5))) (do ((i 0 (1+ i))) ((= i 44100)) (outa i (env-any e (lambda (y) (* y y))))))) ;; or connect the dots with a sinusoid: (define (sine-env e) (env-any e (lambda (y) (* 0.5 (+ 1.0 (sin (+ (* -0.5 pi) (* pi y)))))))) (with-sound () (let ((e (make-env '(0 0 1 1 2 .25 3 1 4 0) :duration 0.5))) (run (lambda () (do ((i 0 (1+ i))) ((= i 44100)) (outa i (sine-env e))))))) |
![]() |
The env-any connecting function takes one argument, the current envelope value treated as going between 0.0 and 1.0 between each two points. It returns a value that is then fitted back into the original (scaled, offset) envelope. There are a couple more of these functions in generators.scm, one to apply a blackman4 window between the points, and the other to cycle through a set of exponents.
mus-reset of an env causes it to start all over again from the beginning. mus-reset is called internally if you use mus-scaler to set an env's scaler (and similarly for offset and length). 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) :length 2000)) |
To copy an env while changing one aspect (say duration), it's simplest to use make-env:
(define (change-env-dur e dur) (make-env (mus-data e) :scaler (mus-scaler e) :offset (mus-offset e) :base (mus-increment e) :duration dur)) |
make-env signals an error if the envelope breakpoints are either out of order, or an x axis value occurs twice. The default error handler in with-sound may not give you the information you need to track down the offending note, even given the original envelope. Here's one way to trap the error and get more info (in this case, the begin time and duration of the enclosing note):
(define* (make-env-with-catch beg dur :rest args) (catch 'mus-error (lambda () (apply make-env args)) (lambda args (display (format #f ";~A ~A: ~A~%" beg dur args))))) |
In cases like this, remember to set run-safety to 1 so that with-sound will be able to display the stack backtrace when an error occurs.
An envelope applied to the amplitude of a signal is a form of amplitude modulation, and glissando is frequency modulation. Both cause a broadening of the spectral components:
![]() |
![]() | |
multiplied by sinusoid at 50Hz | sinusoid from 100Hz to 300Hz |
The amplitude case reflects the spectrum of the amplitude envelope all by itself, translated (by multiplication) up to the sinusoid's pitch. The sidebands are about 1 Hz apart (the envelope takes 1 second to go linearly from 0 to 1). Despite appearances, we hear this (are you sitting down?) as a changing amplitude, not a timbral mess. Spectra can be tricky to interpret, and I've tried to choose parameters for this display that emphasize the broadening.
Envelopes | ||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
table-lookup |
make-table-lookup :optional-key (frequency *clm-default-frequency*) ; table repetition rate in Hz (initial-phase 0.0) ; starting point in radians (pi = mid-table) wave ; a vct containing the signal (size *clm-table-size*) ; table size if wave not specified (type mus-interp-linear) ; interpolation type table-lookup tl :optional (fm-input 0.0) table-lookup? tl make-table-lookup-with-env :optional-key frequency env size
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
, or mus-interp-hermite
.
make-table-lookup-with-env returns a new table-lookup generator with the envelope 'env' loaded into its table.
mus-frequency | frequency in Hz |
mus-phase | phase in radians |
mus-data | wave vct |
mus-length | wave size (no set!) |
mus-interp-type | interpolation choice (no set!) |
mus-increment | table 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 or vct 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 or vct 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)))))) |
table-lookup can also be used as a sort of "freeze" function, looping through a sound repeatedly, based on some previously chosen loop positions:
(define (looper start dur sound freq amp) (let* ((beg (seconds->samples start)) (end (+ beg (seconds->samples dur))) (loop-data (mus-sound-loop-info sound))) (if (or (null? loop-data) (<= (cadr loop-data) (car loop-data))) (throw 'no-loop-positions) (let* ((loop-start (car loop-data)) (loop-end (cadr loop-data)) (loop-length (1+ (- loop-end loop-start))) (sound-section (file->array sound 0 loop-start loop-length (make-vct loop-length))) (original-loop-duration (/ loop-length (mus-sound-srate sound))) (tbl (make-table-lookup :frequency (/ freq original-loop-duration) :wave sound-section))) ;; "freq" here is how fast we read (transpose) the sound — 1.0 returns the original (run (lambda () (do ((i beg (1+ i))) ((= i end)) (outa i (* amp (table-lookup tbl)))))))))) (with-sound (:srate 44100) (looper 0 10 "/home/bil/sf1/forest.aiff" 1.0 0.5)) |
And for total confusion, here's a table-lookup that modulates a sound where we specify the modulation deviation in samples:
(definstrument (fm-table file start dur amp read-speed modulator-freq index-in-samples) (let* ((beg (seconds->samples start)) (end (+ beg (seconds->samples dur))) (table-length (mus-sound-frames file)) (tab (make-table-lookup :frequency (/ read-speed (mus-sound-duration file)) :wave (file->array file 0 0 table-length (make-vct table-length)))) (osc (make-oscil modulator-freq)) (index (/ (* (hz->radians modulator-freq) 2 pi index-in-samples) table-length))) (run (lambda () (do ((i beg (1+ i))) ((= i end)) (outa i (* amp (table-lookup tab (* index (oscil osc)))))))))) |
Lessee.. there's a factor of table-length/(2*pi) in table-lookup, so that a table with a sinusoid behaves the same as an oscil even with FM; hz->radians adds a factor of (2*pi)/srate; so we've cancelled the internal 2*pi and table-length, and we have an actual deviation of mfreq*2*pi*index/srate, which looks like FM; hmmm. See srcer below for an src-based way to do the same thing.
There is one annoying problem with table-lookup: noise.
Say we have a sine wave in a table with L elements, and we want to read it at a frequency of
f Hz at a sampling rate of Fs. This requires that we read the table at locations that are multiples of
L * f / Fs. This is ordinarily not an integer (that is, we've fallen between the
table elements). We have no data between the elements, but we can make (plenty of)
assumptions about what ought to be there. In the no-interpolation case (type = mus-interp-none
), we simply take the floor of
the table-relative phase, returning a squared-off sine-wave:
In addition to the sine at 100 Hz, we're getting lots of pairs of components, each pair centered around n * L * f, (10000 = 100 * 100 is the first),
and separated from it by f, (9900 and 10100),
and the amplitude of each pair is 1/(nL): -40 dB is 1/100 for the n=1 case.
This spectrum says "amplitude modulation" (the fast square wave times the slow sinusoid).
After scribbling a bit on the back of an envelope, we announce with a confident air that
the sawtooth error signal gives us the 1/n (it is a sum of sin nx/n), and its amplitude gives us the 1/L.
Now we try linear interpolation (mus-interp-linear
), and get the same components as before, but
the amplitude is going (essentially) as 1.0 / (n * n * L * L). So the interpolation
reduces the original problem by a factor of n * L:
We can view this also as amplitude modulation: the sinusoid at frequency f times the little blip during each table sample at frequency L * f. Each component is at n * L * f, as before, and split in half by the modulation. Since L * f is normally a very high frequency, and sampling rates are not in the megahertz range (as in our examples), these components alias to such an extent that they look like noise, but they are noise only in the sense that we wish they weren't there.
The table length (L above) is the "effective" length. If we store an nth harmonic in the table, each period gets L/n elements (we want to avoid clicks caused by discontinuities between the first and last table elements), so the amplitude of the nth harmonic's noise components is higher (by n^2) than the fundamental's. We either have to use enormous tables or stick to low numbered partials. To keep the noise components out of sight in 16-bit output (down 90 dB), we need 180 elements per period. So a table with a 50th harmonic has to be at least length 8192. It's odd that the cutoff here is basically the same as in the waveshaping case; a 50-th harmonic is trouble in either case. (This leaves an opening for ncos and friends even when dynamic spectra aren't the issue).
We can try fancier interpolations. mus-interp-lagrange
and mus-interp-hermite
reduce the components (which are at the same frequencies as before) by about another factor of L.
But these interpolations are expensive and ugly.
So (this story is spinning out much longer than I intended), in the
polywave case (see below), if the number of the highest harmonic seems too high,
the generator uses a bank of sines (i.e. straight additive synthesis), rather than degenerating into a racket.
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.
polywave (polyshape and waveshape) |
make-polywave :optional-key (frequency *clm-default-frequency*) (partials '(1 1)) ; a list of harmonic numbers and their associated amplitudes (type mus-chebyshev-first-kind) ; Chebyshev polynomial choice polywave w :optional (fm 0.0) polywave? w make-waveshape :optional-key (frequency *clm-default-frequency*) (partials '(1 1)) wave (size *clm-table-size*) waveshape w :optional (index 1.0) (fm 0.0) waveshape? w make-polyshape :optional-key (frequency *clm-default-frequency*) (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)
polywave is the new form of polyshape; waveshape will soon be deprecated. These three generators drive a sum of scaled Chebyshev polynomials with a sinusoid, creating a sort of cross between additive synthesis and FM; see "Digital Waveshaping Synthesis" by Marc Le Brun in JAES 1979 April, vol 27, no 4, p250. The basic idea is:
We can add scaled Tns (polynomials) to get the spectrum we want, producing in the simplest case an inexpensive additive synthesis. We can vary the peak amplitude of the input (cos theta) to get effects similar to those of FM.
waveshape uses an internal table-lookup generator, whereas polyshape and polywave use the polynomial function. The "partials" argument to the make function can be either a list or a vct. The "type" or "kind" argument determines which kind of Chebyshev polynomial is used internally: mus-chebyshev-first-kind or mus-chebyshev-second-kind. (The ncos generator produces Chebyshev polynomials of the fourth kind).
The initial-phase defaults to 0.0 (i.e. a sine, not a cosine) for historical reasons. This can be slightly confusing if you're comparing waveforms. Using sin doesn't change the shape of the output waveform, but affects its components' initial phases. To get a sum of cosines, set the initial-phase to pi/2. In the polywave case, set mus-phase to pi/2 after creating the generator. |
mus-frequency | frequency in Hz |
mus-scaler | index (polywave only) |
mus-phase | phase in radians |
mus-data | polynomial coeffs |
mus-length | number of partials |
mus-increment | frequency in radians per sample |
(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 10000)) (outa i (waveshape wav))))) |
![]() |
we can also use partials->polynomial with polyshape and polywave:
(definstrument (bigbird start duration frequency freqskew amplitude freq-env amp-env partials) (let* ((beg (seconds->samples start)) (end (+ beg (seconds->samples duration))) (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)))))))))) (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 |
See animals.scm for many more examples along these lines. partials->waveshape with waveshape produces the same output as partials->polynomial with polyshape. The fm-violin uses polyshape for the multiple FM section in some cases. We can get single side-band spectra by using Chebyshev polynomials of the second kind. The pqw and pqwvox instruments use this technique. Here is a simplified example:
(definstrument (pqw start dur spacing carrier partials) (let* ((spacing-cos (make-oscil spacing (/ pi 2.0))) (spacing-sin (make-oscil spacing)) (carrier-cos (make-oscil carrier (/ pi 2.0))) (carrier-sin (make-oscil carrier)) (sin-coeffs (partials->polynomial partials mus-chebyshev-second-kind)) (cos-coeffs (partials->polynomial partials mus-chebyshev-first-kind)) (beg (seconds->samples start)) (end (+ beg (seconds->samples dur)))) (run (lambda () (do ((i beg (1+ i))) ((= i end)) (let ((ax (oscil spacing-cos))) (outa i (- (* (oscil carrier-sin) (oscil spacing-sin) (polynomial sin-coeffs ax)) (* (oscil carrier-cos) (polynomial cos-coeffs ax)))))))))) (with-sound () (pqw 0 1 200.0 1000.0 '(2 .2 3 .3 6 .5))) |
![]() |
We can use waveshaping to make a band-limited triangle-wave:
(def-optkey-fun (make-band-limited-triangle-wave (frequency *clm-default-frequency*) (order 1)) (let ((freqs '())) (do ((i 1 (1+ i)) (j 1 (+ j 2))) ((> i order)) (set! freqs (cons (/ 1.0 (* j j)) (cons j freqs)))) (make-waveshape frequency :wave (partials->waveshape (reverse freqs))))) (define* (band-limited-triangle-wave gen :optional (fm 0.0)) (waveshape gen 1.0 fm)) |
Band-limited square or sawtooth waves need sines (as opposed to cosines), so if we absolutely insist on using waveshaping, we could do it this way:
(definstrument (bl-saw start dur frequency order) (let* ((norm (if (= order 1) 1.0 ; these peak amps were determined empirically (if (= order 2) 1.3 ; actual limit is supposed to be pi/2 (G&R 1.441) (if (< order 9) 1.7 ; but Gibbs phenomenon pushes it to 1.851 1.9)))) ; if order>25, numerical troubles — use table-lookup (freqs '())) (do ((i 1 (1+ i))) ((> i order)) (set! freqs (cons (/ 1.0 (* norm i)) (cons i freqs)))) (let* ((ccos (make-oscil frequency (/ pi 2.0))) (csin (make-oscil frequency)) (coeffs (partials->polynomial (reverse freqs) mus-chebyshev-second-kind)) (beg (seconds->samples start)) (end (+ beg (seconds->samples dur)))) (run (lambda () (do ((i beg (1+ i))) ((= i end)) (outa i (* (oscil csin) (polynomial coeffs (oscil ccos)))))))))) |
The "fm" argument to these generators is intended mainly for vibrato and frequency envelopes. If you use it for frequency modulation, you'll notice that the result is not the necessarily same as applying that modulation to the equivalent bank of oscillators, but it is the same as (for example) applying it to a ncos generator, or most of the other generators (table-lookup, nsin, etc). The polynomial in cos(x) produces a sum of cos(nx) for various "n", but if "x" is itself a sinusoid, its effective index includes the factor of "n" (the partial number). This is what you want if all the components should move together (as in vibrato). If you need better control of the FM spectrum, use a bank of oscils where you can set each index independently. Here we used '(1 1 2 1 3 1) and polyshape with sinusoidal FM with an index of 1. |
![]() |
The same thing happens if you use polyshape or ncos (or whatever) as the (complex) modulating signal to an oscil (the reverse of the situation above). The effective index of each partial is divided by the partial number (and in ncos, for example, the output is scaled to be -1..1, so that adds another layer of confusion). There's a longer discussion of this under ncos.
To get the FM effect of a spectrum centered around a carrier, multiply the waveshape output by the carrier:
(with-sound () (let ((modulator (make-polyshape 100.0 :partials (list 1 .5 2 .25 3 .125 4 .125))) (carrier (make-oscil 1000.0))) (do ((i 0 (1+ i))) ((= i 20000)) (outa i (* .5 (oscil carrier) (polyshape modulator)))))) |
The simplest way to get get changing spectra is to interpolate between two or more sets of coefficients.
(+ (* interp (polywave p1 ...)) ; see animals.scm for many examples (* (- 1.0 interp) (polywave p2 ...)))
But we can also vary the index (the amplitude of the sinusoid driving the sum of polynomials), much as in FM. The kth partial's amplitude at a given index, given a set h[k] of coefficients, is:
(This formula is implemented by cheby-hka in dsp.scm). The function traced out by the harmonic (analogous to the role the Bessel function Jn plays in FM) is a polynomial in the index whose order depends on the number of coefficients. When the index is less than 1.0, energy appears in lower harmonics even if they are not included in the index=1.0 list:
> (cheby-hka 3 0.25 (vct 0 0 0 0 1.0 1.0)) -0.0732421875 > (cheby-hka 2 0.25 (vct 0 0 0 0 1.0 1.0)) -0.234375 > (cheby-hka 1 0.25 (vct 0 0 0 0 1.0 1.0)) 1.025390625 > (cheby-hka 0 0.25 (vct 0 0 0 0 1.0 1.0)) 1.5234375
Below we sweep the index from 0.0 to 1.0 (sticking at 1.0 for a moment at the end), with a partials list of '(11 1.0 20 1.0). These numbers were chosen to show that the even and odd harmonics are independent:
(with-sound () (let ((gen (make-polyshape 100.0 :partials (list 11 1 20 1))) (ampf (make-env '(0 0 1 1 20 1 21 0) :scaler .4 :length 88200))) (indf (make-env '(0 0 1 1 1.1 1) :length 88200))) (do ((i 0 (1+ i))) ((= i 88200)) (outa i (* (env ampf) (polyshape gen (env indf)))))) | |
![]() |
![]() |
You can see there's another annoying "gotcha": the DC component can be arbitrarily large. If we don't counteract it in some way, we lose dynamic range, and we get a big click when the generator stops. In addition (as the right graph shows, although in this case the effect is minor), the peak amplitude is dependent on the index. We can reduce this problem somewhat by changing the signs of the harmonics to follow the pattern + + - -:
(list 1 .5 2 .25 3 -.125 4 -.125) ; squeeze the amplitude change toward index=0
To follow an amplitude envelope despite a changing index, we can use a moving-max generator (from dsp.scm):
(with-sound () (let ((gen (make-polyshape 1000.0 :partials (list 1 .25 2 .25 3 .125 4 .125 5 .25))) (indf (make-env '(0 0 1 1 2 0) :duration 2.0)) ; index env (ampf (make-env '(0 0 1 1 2 1 3 0) :duration 2.0)) ; desired amp env (mx (make-moving-max 256)) ; track actual current amp (samps (seconds->samples 2.0))) (do ((i 0 (1+ i))) ((= i samps)) (let ((val (polyshape gen (env indf)))) ; polyshape with index env (outa i (/ (* (env ampf) val) (max 0.001 (moving-max mx val)))))))) |
The harmonic amplitude formula for the Chebyshev polynomials of the second kind is:
On a related topic, if we drive the sum of Chebyshev polynomials with more than one sinusoid, we get sum and difference tones, much as in complex FM:
(with-sound () (let ((pcoeffs (partials->polynomial (vct 5 1))) (gen1 (make-oscil 100.0)) (gen2 (make-oscil 2000.0))) (do ((i 0 (1+ i))) ((= i 44100)) (outa i (polynomial pcoeffs (+ (* 0.5 (oscil gen1)) (* 0.5 (oscil gen2)))))))) |
![]() |
This kind of output is typical; I get the impression that the cross products are much more noticeable here than in FM. If we use a recorded sound as the argument to the polynomial, the output is a brightened or distorted version of the original:
(define (brighten-slightly coeffs) (let ((pcoeffs (partials->polynomial coeffs)) (mx (maxamp))) (map-channel (lambda (y) (* mx (polynomial pcoeffs (/ y mx)))))))
but watch out for clicks from the DC component if any of the "n" in the Tn are even. When I use this idea, I either use only odd numbered partials in the partials->polynomial list, or add an amplitude envelope to make sure the result ends at 0. A related, largely unexplored vein involves the "spread polynomials" where Tn(1 - 2s) = 1 - 2Sn(s).
Looking back at the bl-saw instrument, why does a high harmonic number give numerical problems? The polynomials are related to each other
via the recursion: , so the first
few polynomials are:
![]() |
![]() |
The first coefficient is 2^n or 2^(n-1). This is bad news if "n" is large because we are expecting a bunch of huge numbers to add up to something in the vicinity of 0.0 or 1.0. If we're using 32-bit floats, the first sign of trouble comes when the order is around 26. If you look at some of the coefficients, you'll see numbers like -129026688.000 (in the 32 bit case), which should be -129026680.721 — we have run out of bits in the mantissa! Even if we build Snd --with-doubles, we can only push the order up to around 46.
With all these problems, we might ask why we'd want polyshape at all. Leaving aside speed (the polynomial computation is much faster than the equivalent sum of oscils) and memory (waveshape and table-lookup use a table that has to be loaded), the main reason to use polyshape is accuracy. polyshape produces output that is cleaner than the equivalent sum of oscils, whereas table-lookup and waveshape, both of which interpolate into a sampled version of the desired function, are noisy. To make the difference almost appalling, here are spectra comparing a sum of oscils, polyshape, waveshape, and table-lookup.
The table size is 512, but that almost doesn't matter; you'd have to use a table size of at least 8192 to approach the oscil and polyshape cases. The FFT size is 1048576, with no data window ("rectangular"), and the y-axis is in dB, going down to -120 dB. The choice of fft window can make a big difference; using no window, but a huge fft seems like the least confusing way to present this result. Notice the lower peaks in the table-lookup case. partials->wave puts n periods of the nth harmonic in the table, so the nth harmonic has an effective table length of table-length/n. n * 1/n = 1, so all our components have their first interpolation noise peak centered (in this case) around 7100 Hz ((512 * 100) mod 22050). Since the 1600 Hz component has an effective table size of only 32 samples, it creates big sidebands at 5500 Hz and 8700 Hz. The 800 Hz component makes smaller peaks (by a factor of 4, since this is proportional to n^2) at 6300 Hz and 7900 Hz, and the 100 Hz cases are at 7000 Hz and 7200 Hz (down in amplitude by 16^2). The highest peaks are down only 60 dB. See table-lookup for more discussion of interpolation noise (it's actually amplitude modulation of the stored signal and the linear interpolating signal with severe aliasing). To repeat the punch line of that discussion, if the highest harmonic requested in polywave is too high, the generator uses a sum of sines (i.e. cos or sin directly) so that you always get a clean result. The waveshaping noise is much worse because the polynomial is so sensitive numerically. Here is a portion of the error signal at the point where the driving sinusoid is at its maximum: ![]() |
Perhaps a better question is, why waveshape? Here history intrudes. We couldn't handle the polynomial on the Samson box, so we used the table-lookup version. I've tried to maintain reasonable backwards compatibility with old instruments, but someday I think I'll remove the waveshape generator.
sawtooth-wave, triangle-wave, pulse-train, square-wave |
make-triangle-wave :optional-key (frequency *clm-default-frequency*) (amplitude 1.0) (initial-phase pi) triangle-wave s :optional (fm 0.0) triangle-wave? s make-square-wave :optional-key (frequency *clm-default-frequency*) (amplitude 1.0) (initial-phase 0) square-wave s :optional (fm 0.0) square-wave? s make-sawtooth-wave :optional-key (frequency *clm-default-frequency*) (amplitude 1.0) (initial-phase pi) sawtooth-wave s :optional (fm 0.0) sawtooth-wave? s make-pulse-train :optional-key (frequency *clm-default-frequency*) (amplitude 1.0) (initial-phase (* 2 pi)) pulse-train s :optional (fm 0.0) pulse-train? s
mus-frequency | frequency in Hz |
mus-phase | phase in radians |
mus-scaler | amplitude arg used in make-<gen> |
mus-width | width of square-wave pulse (0.0 to 1.0) |
mus-increment | frequency in radians per sample |
One popular kind of vibrato is: (+ (triangle-wave pervib) (rand-interp ranvib))
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. To get a square-wave with control over the "duty-factor":
(with-sound () (let* ((duty-factor .25) ; ratio of pulse duration to pulse period (p-on (make-pulse-train 100 0.5)) (p-off (make-pulse-train 100 -0.5 (* 2 pi (- 1.0 duty-factor)))) (sum 0.0)) (do ((i 0 (1+ i))) ((= i 44100)) (set! sum (+ sum (pulse-train p-on) (pulse-train p-off))) (outa i sum)))) |
This is the adjustable-square-wave generator in generators.scm.
That file also defines adjustable-triangle-wave and
adjustable-sawtooth-wave.
All of these generators produce non-band-limited output; if the frequency is too high, you can get foldover.
A more reasonable square-wave can be generated via
(tanh (* n (sin theta)))
, where "n" (a float) sets how squared-off it is:
![]() |
![]() |
![]() |
The spectrum of tanh(sin) can be obtained by expanding tanh as a power series: ![]() plugging in "sin" for "x", expanding the sine powers, and collecting terms (very tedious!): ![]() which is promising since a square wave is made up of odd harmonics with amplitude 1/n. As the "n" in tanh(n sin(x)) increases, this series doesn't apply, but we can check the formula for tanh, and see that the e^-x term will vanish (in the positive x case), giving 1.0. So we do get a square wave, but it's not band limited. If a complex signal replaces the sin(x), we get "intermodulation products" (sum and difference tones). (It is reassuring after all that arithmetic that 13319/241920 / 140069/172800 is 0.068 — we got .070 in the fft above, and 1973/483840 / 140069/172800 is 0.005 — we got .006). |
Another square-wave choice is eoddcos in generators.scm,
based on atan; as its "r" parameter approaches 0.0, you get closer to a square wave.
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 (with-sound () (let ((angle 0.0)) (do ((i 0 (1+ i)) (angle 0.0 (+ angle 0.02))) ((= i 44100)) (outa i (* 0.5 (+ 1.0 (sqsq 1.001 angle))))))) |
For sawtooth output, see also rksin. In these generators, the "fm" argument is useful mainly for various sci-fi sound effects:
(define (tritri start dur freq amp index mcr) (let* ((beg (seconds->samples start)) (end (+ beg (seconds->samples dur))) (carrier (make-triangle-wave freq)) (modulator (make-triangle-wave (* mcr freq)))) (run (lambda () (do ((i beg (1+ i))) ((= i end)) (outa i (* amp (triangle-wave carrier (* index (triangle-wave modulator)))))))))) (with-sound (:srate 44100) (tritri 0 1 1000.0 0.5 0.1 0.01)) ; sci-fi laser gun (with-sound (:srate 44100) (tritri 0 1 4000.0 0.7 0.1 0.01)) ; a sparrow? |
On the other hand, animals.scm uses pulse-train's fm argument to track a frequency envelope, triggering a new peep each time the pulse goes by.
I think just about every combination of oscil/triangle-wave/sawtooth-wave/square-wave has been used. Even triangle-wave(square-wave) can make funny noises. See ncos for more dicussion about using these generators as FM modulators.
ncos (sum-of-cosines) and nsin (sum-of-sines) |
make-ncos :optional-key (frequency *clm-default-frequency*) (n 1) ncos cs :optional (fm 0.0) ncos? cs make-nsin :optional-key (frequency *clm-default-frequency*) (n 1) nsin cs :optional (fm 0.0) nsin? cs make-sum-of-cosines :optional-key (cosines 1) (frequency *clm-default-frequency*) (initial-phase 0.0) sum-of-cosines cs :optional (fm 0.0) sum-of-cosines? cs make-sum-of-sines :optional-key (sines 1) (frequency *clm-default-frequency*) (initial-phase 0.0) sum-of-sines cs :optional (fm 0.0) sum-of-sines? cs
ncos is the new form of sum-of-cosines, and nsin is the new form of sum-of sines.
ncos produces a band-limited pulse train containing "n" cosines. I think this was originally viewed as a way to get a speech-oriented pulse train that would then be passed through formant filters (see pulse-voice in examp.scm). There are many similar formulas: see ncos2 and friends in generators.scm. "Trigonometric Delights" by Eli Maor has a derivation of a nsin formula and a neat geometric explanation. For a derivation of the ncos formula, see "Fourier Analysis" by Stein and Shakarchi, or (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.
mus-frequency | frequency in Hz |
mus-phase | phase in radians |
mus-scaler | (/ 1.0 cosines) |
mus-length | n or cosines arg used in make-<gen> |
mus-increment | frequency in radians per sample |
based on:
the Dirichlet kernel see also generators.scm
(define (simple-soc beg dur freq amp) (let* ((os (make-ncos freq 10)) (start (seconds->samples beg)) (end (+ start (seconds->samples dur)))) (run (lambda () (do ((i start (1+ i))) ((= i end)) (outa i (* amp (ncos os)))))))) (with-sound () (simple-soc 0 1 100 1.0)) |
![]() |
Almost identical is the following sinc-train generator:
(define* (make-sinc-train :optional (frequency *clm-default-frequency*) (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 use ncos as the FM modulating signal, you may be surprised and disappointed. As the modulating signal approaches a spike (as n increases), the bulk of the energy collapses back onto the carrier:
(with-sound () (for-each (lambda (arg) (let ((car1 (make-oscil 1000)) (mod1 (make-ncos 100 (cadr arg))) (start (seconds->samples (car arg))) (samps (seconds->samples 1.0)) (ampf (make-env '(0 0 1 1 20 1 21 0) :duration 1.0 :scaler .8)) (index (hz->radians (* 100 3.0)))) (do ((i start (1+ i))) ((= i (+ start samps))) (outa i (* (env ampf) (oscil car1 (* index (ncos mod1)))))))) (list (list 0.0 1) (list 2.0 2) (list 4.0 4) (list 6.0 8) (list 8.0 16) (list 10.0 32) (list 12.0 64) (list 14.0 128)))) |
![]() |
If you go all the way and use a pulse-train as the FM source, you get a
large component for the carrier, and all the others are very small.
One very sketchy explanation is that since a pulse is close to 0 most of the time,
the modulation during that time is nearly 0, so the carrier is affected less and less
as we get closer to a spike. I believe this is the basis of FM radio's
noise reduction: we can filter out the high stuff which is weak anyway, and otherwise
the spike has no effect on our carrier.
Another handwaving approach is:
let "n" be the number of components ncos creates, "B" the amplitude of the modulating signal,
and "k" be the component number.
Since each component produced by ncos has the same
amplitude (B/n), if it is treated as an FM component its FM index is B/(k*n); B/n because
that's the amplitude of each component, and 1/k since we have to cancel the factor of the
modulating frequency in its index calculation.
As more
components are produced (as n increases),
the higher ones only matter when all the Ji(B/(k * n))'s are not negligible (since
its a huge product of J's, — see fm.html).
B/(k * n) is heading for 0, so the product of J's is non-negligible
only when all the J's are J0 except the solitary J1 that hits that component directly (i.e. the kth).
J0(0) is 1, and J1(0) is 0, and the factor of n is scrunching all our components closer
and closer to 0, so the carrier (all J0's) approaches 1, whereas the others (J0's except
for J1(B/(n*k))) approach 0.
![]() |
(define (ncfm freq-we-want wc modfreq baseindex n) ;; get amplitude of "freq-we-want" given ncos as FM, ;; "wc" as carrier, "modfreq" as ncos freq, ;; "baseindex" as FM-index of 1st harmonic, ;; "n" as number of harmonics (let ((harms '()) (amps '())) (do ((i 1 (1+ i))) ((> i n)) (set! harms (cons (* i modfreq) harms)) (set! amps (cons (/ baseindex (* i n)) amps))) (fm-parallel-component freq-we-want wc (reverse harms) (reverse amps) '() '() #f))) | |||||||||||||||||||||||||||||||
|
|
If you use ncos (or nsin) as both the carrier and modulator, you get a very similar effect. As n increases, the ncos(wc + ncos(wm)) output gradually approaches the unmodulated ncos output — the crunch happens on each carrier component, but most strongly on the earlier ones (the "effective index" is less on those components, as mentioned under polywave). So, a pulse-train modulated by a pulse-train is a pulse-train. Are there any other cases where gen(wc + gen(wm)) = gen(wc)? My first thought was rand, but that has a hidden surprise: the modulation obscures the underlying square-wave!
If you sweep ncos upwards in frequency, you'll eventually get foldover; the generator produces its preset number of cosines no matter what. It is possible to vary the spectrum smoothly:
(with-sound () (let ((os (make-ncos 100.0 4)) (pow (make-env '(0 1.0 1 30.0) :length 10000))) ; our "index" envelope in FM jargon (do ((i 0 (1+ i))) ((= i 10000)) (let ((val (ncos os))) (outa i (* (signum val) ; signum is in dsp.scm (expt (abs val) (env pow)))))))) |
This is not a very polite sound. The same trick works on all the pulse-train functions in generators.scm (or an oscil for that matter!), but perhaps a filter is a simpler approach. There are a lot more of these "kernels" in generators.scm.
![]() |
![]() |
![]() |
nsin produces a sum of equal amplitude sines. It is very similar (good and bad) to ncos.
mus-frequency | frequency in Hz |
mus-phase | phase in radians |
mus-scaler | dependent on number of sines |
mus-length | n or sines arg used in make-<gen> |
mus-increment | frequency in radians per sample |
![]() the conjugate Dirichlet kernel |
![]() |
In terms of speed, ncos and nsin outpace polywave only when the latter has to shift away from the Chebyshev polynomials (around n=30). polywave appears to be more accurate when n is small. Both ncos and nsin could be implemented as polynomials in cos x, just as in polyshape; in fact, ncos is almost the same as the Chebyshev polynomial of the 4th kind. But by sticking with the current formulas, we avoid the numerical troubles that limit polyshape to n < 50. See also the nrxycos generator, and generators.scm.
As far as I know, most of these formulas have never been explored.
There are many formulas that produce exponentially decaying or bell-curve shaped spectra;
I think these all sound about the same, so I have included only a representative sample of them.
A couple of the formulas are special cases of the "Bessel function summation theorem", G&R 8.530:
,
where Z stands for any of the various Bessel functions (J, Y, etc),
and R stands for the Poisson-like business (or is it Legendre?) in the square root.
Don't shy away from the sums to infinity just because you've heard shouting about "band-limited waveforms" — FM is an infinite sum:
|
You can calculate the maximum "r" factor given the current frequency and sampling rate:
(expt .001 (/ 1.0 (floor (/ (mus-srate) (* 3 freq)))))
or the equivalent where there's some factor in the denominator. The factorials overwhelm r^k, so you can get away with very large "r"s in those cases; the center of the resultant spectrum is around r * freq.
def-clm-struct makes it easy to try out new ideas; take the first one in the Jolley section:
(def-clm-struct (k2sin :make-wrapper (lambda (g) (set! (k2sin-incr g) (hz->radians (k2sin-frequency g))) g)) (frequency 0.0) (angle 0.0) (incr 0.0)) (define (k2sin gen fm) (declare (gen k2sin) (fm float)) (let ((x (k2sin-angle gen))) (set! (k2sin-angle gen) (+ x fm (k2sin-incr gen))) (/ (* 3.0 (sin x)) ; 3 rather than 4 for normalization (- 5.0 (* 4.0 (cos x)))))) (with-sound () (let ((gen (make-k2sin 440.0))) (run (lambda () (do ((i 0 (1+ i))) ((= i 10000)) (outa i (k2sin gen 0.0))))))) |
Most of the formulas are implemented as generators in generators.scm, along with the single side-band cases, where possible. As J. A. Moorer and Marc Le Brun pointed out long ago, there are many more such formulas "buried in reference works". Well, they've done nobody any good being buried for a generation, so I say: dig them up! If you know of any curious formula, anything that might trigger an interesting train of reflection, please send it to me, and I'll add it to this collection.
nrxysin (sine-summation) and nrxycos |
make-nrxysin :optional-key (frequency *clm-default-frequency*) (ratio 1.0) ; ratio between frequency and the spacing between successive sidebands (n 1) ; number of sidebands (r .5) ; amplitude ratio between successive sidebands (-1.0 < r < 1.0) nrxysin s :optional (fm 0.0) nrxysin? s make-nrxycos :optional-key (frequency *clm-default-frequency*) (ratio 1.0) (n 1) (r .5) nrxycos s :optional (fm 0.0) nrxycos? s make-sine-summation :optional-key (frequency *clm-default-frequency*) (initial-phase 0.0) (n 1) (a .5) (ratio 1.0) sine-summation s :optional (fm 0.0) sine-summation? s
|
|
nxrysin is the new form of sine-summation. These three generators produce a kind of additive synthesis. "n" is the number of sidebands (0 gives a sine wave), "r" is the amplitude ratio between successive sidebands (don't set it to 1.0), and "ratio" is the ratio between the carrier frequency and the spacing between successive sidebands. A "ratio" of 2 gives odd-numbered harmonics for a (vaguely) clarinet-like sound. A negative ratio puts the side-bands below the carrier. A negative r is the same as shifting the initial phase by pi (instead of lining up for the spike at multiples of 2*pi, the (-1)^n causes them to line up at (2k-1)*pi, but the waveform is the same otherwise). The basic idea is very similar to that used in the ncos generator, but you have control of the fall-off of the spectrum and the spacing of the partials.
The peak amplitude of the nrxysin and sine-summation generators is hard to predict. I think nrxysin is close to the -1.0..1.0 ideal, and won't go over 1.0, whereas sine-summation, for historical reasons, is less well behaved. nrxycos is normalized correctly, and gives you the same kinds of controls you get in sine-summation.
Besides the usual FM input, you can also vary the "r" parameter (via mus-scaler) to get changing spectra. In the next example, we add a glissando envelope, and use the same envelope to vary "r" so that as the frequency goes up, "r" goes down (to avoid foldover, or whatever).
(definstrument (ss beg dur freq amp :optional (n 1) (r .5) (ratio 1.0) (frqf #f)) (let* ((st (seconds->samples beg)) (nd (+ st (seconds->samples dur))) (sgen (make-nrxysin freq ratio n r)) (frq-env (if frqf (make-env frqf :scaler (hz->radians freq) :duration dur) #f)) (spectr-env (if frqf (make-env frqf :duration dur) #f)) (amp-env (make-env '(0 0 1 1 2 1 3 0) :scaler amp :duration dur))) (run (lambda () (do ((i st (1+ i))) ((= i nd)) (if spectr-env (set! (mus-scaler sgen) (* r (exp (- (env spectr-env)))))) (outa i (* (env amp-env) (nrxysin sgen (if frq-env (env frq-env) 0.0))))))))) (with-sound () (ss 0 1 400.0 1.0 5 0.5 1.0 '(0 0 1 2))) |
"r" can also be used in the same way as an FM index, but with much simpler spectral evolution (x^n, x between -1.0 and 1.0, rather than Jn(x)). In the graph, r is 0 at the midpoint, r goes from -1.0 to 1.0 along the horizontal axis — I forgot to label the axes.
![]() |
(with-sound () (let ((gen1 (make-nrxycos 400 1 15 0.95)) (indr (make-env '(0 -1 1 1) :length 80000 :scaler 0.9999))) (do ((i 0 (1+ i))) ((= i 80000)) (set! (mus-scaler gen1) (env indr)) ; this sets r (outa i (* .5 (nrxycos gen1 0.0)))))) |
ssb-am |
make-ssb-am :optional-key (frequency *clm-default-frequency*) (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.
mus-frequency | frequency in Hz |
mus-phase | phase (of embedded sin osc) in radians |
mus-order | embedded delay line size |
mus-length | same as mus-order |
mus-interp-type | mus-interp-none |
mus-xcoeff | FIR filter coeff |
mus-xcoeffs | embedded Hilbert transform FIR filter coeffs |
mus-data | embedded filter state |
mus-increment | frequency in radians per sample |
(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))))))))
(definstrument (shift-pitch beg dur file freq :optional (order 40)) (let* ((st (seconds->samples beg)) (nd (+ st (seconds->samples dur))) (gen (make-ssb-am freq order)) (rd (make-readin file))) (run (lambda () (do ((i st (1+ i))) ((= i nd)) (outa i (ssb-am gen (readin rd)))))))) (with-sound () (shift-pitch 0 3 "oboe.snd" 1108.0)) |
Normal amplitude modulation, cos(x) * (amp + Y(t)), where Y is some signal, produces the carrier (cos(x)), and symmetric sidebands at x+/-frq where frq is each spectral component of Y. This is just an elaboration of
cos(x) * (amp + cos(y)) = amp * cos(x) + 1/2(cos(x - y) + cos(x + y))
So, the Y spectrum (the first picture below) is shifted up by cos(x) and mirrored on either side of it (the second picture below; the spectral components on the left side are folding under 0). In single side-band AM, we create both the Y spectrum, and, via the hilbert transform, a version of Y in which the phases are shifted too. Then we can add these two copies, using the phase differences to cancel one side of the symmetric spectrum (this is the third picture below; the new spectral components are not harmonically related however). Once we can shift a pitch without creating its symmetric twin, we can split a spectrum into many bands, shift each band separately, and thereby retain its original harmonic spacing (the fourth picture). We have the original, but at a higher pitch. If we then use src to convert it back to its pre-shift pitch, we have the original, but with a different length. We have decoupled the pitch from the duration, much as in a phase vocoder (which uses an FFT rather than a filter bank, and an inverse FFT of the moved spectrum, rather than ssb-am).
|
The second picture was created from oboe.snd (the original) via:
(let ((osc (make-oscil 1000.0))) (map-channel (lambda (y) (* .5 (amplitude-modulate .01 (oscil osc) y)))))
The third picture was created by:
(let ((am (make-ssb-am 1000 40))) (map-channel (lambda (y) (ssb-am am y))))
And the fourth used the ssb-am-bank function in dsp.scm rewritten here for with-sound:
(definstrument (repitch beg dur sound old-freq new-freq :optional (amp 1.0) (pairs 10) (order 40) (bw 50.0)) (let* ((start (seconds->samples beg)) (end (+ start (seconds->samples dur))) (ssbs (make-vector pairs)) (bands (make-vector pairs)) (factor (/ (- new-freq old-freq) old-freq)) (rd (make-readin sound))) (do ((i 1 (1+ i))) ((> i pairs)) (let* ((aff (* i old-freq)) (bwf (* bw (+ 1.0 (/ i (* 2 pairs)))))) (vector-set! ssbs (1- i) (make-ssb-am (* i factor old-freq))) (vector-set! bands (1- i) (make-bandpass (hz->radians (- aff bwf)) ; bandpass is in dsp.scm (hz->radians (+ aff bwf)) order)))) (run (lambda () (do ((i start (1+ i))) ((= i end)) (let ((sum 0.0) (y (readin rd))) (do ((band 0 (1+ band))) ((= band pairs)) (set! sum (+ sum (ssb-am (vector-ref ssbs band) (bandpass (vector-ref bands band) y))))) (outa i (* amp sum)))))))) (let* ((sound "oboe.snd") (mx (cadr (mus-sound-maxamp sound))) (dur (mus-sound-duration sound))) (with-sound (:scaled-to mx :srate (mus-sound-srate sound)) (repitch 0 dur sound 554 1000))) |
If you'd like to move formants independently of the fundamental, add or subtract integer multiples of the new fundamental from the make-ssb-am frequency argument. In the repitch instrument above, say we wanted to add a "stretch" argument to spread out or squeeze down the spectrum. We would replace the current make-ssb-am line with:
(vector-set! ssbs (1- i) (make-ssb-am (+ (* i factor old-freq) (* new-freq (inexact->exact (round (* i stretch)))))))
wave-train |
make-wave-train :optional-key (frequency *clm-default-frequency*) (initial-phase 0.0) wave (size *clm-table-size*) (type mus-interp-linear) wave-train w :optional (fm 0.0) wave-train? w make-wave-train-with-env :optional-key frequency env size
wave-train adds a copy its "wave" (a "grain" in more modern parlance) into its output at "frequency" times per second. These copies can overlap or have long intervals of silence in between, so wave train can be viewed either as an extension of pulse-train and table-lookup, or as a primitive form of granular synthesis. make-wave-train-with-env returns a new wave-train generator with the envelope 'env' loaded into its table.
mus-frequency | frequency in Hz |
mus-phase | phase in radians |
mus-data | wave array (no set!) |
mus-length | length of wave array (no set!) |
mus-interp-type | interpolation choice (no set!) |
With some simple envelopes or filters, you can use this for VOSIM and other related techniques. 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 (seconds->samples beg)) (end (+ start (seconds->samples dur))) (ampf (make-env (or ae (list 0 0 25 1 75 1 100 0)) :scaler amp :duration dur)) (frq0 (hz->radians f0)) (frq1 (hz->radians f1)) (frq2 (hz->radians f2)) (foflen (if (= (mus-srate) 22050) 100 200)) (vibr (make-oscil 6)) (vibenv (make-env (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)))))))))) (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))) |
The "wave" is a vct accessible via mus-data. The "fm" argument affects the frequency of repetition. Here is a wave-train instrument that increasingly filters its grain (the word "now", for example) while increasing the repetition rate. We're also using a pulse train as a sort of internal click track, using the same frequency envelope as the wave-train, so we have some idea when to refilter the grain.
(definstrument (when? start-time duration start-freq end-freq grain-file) (let* ((beg (seconds->samples start-time)) (len (seconds->samples duration)) (end (+ beg len)) (grain-dur (mus-sound-duration grain-file)) (frqf (make-env '(0 0 1 1) :scaler (hz->radians (- end-freq start-freq)) :duration duration)) (click-track (make-pulse-train start-freq)) (grain-size (seconds->samples grain-dur)) (grains (make-wave-train :size grain-size :frequency start-freq)) (ampf (make-env '(0 1 1 0) :scaler .7 :offset .3 :duration duration :base 3.0)) (grain (mus-data grains))) (file->array grain-file 0 0 grain-size grain) (let ((original-grain (vct-copy grain))) (run (lambda () (do ((i beg (1+ i))) ((= i end)) (let* ((gliss (env frqf))) (outa i (* (env ampf) (wave-train grains gliss))) (let ((click (pulse-train click-track gliss))) (if (> click 0.0) (let* ((scaler (max 0.1 (exact->inexact (/ (- i beg) len)))) (comb-len 32) (c1 (make-comb scaler comb-len)) (c2 (make-comb scaler (inexact->exact (floor (* comb-len .75))))) (c3 (make-comb scaler (inexact->exact (floor (* comb-len 1.25)))))) (do ((k 0 (1+ k))) ((= k grain-size)) (let ((x (vct-ref original-grain k))) (vct-set! grain k (+ (comb c1 x) (comb c2 x) (comb c3 x))))))))))))))) (with-sound () (when? 0 4 2.0 8.0 "right-now.snd")) |
rand, rand-interp |
make-rand :optional-key (frequency *clm-default-frequency*) ; 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 *clm-default-frequency*) (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.
mus-frequency | frequency in Hz |
mus-phase | phase in radians |
mus-scaler | amplitude arg used in make-<gen> |
mus-length | distribution table (vct) length |
mus-data | distribution table (vct), if any |
mus-increment | frequency in radians per sample |
rand: (if (>= phase (* 2 pi)) (set! output (mus-random amplitude))) (set! phase (+ phase (hz->radians frequency) sweep))
Since rand is producing a sequence of square-waves, and rand-interp a sequence of triangle-waves, both reflect that in their spectra (spectrum y axis is in dB):
![]() | ![]() |
|
![]() | ![]() |
|
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 "envelope" 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)) |
If you want a particular set of values, it's simplest to fill a vct with those values, then use random as the index into the array. Say we want 0.0, 0.5, and 1.0 at random, but 0.5 should happen three times as often as either of the others:
(let ((vals (vct 0.0 0.5 0.5 0.5 1.0))) (do ((i 0 (1+ i))) ((= i 10)) (display (format #f ";~A " (vct-ref vals (inexact->exact (floor (random 5.0))))))))
These "distributions" refer to the values returned by the random number generator, but all of them produce white noise (all frequencies are equally likely). You can, of course, filter the output of rand to get a different frequency distribution. See, for example, round-interp in generators.scm. It uses a moving-average generator to low-pass filter the output of a rand-interp generator; the result is a rand-interp signal with rounded corners. 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. (The brownian graph below has a different dB range).
| ||||||
|
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 (seconds->samples start)) (end (+ beg (seconds->samples duration)))) (run (lambda () (do ((i beg (1+ i))) ((= i end)) (outa i (* amp x)) (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 (seconds->samples beg)) (nd (+ st (seconds->samples dur))) (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))))))) |
which gives brass-like sounds! We can also get all the period doublings and so on from sin:
(with-sound (:clipped #f :scaled-to 0.5) (let ((x 0.5)) (do ((i 0 (1+ i))) ((= i 44100)) (outa i x) (set! x (* 4 (sin (* pi x))))))) |
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. If you're curious about filters, Julius Smith's on-line Introduction to Digital Filters is excellent.
mus-xcoeff | a0, a1, a2 in equations |
mus-ycoeff | b1, b2 in equations |
mus-order | 1 or 2 (no set!) |
mus-scaler | two-pole and two-zero radius |
mus-frequency | two-pole and two-zero center frequency |
one-zero y(n) = a0 x(n) + a1 x(n-1) one-pole y(n) = a0 x(n) - b1 y(n-1) two-pole y(n) = a0 x(n) - b1 y(n-1) - b2 y(n-2) two-zero y(n) = a0 x(n) + a1 x(n-1) + a2 x(n-2)
The "a0, b1" nomenclature is taken from Julius Smith's "An Introduction to Digital Filter Theory" in Strawn "Digital Audio Signal Processing", and is different from that used in the more general filters such as fir-filter. In make-two-pole and make-two-zero you can specify either the actual desired coefficients ("a0" and friends), or the center frequency and radius of the filter ("frequency" and "radius"). 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>
A quick way to see the frequency response of a filter is to drive it with a sine wave sweeping from 0 Hz to half the sampling rate; if the sound length is 0.5 seconds, you can read off the time axis as the response at that frequency (in terms of a sampling rate of 1.0):
(define (test-filter flt) (let* ((osc (make-oscil)) (samps (seconds->samples 0.5)) (ramp (make-env '(0 0 1 1) :scaler (hz->radians samps) :length samps))) (with-sound () (do ((i 0 (1+ i))) ((= i samps)) (outa i (flt (oscil osc (env ramp)))))))) (test-filter (make-one-zero 0.5 0.5)) (test-filter (make-one-pole 0.1 -0.9)) (test-filter (make-two-pole 0.1 0.1 0.9)) (test-filter (make-two-zero 0.5 0.2 0.3)) |
![]() |
formant |
make-formant :optional-key frequency ; resonance center frequency in Hz radius ; resonance width, indirectly formant f input :optional center-frequency-in-radians formant? f make-firmant :optional-key frequency radius firmant f input :optional center-frequency-in-radians firmant? f
formant and firmant are resonators (two-pole, two-zero bandpass filters) centered at "frequency", with the bandwidth set by "radius".
|
formant: y(n) = x(n) - r * x(n-2) + 2 * r * cos(frq) * y(n-1) - r * r * y(n-2) firmant: x(n+1) = r * (x(n) - 2 * sin(frq/2) * y(n)) + input y(n+1) = r * (2 * sin(frq/2) * x(n+1) + y(n)) |
The formant generator is described in "A Constant-gain Digital Resonator Tuned By a Single Coefficient" by Julius O. Smith and James B. Angell in Computer Music Journal Vol. 6 No. 4 (winter 1982) and "A note on Constant-Gain Digital Resonators" by Ken Steiglitz, CMJ vol 18 No. 4 pp.8-10 (winter 1994). The formant bandwidth is a function of the "radius", and its center frequency is set by "frequency". As the radius approaches 1.0 (the unit circle), the resonance gets narrower. Use mus-frequency to change the center frequency, and mus-scaler to change the radius. The radius can be set in terms of desired bandwidth in Hz via:
(exp (* -0.5 (hz->radians bandwidth)))
If you change the radius, the peak amplitude of the output changes. The firmant generator is the "modified coupled form" of the formant generator, developed by Max Mathews and Julius Smith in "Methods for Synthesizing Very High Q Parametrically Well Behaved Two Pole Filters". Here are some graphs showing the formant and firmant filtering white noise as we sweep either the frequency or the radius:
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 moves a set of harmonically related formants through a sound. If "radius" is .99, you get a glass-harmonica effect; if it's less, you get more of an FM index envelope effect.
(definstrument (move-formants start file amp radius move-env num-formants) (let* ((frms (make-vector num-formants)) (beg (seconds->samples start)) (dur (mus-sound-frames file)) (end (+ beg dur)) (rd (make-readin file)) (menv (make-env move-env :length dur))) (let ((start-frq (env menv))) (do ((i 0 (1+ i))) ((= i num-formants)) (vector-set! frms i (make-formant (* (+ i 1) start-frq) radius)))) (run (lambda () (do ((k beg (1+ k))) ((= k end)) (let ((sum 0.0) (x (readin rd)) (frq (env menv))) (do ((i 0 (1+ i))) ((= i num-formants)) (set! sum (+ sum (formant (vector-ref frms i) x))) (let ((curfrq (* (+ i 1) frq))) (if (< (* 2 curfrq) (mus-srate)) (set! (mus-frequency (vector-ref frms i)) curfrq)))) (outa k (* amp sum)))))))) (with-sound () (move-formants 0 "oboe.snd" 2.0 0.99 '(0 1200 1.6 2400 2 1400) 4)) |
The clm-3 formant gain calculation was incorrect. To translate from the old formant to the new one, multiply the old gain by (* 2 (sin (hz->radians frequency))).
If you change the radius or frequency rapidly, the formant generator will either produce clicks or overflow, but firmant gives good output. Here's an example that puts formant on the edge of disaster (the glitch is about to explode), but firmant plugs away happily:
(with-sound (:channels 2) (let* ((dur 3) (samps (seconds->samples dur)) (flta (make-formant 100 .999)) (fltc (make-firmant 100 .999)) (vib (make-oscil 10)) (index (hz->radians 100)) (click (make-ncos 40 500))) (run (lambda () (do ((i 0 (1+ i))) ((= i samps)) (let* ((vib (* index (+ 1 (oscil vib)))) (pulse (ncos click))) (outa i (* 10 (formant flta pulse vib))) (outb i (* 10 (firmant fltc pulse vib))) )))))) |
![]() |
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 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).
|
(let ((xout 0.0)) (vct-set! state 0 input) (do ((j (1- order) (1- j))) ((= j 0)) (set! xout (+ xout (* (vct-ref xcoeffs j) (vct-ref state j)))) (vct-set! state 0 (- (vct-ref state 0) (* (vct-ref ycoeffs j) (vct-ref state j)))) (vct-set! state j (vct-ref state (1- j)))) (+ xout (* (vct-ref state 0) (vct-ref 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 |
---|
|
delay, tap |
make-delay :optional-key size ; delay length initial-contents ; delay line's initial values (a vct or a list) (initial-element 0.0) ; delay line's initial element max-size ; maximum delay size in case the delay changes type ; interpolation 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 case of fractional delays: mus-interp-none, mus-interp-linear, mus-interp-all-pass, mus-interp-lagrange, mus-interp-bezier, or mus-interp-hermite.
mus-length | length of delay |
mus-order | same as mus-length |
mus-data | delay line itself (no set!) |
mus-interp-type | interpolation choice (no set!) |
mus-scaler | 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 (seconds->samples secs))) (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))))))))))) (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 mus-scaler 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: y(n) = x(n - size) + scaler * y(n - size) notch: y(n) = x(n) * scaler + x(n - size) filtered-comb: y(n) = x(n - size) + scaler * filter(y(n - size)) ![]() |
As a rule of thumb, the decay time of the feedback 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 (seconds->samples time)) (end (+ beg (seconds->samples dur))) (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))))))))) (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 (seconds->samples beg)) (end (+ start (seconds->samples dur))) (dly (make-comb :scaler .9 :size size)) (flt (make-fir-filter :order size :xcoeffs (mus-data dly))) ; comb delay line as FIR coeffs (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)))))))))) (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)) |
Here's another that fluctuates between two sets of combs; it usually works best with voice sounds:
(definstrument (flux start-time file frequency combs0 combs1 :optional (scaler 0.99) (comb-len 32)) (let* ((beg (seconds->samples start-time)) (len (mus-sound-frames file)) (end (+ beg len)) (num-combs0 (length combs0)) (num-combs1 (length combs1)) (cmbs0 (make-vector num-combs0)) (cmbs1 (make-vector num-combs1)) (osc (make-oscil frequency)) (rd (make-readin file))) (do ((k 0 (1+ k))) ((= k num-combs0)) (vector-set! cmbs0 k (make-comb scaler (inexact->exact (floor (* comb-len (list-ref combs0 k))))))) (do ((k 0 (1+ k))) ((= k num-combs1)) (vector-set! cmbs1 k (make-comb scaler (inexact->exact (floor (* comb-len (list-ref combs1 k))))))) (run (lambda () (do ((i beg (1+ i))) ((= i end)) (let* ((interp (oscil osc)) (sum0 0.0) (sum1 0.0) (x (readin rd))) (do ((k 0 (1+ k))) ((= k num-combs0)) (set! sum0 (+ sum0 (comb (vector-ref cmbs0 k) x)))) (do ((k 0 (1+ k))) ((= k num-combs1)) (set! sum1 (+ sum1 (comb (vector-ref cmbs1 k) x)))) (outa i (+ (* interp sum0) (* (- 1.0 interp) sum1))))))))) (with-sound (:scaled-to .5) (flux 0 "oboe.snd" 10.0 '(1.0 1.25 1.5) '(1.0 1.333 1.6)) ; bowed oboe? (flux 2 "now.snd" 4.0 '(1.0 1.25 1.5) '(1.0 1.333 1.6 2.0 3.0)) (flux 4 "now.snd" 1.0 '(1.0 1.25 1.5) '(1.0 1.333 1.6 2.0 3.0) 0.995 20) (flux 6 "now.snd" 10.0 '(1.0 1.25 1.5) '(1.0 1.333 1.6 2.0 3.0) 0.99 10) (flux 8 "now.snd" 10.0 '(2.0) '(1.0 1.333 1.6 2.0 3.0) 0.99 120) (flux 10 "fyow.snd" .50 '(1.0 2.0 1.5) '(1.0 1.333 1.6 2.0 3.0) 0.99 120)) |
For more comb filter examples, see examp.scm, chordalize in dsp.scm, or any of the standard reverbs such as nrev. filtered-comb is used in freeverb where 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 comb filter. If both scale terms are 0.0, we get a pure delay line.
mus-length | length of delay |
mus-order | same as mus-length |
mus-data | delay line itself (no set!) |
mus-feedback | feedback scaler |
mus-feedforward | feedforward scaler |
mus-interp-type | interpolation 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 or nrev.
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.
mus-length | length of table |
mus-order | same as mus-length |
mus-data | table of last 'size' values |
result = sum-of-last-n-inputs / n
moving-average is used 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-max, 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
mus-increment | srate 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 srt-env :duration duration)) (beg (seconds->samples start-time)) (end (+ beg (seconds->samples duration))) (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))))))))) (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 fmfreq)) (beg (seconds->samples start-time)) (end (+ beg (seconds->samples duration))) (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)))))))))) (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. To scale an src envelope so that the result has a given duration, use scr-fit-envelope.
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")
mus-length | fft 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 (seconds->samples beg)) (end (+ start (seconds->samples dur))) (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))))))) (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 normalize 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.
The convolve generator is the modern way to add reverb. There are impulse responses of various concert halls floating around the web. convolve and fir-filter actually perform the same mathematical operation, but convolve uses an FFT internally, rather than a laborious dot-product.
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
mus-frequency | time (seconds) between output grains (hop) |
mus-ramp | length (samples) of grain envelope ramp segment |
mus-hop | time (samples) between output grains (hop) |
mus-scaler | grain amp (scaler) |
mus-increment | expansion |
mus-length | grain length (samples) |
mus-data | grain samples (a vct) |
mus-location | granulate's local random number seed |
result = overlap add many tiny slices from input
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 (seconds->samples beg)) (new-dur (or dur (- (mus-sound-duration file) orig-beg))) (exA (make-granulate :input f :expansion exp-amt)) (nd (+ st (seconds->samples new-dur)))) (run (lambda () (do ((i st (1+ i))) ((= i nd)) (outa i (granulate exA))))))) (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))))))))) (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
mus-frequency | pitch shift |
mus-length | fft-size |
mus-increment | interp |
mus-hop | fft-size / overlap |
mus-location | outctr (counter to next fft) |
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 (seconds->samples beg)) (end (+ start (seconds->samples dur))) (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)))))))) (with-sound () (simple-pvoc 0 2.0 1.0 512 "oboe.snd")) |
There are several functions giving access to the phase-vocoder data:
phase-vocoder-amps gen phase-vocoder-freqs gen phase-vocoder-phases gen phase-vocoder-amp-increments gen phase-vocoder-phase-increments gen
These are arrays (vcts) containing the spectral data the phase-vocoder uses to reconstruct the sound. See clm23.scm for examples, in particular 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 *clm-default-frequency*) (initial-phase 0.0) (r 1.0) ; amplitude ratio between successive sidebands (ratio 1.0) ; ratio between carrier and sideband spacing asymmetric-fm af index :optional (fm 0.0) asymmetric-fm? af
mus-frequency | frequency in Hz |
mus-phase | phase in radians |
mus-scaler | "r" parameter; sideband scaler |
mus-offset | "ratio" parameter |
mus-increment | frequency in radians per sample |
based on:
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. P&P use sin(sin), but I'm using cos(sin) so that we get a sum of cosines, and can therefore easily normalize the peak amplitude to -1.0..1.0.
"r" is the ratio between successive sideband amplitudes, r < 0.0 or r > 1.0 pushes energy above the carrier, whereas r between 0.0 and 1.0 pushes it below. (r = 1.0 gives normal FM). The mirror image of r (around a given carrier) is produced by -1/r. "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. In this instrument we sweep "r" with an envelope:
(definstrument (asy beg dur freq amp index :optional (ratio 1.0)) (let* ((st (seconds->samples beg)) (nd (+ st (seconds->samples dur))) (r-env (make-env '(0 -1 1 -20) :duration dur)) (asyf (make-asymmetric-fm :ratio ratio :frequency freq))) (do ((i st (1+ i))) ((= i nd)) (set! (mus-scaler asyf) (env r-env)) ; this sets "r" (outa i (* amp (asymmetric-fm asyf index)))))) |
For the other kind of asymmetric-fm see generators.scm, 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 args | create frame and load it with args | |
make-frame! chans :rest args | create frame of any size and load it with args | |
frame? obj | is obj a frame | |
frame-ref f1 chan | return f1[chan] | |
frame-set! f1 chan val | f1[chan] = val (also set! with frame-ref) | |
frame+ f1 f2 :optional outf | add f1 and f2 element-wise, return new frame (or outf) | |
frame* f1 f2 :optional outf | multiply f1 and f2 element-size, return new frame (or outf) | |
make-mixer chans :rest args | create a mixer and load it with args | |
make-mixer! chans :rest args | create a mixer of any size and load it with args | |
make-scalar-mixer chans scl | create a mixer with scl on the diagonal | |
mixer? obj | is obj a mixer | |
mixer-ref m1 in out | m1[in,out] (use set! to change) | |
mixer-set! m1 in out val | m1[in,out] = val (also set! with mixer-ref) | |
mixer* m1 m2 :optional outm | matrix multiply of m1 and m2, return new mixer (or outm) | |
mixer+ m1 m2 :optional outm | matrix add of m1 and m2, return new mixer (or outm) | |
frame->frame mf mf :optional outf | pass frame through mixer, return new frame (or outf) | |
frame->list frame | return list of frame's contents | |
sample->frame mf sample :optional outf | pass sample through mf (a frame or mixer), return new frame (or outf) | |
frame->sample mf frame | pass frame through mf (a frame or mixer), return sample |
mus-channels | number of channels accommodated |
mus-length | same as mus-channels |
mus-data | frame 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> >(frame-set! f1 1 -.5) ; same as (set! (frame-ref f1 1) -.5) -0.5 >f1 #<frame[3]: [1.000 -0.500 0.100]> >(mixer-set! m1 0 1 0.5) ; same as (set! (mixer-ref m1 0 1) 0.5) 0.5 >m1 #<mixer: chans: 3, [ 1.000 0.500 0.000 0.000 1.000 0.000 0.000 0.000 2.000 ]>
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 (seconds->samples beg)) (end (+ start (seconds->samples dur))) (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) (set! ctr (1+ ctr))))))) |
If you forget to call mus-close, the GC will eventually do it for you.
readin |
make-readin :optional-key file (channel 0) (start 0) (direction 1) size readin rd readin? rd
mus-channel | channel arg to make-readin (no set!) |
mus-location | current location in file |
mus-increment | sample increment (direction arg to make-readin) |
mus-file-name | name of file associated with gen |
mus-length | number of frames in file associated with gen |
readin returns successive samples from a file; it is an elaboration of file->sample that keeps track of the current read location and channel number for you. 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 (seconds->samples beg)) (dur (mus-sound-duration file)) (rev-amount .01) (rdA (make-readin file)) (ampf (make-env amp-env amp dur)) (nd (+ st (seconds->samples dur)))) (run (lambda () (do ((i st (1+ i))) ((= i nd)) (let ((outval (* (env ampf) (readin rdA)))) (outa i outval) (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))) |
in-any, out-any |
out-any loc data channel (output *output*) outa loc data (output *output*) outb loc data (output *output*) outc loc data (output *output*) outd loc data (output *output*) in-any loc channel input ina loc input inb loc input
These are the "generic" input and output functions. out-any adds its "data" argument (a sound sample) into the "output" object at sample position "loc". The "output" argument can be a vct, a sound-data object, or a function, as well as the more usual frame->file object. or any output-capable CLM generator. 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.
in-any returns the sample at position "loc" in "input". ina is the same as in-any with a channel of 0. As in out-any and friends, the "input" argument can be a file->frame object, a vct, sound-data object, or a function. If "input" is a function, it should take two arguments, the location and the channel number.
(definstrument (simple-ina beg dur amp file) (let* ((start (seconds->samples beg)) (end (+ start (seconds->samples dur))) (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) (with-sound () (simple-ina 0 1 .5 "oboe.snd")) |
To write from with-sound to a vct or sound-data object, rather than a file, use its :output argument:
(with-sound (:output (make-vct 44100)) ; this sets *output*, the default output location (fm-violin 0 1 440 .1)) ;; returns #<vct[len=44100]: 0.000 0.000 0.000 ...> |
If *output* is a function, it should take 3 arguments, the sample number, current output value, and channel.
(let ((avg 0.0) (samps 0)) (with-sound (:output (lambda (frame val chan) ; get the average of all the samples (set! avg (+ avg val)) (set! samps (1+ samps)) val)) (do ((i 0 (1+ i))) ((> i 10)) (outa i (* i .1)))) (/ avg samps)) ;; returns 0.5 |
Similarly, if in-any's "input" argument is a function, it takes the input location (sample number), and channel (0-based).
(let ((input (make-readin "oboe.snd" :start 1000))) (with-sound (:output (make-vct 10)) (do ((i 0 (1+ i))) ((= i 10)) (outa i (ina i (lambda (loc chn) (readin input))))))) |
(let ((outv (make-vct 10))) (with-sound () (run (lambda () (do ((i 0 (1+ i))) ((= i 10)) (outa i (* i .1) (lambda (loc val chan) (vct-set! outv loc val))))))) outv) ; this is equivalent to using :output (make-vct 10) as a with-sound argument |
locsig |
make-locsig :optional-key (degree 0.0) (distance 1.0) (reverb 0.0) ; reverb amount (output *output*) ; output generator or location (revout *reverb*) ; reverb output generator or location (channels (mus-channels output)) (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 ()
mus-data | output scalers (a vct) |
mus-xcoeff | reverb scaler |
mus-xcoeffs | reverb scalers (a vct) |
mus-channels | output channels |
mus-length | output channels |
locsig normally takes the place of out-any in an
instrument. It tries to place a signal 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 (seconds->samples onset)) (end (+ beg (seconds->samples duration))) (loc (make-locsig :degree 0 :distance 1 :reverb reverb-amount)) (rdA (make-readin :file file)) (dist-env (make-env distance-env :duration duration)) (amp-env (make-env amplitude-env :duration duration)) (deg-env (make-env 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:
|
![]() |
![]() |
move-sound |
make-move-sound dlocs-list (output *output*) (revout *reverb*) move-sound dloc i in-sig move-sound? dloc
move-sound is intended as the run-time portion of dlocsig. make-dlocsig 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:
(define (simple-dloc beg dur freq amp) "(simple-dloc-4 beg dur freq amp) test instrument for dlocsig" (let* ((os (make-oscil freq)) (start (seconds->samples beg)) (end (+ start (seconds->samples dur))) (loc (make-move-sound (list start end 4 0 (make-delay 12) (make-env '(0 0 10 1) :length dur) #f (make-vector 4 #f) (vector (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)) #f (vector 0 1 2 3))))) (run (lambda () (do ((i start (1+ i))) ((= i end)) (move-sound loc i (* amp (oscil os)))))))) (with-sound (:channels 4) (simple-dloc 0 2 440 .5)) |
Generic Functions |
The generators respond to a set of "generic" functions. mus-frequency, for example, tries to return (or set) a generator's frequency, wherever that makes sense. The generic functions are:
|
Many of these are settable:
(set! (mus-frequency osc1) 440.0)
sets osc1's phase increment to (hz->radians 440.0).
When I have a cold, I sometimes use the following function to see how high I can hear; count
the audible tones and multiply by 1000:
(define (quick-check) (with-sound () (let ((gen (make-oscil 1000))) (run (lambda () (do ((i 0 (1+ i))) ((= i 400000)) (if (= (modulo i 20000) 0) (set! (mus-frequency gen) (+ 1000 (/ i 20)))) (outa i (* .5 (oscil gen))))))))) |
Another example is run-with-fm-and-pm in generators.scm which applies phase modulation (as well as the default frequency modulation) to any generator:
(define (run-with-fm-and-pm gen fm pm) (set! (mus-phase gen) (+ (mus-phase gen) pm)) (let ((result (mus-run gen fm 0.0))) (set! (mus-phase gen) (- (mus-phase gen) pm)) result)) |
mus-generator? returns #t if its argument is a generator. A generator defined via def-clm-struct can take part in these methods. If the last element of the list (returned by def-clm-struct) is an association list, the generic functions will search that list for their name, and use the functions that follow to implement their method (see defgenerator in generators.scm for an extended example).
Other Generators |
(this section is work in progress...)
There are dozens of generators scattered around the *.scm files that come with Snd. Some that come to mind:
analog-filter.scm: filter: butterworth-lowpass|highpass|bandpass|bandstop, chebyshev-lowpass|highpass|bandpass|bandstop, inverse-chebyshev-lowpass|highpass|bandpass|bandstop, elliptic-lowpass|highpass|bandpass|bandstop, bessel-lowpass|highpass|bandpass|bandstop clm-ins.scm: rms gain balance dsp.scm: fir-filter: hilbert-transform, highpass, lowpass, bandpass, bandstop, differentiator, make-spencer-filter, savitzky-golay-filter filter: butter-high-pass, butter-low-pass, butter-band-pass, butter-band-reject, biquad, iir-low-pass, iir-high-pass, iir-band-pass, iir-band-stop, peaking, butter-lp, butter-hp, butter-bp, butter-bs delay: moving-max average: moving-sum, moving-rms, moving-length, weighted-moving-average one-pole: exponentially-weighted-moving-average volterra-filter env.scm: power-env (and many env makers/modifiers) extensions.scm: env-expt-channel (and many related env modifiers) examp.scm: ramp, sound-interp green.scm: rand and rand-interp: green-noise, brownian-noise moog.scm: moog-filter prc95.scm: reed, bowtable, jettable, onep, lip, dc-block, delaya, delayl sndclm.html: sinc-train, 1f-noise, blackman4 zip.scm: zipper
In this section, we concentrate on the generators defined in generators.scm. Nearly all of them respond to the generic functions mus-name, mus-reset, mus-describe, mus-frequency, mus-scaler, mus-offset, mus-phase, and mus-order. The parameters are generally "frequency", "n" (the number of sidebands), "r" (the ratio between successive sideband amplitudes), and "ratio" (the ratio between the frequency and the spacing between successive sidebands).
make-asyfm :optional-key (frequency 0.0) (ratio 1.0) (r 1.0) (index 1.0) asyfm-J gen fm asyfm-I gen fm asyfm? gen
These two generators produce the two flavors of asymmetric-fm. asyfm-J is the same as the built-in asymmetric generator; asyfm-I is the modified Bessel function version (the second formula in the asymmetric-fm section).
make-fmssb :optional-key (frequency 0.0) (ratio 1.0) (index 1.0) fmssb gen fm fmssb? gen
This generator produces the "gapped" spectra mentioned in fm.html. It is used extensively in the various "imaginary machines". Also included in this section of generators.scm is fpmc, an instrument that performs FM with a complex index (complex in the sense of complex numbers). It can be used in with-sound, but not within run (the run macro does not currently handle complex numbers).
make-pulsed-env :optional-key envelope duration frequency pulsed-env gen fm pulsed-env? gen
This produces a repeating envelope. env sticks at its last value, but pulsed-env repeats it over and over. "duration" is the envelope duration, and "frequency" is the repeitition rate, changeable via the "fm" argument to the pulsed-env generator. Many of the instruments in animals.scm use this generator. (In a sense, this is the original Music V OSC generator).
make-blackman :optional-key frequency n ; 1 <= n <= 10 blackman gen fm blackman? gen
This produces a Blackman-Harris sum of cosines of order 'n'.
make-adjustable-square-wave :optional-key frequency (duty-factor 0.5) (amplitude 1.0) adjustable-square-wave gen fm adjustable-square-wave? gen make-adjustable-triangle-wave :optional-key frequency (duty-factor 0.5) (amplitude 1.0) adjustable-triangle-wave gen fm adjustable-triangle-wave? gen make-adjustable-sawtooth-wave :optional-key frequency (duty-factor 0.5) (amplitude 1.0) adjustable-sawtooth-wave gen fm adjustable-sawtooth-wave? gen
adjustable-square-wave produces a square-wave with optional "duty-factor" (ratio of pulse duration to pulse period). The other two are similar, producing triangle and sawtooth waves. There is also an adjustable-oscil. Use mus-scaler to set the duty-factor at run-time.
make-round-interp :optional-key frequency n amplitude round-interp gen fm round-interp? gen
This is a rand-interp generator feeding a moving-average generator. "n" is the length of the moving-average; the higher "n", the more low-passed the output.
Bessel functions |
make-bess :optional-key (frequency 0.0) (n 0) bess gen fm bess? gen
bess produces the nth Bessel function. The generator output is scaled to have a maximum of 1.0, so bess's output is not the same as the raw bessel function value returned by bes-jn. The "frequency" argument actually makes sense here because the Bessel functions are close to damped sinusoids after their initial hesitation:
where the variables other than x remain bounded as x increases. This explains, in a sketchy way, why Jn(cos) and Jn(Jn) behave like FM. To see how close these are to FM, compare the expansion of J0(sin) with FM's cos(sin):
The rest of the generators in this section suffer a similar fate. From a waveshaping perspective, we're using a sinusoid, or a modulated sinusoid, to index into the near-zero portion of a Bessel function, and the result is sadly reminiscent of standard FM. But they're such pretty formulas; I must be missing something.
make-j0evencos :optional-key (frequency 0.0) (index 1.0) j0evencos gen fm j0evencos? gen
j0evencos produces the J0(index * sin(x)) case mentioned above (with the DC component subtracted out). If you sweep the index, the bandwidth is the same as in normal FM (J2k(B) is about 3log(k)*Jk(B/2)^2), but the B/2 factor causes the individual component amplitudes to follow the Bessel functions half as fast. So j0evencos produces a spectral sweep that is like FM's but smoother. The effect is very subtle — the Man will not be impressed.
![]() |
![]() |
(with-sound (:channels 2) (let* ((dur 1.0) (end (seconds->samples dur)) (jmd (make-j0evencos 200.0)) (fmd (make-oscil 200.0)) (ampf (make-env '(0 0 1 1 20 1 21 0) :scaler 0.5 :duration dur)) (indf (make-env '(0 0 1 20) :duration dur))) (do ((i 0 (1+ i))) ((= i end)) (let ((ind (env indf)) (vol (env ampf))) (set! (j0evencos-index jmd) ind) (outa i (* vol (- (cos (* ind (oscil fmd))) (bes-j0 ind)))) ; subtract out DC (see cos(B sin x) above) (outb i (* vol (j0evencos jmd 0.0))))))) |
make-j0j1cos :optional-key (frequency 0.0) (index 0.0) j0j1cos gen fm j0j1cos? gen
This uses J0(index * cos(x)) + J1(index * cos(x)) to produce a full set of cosines. It is not yet normalized correctly, and is very similar to normal FM.
make-izcos :optional-key (frequency 0.0) (r 1.0) izcos gen fm izcos? gen
This produces a sum of cosines scaled by In(r), again very similar to normal FM.
make-jjcos :optional-key (frequency 0.0) (r 0.0) (a 1.0) (k 1) jjcos gen fm jjcos? gen make-j2cos :optional-key (frequency 0.0) (r 0.0) (n 1) j2cos gen fm j2cos? gen make-jpcos :optional-key (frequency 0.0) (r 0.0) (a 0.0) (k 1) jpcos gen fm jpcos? gen make-jncos :optional-key (frequency 0.0) (r 0.0) (a 1.0) (n 0) jncos gen fm jncos? gen
These produce a sum of cosines scaled by a product of Bessel functions; in a sense, there are two, or maybe three "indices". Normalization is not handled correctly in all cases.
jjcos: | ![]() | |
j2cos: | ![]() | |
jpcos: | ![]() | |
jncos: | ![]() |
make-jycos :optional-key (frequency 0.0) (r 1.0) (a 1.0) jycos gen fm jycos? gen
This uses bes-y0 to produce components scaled by Yn(r)*Jn(a). bes-y0(0) is -inf, so a^2 + r^2 should be greater than 2ar, and r should be greater than 0.0. Tricky to use. (If you get and inf or a NaN from division by zero or whatever in Scheme, both the time and frequency graphs will be unhappy; perhaps I should add an explicit warning when that happens).
finite sums |
If n (the number of partials) is less than 25 and r is constant, these could be done with polywave. I don't think there would be any difference, even taking FM into account. For n greater than 25, these are faster, but also similar to each other musically.
make-nssb :optional-key (frequency 0.0) (ratio 1.0) (n 1) nssb gen fm nssb? gen
nssb is the single side-band version of ncos and nsin. It is very similar to nxysin and nxycos.
make-ncos2 :optional-key (frequency 0.0) (n 1) ncos2 gen fm ncos2? gen
This is the Fejer kernel. The i-th harmonic amplitude is (n-i)/(n+1).
make-ncos4 :optional-key (frequency 0.0) (n 1) ncos4 gen fm ncos4? gen
This is the Jackson kernel, the square of ncos2.
make-npcos :optional-key (frequency 0.0) (n 1) npcos gen fm npcos? gen
This is the Poussin kernel, a combination of two ncos2 generators, one at "n" subtracted from twice another at 2n+1.
make-n1cos :optional-key (frequency 0.0) (n 1) n1cos gen fm n1cos? gen
Another spikey waveform, very similar to ncos2 above.
make-nxycos :optional-key (frequency 0.0) (ratio 1.0) (n 1) nxycos gen fm nxycos? gen make-nxysin :optional-key (frequency 0.0) (ratio 1.0) (n 1) nxysin gen fm nxysin? gen make-nxy1cos :optional-key (frequency 0.0) (ratio 1.0) (n 1) nxy1cos gen fm nxy1cos? gen make-nxy1sin :optional-key (frequency 0.0) (ratio 1.0) (n 1) nxy1sin gen fm nxy1sin? gen
These produce a sum of "n" sinsoids starting at "frequency", spaced by "ratio". Since "frequency" can be treated as the carrier, there's no point in an ssb version. nxysin output is not normalized. nxy1cos is the same as nxycos, but every other component is multiplied by -1, and "n" produces 2n-1 components.
make-noddcos :optional-key (frequency 0.0) (n 1) noddcos gen fm noddcos? gen make-noddsin :optional-key (frequency 0.0) (n 1) noddsin gen fm noddsin? gen make-noddssb :optional-key (frequency 0.0) (ratio 1.0) (n 1) noddssb gen fm noddssb? gen
These produce the sum of "n" equal amplitude odd-numbered sinusoids:
The corresponding "even" case is the same as ncos with twice the frequency. noddsin produces a somewhat clarinet-like tone:
(with-sound (:play #t) (let ((gen (make-noddsin 300 :n 3)) (ampf (make-env '(0 0 1 1 2 1 3 0) :length 40000 :scaler .5))) (run (lambda () (do ((i 0 (1+ i))) ((= i 40000)) (outa i (* (env ampf) (noddsin gen 0.0))))))))
make-nrcos :optional-key (frequency 0.0) (n 1) (r 0.0) ; -1.0 < r < 1.0 nrcos gen fm nrcos? gen make-nrsin :optional-key (frequency 0.0) (n 1) (r 0.0) ; -1.0 < r < 1.0 nrsin gen fm nrsin? gen make-nrssb :optional-key (frequency 0.0) (ratio 1.0) (n 1) (r 0.0) ; 0.0 <= r < 1.0 nrssb gen fm nrssb-interp gen fm interp nrssb? gen
These produce the sum of "n" sinusoids, with successive sinusoids scaled by "r"; the nth component has amplitude r^n. nrsin is just a wrapper for nrxysin, and the other two are obviously variants of nrxycos. In the nrssb-interp generator, the "interp" argument interpolates between the upper (interp=1.0) and lower (interp=-1.0) side bands.
The instrument lutish uses nrcos: lutish beg dur freq amp
:
(with-sound (:play #t) (do ((i 0 (1+ i))) ((= i 10)) (lutish (* i .1) 2 (* 100 (1+ i)) .05)))
The instrument oboish uses nrssb: oboish beg dur freq amp amp-env
:
(with-sound (:play #t) (do ((i 0 (1+ i))) ((= i 10)) (oboish (* i .3) .4 (+ 100 (* 50 i)) .05 '(0 0 1 1 2 1 3 0))))
organish also uses nrssb: organish beg dur freq amp fm-index amp-env
:
(with-sound (:play #t) (do ((i 0 (1+ i))) ((= i 10)) (organish (* i .3) .4 (+ 100 (* 50 i)) .5 1.0 #f)))
make-nkssb :optional-key (frequency 0.0) (ratio 1.0) (n 1) nkssb gen fm nkssb-interp gen fm interp nkssb? gen
This generator produces the single side-band version of the sum of "n" sinusoids, where the nth component has amplitude n. In the nkssb-interp generator, the "interp" argument interpolates between the upper and lower side bands. The instrument nkssber uses nkssb-interp:
(with-sound (:play #t) (nkssber 0 1 1000 100 5 5 0.5) (nkssber 1 2 600 100 4 1 0.5) (nkssber 3 2 1000 540 3 3 0.5) (nkssber 5 4 300 120 2 0.25 0.5) (nkssber 9 1 30 4 40 0.5 0.5) (nkssber 10 1 20 6 80 0.5 0.5))
make-nsincos :optional-key (frequency 0.0) (n 1) nsincos gen fm nsincos? gen
This generator produces a sum of n cosines scaled by sin(k*pi/(n+1))/sin(pi/(n+1)).
make-nchoosekcos :optional-key (frequency 0.0) (ratio 1.0) (n 1) nchoosekcos gen fm nchoosekcos? gen
This generator produces a sum of n cosines scaled by the binomial coefficients. If n is even, the last term is halved.
infinite sums |
make-rcos :optional-key (frequency 0.0) (r 0.0) ; -1.0 < r < 1.0 rcos gen fm rcos? gen make-rssb :optional-key (frequency 0.0) (ratio 1.0) (r 0.0) ; -1.0 < r < 1.0 rssb gen fm rssb-interp gen fm interp rssb? gen make-rxycos :optional-key (frequency 0.0) (ratio 1.0) (r 0.0) ; -1.0 < r < 1.0 rxycos gen fm rxycos? gen make-rxysin :optional-key (frequency 0.0) (ratio 1.0) (r 0.0) ; -1.0 < r < 1.0 rxysin gen fm rxysin? gen
These generators produce an infinite sum of sinusoids, each successive component scaled by "r" (so the nth component has amplitude r^n).
The bump instrument uses rssb-interp: bump beg dur freq amp f0 f1 f2
:
(with-sound (:play #t) (do ((k 0 (1+ k))) ((= k 10)) (bump (* 0.4 k) 1 (* 16.3 (expt 2.0 (+ 3 (/ k 12)))) .5 520 1190 2390)) (do ((k 0 (1+ k))) ((= k 10)) (let* ((freq (* 16.3 (expt 2.0 (+ 3 (/ k 12))))) (scl (sqrt (/ freq 120)))) (bump (+ 4 (* 0.4 k)) 1 freq .5 (* scl 520) (* scl 1190) (* scl 2390))))) |
As with all the "infinite sums" generators, aliasing is a major concern. We can use the following relatively conservative function to find the highest safe "r" given the current fundamental and sampling rate:
(define (safe-r-max freq srate) (expt .001 (/ 1.0 (floor (/ srate (* 3 freq))))))
If you go over that value, be prepared for some very unintuitive behavior! For example, at an srate of 44100:
In the first case, all the aliased harmonics line up perfectly with the unaliased ones because 21*1050 is 22050, but in the second case, we get (for example) the strong 84 Hz component because the 42nd harmonic which falls at 44100 - 42*1048 = 84 still has an amplitude of 0.99^42 = .66! |
![]() |
Another artifact of aliasing is that at some frequencies, for example at 100 Hz, and a sampling rate of 44100, if r is -0.99 and the initial phase is 0.5*pi, or if r is 0.99 and the initial phase is 1.5*pi, the peak amp is only 0.6639. Finally(?), there's a sharp discontinuity (a click) as you sweep r through 0.0. As in nrxycos, the waveforms produced by r and -r are the same, but there's an overall phase difference of pi.
Other notes: the output of rssb is not normalized, nor is rxysin.
make-ercos :optional-key (frequency 0.0) (r 0.0) ; r > 0.0 ercos gen fm ercos? gen make-erssb :optional-key (frequency 0.0) (ratio 1.0) (r 0.0) erssb gen fm erssb? gen
These produce a sum of sinusoids, each scaled by e^(-kr), a special case of rcos. Our safe (minimum) "r" here becomes (/ (log 0.001) (floor (/ srate (* -3 freq))))
.
The ercoser instrument uses ercos:
ercoser beg dur freq amp r
:
(with-sound (:play #t) (ercoser 0 1 100 .5 0.1))
make-eoddcos :optional-key (frequency 0.0) (r 0.0) eoddcos gen fm eoddcos? gen
This produces a sum of odd harmonics, each scaled by e^r(2k-1)/(2k-1). As "r" approches 0.0, this approaches a square wave.
(with-sound (:play #t) (let ((gen1 (make-eoddcos 400.0 :r 0.0)) (gen2 (make-oscil 400.0)) (a-env (make-env '(0 0 1 1) :length 10000))) (run (lambda () (do ((i 0 (1+ i))) ((= i 10000)) (set! (eoddcos-r gen1) (env a-env)) (outa i (* .5 (eoddcos gen1 (* .1 (oscil gen2))))))))))
make-rkcos :optional-key (frequency 0.0) (r 0.0) ; -1.0 < r < 1.0 rkcos gen fm rkcos? gen make-rksin :optional-key (frequency 0.0) (r 0.0) ; -1.0 < r < 1.0 rksin gen fm rksin? gen make-rkssb :optional-key (frequency 0.0) (ratio 1.0) (r 0.0) ; -1.0 < r < 1.0 rkssb gen rkssb? gen
These produce a sum of sinusoids scaled by (r^k)/k. As r approaches 1.0 or -1.0, rksin approaches a sawtooth. As with rcos, we can calculate the safe maximum r, given the current srate and frequency (this function is perhaps too cautious...):
(define (safe-rk-max freq srate) (let ((topk (floor (/ srate (* 3 freq))))) (expt (* .001 topk) (/ 1.0 topk))))
rksin and rkcos provide a nice demonstration of how insensitive the ear is to phase. These two waveforms look different, but have the same timbre. The sawtooth sounds louder to me, despite having the same peak amplitude.
(with-sound (:channels 2) (let ((gen1 (make-rkcos 200.0 :r 0.9)) (gen2 (make-rksin 200.0 :r 0.9))) (do ((i 0 (1+ i))) ((= i 100000)) (outa i (* .95 (rkcos gen1 0.0))) (outb i (* .95 (rksin gen2 0.0)))))) > (channel-rms 0 0) ; from dsp.scm 0.305301097090353 > (channel-rms 0 1) 0.627769794744852 |
![]() |
We might conclude that the RMS value gives the perceived amplitude, but in the next case, the RMS values are the same, and the peak amplitudes differ by a factor of 3. I think the one with the higher peak amplitude sounds louder.
(with-sound (:channels 2) (let* ((gen1 (make-adjustable-square-wave 400 :duty-factor .75 :amplitude .25)) (gen2 (make-adjustable-square-wave 400 :duty-factor .11 :amplitude .75)) (flt1 (make-moving-average 10)) (flt2 (make-moving-average 10))) (do ((i 0 (1+ i))) ((= i 50000)) (outa i (moving-average flt1 (adjustable-square-wave gen1 0.0))) (outb i (moving-average flt2 (adjustable-square-wave gen2 0.0)))))) |
![]() |
Since clipping is a disaster, we focus on peak amplitudes in the generators.
make-rk!cos :optional-key (frequency 0.0) (r 0.0) rk!cos gen fm rk!cos? gen make-rk!ssb :optional-key (frequency 0.0) (ratio 1.0) (r 0.0) rk!ssb gen fm rk!ssb? gen make-rxyk!cos :optional-key (frequency 0.0) (ratio 1.0) (r 0.0) rxyk!cos gen fm rxyk!cos? gen make-rxyk!sin :optional-key (frequency 0.0) (ratio 1.0) (r 0.0) rxyk!sin gen fm rxyk!sin? gen
These produce a sum of sinusoids scaled by (r^k)/k!. rk!cos is a special case of rxyk!cos. The k! denominator dominates eventually, so r * ratio * frequency is approximately the spectral center. That is, the ratio between successive harmonic amplitudes is (r^(k+1)/(k+1)!)/(r^k/k!) = r/(k+1), which becomes less than 1.0 at k=r. For example, if frequency is 100 and r is 30, the center of the spectrum is around 3kHz. Negative "r" gives the same spectrum as positive, but the waveform's initial-phase is shifted by pi. The (very) safe maximum "r" is: (define (safe-rk!-max freq srate) (let ((topk (floor (/ srate (* 3 freq))))) (expt (* .001 (factorial topk)) (/ 1.0 topk)))) ;; factorial is in numerics.scm |
![]() |
As in other such cases, varying "r" gives changing spectra. Here is a use of rk!cos to make a bird twitter:
(with-sound (:play #t :scaled-to .5) (do ((k 0 (1+ k))) ((= k 6)) (let ((gen (make-rk!cos 3000.0 :r 0.6)) (ampf (make-env '(0 0 1 1 2 1 3 0) :length 3000)) (frqf (make-env '(0 0 1 1) :base .1 :scaler (hz->radians 2000) :length 3000))) (run (lambda () (do ((i 0 (1+ i))) ((= i 3000)) (outa (+ i (* k 4000)) (* (env ampf) (rk!cos gen (env frqf))))))))))
The instrument bouncy uses rk!ssb: bouncy beg dur freq amp :optional (bounce-freq 5) (bounce-amp 20)
(with-sound (:play #t) (bouncy 0 2 200 .5 3 2))
make-r2k!cos :optional-key (frequency 0.0) (r 0.0) (k 0.0) r2k!cos gen fm r2k!cos? gen
![]() |
![]() |
The instruments pianoy and pianoy1 use r2k!cos: pianoy beg dur freq amp
, and
pianoy1 beg dur freq amp :optional (bounce-freq 5) (bounce-amp 20)
:
(with-sound (:play #t) (pianoy 0 3 100 .5)) (with-sound (:play #t) (pianoy1 0 4 200 .5 1 .1))
pianoy2 combines r2k!cos with fmssb to try to get closer to the hammer sound:
(with-sound (:play #t) (pianoy2 0 1 100 .5))
make-k2sin :optional-key (frequency 0.0) k2sin gen fm k2sin? gen make-k2cos :optional-key (frequency 0.0) k2cos gen fm k2cos? gen make-k2ssb :optional-key (frequency 0.0) (ratio 1.0) k2ssb gen fm k2ssb? gen
These produce a sum of sinusoids scaled by 1/(2^k).
make-rkoddssb :optional-key (frequency 0.0) (ratio 1.0) (r 0.0) rkoddssb gen fm rkoddssb? gen
This produces a sum of odd-numbered harmonics scaled by (r^(2k-1))/(2k-1).
The instrument stringy uses rkoddssb and rcos: stringy beg dur freq amp
:
(with-sound (:play #t) (do ((i 0 (1+ i))) ((= i 10)) (stringy (* i .3) .3 (+ 200 (* 100 i)) .5)))
glassy also uses rkoddssb: glassy beg dur freq amp
:
(with-sound (:play #t) (do ((i 0 (1+ i))) ((= i 10)) (glassy (* i .3) .1 (+ 400 (* 100 i)) .5)))
make-k3sin :optional-key (frequency 0.0) k3sin gen fm k3sin? gen
This produces a sum of sines scaled by 1.0/(k^3).
make-krksin :optional-key (frequency 0.0) (r 0.0) krksin gen fm krksin? gen
This produces a sum of sinusoids scaled by kr^k. Its output is not normalized.
make-abssin :optional-key (frequency 0.0) abssin gen fm abssin? gen
This produces abs(oscil) — not very useful, I fear.
make-abcos :optional-key (frequency 0.0) (a 0.0) (b 0.0) abcos gen fm abcos? gen make-absin :optional-key (frequency 0.0) (a 0.0) (b 0.0) absin gen fm absin? gen
These produce a sum of sinusoids scaled as follows:
make-r2sin :optional-key (frequency 0.0) (r 0.0) r2sin gen fm r2sin? gen make-r2cos :optional-key (frequency 0.0) (r 0.0) r2cos gen fm r2cos? gen make-r2ssb :optional-key (frequency 0.0) (ratio 1.0) (r 0.0) r2ssb gen r2ssb? gen
These produce a sum of even numbered sinusoids (er, think of it as up an octave or something) with each component scaled by (r^(2k))/(2k)!. r2sin output is not normalized.
make-r2k2cos :optional-key (frequency 0.0) (r 0.0) r2k2cos gen fm r2k2cos? gen
This produces a sum of cosines, each scaled by 1/(r^2+k^2).
defgenerator |
defgenerator name fields
defgenerator is an extension of def-clm-struct that sets up all the usual generator methods (discussed under "generic functions" above). It is used to define all the generators in generators.scm. An oscillator could be:
(defgenerator (osc :make-wrapper (lambda (gen) (set! (osc-frequency gen) (hz->radians (osc-frequency gen))) gen)) (frequency 0.0) (phase 0.0)) (define (osc gen fm) (declare (gen osc) (fm float)) (let ((result (sin (osc-phase gen)))) (set! (osc-phase gen) (+ (osc-phase gen) (osc-frequency gen) fm)) result)) |
defgenerator defines make-osc, osc?, and all the usual methods for us: mus-name ("osc"), mus-frequency (radians->hz of the frequency field), mus-phase, mus-reset, mus-run, and mus-describe. If the generator has a field named "n" or "order", it defines mus-order. If it has a field named "r" or "amplitude", it defines mus-scaler.
Other Functions |
There are several functions closely tied to the generators and instruments.
hz->radians freq | convert freq to radians per sample (using mus-srate): (freq * 2 * pi) / srate | |
radians->hz rads | convert rads to Hz (using mus-srate): (rads * srate) / (2 * pi) | |
db->linear dB | convert dB to linear value: 10^(dB/20) | |
linear->db val | convert val to dB: 20 * log(x) / log(10) | |
times->samples start duration | convert start and duration from seconds to samples (beg+dur in latter case) | |
samples->seconds samps | convert samples to seconds (using mus-srate): samps / srate | |
seconds->samples secs | convert seconds to samples (using mus-srate): secs * srate | |
degrees->radians degs | convert degrees to radians: (degs * 2 * pi) / 360 | |
radians->degrees rads | convert radians to degrees: (rads * 360) / (2 * pi) | |
clear-array arr | set all values in arr (a vct) to 0.0 | |
mus-srate | sampling rate 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).
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 being accessed on every sample, as an increment of a phase variable.
And a couple functions that affect things like the generator equality check:
mus-float-equal-fudge-factor
This function sets how far apart generator vct elements can be and still be considered equal in equal?, and
mus-array-print-length
determines how many vct elements are printed by mus-describe.
polynomial |
polynomial coeffs x
The polynomial function evaluates a polynomial, defined by giving its coefficients, at the point "x". "coeffs" is a vct of coefficients where coeffs[0] is the constant term, and so on.
>(polynomial (vct 0.0 1.0) 2.0) ; x 2.0 >(polynomial (vct 1.0 2.0 3.0) 2.0) ; 3x*x + 2x + 1 17.0
poly.scm has a variety of polynomial-related functions. Abramowitz and Stegun, "A Handbook of Mathematical Functions" is a treasure-trove of interesting polynomials. For waveshaping, use the function partials->polynomial to generate the coefficients, then drive polynomial with a cosine; this is what the polyshape generator does for you (see the bigbird and fm-violin instruments). Here's a generator that uses polynomial to produces a Blackman4 waveform (see also make-blackman in generators.scm):
(def-clm-struct (blackman4 :make-wrapper (lambda (g) (set! (blackman4-incr g) (hz->radians (blackman4-frequency g))) (set! (blackman4-coeffs g) (vct .084037 -.29145 .375696 -.20762 .041194)) g)) (frequency 0.0) (initial-phase 0.0) (coeffs #f :type vct) (angle 0.0) (incr 0.0)) (define (blackman4 gen fm) (declare (gen blackman4) (fm float)) (let ((x (blackman4-angle gen))) (set! (blackman4-angle gen) (+ x fm (blackman4-incr gen))) (polynomial (blackman4-coeffs gen) (cos x)))) (with-sound () (let ((black4 (make-blackman4 440.0))) (run (lambda () (do ((i 0 (1+ i))) ((= i 20000)) (outa i (blackman4 black4 0.0))))))) |
array-interp, dot-product |
array-interp fn x :optional size dot-product in1 in2 edot-product freq data mus-interpolate type x v size y1
array-interp interpolates in the array "fn" at the point "x". It underlies the table-lookup generator, among others. Here's array-interp as a "compander":
(define compand-table (vct -1.0 -0.96 -0.90 -0.82 -0.72 -0.60 -0.45 -0.25 0.0 0.25 0.45 0.60 0.72 0.82 0.90 0.96 1.0)) (map-channel (lambda (inval) (let ((index (+ 8.0 (* 8.0 inval)))) (array-interp compand-table index 17)))) |
sound-interp in examp.scm fills an array with an entire sound, then uses array-interp to read it.
dot-product is the usual "inner product" or "scalar product" (a name that should be banned from polite society). 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 the complex dot-product of the "data" argument (a vct or a vector) with (exp (* freq i))
.
Here, "i" goes from 0 to data's size - 1.
"freq" and the elements of "data" can be complex, as can the return value. See stretch-sound-via-dft
for an example.
mus-interpolate is the function used whenever table lookup interpolation is requested, as in
delay or wave-train.
The "type" argument 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:
(sin (+ (* input pi 0.5) (* index (sin (* input pi 2)))))
This brightens the sound, helping it cut through a huge mix. A similar (slightly simpler) effect 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. And a sort of converse is sound-interp.
ring-modulate, amplitude-modulate |
ring-modulate in1 in2 ; returns(* in1 in2)
amplitude-modulate am-carrier in1 in2 ; returns(* in1 (+ am-carrier in2))
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 "in1" as "carrier". Normal amplitude modulation using this function would be:
(define carrier (make-oscil carrier-freq (* .5 pi))) ... (amplitude-modulate 1.0 (oscil carrier) signal)
Both of these functions take advantage of the "Modulation Theorem"; since multiplying a signal by e^(iwt) translates its spectrum by w / two pi Hz, multiplying by a sinusoid splits its spectrum into two equal parts translated up and down by w/(two pi) Hz:
The upper sidebands may foldover (alias); low-pass filter the inputs to keep that from happening.
FFT (fourier transform) |
mus-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 autocorrelate data correlate data1 data2
mus-fft, spectrum, and convolution are the standard functions used everywhere. fft is the Fourier transform, convolution convolves its arguments, and spectrum returns (magnitude (rectangular->polar (fft))). The results are in dB (if "norm-type" is 0), or linear and normalized to 1.0 ("norm-type" = 1), or linear unnormalized. The name "mus-fft" is used to distuinguish clm's fft routine from Snd's; the only difference is that mus-fft includes the fft length as an argument, whereas fft does not.
make-fft-window can return many of the standard windows including:
bartlett-hann-window bartlett-window blackman2-window blackman3-window blackman4-window bohman-window cauchy-window connes-window dolph-chebyshev-window exponential-window flat-top-window gaussian-window hamming-window hann-poisson-window hann-window kaiser-window parzen-window poisson-window rectangular-window riemann-window samaraki-window tukey-window ultraspherical-window welch-window blackman5-window blackman6-window blackman7-window blackman8-window blackman9-window blackman10-window rv2-window rv3-window rv4-window mlt-sine-window
See also make-dpss-window and make-papoulis-window in dsp.scm.
rectangular->polar and polar->rectangular change how we view the FFT data: in polar or rectangular coordinates. multiply-arrays does an element-wise multiply of two vcts. autocorrelate performs an (in place) autocorrelation of 'data' (a vct). See spot-freq in dsp.scm, or rubber.scm. correlate performs an in-place cross-correlation of data1 and data2 (see, for example, snddiff).
Instruments |
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 (in with-sound) and generate its own sound. There are hundreds of instruments scattered around the documentation, and most of the map-channel functions can be recast as instruments. There are also several that represent "classic" computer music instruments; they are listed here, documented in sndscm.html, and tested (via sample runs) in test 23 in snd-test.
instrument | function | CL | Scheme | Ruby | Forth |
---|---|---|---|---|---|
complete-add | additive synthesis | add.ins | |||
addflts | filters | addflt.ins | dsp.scm | dsp.rb | |
add-sound | mix in a sound file | addsnd.ins | |||
bullfrog et al | many animals (frogs, insects, birds) | animals.scm | |||
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 | sndclm.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 | |||
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 | |
track-rms | rms envelope of sound file (Michael Edwards) | track-rms.ins | |||
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).
definstrument, the individual instruments, and with-sound are documented in sndscm.html.
related documentation: | snd.html | extsnd.html | grfsnd.html | sndscm.html | fm.html | sndlib.html | libxm.html | index.html |