Logo loophole letters

Audio DSP Level 1

code

In my last 3 posts, I’ve started to learn how to do DSP on the web:

  1. Making Music with Buffers: Pregenerated Buffers
  2. Real Time Synthesis: Naively Chaining Buffers
  3. Real Time Synthesis pt 2: Using AudioWorklets

In this post, I want to focus more an actual DSP techniques!

This post will start with the absolute basics. There will most likely be more posts building on top of this one.

Mastering the techniques of DSP programming allows you to create anything with audio! These techniques are not specific to the web and could be easily transfered to another language. Here’s an example from someone who already understands how it works:

/*!
*
* stagas - early morning
*
*/
var bpm = 120;
var tuning = 440;
var transpose = 12;
// constants
var tau = 2 * Math.PI;
// time coefficients
var t, tt;
// adjust tuning to bpm
tuning *= 120 / bpm;
// patterns
var chords = [
[7, 9, 12, 16],
[2, 5, 9, 12],
[4, 7, 11, 14],
[2, 5, 9, 12],
].map(function(chord){
return chord.map(function(n){
return note(n);
});
}).reverse();
var hat_pattern = [
0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0,
0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0,
];
var plucked = Pluck(60, 0.9, 0.93, 1.0, 0.9);
var kick_note = note(2, -1);
var bass_lp = LP2(6200);
function dsp(t){
clock(t);
var hat_note = note(9, 1 + tri(1/6) * 2 | 0);
var c = sequence(1/8, chords);

I am clearly not there yet, so I am eager to learn to someday arrive at that level! I’ve found this example on wavepot, which is a web based DSP editor I’ve just found out about!

Just a single function

All of the below examples will use a text field with at least a dsp function in it:

function dsp(t) {
return Math.sin(220*t*2*Math.PI)/4
}

This dsp function will get called by the audio system 44100 times per second! The function has only one argument t, which is the time in seconds since the playback started. The audio system expects the dsp function to return a single number between -1 and 1. This number will control the position of the speakers at the time t! This is all we need to implement any synth or effect we want!

Oscillators

Here are 4 different oscillators:

let dsp = (t) => sqr(110, t)/4
//
// oscillators
let sin = (x, t) => Math.sin(2 * Math.PI * t * x)
let saw = (x, t) => ((x * t % 1) - 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

We can mix oscillators by just adding them:

let dsp = t => (saw(110, t) + saw(111, t))/5
//
// oscillators
let sin = (x, t) => Math.sin(2 * Math.PI * t * x)
let saw = (x, t) => ((x * t % 1) - 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

This will create a gritty phasing effect!

Modulation

We can modulate the amplitude by multiplying with another oscillator, giving a so called tremolo:

let dsp = t => (saw(110, t) * sin(2, t))/5
//
// oscillators
let sin = (x, t) => Math.sin(2 * Math.PI * t * x)
let saw = (x, t) => ((x * t % 1) - 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

That modulation is maybe a bit extreme, let’s create some helpers:

let dsp = t => (saw(110, t) * sinmod(2,t,.5,1))/5
//
// 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)
//
// oscillators
let sin = (x, t) => Math.sin(2 * Math.PI * t * x)
let saw = (x, t) => ((x * t % 1) - 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

Let’s modulate the frequency:

let dsp = t => {
let f = 110 * sinmod(4,t,.9,1);
return sin(f, t)/5;
}
//
// 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)
//
// oscillators
let sin = (x, t) => Math.sin(2 * Math.PI * t * x)
let saw = (x, t) => ((x * t % 1) - 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

Hm, something odd is going on here.. I would have expected that the frequency just oscillates between 90 and 110.. For now, let’s leave it like that…

Sequences

A standing note is rather boring.. We could invent a simple sequence mechanism:

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;
}
//
// 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)
//
// oscillators
let sin = (x, t) => Math.sin(2 * Math.PI * t * x)
let saw = (x, t) => ((x * t % 1) - 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

Instead of typing frequencies, it should be useful to be able to type in midi numbers in semitones:

let midi = n => Math.pow(2, (n - 69) / 12) * 440;
let bassline = [40,43,47,50].map(midi);
let dsp = t => {
let freq = seq(bassline, 4, t);
let bass = (saw(freq, t) + saw(freq+1, t))/2;
return bass/5;
}
//
// 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)
//
// oscillators
let sin = (x, t) => Math.sin(2 * Math.PI * t * x)
let saw = (x, t) => ((x * t % 1) - 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

Envelopes

We can create a linear attack by multiplying with a slow sawmod:

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(2,t);
}
//
// 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)
// oscillators
let sin = (x, t) => Math.sin(2 * Math.PI * t * x)
let saw = (x, t) => ((x * t % 1) - 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

…or create decays by inverting the range:

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(2,t,1,0);
}
//
// 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)
// oscillators
let sin = (x, t) => Math.sin(2 * Math.PI * t * x)
let saw = (x, t) => ((x * t % 1) - 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

We could even sequence the envelope speed:

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,1,0);
}
//
// 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)
// oscillators
let sin = (x, t) => Math.sin(2 * Math.PI * t * x)
let saw = (x, t) => ((x * t % 1) - 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

End of Level

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

Credits

After creating the last posts, I’ve found wavepot (thanks Raphael for the link), which inspired the idea to use a seperate dsp function instead of writing an expression directly.

The example at the top is also from that page.

Bonus: Doughbeat

A minimal version of the editor you’ve seen in this post is also available here: https://github.com/felixroos/doughbeat

On the github page, there are many more examples with more sophisticated DSP taken from wavepot, which seems like a great learning resource for the future.

Here is one more example:

/*!
*
* stagas - late morning
*
*/
var bpm = 120;
var tuning = 440;
var transpose = 12; // constants
var tau = 2 * Math.PI; // time coefficients
var t, tt; // adjust tuning to bpm
tuning *= 120 / bpm; // patterns
var chords = [
[7, 9, 12, 16],
[2, 5, 9, 12],
[4, 7, 11, 14],
[2, 5, 9, 12],
]
.map(function (chord) {
return chord.map(function (n) {
return note(n);
});
})
.reverse();
var hat_pattern = [
0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5,
0.0,
];
var plucked = Pluck(60, 0.9, 0.93, 1.0, 0.9);
var bass_lp = LP2(6500);
function dsp(t) {
clock(t); // chord
var c = sequence(1 / 8, chords); // noise
var noise = Noise(); // bass
var bass_osc =
0.05 *
sin(

❤️ 2024 Felix Roos | mastodon | github