Logo loophole letters

Audio DSP Level 2

code

This is level 2 of my DSP journey. Be sure to read level 1 first

Exponential Saws

An exponential saw works like this:

let dsp = t => {
return sawx(110,8,t)/8
}
let sawx = (x,c,t) => (Math.exp((t*x)%1 * c - c)-0.5)*2
let isawx = (x,c,t) => (Math.exp(-(t*x)%1 * c)-0.5)*2

It can either be used as an oscillator, or as a modulator, e.g. as an envelope:

Hihat

We can create a simple hihat with noise and an exponential saw as envelope:

let dsp = t => hihat(4, 20, t)
let hihat = (f,damp,t) => isawxmod(f,damp,t)/2 * noise()/2
let norm = (bipolar) => (bipolar +1)/2
let range = (normalized, min, max) => normalized*(max-min)+min
let sawxmod = (x,c,t,a=0,b=1) => range(norm(sawx(x,c,t)),a,b)
let isawxmod = (x,c,t,a=0,b=1) => range(norm(isawx(x,c,t)),a,b)
let sawx = (x,c,t) => (Math.exp((t*x)%1 * c - c)-0.5)*2
let isawx = (x,c,t) => (Math.exp(-(t*x)%1 * c)-0.5)*2
let noise = () => Math.random() * 2 - 1

Snare

Similarly, a snare can be created with noise + a low sine oscillator:

let dsp = t => snare(2, 20, t)/4
let snare = (f,damp,t) => (sin(110, t) * 0.3 + noise() * 0.7) * isawxmod(f,damp,t);
let norm = (bipolar) => (bipolar +1)/2
let range = (normalized, min, max) => normalized*(max-min)+min
let sawxmod = (x,c,t,a=0,b=1) => range(norm(sawx(x,c,t)),a,b)
let isawxmod = (x,c,t,a=0,b=1) => range(norm(isawx(x,c,t)),a,b)
let sawx = (x,c,t) => (Math.exp((t*x)%1 * c - c)-0.5)*2
let sin = (x,t) => Math.sin(x*t*2*Math.PI)
let isawx = (x,c,t) => (Math.exp(-(t*x)%1 * c)-0.5)*2
let noise = () => Math.random() * 2 - 1

Kick

Let’s add a kick:

let kick = (x,y,t) => Math.sin(x * Math.exp(-t%.5 * y));
let dsp = t => kick(51, 45, t)/4;

It’s just a sine wave that starts fast and exponentially gets slower. Try tweaking the numbers to get a feel for the different sounds you can get. There are a lot more possibilities for kick synthesis, but let’s keep it simple for now.

Kick + Hihat + Snare:

let dsp = t => (hihat(2, 60, t+.25) + kick(51, 45, t) + snare(1,40,t+.5))/4
let kick = (x,y,t) => Math.sin(x * Math.exp(-t%.5 * y));
let hihat = (f,damp,t) => isawxmod(f,damp,t)/2 * noise()
let snare = (f,damp,t) => (sin(110, t) * 0.3 + noise() * 0.7) * isawxmod(f,damp,t)
let norm = (bipolar) => (bipolar +1)/2
let range = (normalized, min, max) => normalized*(max-min)+min
let sawxmod = (x,c,t,a=0,b=1) => range(norm(sawx(x,c,t)),a,b)
let isawxmod = (x,c,t,a=0,b=1) => range(norm(isawx(x,c,t)),a,b)
let sawx = (x,c,t) => (Math.exp((t*x)%1 * c - c)-0.5)*2
let sin = (x,t) => Math.sin(x*t*2*Math.PI)
let isawx = (x,c,t) => (Math.exp(-(t*x)%1 * c)-0.5)*2
let noise = () => Math.random() * 2 - 1

Note that the hihat uses t+.25 to shift the hihat to the offbeat! Similarly, the snare is shifted with +.5.

Let’s add the bass from the last level on top:

