It’s my exercise book for studying “The Theory and Technique of Electronic Music” by Miller Puckette. Work is done in Ad Libitum instead of PureData, and some pieces of code migrate back to Ad Libitum standard library.
You might want to read this file here http://ul.mantike.pro/ad-libitum/TTEM.html
Ad Libitum deals with continous representation of time, it’s expressed as a float number of seconds passed from the stream start. For any signal either continous or discrete feels more natural, and there is no clear measure of their shares. Good news that conversion is relatively easy in both directions.
~<
is a syntax sugar which wraps code into function of two parameters:
time
and channel
, which are thrown in scope. Such kind of function is how
Ad Libitum represents audio signals. Think about time as a continuous
counterpart of sample number n
in TTEM. channel
will be discussed later.
Think about ~<
as the thing which animates your formula to produce signal.
(define (sinusoid a ω φ) (~< (* a (cos (+ (* ω time) φ)))))
play!
connects your signal to audio output and you can hear it! Amplitude
range in Ad Libitum is [-1, 1], setting a=0.2 we are producing signal in 1/5
of it.
(play! (sinusoid 0.2 2000.0 0.0))
And the most important function in Ad Libitum — h!
hush!
(h!)
Let’s make sinusoid to receive regular frequency instead of angular one.
(define (ƒ→ω ƒ)
(* 2π ƒ))
(define (ω→ƒ ω)
(/ ω 2π))
(define (sinusoid a ƒ φ)
(~< (* a (cos (+ (* (ƒ→ω ƒ) time) φ)))))
(play! (sinusoid 0.2 440.0 0.0))
(h!)
This is not a blood pact, but many of Ad Libitum code relies on fact that audiosignal is called sample by sample, without skips. Making that assumption we are able to write RMS amplitude measurement even in continuous representation of time.
(define (rms-amplitude window-width s)
(let ([windows (make-vector *channels*)]
[N (-> window-width (* *sample-rate*) (ceiling) (exact))]
[cursor -1])
(do-ec (: i *channels*)
(vector-set! windows i (make-vector N 0.0)))
(~<
(when (zero? channel)
(set! cursor (mod (+ cursor 1) N)))
(let ([window (vector-ref windows channel)]
[x 0.0])
(vector-set! window cursor (<~ s))
(vector-for-each (λ (y) (set! x (+ x (* y y)))) window)
(sqrt (/ x N))))))
Let’s make sinusoid amplitude a signal, and set it to rms amplitude measured
from sinusoid with peak amplitude 1.0
Note <~
syntax sugar to apply audiosignal function to time and channel.
(<~ a)
is equivalent to (a time channel)
Also note how we make constant signal with (~< 1.0)
(define (sinusoid a ƒ φ)
(~< (* (<~ a) (cos (+ (* (ƒ→ω ƒ) time) φ)))))
Note that computing rms in realtime is very expensive. Try to increase window width until you get audio buffer underflow glitches.
(play! (sinusoid (rms-amplitude 2/440 (sinusoid (~< 1.0) 440.0 0.0)) 440.0 0.0))
(h!)
Compare with 1.0 peak itself, it’s louder!
(play! (sinusoid (~< 1.0) 440.0 0.0))
(h!)
Let’s throttle rms-amplitude
to make it less hungry
(define (rms-amplitude window-width s)
(let ([windows (make-vector *channels*)]
[N (-> window-width (* *sample-rate*) (ceiling) (exact))]
[cursor -1]
[amplitudes (make-vector *channels* 0.0)])
(do-ec (: i *channels*)
(vector-set! windows i (make-vector N 0.0)))
(~<
(when (zero? channel)
(set! cursor (mod (+ cursor 1) N)))
(let ([window (vector-ref windows channel)])
(vector-set! window cursor (<~ s))
(when (zero? cursor)
(vector-set!
amplitudes
channel
(let ([x 0.0])
(vector-for-each (λ (y) (set! x (+ x (* y y)))) window)
(sqrt (/ x N))))))
(vector-ref amplitudes channel))))
(play! (sinusoid (rms-amplitude 1 (+~ (sinusoid (~< 1.0) 440.0 0.0))) 440.0 0.0))
(h!)
Chez Scheme GC is so cool that though we produce a lot of garbage every
second it doesn’t interrupt sound! But we still have a noticeable lag on
window initialization. Take it into account if you are going to spawn
capacitive rms-amplitude
signals frequently.
In future for sinusoid we will use built-in Ad Libitum oscillator available
as osc:sine
(takes phasor signal as input, we’ll cover it later) and
osc:sine///
(takes frequency and optional initial phase).
(play! (osc:sine (osc:phasor 440.0 0.0)))
(play! (osc:sine/// 440.0 0.0))
(h!)
Convert amplitude to decibels, with a0 = 1e-5 as suggested in TTEM
(define (amp->dB x)
(* 20.0 (log (* x 1e5) 10.0)))
(amp->dB 1.0)
(amp->dB 0.5)
But setting a0 to 1.0 is also very convenient — maximum amplitude is then 0dB and any one below is just negative. All relations stays the same.
(define (amp->dB x)
(* 20.0 (log x 10.0)))
(amp->dB 0.5)
(amp->dB 1e-5)
And convert decibels back to amplitude:
(define (dB->amp x)
(expt 10.0 (/ x 20.0)))
(dB->amp -100.0)
(dB->amp -50.0)
(dB->amp -10.0)
(dB->amp 0.0)
Amplitude is related in an inexact way to the perceived loudness of a sound. In general, two signals with the same peak or RMS amplitude won’t necessarily have the same loudness at all. But amplifying a signal by 3 dB, say, will fairly reliably make it sound about one “step” louder.
(define *volume-step-dB* 3.0)
Let’s test it!
(define (sinusoid a ƒ φ)
(~< (* a (cos (+ (* (ƒ→ω ƒ) time) φ)))))
(play! (sinusoid (dB->amp -10.0) 440.0 0.0))
(play! (sinusoid (dB->amp (- 10.0 *volume-step-dB*)) 440.0 0.0))
(h!)
Try to change step. For that wave personally I hear 2dB difference.
We already controlled amplitude by multiplying every sample by a
Let’s do it by multiplying sinusoid by constant signal.
(play! (*~ (~< 0.5) (osc:sine/// 440.0)))
(play! (*~ (~< 0.2) (osc:sine/// 440.0)))
(h!)
(define (midi-pitch->frequency m)
(* 440.0 (expt 2.0 (/ (- m 69.0) 12.0))))
(define (frequency->midi-pitch f)
(+ 69 (exact (round (* 12.0 (log (/ f 440.0) 2.0))))))
(play! (osc:sine/// (midi-pitch->frequency 69)))
(play! (osc:sine/// (midi-pitch->frequency 72)))
(h!)
Ad Libitum allows you to use MIDI controller. Support is still incomplete and
relies on many assumptions. Your MIDI input device should be connected and
identified as the first one.
now
is Ad Libitum clock function. It’s required for MIDI module to put
proper timestamps on events.
(midi:start now)
Let’s defined so called control signal for our frequency. We’ll speak about control signals later, but putting it simply, control signal is an audio signal which is updated in non-audio rate by calling its setter.
(ctrl:define-control frequency 440.0)
Let’s set callback which will be called for every control change MIDI event.
(midi:set-cc! (λ (t knob value channel)
(frequency-set! (midi-pitch->frequency value))))
(play! (osc:sine/// frequency~))
(h!)
Notice that abrupt change of frequency cause “pops” discussed in TTEM 1.5
To make transition smooth we could use built-in env:linear-transition
(ctrl:define-control frequency 440.0)
(play! (osc:sine/// (env:linear-transition (~< 0.05) frequency~)))
(h!)
Besides of using MIDI input we could make frequency change programmatically.
schedule
allows you to call any function later at given point of time.
Any function could schedule itself. It is called temporal recursion.
(define (swap-frequency i)
(if (zero? i)
(frequency-set! 440.0)
(frequency-set! 220.0))
(schedule (+ (now) 1/4) 'swap-frequency (- 1 i)))
(swap-frequency 0)
(play! (osc:sine/// frequency~))
(play! (osc:sine/// (env:linear-transition (~< 0.05) frequency~)))
(play! (osc:sine/// (env:quadratic-transition (~< 0.05) frequency~)))
(h!)
To superpose signals in Ad Libitum signal sum operator +~
is available.
(play! (+~ (*~ (~< 0.5) (osc:sine/// (midi-pitch->frequency 69)))
(*~ (~< 0.5) (osc:sine/// (midi-pitch->frequency 72)))))
(h!)
Let’s measure how peak and rms amplitude of sinusoids superposition relates to sum of their amplitudes.
For that we need to define peak-amplitude
signal.
(define (peak-amplitude signal)
(let ([peaks (make-vector *channels* 0.0)])
(~<
(let* ([sample (<~ signal)]
[peak (max sample (vector-ref peaks channel))])
(vector-set! peaks channel peak)
peak))))
Because Ad Libitum signals are kind of pull FRP, we can’t just wrap our
signal with rms-amplitude
and then play initial signal and have RMS one
updated. Let’s define useful signal which keep signals given to it updated,
but plays only first one.
(define (solo audio . muted)
(~<
(for-each (cut <> time channel) muted)
(<~ audio)))
Uncorrelated signals.
(define signal-1 (*~ (~< 0.5) (osc:sine/// (midi-pitch->frequency 69))))
(define signal-2 (*~ (~< 0.5) (osc:sine/// (midi-pitch->frequency 72))))
(define superposed-signal (+~ signal-1 signal-2))
;; signal-1 and signal-2 peaks are obviously 0.5
(define measure-peak (peak-amplitude superposed-signal))
(define measure-rms-signal-1-2 (rms-amplitude 0.1 signal-1))
(define measure-rms (rms-amplitude 0.1 superposed-signal))
(play! (solo superposed-signal measure-peak measure-rms measure-rms-signal-1-2))
(h!)
(measure-peak 0.0 0) ;; => 0.9999931184993082
(measure-rms 0.0 0) ;; => 0.5038381755150125
(measure-rms-signal-1-2 0.0 0) ;; => 0.3535533905956031
Correlated signals.
(define signal-1 (*~ (~< 0.5) (osc:sine/// 440.0)))
(define signal-2 (*~ (~< 0.5) (osc:sine/// 440.0)))
(define superposed-signal (+~ signal-1 signal-2))
;; signal-1 and signal-2 peaks are obviously 0.5
(define measure-peak (peak-amplitude superposed-signal))
(define measure-rms-signal-1-2 (rms-amplitude 0.1 signal-1))
(define measure-rms (rms-amplitude 0.1 superposed-signal))
(play! (solo superposed-signal measure-peak measure-rms measure-rms-signal-1-2))
(h!)
(measure-peak 0.0 0) ;; => 1.0
(measure-rms 0.0 0) ;; => 0.7071067811829478
(measure-rms-signal-1-2 0.0 0) ;; => 0.3535533905914739
To be honest, trick with solo
points to drawbacks in rms-amplitude
and
peak-amplitude
design. It would be better for them to just proxy input
signal and provide some accessor to measurement result.
(define (peak-amplitude signal)
(let ([peaks (make-vector *channels* 0.0)])
(values
(~<
(let* ([sample (<~ signal)]
[peak (max sample (vector-ref peaks channel))])
(vector-set! peaks channel peak)
sample))
(λ () peaks))))
(define (window width signal)
(let ([windows (make-vector *channels*)]
[N (-> width (* *sample-rate*) (ceiling) (exact))]
[cursor -1])
(do-ec (: i *channels*)
(vector-set! windows i (make-vector N 0.0)))
(values
(~<
(when (zero? channel)
(set! cursor (mod (+ cursor 1) N)))
(let ([sample (<~ signal)]
[window (vector-ref windows channel)])
(vector-set! window cursor sample)
sample))
(λ () windows))))
(define (rms-amplitude window-width signal)
(let-values ([(signal windows) (window window-width signal)])
(values
signal
(λ ()
(vector-map
(λ (window)
(let ([x 0.0])
(vector-for-each (λ (y) (set! x (+ x (* y y)))) window)
(sqrt (/ x (vector-length window)))) )
(windows))))))
(define signal-1 (*~ (~< 0.5) (osc:sine/// (midi-pitch->frequency 69))))
(define signal-2 (*~ (~< 0.5) (osc:sine/// (midi-pitch->frequency 72))))
(define superposed-signal (+~ signal-1 signal-2))
(define-values (superposed-signal measure-peak) (peak-amplitude superposed-signal))
(define-values (superposed-signal measure-rms) (rms-amplitude 0.1 superposed-signal))
(play! superposed-signal)
(h!)
(measure-peak)
(measure-rms)
(define s1 (*~ (~< 0.5) (osc:sine/// 220.0 0.0)))
(define s2 (*~ (~< 0.3) (osc:sine/// 440.0 0.1)))
(define s3 (*~ (~< 0.2) (osc:sine/// 660.0 0.2)))
(play! (+~ s1 s2 s3))
(h!)
Oh, well… You are already running Ad Libitum at this point.
Note how Pd boxes corresponds to Ad Libitum expressions in parenthesis, and
instead of graph-like connections tree structure is used. Don’t judge fast
it as limiting, you always can reuse expression by naming it with help of
define
or let
, and we will show later how powerful is textual
representation.
(play!
(*~
(~< 0.05)
(osc:sine/// 440.0)))
(h!)
;;; or
(define sinusoid (osc:sine/// 440.0))
(define amplitude (~< 0.05))
(define scaled-sinusoid (*~ amplitude sinusoid))
(play! scaled-sinusoid)
(h!)
(define amplitude 0.0)
(define amplitude~ (live-value 'amplitude))
(define (set-amplitude! dB)
(set! amplitude (dB->amp dB)))
(play! (*~ amplitude~ (osc:sine/// 440.0)))
(set-amplitude! 0) ;; beware of loud sound
(set-amplitude! -10)
(set-amplitude! -20)
(set-amplitude! -50)
(h!)
Note, that in 𝔄𝔏 instead of messages we just set values or invoke function which do that.
(define (make-line)
(ctrl:define-control value 0.0)
(ctrl:define-control Δt 0.0)
(values
(env:linear-transition Δt~ value~)
value-set!
Δt-set!))
(define-values (line~ set-amplitude! set-Δt!) (make-line))
(define (slow-on)
(set-Δt! 2.0)
(set-amplitude! 0.1))
(define (fast-on)
(set-Δt! 0.05)
(set-amplitude! 0.1))
(define (instant-on)
(set-Δt! 0.0)
(set-amplitude! 0.1))
(define (slow-off)
(set-Δt! 2.0)
(set-amplitude! 0.0))
(define (fast-off)
(set-Δt! 0.05)
(set-amplitude! 0.0))
(define (instant-off)
(set-Δt! 0.0)
(set-amplitude! 0.0))
(play! (*~ (osc:sine/// 440.0) line~))
(slow-on)
(slow-off)
(fast-on)
(fast-off)
(instant-on)
(instant-off)
(h!)
mix
is normalizing +~
(play!
(mix (osc:sine/// 440.0)
(osc:sine/// 550.0)
(osc:sine/// 660.0)))
(h!)
This is covered well before. Only worth to note once more that in 𝔄𝔏 instead of message passing we just give names, set values and call functions.
(ctrl:define-control frequency 0.0)
(define set-pitch! (∘ frequency-set! midi-pitch->frequency))
(define s1 (osc:sine/// frequency~))
(define s2 (*~ (~< 0.1) (osc:sine/// (*~ (~< 2.0) frequency~))))
(define s3 (*~ (~< 0.2) (osc:sine/// (*~ (~< 3.0) frequency~))))
(define s4 (*~ (~< 0.5) (osc:sine/// (*~ (~< 4.0) frequency~))))
(define (switch)
(let ([value 0.0])
(values
(~< value)
(λ () (set! value (- 1.0 value))))))
(define-values (overtones-switch toggle-overtones!) (switch))
(play! (+~ s1
(*~ overtones-switch
(+~ s2 s3 s4))))
(set-pitch! 69)
(toggle-overtones!)
(set-pitch! 50)
(toggle-overtones!)
(h!)
;;; 1
(define φ 0.0)
(define ω (/ π 10.0))
;; ? phase at sample n = 10
(+ (* ω 10) φ)
;;; 2
(define sinusoid-1-period 20)
(define sinusoid-2-period 30)
;; ? period of sum
(lcm sinusoid-1-period sinusoid-2-period)
;;; 3
(amp->dB 1.5)
(amp->dB 2)
(amp->dB 3)
(amp->dB 5)
;;; 4
(define signal-1-rms-amplitude 3)
(define signal-2-rms-amplitude 4)
;; ? rms amplitude of signals sum
(sqrt (+ (* signal-1-rms-amplitude signal-1-rms-amplitude)
(* signal-2-rms-amplitude signal-2-rms-amplitude)))
;; let's double-check with real signal
(define s1 (*~ (~< (* signal-1-rms-amplitude (sqrt 2)))
(osc:sine/// 440.0)))
(define s2 (*~ (~< (* signal-2-rms-amplitude (sqrt 2)))
(osc:sine/// 543.0)))
(define-values (s1* measure-s1-rms) (rms-amplitude 0.1 s1))
(define-values (s2* measure-s2-rms) (rms-amplitude 0.1 s2))
(define ss (+~ s1* s2*))
(define-values (ss* measure-ss-rms) (rms-amplitude 0.1 ss))
(play! (*~ ss* silence))
(h!)
(measure-s1-rms) ;; => #(2.9999999999627187 2.9999999999627187)
(measure-s2-rms) ;; => #(3.998132153590563 3.998896233813645)
(measure-ss-rms) ;; => #(5.0258347452181065 5.0260213046152185)
;;; 5
(define amp-factor (/ (dB->amp 9.0) (dB->amp 0.0)))
;; amp-factor of n equal uncorrelated signals sum is (sqrt n)
(* amp-factor amp-factor)
;;; 6
(/ (* 2π 440.0) 44100.0)
;;; 7
(- (midi-pitch->frequency 61)
(midi-pitch->frequency 60))
;;; 8
;; wtf is cents
;;; 9
;; min (/ (sqrt N))
;; max 1
In 𝔄𝔏 we trade sample-number precision in table pickup for uniform use of
time. Wavetable lookup then receive position in table in range unit of [0.0,
1.0)
and pick nearest sample at that ratio of table length, rounded down.
Note our implementation is mono, which is not consistent with 𝔄𝔏 other signal creators. We will refine it later.
(define (clamp value start end)
(cond
[(< value start) start]
[(> value end) end]
[else value]))
(define (unit->length x n)
(flonum->fixnum (fltruncate (fl* x n))))
(define (wavetable-lookup table position)
(let* ([n (fixnum->flonum (vector-length table))]
[expand (cut unit->length <> n)]
[clamp (cut clamp <> 0 (- n 1))])
(~< (->> (<~ position)
(expand)
(clamp)
(vector-ref table)))))
Let’s make a table and try to play it in different ways. We are going to
precompute sinusoid wave to have sanity check for our wavetable-lookup
.
(define frequency 440.0)
(define sinusoid (osc:sine/// frequency))
(define wavetable-quality-factor 16)
(define N (exact (round (* wavetable-quality-factor (/ *sample-rate* frequency)))))
(define sinusoid-table (make-vector N))
(do-ec (: i N)
(vector-set! sinusoid-table i (sinusoid (/ i *sample-rate* wavetable-quality-factor) 0)))
(define sinusoid-wavetable
(wavetable-lookup sinusoid-table (osc:phasor frequency)))
;; should sound the same
(play! sinusoid)
(play! sinusoid-wavetable)
(define sinusoid-wavetable-fast
(wavetable-lookup sinusoid-table (osc:phasor (* 2.0 frequency))))
;; should sound the same
(play! (osc:sine/// (* 2.0 frequency)))
(play! sinusoid-wavetable-fast)
(define sinusoid-wavetable-slow
(wavetable-lookup sinusoid-table (osc:phasor (* 0.5 frequency))))
;; should sound the same
(play! (osc:sine/// (* 0.5 frequency)))
(play! sinusoid-wavetable-slow)
(define sinusoid-wavetable-tri
(wavetable-lookup
sinusoid-table
(amplitude->phase (osc:tri/// frequency))))
;; now they should sound different
(play! (osc:tri/// frequency))
(play! sinusoid-wavetable-tri)
(define sinusoid-wavetable-sine
(wavetable-lookup
sinusoid-table
(amplitude->phase (osc:sine/// 110.0))))
;; should sound different
(play! (osc:sine/// 110.0))
(play! sinusoid-wavetable-sine)
(h!)
Now let’s try cross-fade two wavetables.
(define (unroll* signal frequency quality-factor)
(let* ([N (exact (round (* quality-factor (/ *sample-rate* frequency))))]
[table (make-vector N)])
(do-ec (: i N)
(vector-set!
table i
(signal (/ i *sample-rate* quality-factor) 0)))
(cut wavetable-lookup table <>)))
(define unroll (cut unroll* <> <> 16))
(define s1* (osc:sine/// 440.0))
(define s2* (osc:tri/// 440.0))
(define w1 (unroll s1* 440.0))
(define w2 (unroll s2* 440.0))
(define p (osc:phasor 440.0))
(define s1 (w1 p))
(define s2 (w2 p))
(play! s1)
(play! s2)
(define-values (toggle toggle!) (switch))
(define ramp (env:linear-transition (~< 2.0) toggle))
(play! (mix s1 (*~ ramp (-~ s2 s1))))
(toggle!)
(play! (mix s1* (*~ ramp (-~ s2* s1*))))
(toggle!)
(h!)
Theoretically wavetable-quality-factor
of 1 should be enough to rebuild
sinusoid without artifacts, but in my experiments 16 was the lowest value for
clear signal. Is it because of table being non-time-aligned? If so,
interpolating table should help.
(define (wavetable-lookup-linear table position)
(let* ([N (vector-length table)]
[n (fixnum->flonum N)])
(~< (let ([p (* n (<~ position))])
(let ([i (flonum->fixnum (fltruncate p))]
[a (mod p 1.0)])
(+ (* (- 1.0 a) (vector-ref table (mod i N)))
(* a (vector-ref table (mod (+ i 1) N)))))))))
(define (unroll* signal frequency quality-factor)
(let* ([N (exact (round (* quality-factor (/ *sample-rate* frequency))))]
[table (make-vector N)])
(do-ec (: i N)
(vector-set!
table i
(signal (/ i *sample-rate* quality-factor) 0)))
(cut wavetable-lookup-linear table <>)))
(define unroll (cut unroll* <> <> 1))
(define sinusoid-table (unroll (osc:sine/// 440.0) 440.0))
(define s (sinusoid-table (osc:phasor 440.0)))
(play! tuner)
(play! s)
(h!)
Yes! It works!
Now, let’s write channel-aware unroll
and wavetable lookup, which we’ll call sampler.
(define (sampler table phase)
(let* ([N (vector-length (vector-ref table 0))]
[n (fixnum->flonum N)])
(~< (let ([p (* n (<~ phase))])
(let ([i (flonum->fixnum (fltruncate p))]
[a (mod p 1.0)]
[table (channel-ref table)])
(+ (* (- 1.0 a) (vector-ref table (mod i N)))
(* a (vector-ref table (mod (+ i 1) N)))))))))
(define (unroll signal base-frequency)
(let* ([n (-> *sample-rate* (/ base-frequency) (round) (exact))]
[table (make-channel-vector)])
(do-ec (: channel *channels*)
(channel-set! table (make-vector n)))
;; channel is in inner loop because many `signal' functions
;; rely on ordered sample-by-sample execution
(do-ec (: sample n)
(: channel *channels*)
(vector-set!
(channel-ref table)
sample
(signal (/ sample *sample-rate*) channel)))
(cut sampler table <>)))
(define sinusoid-table (unroll (osc:sine/// 440.0) 440.0))
(define s (sinusoid-table (osc:phasor 440.0)))
(play! tuner)
(play! s)
(h!)
The Transposition Formulas for Looping Wavetables.
(define (transposition-factor table frequency)
(/ (* (vector-length table) frequency) *sample-rate*))
(define (transposition-halfsteps table frequency)
(* 12.0 (log (transposition-factor table frequency) 2.0)))
(define (transposition-frequency table halfsteps)
(/ (* (expt 2.0 (/ halfsteps 12.0)) *sample-rate*) (vector-length table)))
(define (transposition-table-length frequency halfsteps)
(/ (* (expt 2.0 (/ halfsteps 12.0)) *sample-rate*) frequency))
(define sinusoid-table (unroll (osc:sine/// 55.0) 55.0))
(define s (sinusoid-table (amplitude->phase (osc:sine/// 440.0))))
(play! tuner)
(play! s)
(h!)
For that exercise we need to extend our sampler
to accept arbitrary N
instead of having it strictly equal table length, and also use clamp
instead of wrap
(implemented by mod
earlier). Another useful signal
transformer would be phase->interval
which projects unit interval to
arbitrary one. Combining clamping sampler and interval projection we could
achieve any desired padding of table. Then we need just to superpose such
padded tables with phase shift to get desired effect.
Another small improvement is to make unroll to return just table, not a partially applied sampler. It would enable us to experiment with different sample implementations without re-implementing unroll.
(define (clamp value start end)
(cond
[(< value start) start]
[(> value end) end]
[else value]))
(define (sampler table phase)
(let* ([N (vector-length (vector-ref table 0))]
[N-1 (- N 1)]
[n (fixnum->flonum N)])
(~< (let ([position (* n (<~ phase))])
(let ([i (-> position
(fltruncate)
(flonum->fixnum)
(clamp 0 N-1))]
[a (mod position 1.0)]
[table (channel-ref table)])
(+ (* (- 1.0 a) (vector-ref table i))
(* a (vector-ref table (mod (+ i 1) N)))))))))
(define (unroll signal base-frequency)
(let* ([n (-> *sample-rate* (/ base-frequency) (round) (exact))]
[table (make-channel-vector)])
(do-ec (: channel *channels*)
(channel-set! table (make-vector n)))
;; channel is in inner loop because many `signal' functions
;; rely on ordered sample-by-sample execution
(do-ec (: sample n)
(: channel *channels*)
(vector-set!
(channel-ref table)
sample
(signal (/ sample *sample-rate*) channel)))
table))
(define~ (phase->interval phase start end)
(let ([phase (<~ phase)]
[start (<~ start)]
[end (<~ end)])
(+ start (* phase (- end start)))))
(define sinusoid-table (unroll (osc:sine/// 440.0) 440.0))
(define (make-control x)
(ctrl:define-control x x)
(values
(env:linear-transition (~< 0.05) x~)
x-set!))
(define-values (frequency set-frequency!) (make-control 440.0))
(define-values (start1 set-start1!) (make-control 0.0))
(define-values (end1 set-end1!) (make-control 1.0))
(define-values (start2 set-start2!) (make-control 0.0))
(define-values (end2 set-end2!) (make-control 1.0))
(define phasor1 (phase->interval
(osc:phasor frequency)
start1
end1))
(define phasor2 (phase->interval
(osc:phasor frequency)
start2
end2))
(define s1 (sampler sinusoid-table phasor1))
(define s2 (sampler sinusoid-table phasor2))
;; ^v^v^
(play! tuner)
(play! s1)
;; ....^v^v^....
(set-start1! -1.0)
(set-end1! 2.0)
;; ^v^v^...
;; ...^v^v^
(set-start1! 0.0)
(set-end1! 1.5)
(set-start2! -0.5)
(set-end2! 1.0)
(set-frequency! 220.0)
(play! (mix s1 s2))
;; ^v^
(play! s1)
(set-start1! 0.3)
(set-end1! 0.6)
(set-frequency! 440.0)
(h!)
We’ve already done sampler with linear interpolation, let’s do cubic one.
(define (sampler table phase)
(let* ([N (vector-length (vector-ref table 0))]
[N-1 (- N 1)]
[n (fixnum->flonum N)]
[/6 (/ 1.0 6.0)]
[/2 0.5])
(~< (let ([position (* n (<~ phase))])
(let ([i (-> position
(fltruncate)
(flonum->fixnum)
(clamp 0 N-1)
)]
[f (mod position 1.0)]
[table (channel-ref table)])
(let ([y-0 (vector-ref table i)]
[y-1 (vector-ref table (mod (- i 1) N))]
[y+1 (vector-ref table (mod (+ i 1) N))]
[y+2 (vector-ref table (mod (+ i 2) N))]
[f-2 (- f 2.0)]
[f-1 (- f 1.0)]
[f+1 (+ f 1.0)]
[-f (- f)])
(+ (* -f f-1 f-2 /6 y-1)
(* f+1 f-1 f-2 /2 y-0)
(* -f f+1 f-2 /2 y+1)
(* f+1 f f-1 /6 y+2))))))))
(define sinusoid-table (osc:unroll (osc:sine/// 440.0) 440.0))
(define s (sampler sinusoid-table (osc:phasor 440.0)))
(play! tuner)
(play! s)
(h!)
Now I clearly hear subtle humming in both linear and cubic sampler. Need to figure out why it happens. Is it precession?
At that moment let’s try to just mitigate that issue by using LPF.
(define s (filter:biquad-lpf (~< 1.0) (~< 4400.0) (sampler sinusoid-table (osc:phasor 440.0))))
(play! tuner)
(play! s)
(h!)
Yep, it works. But still not sure how eliminate it on unroll/sampler level.
We already explored that topic in Timbre stretching.
tabread
in 𝔄𝔏 is performed by the same sampler
to which you pass
non-periodic signal instead of phasor. tabwrite
is even simpler, use
Scheme’s vector-set!
for that in control-mode and ctrl:window
to record
audio-signal.
Ditto sans mouse-control.
Overly boring stuff partially already covered earlier and I have no idea how to make it fun atm. Perhaps will return later.
Ditto.
Control streams in 𝔄𝔏 are represented by a pair of audiosignal with a value in a box. I’m thinking about building streams with push semantics, but it will come later naturally in case of need. Atm less magic is the better and bare closure works quite well.