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. 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". (These names are just convenient historical artifacts). The main emphasis here is on the generators; note lists and instruments are described in sndscm.html.
all-pass | all-pass filter | nrxysin | n scaled sines |
asymmetric-fm | asymmetric fm | nsin | n equal amplitude sines |
comb | comb filter | one-pole | one pole filter |
convolve | convolution | one-zero | one zero filter |
delay | delay line | oscil | sine wave and FM |
env | line segment envelope | out-any | sound output |
file->sample | input sample from file | phase-vocoder | vocoder analysis and resynthesis |
file->frample | input frample from file | polyshape and polywave | waveshaping |
filter | direct form FIR/IIR filter | pulse-train | pulse train |
filtered-comb | comb filter with filter on feedback | rand, rand-interp | random numbers, noise |
fir-filter | FIR filter | readin | sound input |
formant and firmant | resonance | sample->file | output sample to file |
frample->file | output frample to file | sawtooth-wave | sawtooth |
granulate | granular synthesis | square-wave | square wave |
iir-filter | IIR filter | src | sampling rate conversion |
in-any | sound file input | ssb-am | single sideband amplitude modulation |
locsig | static sound placement | table-lookup | interpolated table lookup |
move-sound | sound motion | tap | delay line tap |
moving-average | moving window average | triangle-wave | triangle wave |
ncos | n equal amplitude cosines | two-pole | two pole filter |
notch | notch filter | two-zero | two zero filter |
nrxycos | n scaled cosines | wave-train | wave train |
autocorrelate | autocorrelation | dot-product | dot (scalar) product |
amplitude-modulate | sig1 * (car + sig2) | fft | Fourier transform |
array-interp | array interpolation | make-fft-window | various standard windows |
contrast-enhancement | modulate signal | polynomial | Horner's rule |
convolution | convolve signals | ring-modulate | sig * sig |
correlate | cross correlation | spectrum | power spectrum of signal |
Start Snd, open the listener (choose "Show listener" in the View menu), and:
> (load "v.scm") fm-violin > (with-sound () (fm-violin 0 1 440 .1)) "test.snd"
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.
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 s7. 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 (+ i 1))) ((= i 8)) (fm-violin (* i .25) .5 (* 100 (+ i 1)) .1)))
with-sound, instruments, CLM itself are all optional, of course. We could do everything by hand:
(let ((increment (/ (* 440.0 2.0 pi) 22050.0)) (current-phase 0.0)) (new-sound "test.snd" :size 22050) (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 (+ i 1))) ((= 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 reads its arguments. I use parenthesized parameters in the function definitions to indicate that the argument names are keywords, but the keywords themselves are optional. Take the make-oscil call, defined as:
make-oscil (frequency 0.0) (initial-phase 0.0)
This says that make-oscil has two optional arguments, frequency (in Hz), and initial-phase (in radians). The keywords associated with these values are :frequency and :initial-phase. When make-oscil is called, it scans its arguments; if a keyword is seen, that argument and all following arguments are passed unchanged, but if a value is seen, the corresponding keyword is prepended in the argument list:
(make-oscil :frequency 440.0) (make-oscil :frequency 440.0 :initial-phase 0.0) (make-oscil 440.0) (make-oscil 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 the same as that of s7's define*, and 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 (+ i 1))) ((= 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 sampling rate. Our first enhancement is to use seconds:
(define (simp beg dur freq amp) (let ((os (make-oscil freq)) (start (seconds->samples beg)) (end (seconds->samples (+ beg dur)))) (do ((i start (+ i 1))) ((= 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))
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 (seconds->samples (+ beg dur)))) (do ((i start (+ i 1))) ((= i end)) (outa i (* amp (oscil os))))))
Now we can simulate a telephone:
(define (telephone start telephone-number) (do ((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)) (i 0 (+ i 1))) ((= i (length telephone-number))) (let* ((num (telephone-number i)) (frq1 (touch-tab-1 num)) (frq2 (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 (seconds->samples (+ beg dur)))) (do ((i start (+ i 1))) ((= 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 (+ i 1))) ((= i 10000)) (outa i (square-wave sqr)))))
Many people find the syntax of "do" confusing. It's possible to hide that away in a macro:
(define-macro (output beg dur . body) `(do ((i (seconds->samples ,beg) (+ i 1))) ((= i (seconds->samples (+ ,beg ,dur)))) (outa i (begin ,@body)))) (define (simp beg dur freq amp) (let ((o (make-oscil freq))) (output beg dur (* amp (oscil o))))) (with-sound () (simp 0 1 440 .1) (simp .5 .5 660 .1))
It's also possible to use recursion, rather than iteration:
(define (simp1) (let ((freq (hz->radians 440.0))) (let simp-loop ((i 0) (x 0.0)) (outa i (sin x)) (if (< i 44100) (simp-loop (+ i 1) (+ x freq)))))) (define simp2 (let ((freq (hz->radians 440.0))) (lambda* ((i 0) (x 0.0)) (outa i (sin x)) (if (< i 44100) (simp2 (+ i 1) (+ x freq))))))
but the do-loop is faster.
make-oscil (frequency 0.0) (initial-phase 0.0) oscil os (fm-input 0.0) (pm-input 0.0) oscil? os make-oscil-bank freqs phases amps stable oscil-bank os fms oscil-bank? os
oscil methods | |
mus-frequency | frequency in Hz |
mus-phase | phase in radians |
mus-length | 1 (no set!) |
mus-increment | frequency in radians per sample |
oscil produces a sine wave (using sin) with optional frequency change (FM). It might be defined:
(let ((result (sin (+ phase pm-input)))) (set! phase (+ phase (hz->radians frequency) fm-input)) result)
oscil's 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). Here are examples in Scheme, Ruby, and Forth:
(with-sound (:play #t) (let ((gen (make-oscil 440.0))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (* 0.5 (oscil gen)))))) |
with_sound(:play, true) do gen = make_oscil(440.0); 44100.times do |i| outa(i, 0.5 * oscil(gen), $output) end end.output |
lambda: ( -- ) 440.0 make-oscil { gen } 44100 0 do i gen 0 0 oscil f2/ *output* outa drop loop ; :play #t with-sound drop |
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 (- (+ end 1) start) :scaler (hz->radians freq)))) (do ((i start (+ i 1))) ((= 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 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))) (do ((i start (+ i 1))) ((= i end)) (outa i (* (env ampf) (oscil cr (* (env indf) (oscil md)))))))) ;;; (with-sound () (simple-fm 0 1 440 .1 2 1.0))
fm.html has an introduction to FM. FM and PM behave slightly differently during a glissando; FM is the more "natural" in that, left to its own devices, it produces a spectrum that varies inversely with the pitch. Compare these two cases. Both involve a slow glissando up an octave, FM in channel 0, and PM in channel 1. In the first note, I fix up the FM index during the sweep to keep the spectra steady, and in the second, I fix up the PM index.
(with-sound (:channels 2) (let* ((dur 2.0) (samps (seconds->samples dur)) (pitch 1000) (modpitch 100) (pm-index 4.0) (fm-index (hz->radians (* 4.0 modpitch)))) (let ((car1 (make-oscil pitch)) (mod1 (make-oscil modpitch)) (car2 (make-oscil pitch)) (mod2 (make-oscil modpitch)) (frqf (make-env '(0 0 1 1) :duration dur)) (ampf (make-env '(0 0 1 1 20 1 21 0) :duration dur :scaler .5))) (do ((i 0 (+ i 1))) ((= i samps)) (let* ((frq (env frqf)) (rfrq (hz->radians frq)) (amp (env ampf))) (outa i (* amp (oscil car1 (+ (* rfrq pitch) (* fm-index (+ 1 frq) ; keep spectrum the same (oscil mod1 (* rfrq modpitch))))))) (outb i (* amp (oscil car2 (* rfrq pitch) (* pm-index (oscil mod2 (* rfrq modpitch))))))))) (let ((car1 (make-oscil pitch)) (mod1 (make-oscil modpitch)) (car2 (make-oscil pitch)) (mod2 (make-oscil modpitch)) (frqf (make-env '(0 0 1 1) :duration dur)) (ampf (make-env '(0 0 1 1 20 1 21 0) :duration dur :scaler .5))) (do ((i 0 (+ i 1))) ((= i samps)) (let* ((frq (env frqf)) (rfrq (hz->radians frq)) (amp (env ampf))) (outa (+ i samps) (* amp (oscil car1 (+ (* rfrq pitch) (* fm-index ; let spectrum decay (oscil mod1 (* rfrq modpitch))))))) (outb (+ i samps) (* amp (oscil car2 (* rfrq pitch) (* (/ pm-index (+ 1 frq)) (oscil mod2 (* rfrq modpitch)))))))))))
And if you read somewhere that PM can't produce a frequency shift:
(with-sound () (let ((o (make-oscil 200.0)) (e (make-env '(0 0 1 1) :scaler 300.0 :duration 1.0))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (oscil o 0.0 (env e))))))
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.
(define (scheme-bird start dur frequency freqskew amplitude freq-envelope amp-envelope) (let* ((gls-env (make-env freq-envelope (hz->radians freqskew) dur)) (os (make-oscil frequency)) (amp-env (make-env amp-envelope amplitude dur)) (beg (seconds->samples start)) (end (+ beg (seconds->samples dur)))) (do ((i beg (+ i 1))) ((= i end)) (outa i (* (env amp-env) (oscil os (env gls-env))))))) |
(definstrument common-lisp-bird (startime dur frequency freq-skew amplitude freq-envelope amp-envelope) (multiple-value-bind (beg end) (times->samples startime dur) (let* ((amp-env (make-env amp-envelope amplitude dur)) (gls-env (make-env freq-envelope (hz->radians freq-skew) dur)) (os (make-oscil frequency))) (run (loop for i from beg to end do (outa i (* (env amp-env) (oscil os (env gls-env))))))))) |
def ruby_bird(start, dur, freq, freqskew, amp, freq_envelope, amp_envelope) gls_env = make_env(:envelope, freq_envelope, :scaler, hz2radians(freqskew), :duration, dur) os = make_oscil(:frequency, freq) amp_env = make_env(:envelope, amp_envelope, :scaler, amp, :duration, dur) run_instrument(start, dur) do env(amp_env) * oscil(os, env(gls_env)) end end |
instrument: forth-bird { f: start f: dur f: freq f: freq-skew f: amp freqenv ampenv -- } :frequency freq make-oscil { os } :envelope ampenv :scaler amp :duration dur make-env { ampf } :envelope freqenv :scaler freq-skew hz>radians :duration dur make-env { gls-env } 90e random :locsig-degree start dur run-instrument ampf env gls-env env os oscil-1 f* end-run os gen-free ampf gen-free gls-env gen-free ;instrument |
void c_bird(double start, double dur, double frequency, double freqskew, double amplitude, mus_float_t *freqdata, int freqpts, mus_float_t *ampdata, int amppts, mus_any *output) { mus_long_t beg, end, i; mus_any *amp_env, *freq_env, *osc; beg = start * mus_srate(); end = start + dur * mus_srate(); osc = mus_make_oscil(frequency, 0.0); amp_env = mus_make_env(ampdata, amppts, amplitude, 0.0, 1.0, dur, 0, NULL); freq_env = mus_make_env(freqdata, freqpts, mus_hz_to_radians(freqskew), 0.0, 1.0, dur, 0, NULL); for (i = beg; i < end; i++) mus_sample_to_file(output, i, 0, mus_env(amp_env) * mus_oscil(osc, mus_env(freq_env), 0.0)); mus_free(osc); mus_free(amp_env); mus_free(freq_env); } |
Many of the CLM synthesis functions try to make it faster or more convenient to produce a lot of sinusoids, but there are times when nothing but a ton of oscils will do:
(with-sound () (let* ((peaks (list 23 0.0051914 32 0.0090310 63 0.0623477 123 0.1210755 185 0.1971876 209 0.0033631 247 0.5797809 309 1.0000000 370 0.1713255 432 0.9351965 481 0.0369873 495 0.1335089 518 0.0148626 558 0.1178001 617 0.6353443 629 0.1462804 661 0.0208941 680 0.1739281 701 0.0260423 742 0.1203807 760 0.0070301 803 0.0272111 865 0.0418878 926 0.0090197 992 0.0098687 1174 0.00444 1298 0.0039722 2223 0.0033486 2409 0.0083675 2472 0.0100995 2508 0.004262 2533 0.0216248 2580 0.0047732 2596 0.0088663 2612 0.0040592 2657 0.005971 2679 0.0032541 2712 0.0048836 2761 0.0050938 2780 0.0098877 2824 0.003421 2842 0.0134356 2857 0.0050194 2904 0.0147466 2966 0.0338878 3015 0.004832 3027 0.0095497 3040 0.0041434 3092 0.0044802 3151 0.0038269 3460 0.003633 3585 0.0050849 4880 0.0042301 5121 0.0037906 5136 0.0048349 5158 0.004336 5192 0.0037841 5200 0.0038025 5229 0.0035555 5356 0.0045781 5430 0.003687 5450 0.0055170 5462 0.0057821 5660 0.0041789 5673 0.0044932 5695 0.007370 5748 0.0031716 5776 0.0037921 5800 0.0062308 5838 0.0034629 5865 0.005942 5917 0.0032254 6237 0.0046164 6360 0.0034708 6420 0.0044593 6552 0.005939 6569 0.0034665 6752 0.0041965 7211 0.0039695 7446 0.0031611 7468 0.003330 7482 0.0046322 8013 0.0034398 8102 0.0031590 8121 0.0031972 8169 0.003345 8186 0.0037020 8476 0.0035857 8796 0.0036703 8927 0.0042374 9388 0.003173 9443 0.0035844 9469 0.0053484 9527 0.0049137 9739 0.0032365 9853 0.004297 10481 0.0036424 10490 0.0033786 10606 0.0031366)) (len (/ (length peaks) 2)) (dur 10) (oscs (make-vector len)) (amps (make-vector len)) (ramps (make-vector len)) (freqs (make-vector len)) (vib (make-rand-interp 50 (hz->radians .01))) (ampf (make-env '(0 0 1 1 10 1 11 0) :duration dur :scaler .1)) (samps (seconds->samples dur))) (do ((i 0 (+ i 1))) ((= i len)) (set! (freqs i) (peaks (* i 2))) (set! (oscs i) (make-oscil (freqs i) (random pi))) (set! (amps i) (peaks (+ 1 (* 2 i)))) (set! (ramps i) (make-rand-interp (+ 1.0 (* i (/ 20.0 len))) (* (+ .1 (* i (/ 3.0 len))) (amps i))))) (do ((i 0 (+ i 1))) ((= i samps)) (let ((sum 0.0) (fm (rand-interp vib))) (do ((k 0 (+ k 1))) ((= k len)) (set! sum (+ sum (* (+ (amps k) (rand-interp (ramps k))) (oscil (oscs k) (* (freqs k) fm)))))) (outa i (* (env ampf) sum))))))
oscil-bank here would be faster, or mus-chebyshev-t-sum:
... (amps (make-float-vector 10607)) (angle 0.0) (freq (hz->radians 1.0)) ... (do ((i 0 (+ i 1)) (k 0 (+ k 2))) ((= i len)) (set! (amps (peaks k)) (peaks (+ k 1)))) ... (outa i (* (env ampf) (mus-chebyshev-t-sum angle amps))) (set! angle (+ angle freq (rand-interp vib))) ...
Here's a better example: we want to start with a sum of equal amplitude harmonically related cosines (a sequence of spikes), and move slowly to a waveform with the same magnitude spectrum, but with the phases chosen to minimize the peak amplitude.
(let ((98-phases #(0.000000 -0.183194 0.674802 1.163820 -0.147489 1.666302 0.367236 0.494059 0.191339 0.714980 1.719816 0.382307 1.017937 0.548019 0.342322 1.541035 0.966484 0.936993 -0.115147 1.638513 1.644277 0.036575 1.852586 1.211701 1.300475 1.231282 0.026079 0.393108 1.208123 1.645585 -0.152499 0.274978 1.281084 1.674451 1.147440 0.906901 1.137155 1.467770 0.851985 0.437992 0.762219 -0.417594 1.884062 1.725160 -0.230688 0.764342 0.565472 0.612443 0.222826 -0.016453 1.527577 -0.045196 0.585089 0.031829 0.486579 0.557276 -0.040985 1.257633 1.345950 0.061737 0.281650 -0.231535 0.620583 0.504202 0.817304 -0.010580 0.584809 1.234045 0.840674 1.222939 0.685333 1.651765 0.299738 1.890117 0.740013 0.044764 1.547307 0.169892 1.452239 0.352220 0.122254 1.524772 1.183705 0.507801 1.419950 0.851259 0.008092 1.483245 0.608598 0.212267 0.545906 0.255277 1.784889 0.270552 1.164997 -0.083981 0.200818 1.204088)) (freq 10.0) (dur 5.0) (n 98)) (with-sound () (let ((samps (floor (* dur 44100))) (1/n (/ 1.0 n)) (freqs (make-float-vector n)) (phases (make-float-vector n (* pi 0.5)))) (do ((i 0 (+ i 1))) ((= i n)) (let ((off (/ (* pi (- 0.5 (98-phases i))) dur 44100)) (h (hz->radians (* freq (+ i 1))))) (set! (freqs i) (+ h off)))) (let ((ob (make-oscil-bank freqs phases))) (do ((i 0 (+ i 1))) ; get rid of the distracting initial click ((= i 1000)) (oscil-bank ob)) (do ((k 0 (+ k 1))) ((= k samps)) (outa k (* 1/n (oscil-bank ob))))))))
The last argument to make-oscil-bank, "stable", defaults to false. If it is true, oscil-bank can assume that the frequency, phase, and amplitude values passed to make-oscil-bank will not change over the life of the generator.
Related generators are ncos, nsin, asymmetric-fm, and nrxysin. 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. 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 it produces may not look very sinusoidal. Here, for example, is oscil at 440 Hz when the srate is 1000, 4000, and 16000:
make-env envelope ; list or float-vector 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 (base 1.0) ;value of env at x env-any e connecting-function envelope-interp x env (base 1.0) make-pulsed-env envelope duration frequency pulsed-env gen (fm 0.0) pulsed-env? gen
env methods | |
mus-location | number of calls so far on this env |
mus-increment | base |
mus-data | original breakpoint list |
mus-scaler | scaler |
mus-offset | offset |
mus-length | duration in samples |
mus-channels | current position in the break-point list |
An envelope is a list or float-vector 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 (float-vector 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))
.
I used "scaler" decades ago because I didn't like the spelling "scalar". According
to the OED, "scalar" goes back to the 17th century, and derives from "scala", a ladder, ultimately from
Latin. "scaler" is also old, and refers to one who scales a mountain or a fish. Well, I still
like "scaler" better: We're staring at a "peak"! "gain" looks like an escapee from the EE lab. "volume" is too specific.
Maybe "scl" or "*"?
(with-sound (:play #t) (let ((gen (make-oscil 440.0)) (ampf (make-env '(0 0 .01 1 .25 .1 1 0) :scaler 0.5 :length 44100))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (* (env ampf) (oscil gen)))))) |
with_sound(:play, true) do gen = make_oscil(440.0); ampf = make_env( [0, 0, 0.01, 1.0, 0.25, 0.1, 1, 0], :scaler, 0.5, :length, 44100); 44100.times do |i| outa(i, env(ampf) * oscil(gen), $output) end end.output |
lambda: ( -- ) 440.0 make-oscil { gen } '( 0 0 0.01 1 0.25 0.1 1 0 ) :scaler 0.5 :length 44100 make-env { ampf } 44100 0 do i gen 0 0 oscil ampf env f* *output* outa drop loop ; :play #t with-sound drop |
The base argument determines how the break-points are connected. If it is 1.0 (the
default), you get straight line segments. If base is 0.0, you get a step
function (the envelope changes its value suddenly to the new one without any
interpolation). Any other positive value affects the exponent of the exponential curve
connecting the points. A base less than 1.0 gives convex curves (i.e. bowed
out), and a base greater than 1.0 gives concave curves (i.e. sagging).
If you'd rather think in terms of e^-kt, set the base to (exp k)
.
You can get a lot from a couple of envelopes:
> (load "animals.scm") #<unspecified> > (with-sound (:play #t) (pacific-chorus-frog 0 .5)) "test.snd" > (with-sound (:play #t) (house-finch 0 .5)) "test.snd"
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:
(definstrument (mapenv beg dur frq amp en) (let* ((start (seconds->samples beg)) (end (+ start (seconds->samples dur))) (osc (make-oscil frq)) (zv (make-env en 1.0 dur))) (do ((i start (+ i 1))) ((= i end)) (let ((zval (env zv))) (outa i (* amp (sin (* 0.5 pi zval zval zval)) (oscil osc))))))) (with-sound () (mapenv 0 1 440 .5 '(0 0 50 1 75 0 86 .5 100 0)))
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 (+ i 1))) ((= 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))) (do ((i 0 (+ i 1))) ((= 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 (format #t ";~A ~A: ~A~%" beg dur args))))
(envelope-interp x env base) returns value of 'env' at 'x'. If 'base' is 0, 'env' is treated as a step function; if 'base' is 1.0 (the default), the breakpoints of 'env' are connected by a straight line, and any other 'base' connects the breakpoints with a kind of exponential curve:
> (envelope-interp .1 '(0 0 1 1)) 0.1 > (envelope-interp .1 '(0 0 1 1) 32.0) 0.0133617278184869 > (envelope-interp .1 '(0 0 1 1) .012) 0.361774730775292
The corresponding function for a CLM env generator is env-interp. If you'd rather think in terms of e^-kt, set the 'base' to (exp k).
pulsed-env 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.
(with-sound () (let ((e (make-pulsed-env '(0 0 1 1 2 0) .01 1)) (frq (make-env '(0 0 1 1) :duration 1.0 :scaler (hz->radians 50)))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (* .5 (pulsed-env e (env frq)))))))
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:
![]() |
![]() |
truncated pyramid amplitude envelope multiplied by sinusoid at 50Hz |
truncated pyramid frquency envelope 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 |
Various operations on envelopes: env.scm: add-envelopes add two envelopes concatenate-envelopes concatenate a bunch of envelopes envelope-exp interpolate points to approximate exponential curves envelope-interp return the value of an envelope given the x position envelope-last-x return the last x value in an envelope intergrate-envelope return the area under an envelope make-power-env exponential curves with multiple exponents (see also multi-expt-env in generators.scm) map-envelopes apply a function to the breakpoints in two envelopes, returning a new envelope max-envelope return the maximum y value in an envelope (also min-envelope) multiply-envelopes multiply two envelopes normalize-envelope scale the y values of an envelope to peak at 1.0 repeat-envelope concatenate copies of an envelope reverse-envelope reverse the breakpoints in an envelope scale-envelope scale and offset the y values of an envelope stretch-envelope apply attack and decay times to an envelope ("adsr", or "divenv") window-envelope return the portion of an envelope within given x axis bounds envelope sound: env-channel, env-sound other enveloping functions: ramp-channel, xramp-channel, smooth-channel envelope editor: Edit or View and Envelope panning: place-sound in examp.scm read sound indexed through envelope: env-sound-interp repeating envelope: pulsed-env step envelope in pitch: brassy in generators.scm |
make-table-lookup (frequency 0.0) ; table repetition rate in Hz (initial-phase 0.0) ; starting point in radians (pi = mid-table) wave ; a float-vector containing the signal (size *clm-table-size*) ; table size if wave not specified (type mus-interp-linear) ; interpolation type table-lookup tl (fm-input 0.0) table-lookup? tl make-table-lookup-with-env frequency env size
table-lookup methods | |
mus-frequency | frequency in Hz |
mus-phase | phase in radians |
mus-data | wave float-vector |
mus-length | wave size (no set!) |
mus-interp-type | interpolation choice (no set!) |
mus-increment | table increment per sample |
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 (defined in generators.scm) returns a new table-lookup generator with the envelope 'env' loaded into its table.
table-lookup might be defined:
(let ((result (array-interp wave phase))) (set! phase (+ phase (hz->radians frequency) (* fm-input (/ (length wave) 2 pi)))) result)
(with-sound (:play #t) (let ((gen (make-table-lookup 440.0 :wave (partials->wave '(1 .5 2 .5))))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (* 0.5 (table-lookup gen)))))) |
with_sound(:play, true) do gen = make_table_lookup(440.0, :wave, partials2wave([1.0, 0.5, 2.0, 0.5])); 44100.times do |i| outa(i, 0.5 * table_lookup(gen), $output) end end.output |
lambda: ( -- ) 440.0 :wave '( 1 0.5 2 0.5 ) #f #f partials->wave make-table-lookup { gen } 44100 0 do i gen 0 table-lookup f2/ *output* outa drop loop ; :play #t with-sound drop |
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 wave (norm #t) phase-partials->wave synth-data wave (norm #t)
The "synth-data" argument is a list or float-vector 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 float-vector of (partial amp phase) triples with phases in radians. If "wave" is not passed, these functions return a new float-vector.
(definstrument (simple-table dur) (let ((tab (make-table-lookup :wave (partials->wave '(1 .5 2 .5))))) (do ((i 0 (+ i 1))) ((= 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))) (error 'no-loop-positions) (let* ((loop-start (car loop-data)) (loop-length (- (+ (cadr loop-data) 1) loop-start)) (sound-section (file->array sound 0 loop-start loop-length (make-float-vector loop-length))) (original-loop-duration (/ loop-length (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 (do ((i beg (+ i 1))) ((= 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-framples file)) (tab (make-table-lookup :frequency (/ read-speed (mus-sound-duration file)) :wave (file->array file 0 0 table-length (make-float-vector table-length)))) (osc (make-oscil modulator-freq)) (index (/ (* (hz->radians modulator-freq) 2 pi index-in-samples) table-length))) (do ((i beg (+ i 1))) ((= 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 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 so similar to 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.
If you're trying to produce a sum of sinusoids, use polywave — it makes a monkey out of table lookup in every case.
table-lookup of a sine (or some facsimile thereof) probably predates Ptolemy. One neat method of generating the table is that of Bhaskara I, AD 600, India, mentioned in van Brummelen, "The Mathematics of the Heavens and the Earth": use the rational approximation 4x(180-x)/(40500-x(180-x)), x in degrees, or more readably: 4x(pi-x)/(12.337-x(pi-x)), x in radians. The maximum error is 0.00163 at x=11.54 (degrees)!
spectr.scm 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.
make-polywave (frequency 0.0) (partials '(1 1)) ; a list of harmonic numbers and their associated amplitudes (type mus-chebyshev-first-kind) ; Chebyshev polynomial choice xcoeffs ycoeffs ; tn/un for tu-sum case polywave w (fm 0.0) polywave? w make-polyshape (frequency 0.0) (initial-phase 0.0) coeffs (partials '(1 1)) (kind mus-chebyshev-first-kind) polyshape w (index 1.0) (fm 0.0) polyshape? w partials->polynomial partials (kind mus-chebyshev-first-kind) normalize-partials partials mus-chebyshev-tu-sum x t-coeffs u-coeffs mus-chebyshev-t-sum x t-coeffs mus-chebyshev-u-sum x u-coeffs
polywave methods | |
mus-frequency | frequency in Hz |
mus-scaler | index |
mus-phase | phase in radians |
mus-data | polynomial coeffs |
mus-length | number of partials |
mus-increment | frequency in radians per sample |
These two generators drive a sum of scaled Chebyshev polynomials with a cosine, 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. polyshape uses a prebuilt sum of Chebyshev polynomials, whereas polywave uses the underlying Chebyshev recursion. polywave is stable and noise-free even with high partial numbers (I've tried it with 16384 harmonics). The "partials" argument to the make function can be either a list or a float-vector ("vct" in Ruby and Forth). The "type" or "kind" argument determines which kind of Chebyshev polynomial is used internally: mus-chebyshev-first-kind (Tn) which produces a sum of cosines, or mus-chebyshev-second-kind (Un), which produces a sum of sines.
(with-sound (:play #t) (let ((gen (make-polywave 440.0 :partials '(1 .5 2 .5)))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (* 0.5 (polywave gen)))))) |
with_sound(:play, true) do gen = make_polywave(440.0, :partials, [1.0, 0.5, 2.0, 0.5]); 44100.times do |i| outa(i, 0.5 * polywave(gen), $output) end end.output |
lambda: ( -- ) 440.0 :partials '( 1 0.5 2 0.5 ) make-polywave { gen } 44100 0 do i gen 0 polywave f2/ *output* outa drop loop ; :play #t with-sound drop |
normalize-partials takes the list or float-vector of partial number and amplitudes, and returns a float-vector with the amplitudes normalized so that their magnitudes add to 1.0.
> (normalize-partials '(1 1 3 2 6 1)) #(1.0 0.25 3.0 0.5 6.0 0.25); > (normalize-partials (float-vector 1 .1 2 .1 3 -.2)) #(1.0 0.25 2.0 0.25 3.0 -0.5)
partials->polynomial takes a list or float-vector of partial numbers and amplitudes and returns the Chebyshev polynomial coefficients that produce that spectrum. These coefficients can be passed to polyshape (the coeffs argument), or used directly by polynomial (there are examples of both below).
> (partials->polynomial '(1 1 3 2 6 1)) #(-1.0 -5.0 18.0 8.0 -48.0 0.0 32.0) > (partials->polynomial '(1 1 3 2 6 1) mus-chebyshev-second-kind) #(-1.0 6.0 8.0 -32.0 0.0 32.0 0.0) > (partials->polynomial (float-vector 1 .1 2 .1 3 -.2)) #(-0.1 0.7 0.2 -0.8)
mus-chebyshev-tu-sum and friends perform the same function as partials->polynomial, but use the much more stable and accurate underlying recursion (see below for a long-winded explanation). They are the innards of the polywave and polyoid generators. The arguments are "x" (normally a phase), and one or two float-vectors of component amplitudes. These functions makes it easy to do additive synthesis with any number of harmonics (I've tried 16384), each with arbitrary initial-phase and amplitude, and each harmonic independently changeable in phase and amplitude at run-time by setting a float-vector value.
(let ((result (polynomial wave (cos phase)))) (set! phase (+ phase (hz->radians frequency) fm)) result)
In its simplest use, waveshaping is additive synthesis:
(with-sound () (let ((wav (make-polyshape :frequency 500.0 :partials '(1 .5 2 .3 3 .2)))) (do ((i 0 (+ i 1))) ((= i 40000)) (outa i (polyshape 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 (do ((harms (make-float-vector (* 5 2))) ; 5 harmonics, 2 numbers for each (k 1 (+ k 3)) (i 0 (+ i 2))) ((= i 10) harms) (set! (harms i) k) ; harmonic number (k*freq) (set! (harms (+ i 1)) (/ 1.0 (sqrt k)))))) ; harmonic amplitude (ampf (make-env '(0 0 1 1 10 1 11 0) :duration 1.0 :scaler .5))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (* (env ampf) (polywave gen))))))
See animals.scm for many more examples along these lines. normalize-partials makes sure that the component amplitudes (magnitudes) add to 1.0. Its argument can be either a list or float-vector, but it always returns a float-vector. The fm-violin uses polyshape for the multiple FM section in some cases. The pqw and pqwvox instruments use both kinds of Chebyshev polynomials to produce single side-band spectra. Here is a somewhat low-level 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)))) (do ((i beg (+ i 1))) ((= 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:
(define* (make-band-limited-triangle-wave (frequency 0.0) (order 1)) (do ((freqs ()) (i 1 (+ i 1)) (j 1 (+ j 2))) ((> i order) (make-polywave frequency :partials (reverse freqs))) (set! freqs (cons (/ 1.0 j j) (cons j freqs))))) (define* (band-limited-triangle-wave gen (fm 0.0)) (polywave gen fm))
Band-limited square or sawtooth waves:
(definstrument (bl-saw start dur frequency order) (let ((norm (cond ((assoc order '((1 . 1.0) (2 . 1.3)) =) => cdr) ; these peak amps were determined empirically ((< order 9) 1.7) ; actual limit is supposed to be pi/2 (G&R 1.441) (else 1.852))) ; but Gibbs phenomenon pushes it to 1.851 (freqs ())) (do ((i 1 (+ i 1))) ((> i order)) (set! freqs (cons (/ 1.0 norm i) (cons i freqs)))) (let* ((gen (make-polywave frequency :partials (reverse freqs) :type mus-chebyshev-second-kind)) (beg (seconds->samples start)) (end (+ beg (seconds->samples dur)))) (do ((i beg (+ i 1))) ((= i end)) (outa i (polywave gen))))))
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 an 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 waveshaping output by the carrier (the 0Hz term gives us the carrier):
(with-sound () (let ((modulator (make-polyshape 100.0 :partials (list 0 .4 1 .4 2 .1 3 .05 4 .05))) (carrier (make-oscil 1000.0))) (do ((i 0 (+ i 1))) ((= i 20000)) (outa i (* .5 (oscil carrier) (polyshape modulator))))))
The simplest way to 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 ...)))
Or use mus-chebyshev-*-sum and set the component amplitudes directly:
(with-sound () (let* ((dur 1.0) (samps (seconds->samples dur)) (coeffs (float-vector 0.0 0.5 0.25 0.125 0.125)) (x 0.0) (incr (hz->radians 100.0)) (ampf (make-env '(0 0 1 1 10 1 11 0) :duration dur :scaler .5)) (harmf (make-env '(0 .125 1 .25) :duration dur))) (do ((i 0 (+ i 1))) ((= i samps)) (let ((harm (env harmf))) (set! (coeffs 3) harm) (set! (coeffs 4) (- .25 harm))) (outa i (* (env ampf) (mus-chebyshev-t-sum x coeffs))) (set! x (+ x incr)))))
But we can also vary the index (the amplitude of the cosine 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 (float-vector 0 0 0 0 1.0 1.0)) -0.0732421875 > (cheby-hka 2 0.25 (float-vector 0 0 0 0 1.0 1.0)) -0.234375 > (cheby-hka 1 0.25 (float-vector 0 0 0 0 1.0 1.0)) 1.025390625 > (cheby-hka 0 0.25 (float-vector 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 (+ i 1))) ((= 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
but now the peak amplitude is hard to predict (it's .6242 in this example). Perhaps flatten-partials would be a better choice here. To follow an amplitude envelope despite a changing index, we can use a moving-max generator:
(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 (+ i 1))) ((= 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:
T5 driven with sinusoids at 100Hz and 2000Hz
(with-sound () (let ((pcoeffs (partials->polynomial (float-vector 5 1))) (gen1 (make-oscil 100.0)) (gen2 (make-oscil 2000.0))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (polynomial pcoeffs (* 0.5 (+ (oscil gen1) (oscil gen2)))))))) ![]() |
![]() |
This kind of output is typical; I get the impression that the cross products are much more noticeable here than in FM. Of course, we can take advantage of that:
(with-sound (:channels 2) (let* ((dur 2.0) (samps (seconds->samples dur)) (p1 (make-polywave 800 (list 1 .1 2 .3 3 .4 5 .2))) (p2 (make-polywave 400 (list 1 .1 2 .3 3 .4 5 .2))) (interpf (make-env '(0 0 1 1) :duration dur)) (p3 (partials->polynomial (list 1 .1 2 .3 3 .4 5 .2))) (g1 (make-oscil 800)) (g2 (make-oscil 400)) (ampf (make-env '(0 0 1 1 10 1 11 0) :duration dur))) (do ((i 0 (+ i 1))) ((= i samps)) (let ((interp (env interpf)) (amp (env ampf))) ;; chan A: interpolate from one spectrum to the next directly (outa i (* amp (+ (* interp (polywave p1)) (* (- 1.0 interp) (polywave p2))))) ;; chan B: interpolate inside the sum of Tns! (outb i (* amp (polynomial p3 (+ (* interp (oscil g1)) (* (- 1.0 interp) (oscil g2))))))))))
If we use an arbitrary 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. I suppose you could also subtract out the DC term (coeffs[0]), but I haven't tried this.
If you push the polyshape generator into high harmonics (above say 30), you'll
run into numerical trouble (the polywave generator is immune to this bug).
Where does the trouble lie?
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! With doubles we can only push the order up to around 46. polywave, on the other hand, builds up the sum of sines from the underlying recursion, which is only slightly slower than using the polynomial, and it is not bothered by these numerical problems. I have run polywave with 16384 harmonics, and the maximum error compared to the equivalent sum of sinusoids was around 5.0e-12.
Since it is primarily used for additive synthesis, and we can always do that with oscils or table-lookup, we might ask why we'd want polywave at all. Leaving aside speed (the Chebyshev computation is 10 to 20 times faster than the equivalent sum of oscils) and memory (the defunct table-lookup based waveshape generator and table-lookup itself use a table that has to be loaded), the main reason to use polywave is accuracy. polywave produces output that is as clean as the equivalent sum of oscils, whereas table-lookup and poor old 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, (table-lookup based) 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).
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:
See also polyoid and noid in generators.scm.
make-triangle-wave (frequency 0.0) (amplitude 1.0) (initial-phase pi) triangle-wave s (fm 0.0) triangle-wave? s make-square-wave (frequency 0.0) (amplitude 1.0) (initial-phase 0) square-wave s (fm 0.0) square-wave? s make-sawtooth-wave (frequency 0.0) (amplitude 1.0) (initial-phase pi) sawtooth-wave s (fm 0.0) sawtooth-wave? s make-pulse-train (frequency 0.0) (amplitude 1.0) (initial-phase (* 2 pi)) pulse-train s (fm 0.0) pulse-train? s
saw-tooth and friends' methods | |
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 |
These generators produce some standard old-timey wave forms that are still occasionally useful (well, triangle-wave is useful; the others are silly). One popular kind of vibrato is:
(+ (triangle-wave pervib) (rand-interp ranvib))
sawtooth-wave ramps from -1 to 1, then goes immediately back to -1. Use a negative frequency to turn the "teeth" the other way. To get a sawtooth from 0 to 1, you can use modulo:
(with-sound () (do ((i 0 (+ i 1)) (x 0.0 (+ x .01))) ((= i 22050)) (outa i (modulo x 1.0))))
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.
(with-sound (:play #t) (let ((gen (make-triangle-wave 440.0))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (* 0.5 (triangle-wave gen)))))) |
with_sound(:play, true) do gen = make_triangle_wave(440.0); 44100.times do |i| outa(i, 0.5 * triangle_wave(gen), $output) end end.output |
lambda: ( -- ) 440.0 make-triangle-wave { gen } 44100 0 do i gen 0 triangle-wave f2/ *output* outa drop loop ; :play #t with-sound drop |
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))))) (do ((sum 0.0) (i 0 (+ i 1))) ((= 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 (* B (sin theta)))
, where "B" (a float) sets how squared-off it is:
B: 1.0 | B: 3.0 | B: 100.0 |
![]() |
![]() |
![]() |
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 — use maxima!):
which is promising since a square wave is made up of odd harmonics with amplitude 1/n. As the "B" in tanh(B sin(x)) increases above pi/2, this series doesn't apply.
but I haven't found a completion of this expansion that isn't ugly when B > pi/2. In any case, 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); this use of tanh as a soft clipper goes way back — I don't know who invented it.
If you try to make a square wave by adding harmonics at amplitude 1/n, you run into "Gibb's phenomenon": although the sum converges on a square wave, it does so "pointwise" — each point converges to the square wave, but the sum always has an overshoot. To get something that looks square, we need to round-off the corners. Bill Gosper shows one mathematical way to do this (gibbs.html). We could also use with-mixed-sound and the Mixes dialog:
(definstrument (sine-wave start dur freq amp) (let* ((beg (seconds->samples start)) (end (+ beg (seconds->samples dur))) (osc (make-oscil freq))) (do ((i beg (+ i 1))) ((= i end)) (outa i (* amp (oscil osc)))))) (with-mixed-sound () (sine-wave 0 1 10.0 1.0) (sine-wave 0 1 30.0 .333) (sine-wave 0 1 50.0 .2) (sine-wave 0 1 70.0 .143))
Now we can play with the individual sinewave amplitudes in the Mixes dialog, seeing "in realtime" what effect an amplitude has on the waveform. In the graph below, we've taken the original set of four sines and chosen amplitudes 1.16, .87, .46, .14 (these are multipliers on the original 1/n amps). The first graph is the original waveform, the last is the result of the amplitude changes, and the middle one shows 100 sines (it is the usual demo that the Gibbs overshoot is not reduced by adding lots more components). The peak amplitude should be pi/4, but the Gibbs phenomenon adds .14.
But goofing with individual amplitudes quickly becomes tiresome. This "realtime" business depends on luck; if we have some idea of what we're doing, we don't have to get lucky. Since tanh(B sin(x)) produces a nice square wave, we can truncate its spectrum at the desired number of harmonics:
(define square-wave->coeffs (let ((previous-results (make-vector 128 #f))) (lambda* (n B) (or (and (< n 128) (not B) (previous-results n)) (let* ((coeffs (make-float-vector (* 2 n))) (size (expt 2 12)) (rl (make-float-vector size))) (do ((incr (/ (* 2 pi) size)) (index (or B (max 1 (floor (/ n 2))))) (i 0 (+ i 1)) (x 0.0 (+ x incr))) ((= i size)) (set! (rl i) (tanh (* index (sin x))))) ; make our desired square wave (spectrum rl (make-float-vector size) #f 2) ; get its spectrum (do ((i 0 (+ i 1)) (j 0 (+ j 2))) ((= i n)) (set! (coeffs j) (+ j 1)) (set! (coeffs (+ j 1)) (/ (* 2 (rl (+ j 1))) size))) (if (and (< n 128) ; save this set so we don't have to compute it again (not B)) (set! (previous-results n) coeffs)) coeffs))))) (with-sound () (let* ((samps (seconds->samples 1.0)) (wave (make-polywave 100.0 :partials (square-wave->coeffs 16) :type mus-chebyshev-second-kind))) (do ((i 0 (+ i 1))) ((= i samps)) (outa i (* 0.5 (polywave wave))))))
![]() |
See also tanhsin in generators.scm. 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 (related to tanh(sin)):
(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 (- ...)) (cm1c (expt (- c 1.0) cs)) (cp1c (expt (+ c 1.0) 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 (+ i 1)) (angle 0.0 (+ angle 0.02))) ((= i 44100)) (outa i (* 0.5 (+ 1.0 (sqsq 1.001 angle)))))))
And in the slightly batty category is this method which uses only nested sines:
(with-sound () (let ((angle 0.0) (z 1.18) (incr (hz->radians 100.0))) (do ((i 0 (+ i 1))) ((= i 20000)) (let ((result (* z (sin angle)))) (do ((k 0 (+ k 1))) ((= k 100)) ; the limit here sets how square it is, and also the overall amplitude (set! result (* z (sin result)))) (set! angle (+ angle incr)) (outa i result)))))
The continuously variable square-wave, tanh(B sin), can be differentiated to get a variable pulse-train, or integrated to get a variable triangle-wave. The derivative is B * cos(x) / (cosh^2(B * sin(x))):
(with-sound () (let ((Benv (make-env '(0 .1 .1 1 .7 2 2 5) :end 10000)) (osc (make-oscil 100))) (do ((i 0 (+ i 1))) ((= i 10000)) (let* ((B (env Benv)) (num (cos (mus-phase osc))) (den (cosh (* B (oscil osc))))) (outa i (/ num den den))))))
Similar, but simpler is B*cos(x)/(e^(B*cos(x)) - 1):
(with-sound () (let ((gen (make-oscil 40.0)) (Benv (make-env '(0 .75 1 1.5 2 20) :end 10000))) (do ((i 0 (+ i 1))) ((= i 10000)) (let* ((B (env Benv)) (arg (* B pi (+ 1.0 (oscil gen))))) (outa i (/ arg (- (exp arg) 1)))))))
When we integrate tanh(B sin), the peak amp depends on both the frequency and the "B" factor (which sets how close we get to a triangle wave):
(with-sound () (let ((gen (make-oscil 30.0)) (Benv (make-env '(0 .1 .25 1 2 3 3 10) :end 20000)) (scl (hz->radians 30.0)) (sum 0.0)) (do ((i 0 (+ i 1))) ((= i 20000)) (let* ((B (env Benv)) (val (/ (* scl (max 1.0 (log B)) (tanh (* B (oscil gen)))) B))) (outa i (- sum 1.0)) (set! sum (+ sum val))))))
The amplitude scaling is obviously not right (if "B" > 3, it works to use (* (/ scl 1.6) (tanh (* B (oscil gen)))) and (outa i (- sum .83)), but if "B" is following an envelope, the integration makes it hard to keep everything centered and normalized). 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)))) (do ((i beg (+ i 1))) ((= 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.
make-ncos (frequency 0.0) (n 1) ncos nc (fm 0.0) ncos? nc make-nsin (frequency 0.0) (n 1) nsin ns (fm 0.0) nsin? ns
ncos methods | |
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 |
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). Set "n" to srate/2 to get a pulse-train (a single non-zero sample). These generators are based on the Dirichlet kernel:
(with-sound (:play #t) (let ((gen (make-ncos 440.0 10))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (* 0.5 (ncos gen)))))) |
with_sound(:play, true) do gen = make_ncos(440.0, 10); 44100.times do |i| outa(i, 0.5 * ncos(gen), $output) end end.output |
lambda: ( -- ) 440.0 10 make-ncos { gen } 44100 0 do i gen 0 ncos f2/ *output* outa drop loop ; :play #t with-sound drop |
There are many similar formulas: see ncos2 and friends in generators.scm. "Trigonometric Delights" by Eli Maor has a derivation of the 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.
(define (simple-soc beg dur freq amp) (let* ((os (make-ncos freq 10)) (start (seconds->samples beg)) (end (+ start (seconds->samples dur)))) (do ((i start (+ i 1))) ((= i end)) (outa i (* amp (ncos os)))))) (with-sound () (simple-soc 0 1 100 1.0))
The sinc-train generator (in generators.scm) is very similar to ncos. 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 (+ i 1))) ((= i (+ start samps))) (outa i (* (env ampf) (oscil car1 (* index (ncos mod1)))))))) '((0.0 1) (2.0 2) (4.0 4) (6.0 8) (8.0 16) (10.0 32) (12.0 64) (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.
(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 first harmonic, ;; "n" as number of harmonics (do ((harms ()) (amps ()) (i 1 (+ i 1))) ((> i n) (fm-parallel-component freq-we-want wc (reverse harms) (reverse amps) () () #f)) (set! harms (cons (* i modfreq) harms)) (set! amps (cons (/ baseindex i n) amps))))
4 components: (ncfm x 1000 100 3.0 4) | ||
x=1000 | 0.81 | 0.81 from J0(3/(4*k)) '(0 0 0 0) |
x=900 | -0.44 | -0.32 from J1(3/4)*J0s '(-1 0 0 0) |
x=800 | -0.14 | -0.16 from J1(3/8)*J0s '(0 -1 0 0) |
x=700 | -0.06 | -0.10 from J1(3/12)*J0s '(0 0 -1 0) |
24 components: (ncfm x 1000 100 3.0 24) | ||
x=1000 | 0.99 | 0.99 from J0(3/(24*k)) '(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0) |
x=900 | -0.06 | -0.06 from J1(3/24)*J0s '(-1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0) |
x=800 | -0.03 | -0.03 from J1(3/48)*J0s '(0 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0) |
x=700 | -0.02 | -0.02 from J1(3/96)*J0s '(0 0 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0) |
You can multiply the index by n to counteract the effect of the n modulators (in the n=128 case mentioned above, the index becomes 384!). I find it surprising how smooth the spectral evolution is in this context. Here we sweep the index from 0 to 48 using n=16:
![]() |
ncos (n=16) as FM, index from 0 to 48 |
But if our second analysis is correct, there's nothing special about the spike waveform that ncos produces. We only need a lot of components of decreasing effective FM index. If we randomize the initial phases of the n harmonically related equal amplitude sinusoids, we can minimize the peak amplitude (to reduce the spike), getting waveforms and results like these:
![]() |
FM of sum of n sinusoids |
![]() |
sum of n sinusoids minimizing resemblance to pulse-train |
Compare the sound of the n=64 and n=128 cases using ncos and random phases: they sound very different despite having the same spectrum. We confront the burning question: given n equal amplitude harmonically related sinusoids, what is the minimum peak amplitude? For my current best results, see peak-phases.
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). And (for some reason this makes me smile), polywave modulated by ncos behaves the same way:
(with-sound () (let ((modulator (make-ncos 100 :n 128)) (carrier (make-polywave 1000 (list 1 .5 3 .25 6 .25)))) (do ((i 0 (+ i 1))) ((= i 20000)) (outa i (* .5 (polywave carrier (* (hz->radians (* 3 100)) (ncos modulator 0.0))))))))
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!
What FM input (to oscil, for a given index) would give the most dispersed output? My first guess was square-wave, but looking at graphs, I'd say rand gives it a good contest. 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 (+ i 1))) ((= 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.
ncos2 (Fejer, n=10) | npcos (Poussin, n=5) | ncos4 (Jackson, n=10) |
![]() |
![]() |
![]() |
nsin produces a sum of equal amplitude sines. It is very similar (good and bad) to ncos. For n greater than 10 or so, its peak amplitude occurs at approximately 3pi/4n, and is about .7245*n (that is, 8n*(sin^2(3pi/8))/3pi). The nsin generator scales its output to be between -1 and 1 for any n. We can use nxysin to try any initial-phase in a sum of equal sinusoids. The peak amp in this case varys sinusoidally from a sum of sines n * 0.7245 to a sum of cosines n * 1.0; the peak amp is nsin-max(n) + abs(sin(initial-phase))*(1 - nsin-max(n)). nsin is based on the conjugate Dirichlet kernel:
nsin methods | |
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 |
As with all the paired cos/sin generators (waveshaping, generators.scm, etc), we can vary the initial phase by taking advantage of the trig identity:
that is,
(+ (* (ncos nc) (sin initial-phase)) (* (nsin ns) (cos initial-phase)))
Or vary it via an envelope at run-time:
(with-sound () (let ((nc (make-ncos 500.0 6)) (ns (make-nsin 500.0 6)) (phase (make-env '(0 0 1 1) :length 1000 :scaler (/ pi 2)))) (do ((i 0 (+ i 1))) ((= i 1000)) (let ((angle (env phase))) (outa i (+ (* (ncos nc) (sin angle)) (* (nsin ns) (cos angle))))))))
Compared to ncos or nsin, polywave is probably always faster and more accurate, but less convenient to set up. 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 fourth kind. See also the nrxycos generator, and generators.scm.
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.
Most of the formulas above are implemented as generators in generators.scm,
along with the single side-band cases, where possible.
Don't shy away from the sums to infinity just because you've heard shouting about "band-limited waveforms" — FM is an infinite sum:
make-nrxysin (frequency 0.0) (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 (fm 0.0) nrxysin? s make-nrxycos (frequency 0.0) (ratio 1.0) (n 1) (r .5) nrxycos s (fm 0.0) nrxycos? s
nrxysin methods | |
mus-frequency | frequency in Hz |
mus-phase | phase in radians |
mus-scaler | "r" parameter; sideband scaler |
mus-length | "n" parameter |
mus-increment | frequency in radians per sample |
mus-offset | "ratio" parameter |
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. Here are the underlying formulas:
![]() |
nrxysin, n=5, r=0.5 |
(with-sound (:play #t) (let ((gen (make-nrxycos 440.0 :n 10))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (* 0.5 (nrxycos gen)))))) |
with_sound(:play, true) do gen = make_nrxycos(440.0, 1.0, 10, 0.5); 44100.times do |i| outa(i, 0.5 * nrxycos(gen), $output) end end.output |
lambda: ( -- ) 440.0 :n 10 make-nrxycos { gen } 44100 0 ?do i gen 0 nrxycos f2/ *output* outa drop loop ; :play #t with-sound drop |
The peak amplitude of nrxysin is hard to predict. I think nrxysin is close to the -1.0..1.0 ideal, and won't go over 1.0. nrxycos is normalized correctly. 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 (n 1) (r .5) (ratio 1.0) frqf) (let* ((st (seconds->samples beg)) (nd (+ st (seconds->samples dur))) (sgen (make-nrxysin freq ratio n r)) (frq-env (and frqf (make-env frqf :scaler (hz->radians freq) :duration dur))) (spectr-env (and frqf (make-env frqf :duration dur))) (amp-env (make-env '(0 0 1 1 2 1 3 0) :scaler amp :duration dur))) (do ((i st (+ i 1))) ((= 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 (+ i 1))) ((= i 80000)) (set! (mus-scaler gen1) (env indr)) ; this sets r (outa i (* .5 (nrxycos gen1 0.0))))))
make-ssb-am (frequency 0.0) (order 40) ssb-am gen (insig 0.0) (fm 0.0) ssb-am? gen
ssb-am methods | |
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 |
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.
(with-sound (:play #t :srate 44100) (let ((shifter (make-ssb-am 440.0 20)) (osc (make-oscil 440.0))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (* 0.5 (ssb-am shifter (oscil osc))))))) |
with_sound(:play, true, :srate, 44100) do shifter = make_ssb_am(440.0, 20); osc = make_oscil(440.0); 44100.times do |i| outa(i, 0.5 * ssb_am(shifter, oscil(osc)), $output); end end.output |
lambda: ( -- ) 440.0 20 make-ssb-am { shifter } 440.0 make-oscil { osc } 44100 0 ?do i shifter osc 0 0 oscil 0 ssb-am f2/ *output* outa drop loop ; :play #t :srate 44100 with-sound drop |
(define* (ssb-am freq (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) (* csin yh))))))) (definstrument (shift-pitch beg dur file freq (order 40)) (let* ((st (seconds->samples beg)) (nd (+ st (seconds->samples dur))) (gen (make-ssb-am freq order)) (rd (make-readin file))) (do ((i st (+ i 1))) ((= 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).
![]() |
![]() |
![]() |
![]() |
original | amplitude modulation | ssb-am | ssb-am bank |
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 (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 (+ i 1))) ((> i pairs)) (let ((aff (* i old-freq)) (bwf (* bw (+ 1.0 (/ i 2 pairs))))) (set! (ssbs (- i 1)) (make-ssb-am (* i factor old-freq))) (set! (bands (- i 1)) (make-bandpass (hz->radians (- aff bwf)) ; bandpass is in dsp.scm (hz->radians (+ aff bwf)) order)))) (do ((i start (+ i 1))) ((= i end)) (let ((sum 0.0) (y (readin rd))) (do ((band 0 (+ 1 band))) ((= band pairs)) (set! sum (+ sum (ssb-am (ssbs band) (bandpass (bands band) y))))) (outa i (* amp sum)))))) (let* ((sound "oboe.snd") (mx (maxamp sound)) (dur (mus-sound-duration sound))) (with-sound (:scaled-to mx :srate (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:
(set! (ssbs (- i 1)) (make-ssb-am (+ (* i factor old-freq) (* new-freq (round (* i stretch))))))
make-wave-train (frequency 0.0) (initial-phase 0.0) wave (size *clm-table-size*) (type mus-interp-linear) wave-train w (fm 0.0) wave-train? w make-wave-train-with-env frequency env size
wave-train methods | |
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!) |
wave-train adds a copy of 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 (defined in generators.scm) returns a new wave-train generator with the envelope 'env' loaded into its table.
(with-sound (:play #t) (let ((gen (make-wave-train 440.0 :wave (let ((v (make-float-vector 64)) (g (make-ncos 400 10))) (set! (mus-phase g) (* -0.5 pi)) (do ((i 0 (+ i 1))) ((= i 64)) (set! (v i) (ncos g))) v)))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (* 0.5 (wave-train gen)))))) |
with_sound(:play, true) do v = make_vct(64); g = make_ncos(400, 10); g.phase = -0.5 * 3.14159; 64.times do |i| v[i] = ncos(g); end gen = make_wave_train(440.0, :wave, v); 44100.times do |i| outa(i, 0.5 * wave_train(gen), $output) end end.output |
lambda: ( -- ) 400 10 make-ncos { g } g -0.5 pi f* set-mus-phase drop 64 make-vct map! g 0 ncos end-map { v } 440.0 :wave v make-wave-train { gen } 44100 0 do i gen 0 wave-train f2/ *output* outa drop loop ; :play #t with-sound drop |
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 ve ae) (let* ((start (seconds->samples beg)) (end (+ start (seconds->samples dur))) (ampf (make-env (or ae '(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 (= *clm-srate* 22050) 100 200)) (vibr (make-oscil 6)) (vibenv (make-env (or ve '(0 1 100 1)) :scaler vib :duration dur)) (win-freq (/ (* 2 pi) foflen)) (foftab (make-float-vector foflen)) (wt0 (make-wave-train :wave foftab :frequency frq))) (do ((i 0 (+ i 1))) ((= i foflen)) (set! (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)))))) (do ((i start (+ i 1))) ((= 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-trains's wave is a float-vector 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 (copy grain))) (do ((i beg (+ i 1))) ((= 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 (* 1.0 (/ (- i beg) len)))) (comb-len 32) (c1 (make-comb scaler comb-len)) (c2 (make-comb scaler (floor (* comb-len .75)))) (c3 (make-comb scaler (floor (* comb-len 1.25))))) (do ((k 0 (+ k 1))) ((= k grain-size)) (let ((x (original-grain k))) (set! (grain k) (+ (comb c1 x) (comb c2 x) (comb c3 x))))))))))))) (with-sound () (when? 0 4 2.0 8.0 "right-now.snd"))
wave-train is built on table-lookup and shares all of its questionable aspects. See also the pulsed-enve generator in generators.scm, used in animals.scm. It is often simpler to use pulse-train as the repetition trigger, and mus-reset to restart an envelope.
make-rand (frequency 0.0) ; frequency at which new random numbers occur (amplitude 1.0) ; numbers are between -amplitude and amplitude (envelope '(-1 1 1 1)) ; distribution envelope (uniform distribution is the default) distribution ; pre-computed distribution rand r (sweep 0.0) rand? r make-rand-interp (frequency 0.0) (amplitude 1.0) (envelope '(-1 1 1 1) distribution) rand-interp r (sweep 0.0) rand-interp? r mus-random amp mus-rand-seed
rand and rand-interp methods | |
mus-frequency | frequency in Hz |
mus-phase | phase in radians |
mus-scaler | amplitude arg used in make-<gen> |
mus-length | distribution table (float-vector) length |
mus-data | distribution table (float-vector), if any |
mus-increment | frequency in radians per sample |
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.
(with-sound (:channels 2 :play #t) (let ((ran1 (make-rand 5.0 (hz->radians 220.0))) (ran2 (make-rand-interp 5.0 (hz->radians 220.0))) (osc1 (make-oscil 440.0)) (osc2 (make-oscil 1320.0))) (do ((i 0 (+ i 1))) ((= i 88200)) (outa i (* 0.5 (oscil osc1 (rand ran1)))) (outb i (* 0.5 (oscil osc2 (rand-interp ran2))))))) |
with_sound(:play, true, :channels, 2) do ran1 = make_rand(5.0, hz2radians(220.0)); ran2 = make_rand_interp(5.0, hz2radians(220.0)); osc1 = make_oscil(440.0); osc2 = make_oscil(1320.0); 88200.times do |i| outa(i, 0.5 * oscil(osc1, rand(ran1)), $output); outb(i, 0.5 * oscil(osc2, rand_interp(ran2)), $output); end end.output |
lambda: ( -- ) 5.0 220.0 hz->radians make-rand { ran1 } 5.0 330.0 hz->radians make-rand-interp { ran2 } 440.0 make-oscil { osc1 } 1320.0 make-oscil { osc2 } 88200 0 do i osc1 ran1 0 rand 0 oscil f2/ *output* outa drop i osc2 ran2 0 rand-interp 0 oscil f2/ *output* outb drop loop ; :channels 2 :play #t with-sound drop |
The "frequency" is the rate at which new values are produced, so it makes sense to request a frequency above srate/2. If rand's frequency is the current srate, it produces a new random value on every sample. Since rand is (normally) 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):
![]() | ![]() |
square-wave (freq=1000) | triangle-wave (freq=1000) |
![]() | ![]() |
rand (freq=2000) | rand-interp (freq=2000) |
There are a variety of ways to get a non-uniform random number distribution:
(random (random 1.0))
or (sin (mus-random pi))
are examples. Exponential distribution could be:
(log (max .01 (random 1.0)) .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 by adding rand's together. Orfanidis in "Introduction to Signal Processing" says 12 calls on rand will do perfectly well:
(define (gaussian-noise) (do ((val 0.0) (i 0 (+ i 1))) ((= 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 (+ i 1))) ((= i n)) (set! (rands i) (make-rand :frequency *clm-srate* :amplitude (/ 100 n))) (rand (rands i))) (do ((i 0 (+ i 1))) ((= i 100000)) (do ((sum 0.0) (k 0 (+ k 1))) ((= k n) (let ((bin (floor (+ 100 (round sum))))) (set! (bins bin) (+ (bins bin) 1)))) (set! sum (+ sum (rand (rands k)))))) bins)) (let ((ind (new-sound "test.snd"))) (do ((n 1 (+ n 1))) ((= n 12)) (let* ((bins (vector->float-vector (add-rands n))) (pk (maxamp bins))) (float-vector->channel (float-vector-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 old waveshape generator.
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 float-vector, 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) (do ((e ()) (den (* 2.0 s s)) (i 0 (+ i 1)) (x -1.0 (+ x .1)) (y -4.0 (+ y .4))) ((= i 21) (reverse e)) (set! e (cons (exp (- (/ (* y y) den))) (cons x e))))) (make-rand :envelope (gaussian-envelope 1.0))
If you want a particular set of values, it's simplest to fill a float-vector 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:
(do ((vals (float-vector 0.0 0.5 0.5 0.5 1.0)) (i 0 (+ i 1))) ((= i 10)) (format () ";~A " (vals (random 5))))
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 (do ((rans (make-vector n)) (i 0 (+ i 1))) ((= i n) rans) (set! (rans i) (make-rand :frequency (/ *clm-srate* (expt 2 i)))))) (define (1f-noise rans) (let ((val 0.0) (len (length rans))) (do ((i 0 (+ i 1))) ((= i len) (/ val len)) (set! val (+ val (rand (rans i)))))))
This is the pink-noise generator in generators.scm. See also green-noise — bounded brownian noise that can mimic 1/f noise in some cases. (The brownian graph below has a different dB range, and the rand graph would be flat if we used a frequency of 44100).
random | rand | rand-interp |
![]() |
![]() |
![]() |
1/f | brownian | green |
![]() |
![]() |
![]() |
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)))) (do ((i beg (+ i 1))) ((= 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))) (do ((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) (i st (+ i 1))) ((= 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) (do ((x 0.5) (i 0 (+ i 1))) ((= i 44100)) (outa i x) (set! x (* 4 (sin (* pi x))))))
For an extended discussion of this case, complete with pictures of the period doublings, see Strogatz, "Nonlinear Dynamics and Chaos".
mus-rand-seed provides access to the seed for mus-random's random number generator:
> (set! (mus-rand-seed) 1234) 1234 > (mus-random 1.0) -0.7828369138846 > (mus-random 1.0) -0.880371093652 > (set! (mus-rand-seed) 1234) ; now start again with the same sequence of numbers 1234 > (mus-random 1.0) -0.7828369138846 > (mus-random 1.0) -0.880371093652
The clm random functions discussed here are different from s7's random function. The latter has a random-state record to guide the sequence (and uses a different algorithm), whereas the clm functions just use an integer, mus-rand-seed.
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-noise (bounded Brownian noise).
make-one-pole a0 b1 ; b1 < 0.0 gives lowpass, b1 > 0.0 gives highpass one-pole f input one-pole? f make-one-zero a0 a1 ; a1 > 0.0 gives weak lowpass, a1 < 0.0 highpass one-zero f input one-zero? f make-two-pole frequency [or a0] radius [or b1] b2 two-pole f input two-pole? f make-two-zero frequency [or a0] radius [or a1] a2 two-zero f input two-zero? f
simple filter methods | |
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 |
These are the simplest of filters. If you're curious about filters, Julius Smith's on-line Introduction to Digital Filters is excellent.
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.
(with-sound (:play #t) (let ((flt (make-two-pole 1000.0 0.999)) (ran1 (make-rand 10000.0 .002))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (* 0.5 (two-pole flt (rand ran1))))))) |
with_sound(:play, true) do flt = make_two_pole(1000.0, 0.999); ran1 = make_rand(10000.0, 0.002); 44100.times do |i| outa(i, 0.5 * two_pole(flt, rand(ran1)), $output); end end.output |
lambda: ( -- ) 1000.0 0.999 make-two-pole { flt } 10000.0 0.002 make-rand { ran1 } 44100 0 do i flt ran1 0 rand two-pole f2/ *output* outa drop loop ; :play #t with-sound drop |
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 (+ i 1))) ((= 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))
make-formant frequency ; resonance center frequency in Hz radius ; resonance width, indirectly formant f input center-frequency-in-radians formant? f formant-bank filters input formant-bank? f make-formant-bank filters amps make-firmant frequency radius firmant f input center-frequency-in-radians firmant? f ;; the next two are optimizations that I may remove mus-set-formant-frequency f frequency mus-set-formant-radius-and-frequency f radius frequency
formant methods | |
mus-phase | formant radius |
mus-frequency | formant center frequency |
mus-order | 2 (no set!) |
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))
(with-sound (:play #t) (let ((flt (make-firmant 1000.0 0.999)) (ran1 (make-rand 10000.0 5.0))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (* 0.5 (firmant flt (rand ran1))))))) |
with_sound(:play, true) do flt = make_firmant(1000.0, 0.999); ran1 = make_rand(10000.0, 5.0); 44100.times do |i| outa(i, 0.5 * firmant(flt, rand(ran1)), $output); end end.output |
lambda: ( -- ) 1000.0 0.999 make-firmant { flt } 10000.0 5.0 make-rand { ran1 } 44100 0 do i flt ran1 0 rand #f firmant f2/ *output* outa drop loop ; :play #t with-sound drop |
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 and firmant are often used to sculpt away unwanted spectral components, or emphasize formant regions. In animals.scm, the crow, for example,
(load "animals.scm") (with-sound (:play #t) (american-crow 0 .5))
has three formant filters. Without them, it would sound like this:
(with-sound (:play #t) (american-crow-no-formants 0 .5))
formant generators are also 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 an 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-framples file)) (end (+ beg dur)) (rd (make-readin file)) (menv (make-env move-env :length dur))) (let ((start-frq (env menv))) (do ((i 0 (+ i 1))) ((= i num-formants)) (set! (frms i) (make-formant (* (+ i 1) start-frq) radius)))) (do ((k beg (+ k 1))) ((= k end)) (let ((frq (env menv)) (sum 0.0) (inp (readin rd))) (do ((i 0 (+ i 1))) ((= i num-formants)) (set! sum (+ sum (formant (frms i) inp)))) (outa k (* amp sum)) (do ((i 0 (+ i 1)) (curfrq frq (+ curfrq frq))) ((= i num-formants)) (if (< (* 2 curfrq) *clm-srate*) (set! (mus-frequency (frms i)) curfrq))))))) (with-sound () (move-formants 0 "oboe.snd" 2.0 0.99 '(0 1200 1.6 2400 2 1400) 4))
make-formant-bank creates a formant-bank generator, an array of formant generators that is summed in parallel. The explicit do loop:
(do ((sum 0.0) ; say we have n formant generators in the formants vector, and we're passing each a signal x (i 0 (+ i 1))) ((= i n) sum) (set! sum (+ sum (formant (formants i) x))))
can be replaced with:
(let ((fb (make-formant-bank formants))) ... (formant-bank fb x))
make-formant-bank takes a vector of formant generators as its first argument. Its optional second argument is a float-vector of gains (amplitudes) to scale each formant's contribution to the sum. Similarly, formant-bank's second argument is either a real number or a float-vector. If a float-vector, each element is treated as the input to the corresponding formant in the bank. (formant-bank ignores its constituent formant generator's radius and frequency after make-formant-bank; see move-formant above for a slightly less compact workaround if you want a bank of moving formants).
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* ((samps (seconds->samples 3)) (flta (make-formant 100 .999)) (fltc (make-firmant 100 .999)) (vibosc (make-oscil 10)) (index (hz->radians 100)) (click (make-ncos 40 500))) (do ((i 0 (+ i 1))) ((= i samps)) (let ((vib (* index (+ 1 (oscil vibosc)))) (pulse (ncos click))) (outa i (* 10 (formant flta pulse vib))) (outb i (* 10 (firmant fltc pulse vib)))))))
make-filter order xcoeffs ycoeffs filter fl inp filter? fl make-fir-filter order xcoeffs fir-filter fl inp fir-filter? fl make-iir-filter order ycoeffs iir-filter fl inp iir-filter? fl make-fir-coeffs order v
general filter methods | |
mus-order | filter order |
mus-xcoeff | x (input) coeff |
mus-xcoeffs | x (input) coeffs |
mus-ycoeff | y (output) coeff |
mus-ycoeffs | y (output) coeffs |
mus-data | current state (input values) |
mus-length | same as mus-order |
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 array). The filter generator might be defined:
(let ((xout 0.0)) (set! (state 0) input) (do ((j (- order 1) (- j 1))) ((= j 0)) (set! xout (+ xout (* (xcoeffs j) (state j)))) (set! (state 0) (- (state 0) (* (ycoeffs j) (state j)))) (set! (state j) (state (- j 1)))) (+ xout (* (state 0) (xcoeffs 0))))
(with-sound (:play #t) (let ((flt (make-iir-filter 3 (float-vector 0.0 -1.978 0.998))) (ran1 (make-rand 10000.0 0.002))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (* 0.5 (iir-filter flt (rand ran1))))))) |
with_sound(:play, true) do flt = make_iir_filter(3, vct(0.0, -1.978, 0.998)); ran1 = make_rand(10000.0, 0.002); 44100.times do |i| outa(i, 0.5 * iir_filter(flt, rand(ran1)), $output); end end.output |
lambda: ( -- ) 3 vct( 0.0 -1.978 0.998 ) make-iir-filter { flt } 10000.0 0.002 make-rand { ran1 } 44100 0 do i flt ran1 0 rand iir-filter f2/ *output* outa drop loop ; :play #t with-sound drop |
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 (float-vector 0.0 b1 b2)))
The Hilbert transform can be implemented with an fir-filter:
(define* (make-hilbert-transform (len 30)) (let* ((arrlen (+ 1 (* 2 len))) (arr (make-float-vector arrlen))) (do ((lim (if (even? len) len (+ 1 len))) (i (- len) (+ i 1))) ((= i lim)) (let ((k (+ i len)) (denom (* pi i)) (num (- 1.0 (cos (* pi i))))) (set! (arr k) (if (or (= num 0.0) (= i 0)) 0.0 (* (/ num denom) (+ .54 (* .46 (cos (/ (* i pi) len))))))))) (make-fir-filter arrlen arr))) (define hilbert-transform fir-filter)
make-fir-coeffs translates a frequency response envelope (actually, evenly spaced points in a float-vector) into the corresponding FIR filter coefficients. The order of the filter determines how close you get to the envelope.
Filters |
lowpass filter: make-lowpass in dsp.scm |
make-delay size ; delay length initial-contents ; delay line's initial values (a float-vector 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 (pm 0.0) delay? d tap d (offset 0) tap? d delay-tick d input
delay methods | |
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 |
mus-location | current delay line write position |
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. Delay could be defined:
(let ((result (array-interp line (- loc pm)))) (set! (line loc) input) (set! loc (+ 1 loc)) (if (<= size loc) (set! loc 0)) result)
(with-sound (:play #t) (let ((dly (make-delay (seconds->samples 0.5))) (osc1 (make-oscil 440.0)) (osc2 (make-oscil 660.0))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (* 0.5 (+ (oscil osc1) (delay dly (oscil osc2)))))))) |
with_sound(:play, true) do dly = make_delay(seconds2samples(0.5)); osc1 = make_oscil(440.0); osc2 = make_oscil(660.0); 44100.times do |i| outa(i, 0.5 * (oscil(osc1) + delay(dly, oscil(osc2))), $output); end end.output |
lambda: ( -- ) 0.5 seconds->samples make-delay { dly } 440.0 make-oscil { osc1 } 660.0 make-oscil { osc2 } 44100 0 do i osc1 0 0 oscil dly osc2 0 0 oscil 0 delay f+ f2/ *output* outa drop loop ; :play #t with-sound drop |
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-sampler 0 file))) (do ((i beg (+ i 1))) ((= 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 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. The mus-location field returns the current delay line write position. To access the delay line contents as a sliding window on the input data, use:
(define (delay-ref dly loc) (float-vector-ref (mus-data dly) (modulo (+ loc (mus-location dly)) (mus-length dly))))
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).
make-comb (scaler 1.0) size initial-contents (initial-element 0.0) max-size comb cflt input (pm 0.0) comb? cflt comb-bank combs input comb-bank? object make-comb-bank combs make-filtered-comb (scaler 1.0) size initial-contents (initial-element 0.0) max-size filter filtered-comb cflt input (pm 0.0) filtered-comb? cflt filtered-comb-bank fcombs input filtered-comb-bank? object make-filtered-comb-bank fcombs make-notch (scaler 1.0) size initial-contents (initial-element 0.0) max-size notch cflt input (pm 0.0) notch? cflt
comb, filtered-comb, and notch methods | |
mus-length | length of delay |
mus-order | same as mus-length |
mus-data | delay line itself (no set!) |
mus-feedback | scaler (comb only) |
mus-feedforward | scaler (notch only) |
mus-interp-type | interpolation choice (no set!) |
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))
(with-sound (:play #t) (let ((cmb (make-comb 0.4 (seconds->samples 0.4))) (osc (make-oscil 440.0)) (ampf (make-env '(0 0 1 1 2 1 3 0) :length 4410))) (do ((i 0 (+ i 1))) ((= i 88200)) (outa i (* 0.5 (comb cmb (* (env ampf) (oscil osc)))))))) |
with_sound(:play, true) do cmb = make_comb(0.4, seconds2samples(0.4)); osc = make_oscil(440.0); ampf = make_env([0.0, 0.0, 1.0, 1.0, 2.0, 1.0, 3.0, 0.0], :length, 4410); 88200.times do |i| outa(i, 0.5 * (comb(cmb, env(ampf) * oscil(osc))), $output); end end.output |
lambda: ( -- ) 0.4 0.4 seconds->samples make-comb { cmb } 440.0 make-oscil { osc } '( 0 0 1 1 2 1 3 0 ) :length 4410 make-env { ampf } 88200 0 do i cmb ( gen ) ampf env osc 0 0 oscil f* ( val ) 0 ( pm ) comb f2/ *output* outa drop loop ; :play #t with-sound drop |
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-comb :size size :scaler (- 1.0 (/ (* 7.0 size) feedback-dur *clm-srate*)))
The peak gain is 1.0 / (1.0 - (abs scaler)). The peaks (or valleys in notch's case) are evenly spaced at *clm-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))) (do ((i beg (+ i 1))) ((= 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))
Nearly every actual use of comb filters involves a bank of them, a vector of combs summed in parallel. The comb-bank generator is intended for this kind of application. make-comb-bank takes a vector of combs and returns the comb-bank generator which can be called via comb-bank.
(do ((sum 0.0) (i 0 (+ i 1))) ((= i n) sum) (set! sum (+ sum (comb (combs i) x))))
can be replaced with:
(let ((cb (make-comb-bank combs))) ... (comb-bank cb x))
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 (do ((i start (+ i 1))) ((= 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. We use comb-bank generators:
(definstrument (flux start-time file frequency combs0 combs1 (scaler 0.99) (comb-len 32)) (let* ((beg (seconds->samples start-time)) (end (+ beg (mus-sound-framples file))) (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 (+ k 1))) ((= k num-combs0)) (set! (cmbs0 k) (make-comb scaler (floor (* comb-len (combs0 k)))))) (do ((k 0 (+ k 1))) ((= k num-combs1)) (set! (cmbs1 k) (make-comb scaler (floor (* comb-len (combs1 k)))))) (let ((nc0 (make-comb-bank cmbs0)) (nc1 (make-comb-bank cmbs1))) (do ((i beg (+ i 1))) ((= i end)) (let ((interp (oscil osc)) (x (readin rd))) (outa i (+ (* interp (comb-bank nc0 x)) (* (- 1.0 interp) (comb-bank nc1 x))))))))) (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))
As with the normal comb filter, the filtered-comb-bank generator sums a vector of filtered-comb generators in parallel.
make-all-pass (feedback 0.0) (feedforward 0.0) size initial-contents (initial-element 0.0) max-size all-pass f input (pm 0.0) all-pass? f all-pass-bank all-passes input all-pass-bank? object make-all-pass-bank all-passes make-one-pole-all-pass size coeff one-pole-all-pass f input one-pole-all-pass? f
all-pass methods | |
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!) |
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 feedforward is 0.0, we get a comb filter. If both scale terms are 0.0, we get a pure delay line.
y(n) = feedforward * x(n) + x(n - size) + feedback * y(n - size)
(with-sound (:play #t) (let ((alp (make-all-pass -0.4 0.4 (seconds->samples 0.4))) (osc (make-oscil 440.0)) (ampf (make-env '(0 0 1 1 2 1 3 0) :length 4410))) (do ((i 0 (+ i 1))) ((= i 88200)) (outa i (* 0.5 (all-pass alp (* (env ampf) (oscil osc)))))))) |
with_sound(:play, true) do alp = make_all_pass(-0.4, 0.4, seconds2samples(0.4)); osc = make_oscil(440.0); ampf = make_env([0.0, 0.0, 1.0, 1.0, 2.0, 1.0, 3.0, 0.0], :length, 4410); 88200.times do |i| outa(i, 0.5 * (all_pass(alp, env(ampf) * oscil(osc))), $output); end end.output |
lambda: ( -- ) -0.4 0.4 0.4 seconds->samples make-all-pass { alp } 440.0 make-oscil { osc } '( 0 0 1 1 2 1 3 0 ) :length 4410 make-env { ampf } 88200 0 do i alp ( gen ) ampf env osc 0 0 oscil f* ( val ) 0 ( pm ) all-pass f2/ *output* outa drop loop ; :play #t with-sound drop |
all-pass filters are used extensively in reverberation; see jcrev or nrev. To get the "all-pass" behavior, set feedback equal to -feedforward. Here's an example (based on John Chowning's ancient reverb) that was inspired by the bleed-through you get on old analog tapes — the reverb slightly precedes the direct signal:
(define (later file dly rev) (let ((allpass1 (make-all-pass -0.700 0.700 1051)) (allpass2 (make-all-pass -0.700 0.700 337)) (allpass3 (make-all-pass -0.700 0.700 113)) (comb1 (make-comb 0.742 4799)) (comb2 (make-comb 0.733 4999)) (comb3 (make-comb 0.715 5399)) (comb4 (make-comb 0.697 5801)) (len (floor (+ *clm-srate* (mus-sound-framples file)))) (rd (make-readin file)) ; the direct signal (via sound-let below) (d (make-delay dly))) ; this delays the direct signal (do ((backup (min 4799 dly)) (i 0 (+ i 1))) ((= i len)) (let* ((inval (readin rd)) (allpass-sum (all-pass allpass3 (all-pass allpass2 (all-pass allpass1 (* rev inval))))) (comb-sum (+ (comb comb1 allpass-sum) (comb comb2 allpass-sum) (comb comb3 allpass-sum) (comb comb4 allpass-sum))) (orig (delay d inval))) (if (>= i backup) (outa (- i backup) (+ comb-sum orig))))))) (with-sound () (sound-let ((tmp () (fm-violin 0 .1 440 .1))) (later tmp 10000 .1)))
In all such applications, the all-pass filters are connected in series (each one's output is the input to the next in the set). To package this up in one generator, use an all-pass-bank. An all-pass-bank is slightly different from the other "bank" generators in that it connects the vector of all-passes in series, rather than summing them in parallel. Code of the form:
(all-pass a1 (all-pass a2 input))
can be replaced with:
(all-pass-bank (make-all-pass-bank (vector a1 a2)) input)
one-pole-all-pass is used by piano.scm:
y(n) = x(n) + coeff * (y(n-1) - y(n)) x(n) = y(n-1)
This is repeated "size" times, with the generator input as the first y(n-1) value.
make-moving-average size initial-contents (initial-element 0.0) moving-average f input moving-average? f make-moving-max size initial-contents (initial-element 0.0) moving-max f input moving-max? f make-moving-norm size (scaler 1.0) moving-norm f input moving-norm? f
moving-average methods | |
mus-length | length of table |
mus-order | same as mus-length |
mus-data | table of last 'size' values |
The moving-average or moving window average generator returns the average of the last "size" values input to it.
result = sum-of-last-n-inputs / n
(with-sound (:play #t) (let ((avg (make-moving-average 4410)) (osc (make-oscil 440.0)) (stop (- 44100 4410))) (do ((i 0 (+ i 1))) ((= i stop)) (let ((val (oscil osc))) (outa i (* val (moving-average avg (abs val)))))) (do ((i stop (+ i 1))) ((= i 44100)) (outa i (* (oscil osc) (moving-average avg 0.0)))))) |
with_sound(:play, true) do avg = make_moving_average(4410); osc = make_oscil(440.0); stop = 44100 - 4410; stop.times do |i| val = oscil(osc); outa(i, val * moving_average(avg, val.abs), $output); end 4410.times do |i| outa(stop + i, oscil(osc) * moving_average(avg, 0.0), $output); end end.output |
lambda: ( -- ) 4410 make-moving-average { avg } 440.0 make-oscil { osc } 44100 4410 - { stop } 0.0 { val } stop 0 do osc 0 0 oscil to val i avg val fabs moving-average val f* *output* outa drop loop 44100 stop do i avg 0.0 moving-average osc 0 0 oscil f* *output* outa drop loop ; :play #t with-sound drop |
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)))
moving-max is a specialization
of the delay generator; it produces an envelope that tracks the peak amplitude of the last 'n' samples.
(make-moving-max 256)
returns the generator (this one's window size is 256),
and (moving-max gen y)
then returns the envelope traced out by the signal 'y'.
The harmonicizer uses this generator to normalize an in-coming signal to 1.0
so that the Chebyshev polynomials it is driving will produce a full spectrum at all times.
Here is a similar, but simpler, example; we use the moving-max generator to track the
current peak amplitude over a small window, use that value to drive a contrast-enhancement
generator (so that its output is always fully modulated), and rescale by the same value
upon output (to track the original sound's amplitude envelope):
(define (intensify index) (let ((mx (make-moving-max)) (flt (make-lowpass (* pi .1) 8))) ; smooth the maxamp signal (map-channel (lambda (y) (let ((amp (max .1 (fir-filter flt (moving-max mx y))))) (* amp (contrast-enhancement (/ y amp) index)))))))
moving-norm specializes moving-max to provide automatic gain control. It is essentially a one-pole (low-pass)
filter on the output of moving-max, inverted and multiplied by a scaler. (* input-signal (moving-norm g input-signal))
is the normal usage.
See generators.scm for several related functions: moving-rms, moving-sum, moving-length, weighted-moving-average, and exponentially-weighted-moving-average (the latter being just a one-pole filter).
make-src input (srate 1.0) (width 5) src s (sr-change 0.0) src? s
src methods | |
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.
(with-sound (:play #t :srate 22050) (let* ((rd (make-readin "oboe.snd")) (len (* 2 (mus-sound-framples "oboe.snd"))) (sr (make-src rd 0.5))) (do ((i 0 (+ i 1))) ((= i len)) (outa i (src sr))))) |
with_sound(:play, true, :srate, 22050) do rd = make_readin("oboe.snd"); len = 2 * mus_sound_framples("oboe.snd"); sr = make_src(lambda do |dir| readin(rd) end, 0.5); len.times do |i| outa(i, src(sr), $output); end end.output |
lambda: ( -- ) "oboe.snd" make-readin { rd } rd 0.5 make-src { sr } "oboe.snd" mus-sound-framples 2* ( len ) 0 do i sr 0 #f src *output* outa drop loop ; :play #t :srate 22050 with-sound drop |
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))) (do ((i beg (+ i 1))) ((= 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))) (do ((i beg (+ i 1))) ((= 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 2 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. Here's an example instrument that reads a file with an envelope on the src:
(definstrument (src-change filename start-time duration file-start srcenv) (let* ((beg (seconds->samples start-time)) (end (+ beg (seconds->samples duration))) (loc (seconds->samples file-start)) (src-gen (make-src :srate 0.0)) (e (make-env srcenv :duration duration)) (inp (make-file->sample filename))) (do ((i beg (+ i 1))) ((= i end)) (outa i (src src-gen (env e) (lambda (dir) ; our input function (set! loc (+ loc dir)) (ina loc inp))))))) ;;; (with-sound () (src-change "pistol.snd" 0 2 0 '(0 0.5 1 -1.5)))
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.
make-convolve input filter fft-size filter-size convolve gen convolve? gen convolve-files file1 file2 (maxamp 1.0) (output-file "tmp.snd")
convolve methods | |
mus-length | fft size used in the convolution |
The convolve generator convolves its input with the impulse response "filter" (a float-vector). "input" is a function of one argument that is called whenever convolve needs input.
(with-sound (:play #t :statistics #t) (let ((cnv (make-convolve (make-readin "pistol.snd") (samples 0 (framples "pistol.snd") "oboe.snd")))) (do ((i 0 (+ i 1))) ((= i 88200)) (outa i (* 0.25 (convolve cnv)))))) |
with_sound(:play, true, :statistics, true) do rd = make_readin("oboe.snd"); flt = file2vct("pistol.snd"); # examp.rb cnv = make_convolve(lambda { |dir| readin(rd)}, flt); 88200.times do |i| outa(i, 0.25 * convolve(cnv), $output); end end.output |
lambda: ( -- ) "pistol.snd" make-readin ( rd ) "oboe.snd" file->vct ( v ) make-convolve { cnv } 88200 0 do i cnv #f convolve 0.25 f* *output* outa drop loop ; :play #t :statistics #t with-sound drop |
(with-sound (:play #t) (let* ((tempfile (convolve-files "oboe.snd" "pistol.snd" 0.5 "convolved.snd")) (len (mus-sound-framples tempfile)) (reader (make-readin tempfile))) (do ((i 0 (+ i 1))) ((= i len)) (outa i (readin reader))) (delete-file tempfile))) |
with_sound(:play, true) do tempfile = convolve_files("oboe.snd", "pistol.snd", 0.5, "convolved.snd"); len = mus_sound_framples(tempfile); reader = make_readin(tempfile); len.times do |i| outa(i, readin(reader), $output); end File.unlink(tempfile) end.output |
lambda: ( -- ) "oboe.snd" "pistol.snd" 0.5 "convolved.snd" convolve-files { tempfile } tempfile make-readin { reader } tempfile mus-sound-framples ( len ) 0 do i reader readin *output* outa drop loop tempfile file-delete ; :play #t with-sound drop |
(definstrument (convins beg dur filter file (size 128)) (let* ((start (seconds->samples beg)) (end (+ start (seconds->samples dur))) (ff (make-convolve :input (make-readin file) :fft-size size :filter filter))) (do ((i start (+ i 1))) ((= i end)) (outa i (convolve ff))))) (with-sound () (convins 0 2 (float-vector 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 from using overlap-add (i.e. the generator), so a one-time giant FFT saved as a temporary sound file is much handier. If you're particular about the format of the convolved data:
(define* (convolve-files->aifc file1 file2 (maxamp 1.0) (output-file "test.snd")) (let ((outname (string-append "temp-" output-file))) (convolve-files file1 file2 maxamp outname) (with-sound (:header-type mus-aifc :sample-type mus-bfloat) (let ((len (seconds->samples (mus-sound-duration outname))) (reader (make-readin outname))) (do ((i 0 (+ i 1))) ((= i len)) (outa i (readin reader))))) (delete-file outname) output-file))
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.
make-granulate 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 granulate? e
granulate methods | |
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 float-vector) |
mus-location | granulate's local random number seed |
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.
result = overlap add many tiny slices from input
(with-sound (:play #t) (let ((grn (make-granulate (make-readin "oboe.snd") 2.0))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (granulate grn))))) |
with_sound(:play, true) do rd = make_readin("oboe.snd"); grn = make_granulate(lambda do |dir| readin(rd) end, 2.0); 88200.times do |i| outa(i, granulate(grn), $output); end end.output |
lambda: ( -- ) "oboe.snd" make-readin 2.0 make-granulate { grn } 44100 0 do i grn #f #f granulate *output* outa drop loop ; :play #t with-sound drop |
(with-sound (:play #t) (let* ((osc (make-oscil 440.0)) (sweep (make-env '(0 0 1 1) :scaler (hz->radians 440.0) :length 44100)) (grn (make-granulate (lambda (dir) (* 0.2 (oscil osc (env sweep)))) :expansion 2.0 :length .5))) (do ((i 0 (+ i 1))) ((= i 88200)) (outa i (granulate grn))))) |
with_sound(:play, true) do osc = make_oscil(440.0); sweep = make_env([0.0, 0.0, 1.0, 1.0], :scaler, hz2radians(440.0), :length, 44100); grn = make_granulate(lambda { |dir| 0.2 * oscil(osc, env(sweep))}, :expansion, 2.0, :length, 0.5); 88200.times do |i| outa(i, granulate(grn), $output); end end.output |
: make-granulate-proc { osc sweep -- prc; dir self -- val } 1 proc-create osc , sweep , ( prc ) does> { dir self -- val } self @ ( osc ) self cell+ @ ( sweep ) env 0 oscil 0.2 f* ; lambda: ( -- ) 440.0 make-oscil { osc } '( 0 0 1 1 ) :scaler 440.0 hz->radians :length 44100 make-env { sweep } osc sweep make-granulate-proc :expansion 2.0 :length 0.5 make-granulate { grn } 88200 0 do i grn #f #f granulate *output* outa drop loop ; :play #t with-sound drop |
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 dur (orig-beg 0.0) (exp-amt 1.0)) (let* ((f-srate (srate file)) (f (make-readin file :start (round (* f-srate orig-beg)))) (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)))) (do ((i st (+ i 1))) ((= 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)) (do ((i beg (+ i 1))) ((= i (+ beg dur))) (outa i (granulate exA (lambda (dir) (let ((inval (file->sample fil ctr 0))) (if (> ctr 0) (set! ctr (- ctr 1))) inval))))))) (with-sound () (grev 0 100000 2.0 "pistol.snd" 40000))
But it's unnecessary to write clever input functions. It is just as fast, and much more perspicuous, to use sound-let in cases like this. Here's an example that takes any set of notes and calls granulate on the result:
(define-macro (gran-any beg dur expansion . body) `(sound-let ((tmp () ,@body)) (let* ((start (floor (* *clm-srate* ,beg))) (end (+ start (* *clm-srate* ,dur))) (rd (make-readin tmp)) (gran (make-granulate :input rd :expansion ,expansion))) (do ((i start (+ i 1))) ((= i end)) (outa i (granulate gran)))))) (with-sound () (gran-any 0 2.5 4 (fm-violin 0 .1 440 .1) (fm-violin .2 .1 660 .1) (fm-violin .4 .1 880 .1)))
Any of the input-oriented generators (src, etc) can use this trick.
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) (reverse! grain))) len)))) (rd (make-sampler 0))) (map-channel (lambda (y) (granulate grn (lambda (dir) (rd)))))))
make-phase-vocoder input (fft-size 512) (overlap 4) (interp 128) (pitch 1.0) analyze edit synthesize phase-vocoder pv phase-vocoder? pv
phase-vocoder methods | |
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 sines; so, the default case returns a resynthesis of the original input. The "interp" argument sets the time between ffts (for time stretching, etc).
(with-sound (:play #t) ; new pitch = 2 * old (let ((pv (make-phase-vocoder (make-readin "oboe.snd") :pitch 2.0))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (phase-vocoder pv))))) |
with_sound(:play, true) do rd = make_readin("oboe.snd"); pv = make_phase_vocoder( lambda do |dir| readin(rd) end, :pitch, 2.0); 88200.times do |i| outa(i, phase_vocoder(pv), $output); end end.output |
lambda: ( -- ) "oboe.snd" make-readin :pitch 2.0 make-phase-vocoder { pv } 44100 0 do i pv #f #f #f #f phase-vocoder *output* outa drop loop ; :play #t with-sound drop |
(with-sound (:play #t :srate 22050) ; new dur = 2 * old (let ((pv (make-phase-vocoder (make-readin "oboe.snd") :interp 256)) ; 2 * 512 / 4 ;; 512: fft size, 4: overlap, new dur: 2 * old dur (samps (* 2 (mus-sound-framples "oboe.snd")))) (do ((i 0 (+ i 1))) ((= i samps)) (outa i (phase-vocoder pv))))) |
with_sound(:play, true, :srate, 22050) do rd = make_readin("oboe.snd"); pv = make_phase_vocoder( lambda do |dir| readin(rd) end, :interp, 2 * 512 / 4); samps = 2 * mus_sound_framples("oboe.snd"); samps.times do |i| outa(i, phase_vocoder(pv), $output); end end.output |
lambda: ( -- ) "oboe.snd" make-readin :interp 256 make-phase-vocoder { pv } "oboe.snd" mus-sound-framples 2* ( samps ) 0 do i pv #f #f #f #f phase-vocoder *output* outa drop loop ; :play #t :srate 22050 with-sound drop |
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 (float-vectors) containing the spectral data the phase-vocoder uses to reconstruct the sound. In the next example we use all these special functions to resynthesize down an octave:
(with-sound (:srate 22050 :statistics #t) (let ((pv (make-phase-vocoder (make-readin "oboe.snd") 512 4 128 1.0 #f ; no change to analysis method #f ; no change to spectrum (lambda (gen) ; resynthesis function (float-vector-add! (phase-vocoder-amps gen) (phase-vocoder-amp-increments gen)) (float-vector-add! (phase-vocoder-phase-increments gen) (phase-vocoder-freqs gen)) (float-vector-add! (phase-vocoder-phases gen) (phase-vocoder-phase-increments gen)) (let ((sum 0.0) (n (length (phase-vocoder-amps gen)))) (do ((k 0 (+ k 1))) ((= k n)) (set! sum (+ sum (* (float-vector-ref (phase-vocoder-amps gen) k) (sin (* 0.5 (float-vector-ref (phase-vocoder-phases gen) k))))))) sum))))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (phase-vocoder pv)))))
but, sadly, this code crawls. It won't actually be useful until I optimize handling of the caller's resynthesis function, but I am dragging my feet because I've never felt that this phase-vocoder (as a generator) was the "right thing". The first step toward something less stupid is moving-spectrum in generators.scm.
make-asymmetric-fm (frequency 0.0) (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 (fm 0.0) asymmetric-fm? af
asymmetric-fm methods | |
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 |
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. asymmetric-fm is based on:
(with-sound (:play #t) (let ((fm (make-asymmetric-fm 440.0 0.0 0.9 0.5))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (* 0.5 (asymmetric-fm fm 1.0)))))) |
with_sound(:play, true) do fm = make_asymmetric_fm(440.0, 0.0, 0.9, 0.5); 44100.times do |i| outa(i, 0.5 * asymmetric_fm(fm, 1.0), $output); end end.output |
lambda: ( -- ) 440.0 0.0 0.9 0.5 make-asymmetric-fm { fm } 44100 0 do i fm 1.0 0 asymmetric-fm f2/ *output* outa drop loop ; :play #t with-sound drop |
"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 (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 (+ i 1))) ((= 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 generators.scm.
frample->frample mf inf outf | pass frample through a matrix multiply, return outf |
Sound file IO is based on a set of file readers and writers that deal either in samples or float-vectors. The six functions are file->sample, sample->file, file->frample, frample->file, array->file, and file->array. The name "array" is used here, rather than "float-vector" 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 (buffer-size 8192) make-sample->file name (chans 1) (format mus-lfloat) (type mus-next) comment file->sample? obj sample->file? obj file->sample obj samp chan sample->file obj samp chan val continue-sample->file file make-file->frample name (buffer-size 8192) make-frample->file name (chans 1) (format mus-lfloat) (type mus-next) comment frample->file? obj file->frample? obj file->frample obj samp outf frample->file obj samp val continue-frample->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 (also known as *clm-file-buffer-size*)
(with-sound (:channels 2) ;; swap channels of stereo file (let ((input (make-file->frample "stereo.snd")) (len (mus-sound-framples "stereo.snd")) (frample (make-float-vector 2))) (do ((i 0 (+ i 1))) ((= i len)) (file->frample input i frample) (let ((val (frample 0))) (set! (frample 0) (frample 1)) (set! (frample 1) val)) (frample->file *output* i frample)))) |
with_sound(:channels, 2) do input = make_file2frample("stereo.snd"); len = mus_sound_framples("stereo.snd"); frample = make_frample(2); len.times do |i| file2frample(input, i, frample); val = frample_ref(frample, 0); frample_set!(frample, 0, frample_ref(frample, 1)); frample_set!(frample, 1, val); frample2file($output, i, frample); end end.output |
lambda: ( -- ) "stereo.snd" make-file->frample { input } 2 make-frample { frm } "stereo.snd" mus-sound-framples ( len ) 0 do input i frm file->frample ( frm ) 1 frample-ref ( val1 ) frm 0 frample-ref ( val0 ) frm 1 rot frample-set! drop ( val1 ) frm 0 rot frample-set! drop *output* i frm frample->file drop loop ; :channels 2 :play #t with-sound drop |
file->sample writes a sample to a file, frample->file writes a frample, file->sample reads a sample from a file, and file->frample reads a frample. continue-frample->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. In make-file->sample and make-file->frample, the buffer-size defaults to *clm-file-buffer-size*. There are many examples of these functions in snd-test.scm, and clm-ins.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))) (do ((ctr 0) (i start (+ i 1))) ((= i end)) (out-any i (* amp (file->sample fil ctr 0)) 0) (set! ctr (+ 1 ctr)))))
mus-close flushes any pending output and closes the output stream 'obj'. This is normally done for you by with-sound, but if you have your own output streams, and you forget to call mus-close, the GC will eventually do it for you.
make-readin file (channel 0) (start 0) (direction 1) size readin rd readin? rd
readin methods | |
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 framples 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 frample 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 *clm-file-buffer-size*.
(with-sound (:play #t) (let ((reader (make-readin "oboe.snd"))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (* 2.0 (readin reader)))))) |
with_sound(:play, true) do reader = make_readin("oboe.snd"); 44100.times do |i| outa(i, 2.0 * readin(reader), $output); end end.output |
lambda: ( -- ) "oboe.snd" make-readin { reader } 44100 0 do i reader readin f2/ *output* outa drop loop ; :play #t with-sound drop |
Here is an instrument that applies an envelope to a sound file using readin and env:
(definstrument (env-sound file beg (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)))) (do ((i st (+ i 1))) ((= 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)))
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*) out-bank gens loc input 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 vector as well as the more usual frample->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. It is not an error to try to write to a channel that doesn't exist; the function just returns.
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->frample object, or a vector.
(with-sound (:play #t) (let ((infile (make-file->sample "oboe.snd"))) (do ((i 0 (+ i 1))) ((= i 44100)) (out-any i (in-any i 0 infile) 0)))) |
with_sound(:play, true) do infile = make_file2sample("oboe.snd"); 44100.times do |i| out_any(i, in_any(i, 0, infile), 0, $output); end end.output |
lambda: ( -- ) "oboe.snd" make-file->sample { infile } 44100 0 do i i 0 infile in-any 0 *output* out-any drop loop ; :play #t with-sound drop |
(definstrument (simple-ina beg dur amp file) (let* ((start (seconds->samples beg)) (end (+ start (seconds->samples dur))) (fil (make-file->sample file))) (do ((i start (+ i 1))) ((= 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 vector, rather than a file, use its :output argument:
(with-sound (:output (make-float-vector 44100)) ; this sets *output*, the default output location (fm-violin 0 1 440 .1))
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 (frample val chan) ; get the average of all the samples (set! avg (+ avg val)) (set! samps (+ 1 samps)) val)) (do ((i 0 (+ i 1))) ((> 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 ((make-float-vector 10)) (do ((i 0 (+ i 1))) ((= i 10)) (outa i (ina i (lambda (loc chn) (readin input)))))))
(let ((outv (make-float-vector 10))) (with-sound () (do ((i 0 (+ i 1))) ((= i 10)) (outa i (* i .1) (lambda (loc val chan) (set! (outv loc) val))))) outv) ; this is equivalent to using :output (make-float-vector 10) as a with-sound argument
make-locsig (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 (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 ()
locsig methods | |
mus-data | output scalers (a float-vector) |
mus-xcoeff | reverb scaler |
mus-xcoeffs | reverb scalers (a float-vector) |
mus-channels | output channels |
mus-length | output channels |
locsig places a sound in
an N-channel circle of speakers
by scaling the respective channel amplitudes
("that old trick never works"). It normally replaces out-any.
"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 vector as well as a frample->file generator.
(with-sound (:play #t :channels 2) (let ((loc (make-locsig 60.0)) (osc (make-oscil 440.0))) (do ((i 0 (+ i 1))) ((= i 44100)) (locsig loc i (* 0.5 (oscil osc)))))) |
with_sound(:play, true, :channels, 2) do loc = make_locsig(60.0, :output, $output); osc = make_oscil(440.0); 44100.times do |i| locsig(loc, i, 0.5 * oscil(osc)); end end.output |
lambda: ( -- ) 60.0 make-locsig { loc } 440.0 make-oscil { osc } 44100 0 do loc i osc 0 0 oscil f2/ locsig drop loop ; :play #t :channels 2 with-sound drop |
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! to override the placement decisions. To have full output to both channels,
(locsig-set! loc 0 1.0) (locsig-set! 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 (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)) (do ((i beg (+ i 1))) ((= i end)) (let ((rdval (* (readin rdA) (env amp-env))) (degval (env deg-env))) (set! dist-scaler (/ (env dist-env))) (locsig-set! loc 0 (* (- 1.0 degval) dist-scaler)) (if (> (channels *output*) 1) (locsig-set! loc 1 (* degval dist-scaler))) (if *reverb* (locsig-reverb-set! 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, Fernando Lopez Lezcano's dlocsig, or flocsig (flanged locsig) in generators.scm. Here is an example of move-locsig:
(with-sound (:channels 4) (let ((loc (make-locsig)) (osc (make-oscil 440.0)) (j 0)) (do ((i 0 (+ i 1))) ((= i 360)) (do ((k 0 (+ k 1))) ((= k 1000)) (let ((sig (* .5 (oscil osc)))) (locsig loc j sig) (set! j (+ j 1)))) (move-locsig loc (* 1.0 i) 1.0))))
![]() |
![]() |
linear interp | sinusoidal interp |
The interaction of outa, locsig, and *reverb* seems to be causing confusion, so here are some simple examples:
(load "nrev.scm") (define (simp start end freq amp) (let ((os (make-oscil freq))) (do ((i start (+ i 1))) ((= i end)) (let ((output (* amp (oscil os)))) (outa i output) (if *reverb* (outa i (* output .1) *reverb*)))))) ; (with-sound () (simp 0 44100 440 .1)) ; no reverb ; (with-sound (:reverb nrev) (simp 0 44100 440 .1)); reverb (define (locsimp start end freq amp) (let ((os (make-oscil freq)) (loc (make-locsig :reverb .1))) (do ((i start (+ i 1))) ((= i end)) (locsig loc i (* amp (oscil os)))))) ; (with-sound () (locsimp 0 44100 440 .1)) ; no reverb ; (with-sound (:reverb nrev) (locsimp 0 44100 440 .1)); reverb
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) (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))))) (do ((i start (+ i 1))) ((= i end)) (move-sound loc i (* amp (oscil os)))))) (with-sound (:channels 4) (simple-dloc 0 2 440 .5))
Besides the 30 or so built-in generators, there are around 100 others defined in generators.scm. If we required separate functions for each generator for access to the generator internal state (current phase, for example), we'd end up with hundreds, or even thousands of accessors. Instead, all the generators respond to a set of "generic" functions. mus-frequency, for example, tries to return (or set) a generator's frequency, for any generator that has some sort of frequency field. The generic functions are:
mus-channel | channel being read/written |
mus-channels | channels open |
mus-copy | copy a generator |
mus-data | float-vector of data |
mus-describe | description of current state |
mus-feedback | feedback coefficient |
mus-feedforward | feedforward coefficient |
mus-file-name | file being read/written |
mus-frequency | frequency (Hz) |
mus-hop | hop size for block processing |
mus-increment | various increments |
mus-interp-type | interpolation type (mus-interp-linear, etc) |
mus-length | data length |
mus-location | sample location for reads/writes |
mus-name | generator name ("oscil") |
mus-offset | envelope offset |
mus-order | filter order |
mus-phase | phase (radians) |
mus-ramp | granulate grain envelope ramp setting |
mus-reset | set gen to default starting state |
mus-run | run any generator |
mus-scaler | scaler, normally on an amplitude |
mus-width | width of interpolation tables, etc |
mus-xcoeff | x (input) coefficient |
mus-xcoeffs | float-vector of x (input) coefficients |
mus-ycoeff | y (output, feedback) coefficient |
mus-ycoeffs | float-vector of y (feedback) coefficients |
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))) (do ((i 0 (+ i 1))) ((= 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 defgenerator can also take part in these methods.
(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 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 moog.scm: moog-filter prc95.scm: reed, bowtable, jettable, onep, lip, dc-block, delaya, delayl 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-polyoid (frequency 0.0) (partial-amps-and-phases '(1 1 0.0)) ; a list of harmonic numbers, their associated amplitudes, and their initial-phases polyoid w (fm 0.0) polyoid? w polyoid-env w fm amps phases make-noid (frequency 0.0) (n 1) phases noid w (fm 0.0)
polyoid combines the first and second Chebyshev polynomials to provide
a sum of sinusoids each with arbitrary amplitude and initial-phase.
noid is a wrapper for polyoid that sets up n equal amplitude components, a generalization
of ncos and nsin.
noid's phase argument can be a float-vector, 'min-peak
, 'max-peak
, or omitted (#f).
If omitted, the phases are set to random numbers between 0 and 2 pi; if
a float-vector, the float-vector's values are used as the phases; if 'max-peak, all phases are set
to pi/2 (ncos essentially — use (make-float-vector n)
to get nsin);
and if 'min-peak, the minimum peak amplitude phases in peak-phases.scm are used.
In the 'min-peak and 'max-peak cases, noid's output is normalized to fall between -1.0 and 1.0.
polyoid-env is an extension of polyoid that takes envelopes to control the amplitude and phase of each
harmonic.
We can use the peak-phases.scm phases to reduce the "spikiness" of the waveform with any set of components and component amplitudes. We could, for example, change noid to use
(set! (amps (+ j 1)) (/ (expt r (- i 1)) norm))
where "r" is the ratio between successive component amplitude: "nroid"? This is not as pointless as it might at first appear. Many of these waveforms actually sound different, despite having the same (magnitude) spectrum; the minimum peak version usually sounds raspier, and in the limit it can sound like white noise!
Check out the n=1024 case:
(with-sound () (let ((samps 44100) (gen (make-noid 10.0 1024 'min-peak))) (do ((i 0 (+ i 1))) ((= i samps)) (outa i (* 0.5 (noid gen 0.0))))))
make-asyfm (frequency 0.0) (ratio 1.0) (r 1.0) (index 1.0) asyfm-J gen (fm 0.0) asyfm-I gen (fm 0.0) 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 (frequency 0.0) (ratio 1.0) (index 1.0) fmssb gen (fm 0.0) 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).
make-blackman frequency n ; 1 <= n <= 10 blackman gen (fm 0.0) blackman? gen
This produces a Blackman-Harris sum of cosines of order 'n'. It could be viewed as a special case of pulsed-env, or as yet another "kernel" along the lines of ncos.
make-sinc-train frequency (n 1) sinc-train gen (fm 0.0) sinc-train? gen
This produces a sinc-train ((sin x)/x) with n components. It is very similar to ncos.
make-pink-noise (n 1) pink-noise gen pink-noise? gen
This produces a reasonable approximation to 1/f noise, also known as pink-noise. 'n' sets the number of octaves used (starting at the high end); 12 is the recommended choice. (If n=1, you get white noise).
make-brown-noise frequency (amplitude 1.0) brown-noise gen brown-noise? gen
This produces (unbounded) brownian noise. 'amplitude' sets the maximum size of individual jumps.
make-green-noise (frequency 0.0) (amplitude 1.0) (low -1.0) (high 1.0) green-noise gen (fm 0.0) green-noise? gen make-green-noise-interp (frequency 0.0) (amplitude 1.0) (low -1.0) (high 1.0) green-noise-interp gen (fm 0.0) green-noise-interp? gen
These two generators produce bounded brownian noise; "green-noise" was Michael McNabb's name for it. Unlike CLM's rand or rand-interp which produce white noise centered around 0.0, green-noise wanders around, bouncing off its bounds every now and then. This produces a noise that can be similar to pink noise (see some graphs under rand). My informal explanation is that each time we bounce off an edge, we're transferring energy from a low frequency into some higher frequency. It is still brownian noise however. The 'amplitude' argument controls how large individual steps can be; 'low' and 'high' set the overall output bounds; 'frequency' controls how often a new random number is chosen. Here's an instrument that fuzzes up its amplitude envelope a bit using green noise:
(definstrument (green3 start dur freq amp amp-env noise-freq noise-width noise-max-step) (let* ((grn (make-green-noise-interp :frequency noise-freq :amplitude noise-max-step :high (* 0.5 noise-width) :low (* -0.5 noise-width))) (osc (make-oscil freq)) (e (make-env amp-env :scaler amp :duration dur)) (beg (seconds->samples start)) (end (+ beg (seconds->samples dur)))) (do ((i beg (+ i 1))) ((= i end)) (outa i (* (env e) (+ 1.0 (green-noise-interp grn)) (oscil osc)))))) (with-sound () (green3 0 2.0 440 .5 '(0 0 1 1 2 1 3 0) 100 .2 .02))
make-adjustable-square-wave frequency (duty-factor 0.5) (amplitude 1.0) adjustable-square-wave gen (fm 0.0) adjustable-square-wave? gen make-adjustable-triangle-wave frequency (duty-factor 0.5) (amplitude 1.0) adjustable-triangle-wave gen (fm 0.0) adjustable-triangle-wave? gen make-adjustable-sawtooth-wave frequency (duty-factor 0.5) (amplitude 1.0) adjustable-sawtooth-wave gen (fm 0.0) 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.
A similar trick can make, for example, a squared-off triangle-wave:
(gen (make-triangle-wave 200.0 :amplitude 4)) ; amp sets slope ... (outa i (max -1.0 (min 1.0 (triangle-wave gen))))
make-round-interp frequency n amplitude round-interp gen (fm 0.0) 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.
make-moving-sum (n 128) moving-sum gen y moving-sum? gen make-moving-rms (n 128) moving-rms gen y moving-rms? gen make-moving-length (n 128) moving-length gen y moving-length? gen make-weighted-moving-average n weighted-moving-average gen y weighted-moving-average? gen make-exponentially-weighted-moving-average n exponentially-weighted-moving-average gen y exponentially-weighted-moving-average? gen
The "moving" generators are specializations of the moving-average generator. moving-sum keeps the ongoing sum of absolute values, moving-length the square root of the sum of squares, and moving-rms the square root of the sum of squares divided by the size. moving-rms is used in overlay-rms-env in draw.scm. weighted-moving-average weights the table entries by 1/n. Similarly exponentially-weighted-moving-average applies exponential weights (it is actually just a one-pole filter — this generator wins the "longest-name-for-simplest-effect" award). Also defined, but not tested, is moving-variance; in the same mold, but not defined, are things like moving-inner-product and moving-distance.
make-bess (frequency 0.0) (n 0) bess gen (fm 0.0) 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):
Except for jpcos, 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 (frequency 0.0) (index 1.0) j0evencos gen (fm 0.0) 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.
(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 (+ i 1))) ((= i end)) (let ((ind (env indf)) (vol (env ampf))) (set! (jmd 'index) 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)))))))
make-j0j1cos (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 (frequency 0.0) (r 1.0) izcos gen (fm 0.0) izcos? gen
This produces a sum of cosines scaled by In(r), again very similar to normal FM.
make-jjcos (frequency 0.0) (r 0.5) (a 1.0) (k 1.0) jjcos gen (fm 0.0) jjcos? gen make-j2cos (frequency 0.0) (r 0.5) (n 1) j2cos gen (fm 0.0) j2cos? gen make-jpcos (frequency 0.0) (r 0.5) (a 0.0) (k 1.0) jpcos gen (fm 0.0) jpcos? gen make-jncos (frequency 0.0) (r 0.5) (a 1.0) (n 0) jncos gen (fm 0.0) 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 handled correctly at least for jpcos. Of the four, jpcos seems the most interesting. "a" should not equal "r"; in general as a and r approach 1.0, the spectrum approaches "k" components, sometimes in a highly convoluted manner.
jjcos: | ![]() |
j2cos: | ![]() |
jpcos: | ![]() |
jncos: | ![]() |
make-jycos (frequency 0.0) (r 1.0) (a 1.0) jycos gen (fm 0.0) 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 an inf or a NaN from division by zero or whatever in Scheme, both the time and frequency graphs will be unhappy).
These generators produce a set of n sinusoids. With a bit of bother, they could be done with polywave. I don't think there would be any difference, even taking FM into account.
make-nssb (frequency 0.0) (ratio 1.0) (n 1) nssb gen (fm 0.0) nssb? gen
nssb is the single side-band version of ncos and nsin. It is very similar to nxysin and nxycos.
make-ncos2 (frequency 0.0) (n 1) ncos2 gen (fm 0.0) ncos2? gen
This is the Fejer kernel. The i-th harmonic amplitude is (n-i)/(n+1).
make-ncos4 (frequency 0.0) (n 1) ncos4 gen (fm 0.0) ncos4? gen
This is the Jackson kernel, the square of ncos2.
make-npcos (frequency 0.0) (n 1) npcos gen (fm 0.0) 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 (frequency 0.0) (n 1) n1cos gen (fm 0.0) n1cos? gen
Another spikey waveform, very similar to ncos2 above.
make-nxycos (frequency 0.0) (ratio 1.0) (n 1) nxycos gen (fm 0.0) nxycos? gen make-nxysin (frequency 0.0) (ratio 1.0) (n 1) nxysin gen (fm 0.0) nxysin? gen make-nxy1cos (frequency 0.0) (ratio 1.0) (n 1) nxy1cos gen (fm 0.0) nxy1cos? gen make-nxy1sin (frequency 0.0) (ratio 1.0) (n 1) nxy1sin gen (fm 0.0) 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. nxy1cos is the same as nxycos, but every other component is multiplied by -1, and "n" produces 2n components. Normalization in the "sin" cases is tricky. If ratio is 1, we can use nsin's normalization, and if ratio = 2, noddsin's, but otherwise nxysin currently uses 1/n. This ensures that the generator output is always between -1 and 1, but in some cases (mainly involving low "n" and simple "ratio"), the output might not be full amplitude. nxy1sin is even trickier, so it divides by "n".
make-noddcos (frequency 0.0) (n 1) noddcos gen (fm 0.0) noddcos? gen make-noddsin (frequency 0.0) (n 1) noddsin gen (fm 0.0) noddsin? gen make-noddssb (frequency 0.0) (ratio 1.0) (n 1) noddssb gen (fm 0.0) 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))) (do ((i 0 (+ i 1))) ((= i 40000)) (outa i (* (env ampf) (noddsin gen))))))
noddsin normalization is the same as nsin. The peak happens half as far from the 0 crossing as in nsin (3pi/(4n) for nsin, and 3pi/(8n) for noddsin (assuming large n)), and its amplitude is 8n*sin^2(3pi/8)/(3pi), just as in nsin. The noddsin generator scales its output by the inverse of this, so it is always between -1 and 1.
make-nrcos (frequency 0.0) (n 1) (r 0.5) ; -1.0 < r < 1.0 nrcos gen (fm 0.0) nrcos? gen make-nrsin (frequency 0.0) (n 1) (r 0.5) ; -1.0 < r < 1.0 nrsin gen (fm 0.0) nrsin? gen make-nrssb (frequency 0.0) (ratio 1.0) (n 1) (r 0.5) ; 0.0 <= r < 1.0 nrssb gen (fm 0.0) 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 (+ i 1))) ((= i 10)) (lutish (* i .1) 2 (* 100 (+ i 1)) .05)))
The instrument oboish uses nrssb: oboish beg dur freq amp amp-env
:
(with-sound (:play #t) (do ((i 0 (+ i 1))) ((= 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 (+ i 1))) ((= i 10)) (organish (* i .3) .4 (+ 100 (* 50 i)) .5 1.0 #f)))
make-nkssb (frequency 0.0) (ratio 1.0) (n 1) nkssb gen (fm 0.0) 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 (frequency 0.0) (n 1) nsincos gen (fm 0.0) nsincos? gen
This generator produces a sum of n cosines scaled by sin(k*pi/(n+1))/sin(pi/(n+1)).
make-nchoosekcos (frequency 0.0) (ratio 1.0) (n 1) nchoosekcos gen (fm 0.0) nchoosekcos? gen
This generator produces a sum of n cosines scaled by the binomial coefficients. If n is even, the last term is halved. All these "finite sum" generators are a bit inflexible, and sound more or less the same. One (desperate) countermeasure is amplitude modulation:
(with-sound () (let ((modulator (make-nchoosekcos 100.0 1.0 4)) (carrier (make-nrcos 2000.0 :n 3 :r .5))) (do ((i 0 (+ i 1))) ((= i 20000)) (outa i (* .5 (nrcos carrier) (nchoosekcos modulator))))))
make-rcos (frequency 0.0) (r 0.5) ; -1.0 < r < 1.0 rcos gen (fm 0.0) rcos? gen make-rssb (frequency 0.0) (ratio 1.0) (r 0.5) ; -1.0 < r < 1.0 rssb gen (fm 0.0) rssb-interp gen fm interp rssb? gen make-rxycos (frequency 0.0) (ratio 1.0) (r 0.5) ; -1.0 < r < 1.0 rxycos gen (fm 0.0) rxycos? gen make-rxysin (frequency 0.0) (ratio 1.0) (r 0.5) ; -1.0 < r < 1.0 rxysin gen (fm 0.0) 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 (+ k 1))) ((= k 10)) (bump (* 0.4 k) 1 (* 16.3 (expt 2.0 (+ 3 (/ k 12)))) .5 520 1190 2390)) (do ((k 0 (+ k 1))) ((= 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) ; the safe-rxycos generator in generators.scm has this built-in (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:
(with-sound (:channels 2) (let ((gen1 (make-rcos 1050 0.99)) ;; r=.6 or so is the safe max (gen2 (make-rcos 1048 0.99))) (do ((i 0 (+ i 1))) ((= i 88200)) (outa i (rcos gen1)) (outb i (rcos gen2)))))
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 (frequency 0.0) (r 0.5) ; r > 0.0 ercos gen (fm 0.0) ercos? gen make-erssb (frequency 0.0) (ratio 1.0) (r 0.5) erssb gen (fm 0.0) 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 (frequency 0.0) (r 0.5) eoddcos gen (fm 0.0) 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))) (do ((i 0 (+ i 1))) ((= i 10000)) (set! (gen1 'r) (env a-env)) (outa i (* .5 (eoddcos gen1 (* .1 (oscil gen2))))))))
make-rkcos (frequency 0.0) (r 0.5) ; -1.0 < r < 1.0 rkcos gen (fm 0.0) rkcos? gen make-rksin (frequency 0.0) (r 0.5) ; -1.0 < r < 1.0 rksin gen (fm 0.0) rksin? gen make-rkssb (frequency 0.0) (ratio 1.0) (r 0.5) ; -1.0 < r < 1.0 rkssb gen (fm 0.0) 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))))) (min 0.999999 (expt (* .001 topk) (/ 1.0 topk)))))
Similar to rkcos is (expt (asin (sqrt (oscil x))) 2). 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 (+ i 1))) ((= i 100000)) (outa i (* .95 (rkcos gen1))) (outb i (* .95 (rksin gen2)))))) > (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 (+ i 1))) ((= i 50000)) (outa i (moving-average flt1 (adjustable-square-wave gen1))) (outb i (moving-average flt2 (adjustable-square-wave gen2))))))
Since clipping is a disaster, we focus on peak amplitudes in the generators.
make-rk!cos (frequency 0.0) (r 0.5) ; rk!cos is a special case of rxyk!cos rk!cos gen (fm 0.0) rk!cos? gen make-rk!ssb (frequency 0.0) (ratio 1.0) (r 0.5) rk!ssb gen (fm 0.0) rk!ssb? gen make-rxyk!cos (frequency 0.0) (ratio 1.0) (r 0.5) rxyk!cos gen (fm 0.0) rxyk!cos? gen make-rxyk!sin (frequency 0.0) (ratio 1.0) (r 0.5) rxyk!sin gen (fm 0.0) rxyk!sin? gen
These produce a sum of sinusoids scaled by (r^k)/k!. The k! denominator dominates eventually, so r * ratio * frequency is approximately the spectral center (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, in the graph on the right, the frequency is 100 and r is 30, so 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. You can sweep r through 0 smoothly except in rk!cos where you'll get a click. Coupled with the fm argument, these generators provide an extension of multi-carrier FM, similar in effect to the "leap-frog" FM voice. Here is a use of rk!cos to make a bird twitter:
(with-sound (:play #t :scaled-to .5) (do ((k 0 (+ k 1))) ((= 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))) (do ((i 0 (+ i 1))) ((= i 3000)) (outa (+ i (* k 4000)) (* (env ampf) (rk!cos gen (env frqf))))))))
The instrument bouncy uses rk!ssb: bouncy beg dur freq amp (bounce-freq 5) (bounce-amp 20)
(with-sound (:play #t) (bouncy 0 2 200 .5 3 2))
brassy (also in generators.scm) uses rxyk!cos, but it is more of an experiment with envelopes than spectra. It takes a gliss envelope and turns it into a series of quick jumps between harmonics, handling both the pitch and the index ("r") of the rxyk!cos generator. The effect is vaguely brass-like.
make-r2k!cos (frequency 0.0) (r 0.5) (k 0.0) r2k!cos gen (fm 0.0) r2k!cos? gen
This generator produces a sum of cosines with a complicated-looking formula for the component amplitudes. It's actually pretty simple, as this graph shows. The "F" notation stands for a hypergeometric series, a generalization of sinusoids and Bessel functions.
Negative "r" gives the same output as the corresponding positive "r", and
there is sometimes a lot of DC. Despite appearances, as r increases beyond 1.0,
the spectrum collapses back towards the fundamental. (I think that r and 1/r produce the same spectrum).
Aliasing can be a problem,
especially when r is close to 1.
The instruments pianoy and pianoy1 use r2k!cos: pianoy beg dur freq amp
, and
pianoy1 beg dur freq amp (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-rkoddssb (frequency 0.0) (ratio 1.0) (r 0.5) ; -1.0 < r < 1.0 rkoddssb gen (fm 0.0) rkoddssb? gen
This produces a sum of odd-numbered harmonics scaled by (r^(2k-1))/(2k-1). This kind of spectrum is usually called "clarinet-like". Negative r gives the same output as positive. The (not very) safe maximum r is:
(define (safe-rkodd-max-r freq srate) (let ((k2-1 (- (* 2 (floor (/ srate 3 freq))) 1))) (expt (* .001 k2-1) (/ 1.0 k2-1))))
The instrument stringy uses rkoddssb and rcos: stringy beg dur freq amp
:
(with-sound (:play #t) (do ((i 0 (+ i 1))) ((= 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 (+ i 1))) ((= i 10)) (glassy (* i .3) .1 (+ 400 (* 100 i)) .5)))
make-k2sin (frequency 0.0) k2sin gen (fm 0.0) k2sin? gen make-k2cos (frequency 0.0) k2cos gen (fm 0.0) k2cos? gen make-k2ssb (frequency 0.0) (ratio 1.0) k2ssb gen (fm 0.0) k2ssb? gen
These produce a sum of sinusoids scaled by 1/(2^k).
make-k3sin (frequency 0.0) k3sin gen fm k3sin? gen
This produces a sum of sines scaled by 1.0/(k^3).
make-krksin (frequency 0.0) (r 0.5) krksin gen (fm 0.0) krksin? gen
This produces a sum of sinusoids scaled by kr^k. Its output is not normalized. I think the formula given assumes that r is less than 1.0, and in that case, the safe maximum r is given by:
(define (safe-krk-max-r freq srate) (let ((topk (floor (/ srate 3 freq)))) (expt (/ .001 topk) (/ 1.0 topk))))
However, r can be greater than 1.0 without causing any trouble, and behaves in that case much like r2k!cos — as it increases, the spectrum collapses; I think r in that case is equivalent to 1/r. The only value to avoid is 1.0.
make-abcos (frequency 0.0) (a 0.5) (b 0.25) abcos gen (fm 0.0) abcos? gen make-absin (frequency 0.0) (a 0.5) (b 0.25) absin gen (fm 0.0) absin? gen
These produce a sum of sinusoids scaled as follows:
make-r2k2cos (frequency 0.0) (r 0.5) r2k2cos gen (fm 0.0) r2k2cos? gen
This produces a sum of cosines, each scaled by 1/(r^2+k^2). r shouldn't be 0, but otherwise it almost doesn't matter what it is — this is not a very flexible generator!
There are a dozen or so other generators defined in generators.scm, but most are close variants of those given above.
make-tanhsin (frequency 0.0) (r 1.0) (initial-phase 0.0) tanhsin gen (fm 0.0) tanhsin? gen
This produces tanh(r * sin(x)) which approaches a square wave as "r" increases.
make-moving-fft (input #f) (n 512) (hop 128) moving-fft gen moving-fft? gen
moving-fft provides a sample-by-sample FFT (magnitudes and phases) of its input (currently assumed to be a readin generator). mus-xcoeffs returns the magnitudes, mus-ycoeffs returns the phases, and mus-data returns the current input block. We could mimic the fft display window in the "lisp graph" via:
(let ((ft (make-moving-fft (make-readin "oboe.snd"))) (data (make-float-vector 256))) (set! (lisp-graph?) #t) (do ((i 0 (+ i 1))) ((= i 10000)) (moving-fft ft) (float-vector-subseq (mus-xcoeffs ft) 0 255 data) (graph data "fft" 0.0 11025.0 0.0 0.1 0 0 #t)))
make-moving-spectrum (input #f) (n 512) (hop 128) moving-spectrum gen moving-spectrum? gen
moving-spectrum provides a sample-by-sample spectrum (amplitudes, frequencies, and current phases) of its input (currently assumed to be a readin generator). It is identical to the first (analysis) portion of the phase-vocoder generator (see test-sv in generators.scm for details). To access the current amps and so on, use (gen 'amps), (gen 'phases), and (gen 'freqs).
make-moving-autocorrelation (input #f) (n 512) (hop 128) moving-autocorrelation gen moving-autocorrelation? gen
moving-autocorrelation provides the autocorrelation of the last 'n' samples every 'hop' samples. The samples come from 'input' (currently assumed to be a readin generator). The output is accessible via mus-data.
make-moving-pitch (input #f) (n 512) (hop 128) moving-pitch gen moving-pitch? gen
moving-pitch provides the current pitch of its input, recalculated (via moving-autocorrelation) every 'hop' samples.
(let ((rd (make-readin "oboe.snd")) (cur-srate (srate "oboe.snd"))) (let-temporarily ((*clm-srate* cur-srate)) (let ((scn (make-moving-pitch rd)) (last-pitch 0.0) (pitch 0.0)) (do ((i 0 (+ i 1))) ((= i 22050)) (set! last-pitch pitch) (set! pitch (moving-pitch scn)) (if (not (= last-pitch pitch)) (format () "~A: ~A~%" (* 1.0 (/ i cur-srate)) pitch))))))
make-moving-scentroid (dbfloor -40.0) (rfreq 100.0) (size 4096) moving-scentroid gen moving-scentroid? gen
moving-scentroid provides a generator that mimics Bret Battey's scentroid instrument (in dsp.scm or scentroid.ins).
make-flocsig (reverb-amount 0.0) (frequency 1.0) (amplitude 2.0) offset flocsig gen i val flocsig? gen
flocsig is a version of locsig that adds changing delays between the channels (flanging). The delay amount is set by a rand-interp centered around 'offset', moving as many as 'amplitude' samples (this also affects signal placement), and moving at a speed set by 'frequency'. Currently flocsig assumes stereo output and stereo reverb output. This generator is trying to open up the space in the same manner that flanging does, but hopefully unobtrusively. Here is an example, including a stereo reverb:
(definstrument (jcrev2) (let* ((allpass11 (make-all-pass -0.700 0.700 1051)) (allpass21 (make-all-pass -0.700 0.700 337)) (allpass31 (make-all-pass -0.700 0.700 113)) (comb11 (make-comb 0.742 4799)) (comb21 (make-comb 0.733 4999)) (comb31 (make-comb 0.715 5399)) (comb41 (make-comb 0.697 5801)) (outdel11 (make-delay (seconds->samples .01))) (allpass12 (make-all-pass -0.700 0.700 1051)) (allpass22 (make-all-pass -0.700 0.700 337)) (allpass32 (make-all-pass -0.700 0.700 113)) (comb12 (make-comb 0.742 4799)) (comb22 (make-comb 0.733 4999)) (comb32 (make-comb 0.715 5399)) (comb42 (make-comb 0.697 5801)) (outdel12 (make-delay (seconds->samples .01))) (len (floor (+ *clm-srate* (framples *reverb*))))) (do ((i 0 (+ i 1))) ((= i len)) (let* ((allpass-sum (all-pass allpass31 (all-pass allpass21 (all-pass allpass11 (ina i *reverb*))))) (comb-sum (+ (comb comb11 allpass-sum) (comb comb21 allpass-sum) (comb comb31 allpass-sum) (comb comb41 allpass-sum)))) (outa i (delay outdel11 comb-sum))) (let* ((allpass-sum (all-pass allpass32 (all-pass allpass22 (all-pass allpass12 (inb i *reverb*))))) (comb-sum (+ (comb comb12 allpass-sum) (comb comb22 allpass-sum) (comb comb32 allpass-sum) (comb comb42 allpass-sum)))) (outb i (delay outdel12 comb-sum)))))) (definstrument (simp beg dur (amp 0.5) (freq 440.0) (ramp 2.0) (rfreq 1.0) offset) (let* ((os (make-pulse-train freq)) (floc (make-flocsig :reverb-amount 0.1 :frequency rfreq :amplitude ramp :offset offset)) (start (seconds->samples beg)) (end (+ start (seconds->samples dur)))) (do ((i start (+ i 1))) ((= i end)) (flocsig floc i (* amp (pulse-train os)))))) (with-sound (:channels 2 :reverb-channels 2 :reverb jcrev2) (simp 0 1))
defgenerator name fields
defgenerator defines a generator. Its syntax is modelled after Common Lisp's defstruct. It sets up a structure, an environment with slots that you can get and set. It also defines a "make" function to create an instance of the environment, and a predicate for it. Here is a way to define oscil using defgenerator:
(defgenerator osc freq phase) ;;; make-osc creates an osc, and osc? returns #t if passed an osc. ;;; Once we have an osc (an environment with "freq" and "phase" locals) ;;; we can either use with-let, or refer to the local variables ;;; directly via (gen 'freq) and (gen 'phase). (define (osc gen fm) ; our new generator (let ((result (sin (gen 'phase)))) (set! (gen 'phase) (+ (gen 'phase) (gen 'freq) fm)) result)) ;;; now we can use the osc generator in an instrument: (definstrument (osc-fm beg dur freq amp mc-ratio fm-index) (let* ((start (seconds->samples beg)) (end (+ start (seconds->samples dur))) (carrier (make-osc (hz->radians freq))) (modulator (make-osc (hz->radians (* mc-ratio freq)))) (index (hz->radians (* freq mc-ratio fm-index)))) (do ((i start (+ i 1))) ((= i end)) (outa i (* amp (osc carrier (* index (osc modulator 0.0)))))))) (with-sound () (osc-fm 0 1 440 .1 1 1))
The first argument to defgenerator is the new object's name, and the rest are the fields of that object. Each field has a name and an optional initial value which defaults to 0.0. The "make" function (make-osc in our example) uses define* with the field names and initial values as the optional keys. So make-osc above is declared (by the defgenerator macro) as:
(define* (make-osc (freq 0.0) (phase 0.0)) ...)
which we can invoke in various ways, e.g.:
(make-osc 440) (make-osc :phase (/ pi 2) :freq 440) (make-osc 440 :phase 0.0)
The defgenerator "name" parameter can also be a list; in this case the first element is the actual generator name. The
next elements are :make-wrapper
followed by a function of one argument
(the default object normally returned by defgenerator), and :methods
, followed
by a list of the methods the generator responds to. The make wrapper function can
make any changes it pleases, then return the fixed-up generator. For example, in our
"osc" generator, we had to remember to change frequency in Hz to radians; we can use the
wrapper to handle that:
(defgenerator (osc :make-wrapper (lambda (gen) (set! (gen 'freq) (hz->radians (gen 'freq))) gen)) (freq 0.0) (phase 0.0))
and now the make process in the instrument can be simplified to:
... (carrier (make-osc freq)) (modulator (make-osc (* mc-ratio freq))) ...
If you want the struct to take part in the generic function facility in CLM, add the desired methods as an association list with the keyword :methods:
(defgenerator (osc :make-wrapper (lambda (gen) (set! (gen 'freq) (hz->radians (gen 'freq))) gen) :methods (list (cons 'mus-frequency (dilambda (lambda (g) (radians->hz (g 'freq))) (lambda (g val) (set! (g 'freq) (hz->radians val))))) (cons 'mus-phase (dilambda (lambda (g) (g 'phase)) (lambda (g val) (set! (g 'phase) val)))) (cons 'mus-describe (lambda (g) (format #f "osc freq: ~A, phase: ~A" (mus-frequency g) (mus-phase g)))))) freq phase)
The make-wrapper might more accurately be called an after-method; it is evaluated at the end of the automatically-created make function. All the fields have been set at that point either by arguments to the make function, or from the default values given in the defgenerator declaration. The make function returns whatever the make-wrapper function returns, so you almost always want to return the "gen" argument. There are many examples in generators.scm.
There are several functions closely tied to the generators and instruments.
hz->radians freq | convert freq to radians per sample (using *clm-srate*): (freq * 2 * pi) / srate |
radians->hz rads | convert rads to Hz (using *clm-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 *clm-srate*): samps / srate |
seconds->samples secs | convert seconds to samples (using *clm-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) |
mus-srate | sampling rate in with-sound (better known as *clm-srate*) |
odd-weight x | return a number between 0.0 (x is even) and 1.0 (x is odd) |
even-weight x | return a number between 0.0 (x is odd) and 1.0 (x is even) |
odd-multiple x y | return y times the nearest odd integer to x |
even-multiple x y | return y times the nearest even integer to x |
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.
> *clm-srate* 44100.0 > (hz->radians 440.0) 0.0626893772144902 > (/ (* 440.0 2 pi) 44100.0) 0.0626893772144902 > (linear->db .1) -20.0 > (times->samples 1.0 2.0) (44100 132300) > (seconds->samples 2.0) 88200 > (samples->seconds 44100) 1.0 > (degrees->radians 45) 0.785398163397448 > (radians->degrees (/ pi 4)) 45.0
mus-float-equal-fudge-factor (also known as *mus-float-equal-fudge-factor*)
This function sets how far apart generator float-vector elements can be and still be considered equal in equal?
> *mus-float-equal-fudge-factor* 1.0e-7 > (define v1 (float-vector .1 .1 .101)) #<unspecified> > (define v2 (float-vector .1 .1 .1)) #<unspecified> > (equal? v1 v2) #f > (set! *mus-float-equal-fudge-factor* .01) 1.0e-7 ; set! returns the previous value > (equal? v1 v2) #t
mus-array-print-length (also known as *mus-array-print-length*)
This function determines how many float-vector elements are printed by mus-describe.
polynomial coeffs x
The polynomial function evaluates a polynomial, defined by giving its coefficients, at the point "x". "coeffs" is a vector of coefficients where coeffs[0] is the constant term, and so on.
> (polynomial (float-vector 0.0 1.0) 2.0) ; x 2.0 > (polynomial (float-vector 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.
array-interp fn x 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 (float-vector -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-float-vector (length coeffs)))) (define (fr-filter flt x) (let* ((coeffs (car flt)) (xs (cadr flt)) (xlen (length xs))) (float-vector-move! xs (- xlen 1) (- xlen 2) #t) (set! (xs 0) x) (dot-product coeffs xs xlen)))
edot-product returns the complex dot-product of the "data" argument (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 in-samp (fm-index 1.0)
contrast-enhancement passes its input to sin as a kind of phase modulation.
(sin (+ (* input pi 0.5) (* index (sin (* input pi 2)))))
This brightens the input, 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. See moving-max for a similar function 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 in1 in2 ; returns(* in1 in2)
amplitude-modulate am-carrier in1 in2 ; returns(* in1 (+ am-carrier in2))
(with-sound (:play #t) (let ((osc1 (make-oscil 440.0)) (osc2 (make-oscil 220.0))) (do ((i 0 (+ i 1))) ((= i 44100)) (outa i (* 0.5 (amplitude-modulate 0.3 (oscil osc1) (oscil osc2))))))) |
with_sound(:play, true) do osc1 = make_oscil(440.0); osc2 = make_oscil(220.0); 44100.times do |i| outa(i, 0.5 * amplitude_modulate(0.3, oscil(osc1), oscil(osc2)), $output); end end.output |
lambda: ( -- ) 440.0 make-oscil { osc1 } 220.0 make-oscil { osc2 } 44100 0 do i 0.3 ( car ) osc1 0 0 oscil ( in1 ) osc2 0 0 oscil ( in2 ) amplitude-modulate f2/ *output* outa drop loop ; :play #t with-sound drop |
ring-modulation is sometimes called "double-sideband-suppressed-carrier" modulation — that is, amplitude modulation with the carrier omitted (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 is:
(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:
Waveshaping (via the Chebyshev polynomials) is an elaboration of AM. For example, cos^2x is amplitude modulation of cos x with itself, splitting into cos2x and cos0x. T2 (that is, 2cos^2x - 1) then subtracts the cos0x term to return cos2x.
The upper sidebands may foldover (alias); if it's a problem, low-pass filter the inputs (surely no CLM user needs that silly reminder!).
mus-fft rdat idat fftsize sign make-fft-window type size (beta 0.0) (alpha 0.0) rectangular->polar rdat idat rectangular->magnitudes 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. Here we use mus-fft to low-pass filter a sound:
(let* ((len (mus-sound-framples "oboe.snd")) (fsize (expt 2 (ceiling (log len 2)))) (rdata (make-float-vector fsize)) (idata (make-float-vector fsize))) (file->array "oboe.snd" 0 0 len rdata) (mus-fft rdata idata fsize 1) (let ((fsize2 (/ fsize 2)) (cutoff (round (/ fsize 10)))) (do ((i cutoff (+ i 1)) (j (- fsize 1) (- j 1))) ((= i fsize2)) (set! (rdata i) 0.0) (set! (idata i) 0.0) (set! (rdata j) 0.0) (set! (idata j) 0.0))) (mus-fft rdata idata fsize -1) (array->file "test.snd" (float-vector-scale! rdata (/ 1.0 fsize)) len (srate "oboe.snd") 1) (let ((previous-case (find-sound "test.snd"))) (if (sound? previous-case) (close-sound previous-case))) (open-sound "test.snd"))
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 papoulis-window dpss-window sinc-window
rectangular->polar and polar->rectangular change how we view the FFT data: in polar or rectangular coordinates. rectangular->magnitudes is the same as rectangular->polar, but only calculates the magnitudes. autocorrelate performs an (in place) autocorrelation of 'data' (a float-vector). See moving-pitch in generators.scm, or rubber.scm. correlate performs an in-place cross-correlation of data1 and data2 (see, for example, snddiff).
FFTs |
Hartley transform in Scheme: dht |
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 | pitch estimation (Bret Battey) | autoc.ins | |||
badd | fancier additive synthesis (Doug Fulton) | badd.ins | |||
bandedwg | Juan Reyes banded waveguide instrument | bandedwg.ins | bandedwg.cms | ||
fm-bell | fm bell sounds (Michael McNabb) | bell.ins | clm-ins.scm | clm-ins.rb | clm-ins.fs |
bigbird | waveshaping | bigbird.ins | bird.scm | bird.rb | clm-ins.fs, bird.fs |
singbowl | Juan Reyes Tibetan bowl instrument | bowl.ins | bowl.cms | ||
canter | fm 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 | clm-ins.scm | ||
moving sounds | 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 |
expandn | granular synthesis (Michael Klingbeil) | expandn.ins | clm-ins.scm | ||
granulate-sound | examples granular synthesis | expsrc.ins | clm-ins.scm | clm-ins.rb | clm-ins.fs |
cross-fade | cross-fades 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.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 | a reverberator (see also jlrev) | jcrev.ins | jcrev.scm | clm-ins.rb | clm-ins.fs |
fm-voice | fm voice (John Chowning) | jcvoi.ins | jcvoi.scm | ||
kiprev | a fancier reverberator (Kip Sheeline) | kiprev.ins | |||
lbj-piano | additive synthesis piano (Doug Fulton) | lbjPiano.ins | clm-ins.scm | clm-ins.rb | clm-ins.fs |
rotates | Juan Reyes Leslie instrument | leslie.ins | leslie.cms | ||
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 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 | "cut and paste" (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 (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 | stochastic.scm | ||
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 | |
fm-violin | fm violin (fmviolin.clm, popi.clm) | v.ins | v.scm | v.rb | clm-ins.fs |
vowel | vowels (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.