let bassline = [55,55,110,165];
let dsp = t => {
let freq = seq(bassline, 2, t);
let bass = (saw(freq, t) + saw(freq+1, t))/2;
return bass/5 * sawmod(seq([2,4],1,t),t+.25,1,0)
+(hihat(2, 30, t+.25) + kick(51, 45, t))/4
}
let kick = (x,y,t) => Math.sin(x * Math.exp(-t%.5 * y));
let hihat = (f,damp,t) => isawxmod(f,damp,t)/2 * noise()
// seq
let seq = (items, speed, t) => items[Math.floor(t*speed)%items.length]
// modulation
const norm = (bipolar) => (bipolar +1)/2;
const range = (normalized, min, max) => normalized*(max-min)+min;
const sinmod = (f,t,a=0,b=1) => range(norm(sin(f, t)),a,b)
const sawmod = (f,t,a=0,b=1) => range(norm(saw(f, t)),a,b)
const sqrmod = (f,t,a=0,b=1) => range(norm(sqr(f, t)),a,b)
const noisemod = (a=0,b=1) => range(norm(noise()),a,b)
const sawxmod = (x,c,t,a=0,b=1) => range(norm(sawx(x,c,t)),a,b)
let isawxmod = (x,c,t,a=0,b=1) => range(norm(isawx(x,c,t)),a,b)
// oscillators
let sin = (x, t) => Math.sin(2 * Math.PI * t * x)
let saw = (x, t) => ((x * t % 1) - 0.5) * 2
let sawx = (x,c,t) => (Math.exp((t*x)%1 * c - c)-0.5)*2
let isawx = (x,c,t) => (Math.exp(-(t*x)%1 * c)-0.5)*2
let isaw = (f,t) => -saw(f,t)
let sqr = (x, t) => sin(x, t) > 0 ? 1 : -1
let noise = () => Math.random() * 2 - 1

Clock

So far, we had to pass t to every oscillator function.. We can instead create a clock function that sets t for all functions to use:

let bassline = [55,55,110,165];
let dsp = t => {
clock(t)
let freq = seq(bassline, 2);
let bass = (saw(freq) + saw(freq+1))/2;
return bass/5 * sawmod(seq([2,4],1),.25,1,0)
+(hihat(2, 30, .25) + kick(51, 45))/4
}
let t;
function clock(_t){
t = _t;
}
let kick = (x,y,off=0) => Math.sin(x * Math.exp(-(t+off)%.5 * y));
let hihat = (f,damp=20,off=0) => isawxmod(f,damp,off)/2 * noise()
// seq
let seq = (items, speed, off=0) => items[Math.floor((t+off)*speed)%items.length]
// modulation
const norm = (bipolar) => (bipolar +1)/2;
const range = (normalized, min, max) => normalized*(max-min)+min;
const sinmod = (f,off=0,a=0,b=1) => range(norm(sin(f, off)),a,b)
const sawmod = (f,off=0,a=0,b=1) => range(norm(saw(f, off)),a,b)
const sqrmod = (f,off=0,a=0,b=1) => range(norm(sqr(f, off)),a,b)
const noisemod = (a=0,b=1) => range(norm(noise()),a,b)
const sawxmod = (x,c,off=0,a=0,b=1) => range(norm(sawx(x,c,off)),a,b)
let isawxmod = (x,c,off=0,a=0,b=1) => range(norm(isawx(x,c,off)),a,b)
// oscillators
let sin = (x, off=0) => Math.sin(2 * Math.PI * (t+off) * x)
let saw = (x, off=0) => ((x * (t+off) % 1) - 0.5) * 2
let sawx = (x,c,off=0) => (Math.exp(((t+off)*x)%1 * c - c)-0.5)*2
let isawx = (x,c,off=0) => (Math.exp(-((t+off)*x)%1 * c)-0.5)*2
let isaw = (f,off=0) => -saw(f,t+off)
let sqr = (x, off=0) => sin(x, t+off) > 0 ? 1 : -1
let noise = () => Math.random() * 2 - 1

The dsp function is now much more concise and readable! I found this trick in one of the wavepot tunes

End of Level

If you’ve followed along, congrats for completing this level. The next level will follow soon.

❤️ 2024 Felix Roos | mastodon | github