Playing audio with javascript
A simple audio player + wave visualizer
Useful page if you want to just generate some audio and see and hear the output. Works best if you have a dev server that reloads on file change.
It plays a sound for 1 second at 44100 samples/second, and displays the corresponding wave.
Code below!
<!DOCTYPE html>
<title>Interactive audio</title>
<svg width="44100" height="200">
<path d="M0 2 L 44100,2" stroke="red" fill="none" stroke-width="2" />
<path d="M0 100 L 44100,100" stroke="blue" fill="none" stroke-width="2" stroke-dasharray="8" />
<path id="graph" d="" stroke="black" fill="none" stroke-width="2" />
<path d="M0 198 L 44100,198" stroke="red" fill="none" stroke-width="2" />
</svg>
<audio id="sound" controls autoplay></audio>
<script>
// antialiased saw wave
const saw = (sample, rate, freq) => {
const partial = sample * freq / rate;
const phase = partial - Math.trunc(partial);
const saw = phase * 2 - 1;
// polyblep
if (phase < freq / rate) {
const t = phase / freq;
return saw - (2 * t - (t * t) - 1);
} else if (phase > (1 - freq / rate)) {
const t = (phase - 1) / (freq / rate);
return saw - ((t * t) + 2 * t + 1);
} else {
return saw;
}
};
// filter settings
const falloff = 800;
const resonance = 3.5;
// filter states
let f1 = 0;
let f2 = 0;
let f3 = 0;
let f4 = 0;
// generate audio
const soundfun = (sample, rate) => {
// saw wave
const wave = saw(sample, rate, 120);
// ladder lowpass filter
// https://www.native-instruments.com/fileadmin/ni_media/downloads/pdf/VAFilterDesign_2.1.0.pdf
const a = Math.exp(-(falloff / rate) * 6.28);
const inp = -f4 * resonance + wave;
f1 += (1 - a) * (inp - f1);
f2 += (1 - a) * (f1 - f2);
f3 += (1 - a) * (f2 - f3);
f4 += (1 - a) * (f3 - f4);
return f2;
};
const plot = (rate, fn) => {
let path = "M 0 100";
for (let i = 0; i < rate; i++) {
const sample = fn(i, rate) * 100 + 100;
path += ` L ${i},${sample}`;
}
console.log(path);
return path;
};
// generate sound
const gen = (rate, fn) => {
// buffer
const buf = [];
// header
buf.push('R'.charCodeAt(0), 'I'.charCodeAt(0), 'F'.charCodeAt(0), 'F'.charCodeAt(0));
// file size
const size = 36 + rate * 2;
buf.push(size & 255, (size >> 8) & 255, (size >> 16) & 255, (size >> 24) & 255);
// header
buf.push('W'.charCodeAt(0), 'A'.charCodeAt(0), 'V'.charCodeAt(0), 'E'.charCodeAt(0));
// format
buf.push('f'.charCodeAt(0), 'm'.charCodeAt(0), 't'.charCodeAt(0), ' '.charCodeAt(0)
);
// sub chunk size
buf.push(16, 0, 0, 0);
// format, pcm
buf.push(1, 0);
// channels
buf.push(1, 0);
// rate
buf.push(
rate & 255, (rate >> 8) & 255, (rate >> 16) & 255, (rate >> 24) & 255
);
// byte rate
buf.push((rate * 2) & 255, ((rate * 2) >> 8) & 255, ((rate * 2) >> 16) & 255, ((rate * 2) >> 24) & 255);
// block align
buf.push(2, 0);
// bits per samble
buf.push(16, 0);
// data
buf.push('d'.charCodeAt(0), 'a'.charCodeAt(0), 't'.charCodeAt(0), 'a'.charCodeAt(0));
// section size
const sector = rate * 2;
buf.push(sector & 255, (sector >> 8) & 255, (sector >> 16) & 255, (sector >> 24) & 255);
// add audio
for (let i = 0; i < rate; i++) {
const sample = fn(i, rate) * 32768;
const low = sample & 255;
const high = (sample >> 8) & 255;
buf.push(low, high);
};
// convert to blob
const blob = new Blob([new Uint8Array(buf)], {type: "audio/wav"});
// make url
return URL.createObjectURL(blob);
};
const audio = document.getElementById("sound");
const graph = document.getElementById("graph");
// set the source
audio.src = gen(44100, soundfun);
graph.setAttribute("d", plot(44100, soundfun));
</script>