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))))) |
|
Say we want every third harmonic at amplitude 1/sqrt(harmonic-number) for 5 harmonics total:
(with-sound (:clipped #f :statistics #t :play #t :scaled-to .5) (let* ((gen (make-polywave 200 (let ((harms (make-vct (* 5 2)))) ; 5 harmonics, 2 numbers for each (do ((k 1 (+ k 3)) (i 0 (+ i 2))) ((= i (* 5 2))) (vct-set! harms i k) ; harmonic number (k*freq) (vct-set! harms (+ i 1) (/ 1.0 (sqrt k)))) ; harmonic amplitude harms))) (ampf (make-env '(0 0 1 1 10 1 11 0) :duration 1.0 :scaler .5))) (do ((i 0 (1+ i))) ((= i 44100)) (outa i (* (env ampf) (polywave gen)))))) |
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 wea