Additive Synthesis
In this example, we are going to use higher order functions to conveniently instantiate and control a number of primitive oscillators.
Our basic building block is a sine oscillator. For simplicity, let's use the one that is provided by kronoslang/core.
Import Gen
snd = Gen:Sin(440) * 0.1
The straightforward way to add up sinusoid partials is the good old +
.
snd = (Gen:Sin(440) + Gen:Sin(550) + Gen:Sin(660)) * 0.1
However, that is bound to get a little tedious. Especially if we would like a higher number of oscillators. Luckily, higher order functions help with exactly this kind of repetitive programming.
; Higher order funtional staples are in Algorithm
Import Algorithm
; List of frequencies
freqs = [330 440 550 660 770 880]
; Map applies a function, Gen:Sin in this case, to all elements in the list.
oscs = Algorithm:Map(Gen:Sin freqs)
; Reduce combines elements of a list, 'oscs' in this case, with the supplied
; function, (+), until the list is folded onto a single value.
snd = Algorithm:Reduce((+) oscs) / 20
Even more oscillators? In that case, we may want to generate the frequencies programmatically. There's a higher order function, Expand
, for that.
f0 = 55
N = #50
; Expand makes 'N' elements by applying (+ f0) repeatedly, starting from f0.
fs = Algorithm:Expand(N (+ f0) f0)
fs55 110 165 220 275 330 385 440 495 550 605 660 715 770 825 880 935 990 1045 1100 1155 1210 1265 1320 1375 1430 1485 1540 1595 1650 1705 1760 1815 1870 1925 1980 2035 2090 2145 2200 2255 2310 2365 2420 2475 2530 2585 2640 2695 2750 nil
; By the way, (+ x) is a handy way to make an adder function!
Algorithm:Map((+ 100) 1 2 3 4 5)101 102 103 104 105
; Back to business, let's make the oscillators
os = Algorithm:Map(Gen:Sin fs)
snd = Algorithm:Reduce((+) os) * 0.2 / N
Ok, so maybe you want to combine several lists somehow? We could apply an amplitude to each oscillator.
; Let's weigh each harmonic by the inverse of its number
is = Algorithm:Expand(N (+ 1) 1)
is1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 nil
; (1 /) is a handy way to make a function that divides one by a number.
gains = Algorithm:Map((1 /) is)
gains1 0.5 0.33333334 0.25 0.2 0.16666667 0.14285715 0.125 0.11111111 0.1 0.090909094 0.083333336 0.07692308 0.071428575 0.06666667 0.0625 0.05882353 0.055555556 0.052631579 0.050000001 0.047619049 0.045454547 0.043478262 0.041666668 0.039999999 0.03846154 0.037037037 0.035714287 0.034482758 0.033333335 0.032258064 0.03125 0.030303031 0.029411765 0.028571429 0.027777778 0.027027028 0.02631579 0.025641026 0.025 0.024390243 0.023809524 0.023255814 0.022727273 0.022222223 0.021739131 0.021276595 0.020833334 0.020408163 0.02 nil
; Now we have a list of partial sounds, and a list of gains. Two
; lists can be combined element-wise with a binary function by using
; Zip-With.
saw = Algorithm:Zip-With((*) os gains)
snd = Algorithm:Reduce((+) saw) * 0.2
We could even make a higher order function of our own!
Additive(N freq-fn amp-fn) {
; Generate N partials with the fundamental frequency f0
; The partial frequencies are derived by freq-fn(i)
; The partial amplitudes are derived by amp-fn(i)
; 'Use' lets us make a package prefix implicit
Use Algorithm
is = Expand(N (+ 1) 1)
freqs = Map(freq-fn is)
amps = Map(amp-fn is)
partials = Map(Gen:Sin freqs)
Reduce((+) Zip-With((*) partials amps))
}
; reproduce our earlier pseudo-sawtooth
snd = Additive(#50 (* 55) (1 /)) * 0.2
Because the frequency and amplitude specifications are functions, we can go a bit crazy and make them time-variant:
; stretching and compressing the harmonic series
stretch = i => 55 * Math:Pow(i 1 + Gen:Sin(0.1))
snd = Additive(#50 stretch i => 1) * 0.02
; per-partial vibrato and amplitude envelope
vibr = i => 55 * i * (Gen:Sin(Math:Sqrt(i)) * 0.1 + 1)
aenv = i => Gen:Sin(1 / i)
snd = Additive(#50 vibr aenv) * 0.02
We would typically want at least per-partial envelopes for additive synthesis. We can explore that territory by using time-variant amplitude functions.
; simple exponential decay envelope that starts from 1.
; for each sample, the level is multiplied by a near-one
; coefficient.
Decay(rate) {
level = z-1(1 level * Gen:Signal(rate))
level
}
; Let's try it with a single partial
snd = Gen:Sin(440) * Decay(0.9999)
; Now let's make higher partials decay faster
dcy = i => Decay(Math:Pow(0.99999 i / 4)) / i
; Harmonic series sounds a bit like a plucked string
snd = Additive(#50 (* 80) dcy) * 0.1
; Inharmonic series sounds like a bell or a metallic object
bfreq = i => 110 * Math:Pow(i 0.75)
snd = Additive(#50 bfreq dcy) * 0.1
That's a little taste of additive synthesis and higher order functions. The sky is the limit! That, CPU power and math skills. Oh well.