Skip to content

Latest commit

 

History

History
1131 lines (833 loc) · 30.8 KB

TTEM.org

File metadata and controls

1131 lines (833 loc) · 30.8 KB

The Theory and Technique of Electronic Music Exercises in Ad Libitum

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

Sinusoids, amplitude and frequency

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!)

Measures of Amplitude

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!)

Units of Amplitude

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.

Controlling Amplitude

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!)

Frequency

(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

Synthesizing a sinusoid

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!)

Superposing Signals

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)

Periodic Signals

(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!)

About the Software Examples

Oh, well… You are already running Ad Libitum at this point.

Examples

Constant amplitude scaler

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!)

Amplitude control in decibels

(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!)

Smoothed envelope control with an envelope generator

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!)

Major triad

mix is normalizing +~

(play!
 (mix (osc:sine/// 440.0)
      (osc:sine/// 550.0)
      (osc:sine/// 660.0)))

(h!)

Conversion between frequency and pitch

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.

More additive synthesis

(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!)

Exercises

;;; 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

(/ (*440.0) 44100.0)

;;; 7

(- (midi-pitch->frequency 61)
   (midi-pitch->frequency 60))

;;; 8

;; wtf is cents

;;; 9

;; min (/ (sqrt N))
;; max 1

Wavetables and samplers

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)))))

The Wavetable Oscillator

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!)

Sampling

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))

Enveloping samplers

(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!)

Timbre stretching

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!)

Interpolation

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.

Examples

Wavetable oscillator

We already explored that topic in Timbre stretching.

Wavetable lookup in general

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.

Using wavetable as a sampler

Ditto sans mouse-control.

Looping sampler

Overly boring stuff partially already covered earlier and I have no idea how to make it fun atm. Perhaps will return later.

Overlapping sample looper

Ditto.

Exercises

Audio and control computations

The sampling theorem

Control

Control streams

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.