Posts Tagged ‘html5’

A JavaScript spectrum analyzer

My friend asked me if I could make a music player with a spectrum analyzer for his web page. I’m like, I dunno? Can I?

I can make a spectrum analyzer in my sleep, and browsers can do some audio stuff these days, right? So, maybe?

I can make a spectrum analyzer in my sleep

Here’s how you make a spectrum analyzer. First, run your original signal through a few parallel bandpass filters. The range of human hearing spans about 10 octaves, from 20 Hz to 20k. If we want a 5-band display, we’ll place our bandpass filters at 2-octave intervals. This gives us centers at 40 Hz, 160 Hz, 640 Hz, 2.5k, and 10k.

Filter design is super interesting and fairly complex, but implementing a basic bandpass filter is shockingly simple.

Now we’ve got five (or whatever) new audio signals. We compute the power of each band and draw a bar for each one. There are some details around how you compute the actual size of the bars to make them look good, but that’s the main idea.

Easy! NEXT

Browsers can do some audio stuff these days, right?

Yes, but with difficulty. Dropping in an audio player is straightforward with the audio tag. Codecs are a bit muddled– Firefox won’t play MP3; Safari won’t play Vorbis. But in the grand scheme of browser quirks, this one is charmingly easy to deal with.

Here’s how you implement it:

<audio controls preload="auto">
<source src="/spectest/metal_star.ogg">
<source src="/spectest/metal_star.mp3">
</audio>

We specify both sources so that all browsers can play at least one. And presto:

This is cute, but to add a spectrum analyzer to our player, we need access to the raw PCM stream. The problem is thorny.

In Firefox

Firefox’s Audio Data API makes it easy. Your audio element will fire a MozAudioAvailable event for each little chunk of sound it plays. Create an event listener, and your callback can process the stream however you like. Like this (I use Cobra cause I like it):

var Biquad = new Cobra.Class({
  __init__: function(self, freq, q) {
    var omega = 2*Math.PI*freq/44100.0;
    var alpha = Math.sin(omega)*(2*q);

    self.b0 = alpha;
    self.b1 = 0.0;
    self.b2 = -alpha;
    self.a0 = 1 + alpha;
    self.a1 = -2*Math.cos(omega);
    self.a2 = 1 - alpha;

    self.y1 = self.y2 = self.x1 = self.x2 = 0.0;
  },

  next: function(self, x) {
    var y = (self.b0 / self.a0)*x +
      (self.b1 / self.a0)*self.x1 +
      (self.b2 / self.a0)*self.x2 -
      (self.a1 / self.a0)*self.y1 -
      (self.a2 / self.a0)*self.y2;
    self.y2 = self.y1;
    self.y1 = y;
    self.x2 = self.x1;
    self.x1 = x;
    return y;
  }
});

var bands = [
  new Biquad(40.0, 1.0),
  new Biquad(160.0, 1.0),
  new Biquad(640.0, 1.0),
  new Biquad(2560.0, 1.0),
  new Biquad(10240.0, 1.0)
];

function updateSpectrum(ev) {
  var fb = ev.frameBuffer;
  var sumsq = [0.0, 0.0, 0.0, 0.0, 0.0];

  for (var i = 0; i < fb.length/2; ++i) {
    /* average to get mono channel */
    var center = (fb[2*i] + fb[2*i+1])/2.0;

    /* feed to bp filters */
    for (var j = 0; j < 5; ++j) {
      var out = bands[j].next(center);
      sumsq[j] += out*out;
    }
  }

  for (var i = 0; i < 5; ++i) {
    /* calculate rms power */
    var rms = Math.sqrt(2.0 * sumsq[i] / fb.length);

    /* update bar */
    var db = 6.0 * Math.log(rms) / Math.log(2.0);
    var length = 450 + 10.0*db;
    if (length < 1) {
      length = 1;
    }
    var bar = document.getElementById("bar" + i);
    bar.style.width = length + "px";
  }
}

var audio = document.getElementById("player");
audio.addEventListener("MozAudioAvailable", updateSpectrum, false);

You can see it in action here.

Brilliant! Hopefully WebKit provides an equally easy solution.

In WebKit

Nope! WebKit is implementing the Web Audio API. This API sorta works like a modular. You create chains of AudioNode objects which act as sound sources or processors. There are a bunch of prefab AudioNodes, or you can write your own.

Chrome implements at least part of the Web Audio API. So do Safari nightlies, and I presume Safari 6 will as well.

My plan was to connect an audio element to a custom AudioNode, which would use the same spectrum analyzer code from the Firefox example. This plan was foiled when I discovered that the MediaElementAudioSourceNode, designed to provide integration with the audio and video tags, is silently unimplemented in Chrome. You can create one, but it will just feed you a steady stream of zeroes.

For now, it appears that the preferred solution is to re-implement the audio tag yourself using an XHR and an AudioBufferSource. Which is insane.

Our updateSpectrum only changes slightly:

function updateSpectrum(ev) {
  var fb = ev.inputBuffer;
  var outb = ev.outputBuffer;
  var sumsq = [0.0, 0.0, 0.0, 0.0, 0.0];
  var inputL = fb.getChannelData(0);
  var inputR = fb.getChannelData(1);
  var outputL = outb.getChannelData(0);
  var outputR = outb.getChannelData(1);

  for (var i = 0; i < inputL.length; ++i) {
    /* average to get mono channel */
    var center = (inputL[i] + inputR[i])/2.0;

    /* copy to output */
    outputL[i] = inputL[i];
    outputR[i] = outputR[i];

    /* feed to bp filters */
    for (var j = 0; j < 5; ++j) {
      var out = bands[j].next(center);
      sumsq[j] += out*out;
    }
  }

  for (var i = 0; i < 5; ++i) {
    /* calculate rms amplitude */
    var rms = Math.sqrt(sumsq[i] / inputL.length);

    /* update bar */
    var db = 6.0 * Math.log(rms) / Math.log(2.0);
    var length = 450 + 10.0*db;
    if (length < 1) {
      length = 1;
    }
    var bar = document.getElementById("bar" + i);
    bar.style.width = length + "px";
  }
}

But we need a bunch of new rigmarole to wire up the sound:

var context = new webkitAudioContext();

var processor = context.createJavaScriptNode(512, 1, 1);
processor.onaudioprocess = updateSpectrum;

/* get the sound via http */
var req = new XMLHttpRequest();
req.open("GET", "/spectest/metal_star.mp3", true);
req.responseType = "arraybuffer";
req.onload = function() {
  var audio = context.createBuffer(req.response, false);
  var source = context.createBufferSource();
  source.buffer = audio;
  source.noteOn(0.0);
  source.connect(processor);
  processor.connect(context.destination);
}
req.send();

And here’s the Chrome edition of the demo (warning: autoplays).

Opinions

I quite like the Mozilla Audio Data API. The Web Audio API strikes me as overdesigned.

I hate Flash with the fury of a thousand suns scorned, but given this Byzantine thicket, I understand why developers use it.

Also

If you like the song in the demos, download it here.