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>