Tuesday, September 13, 2016

JavaScript says "beep"

UPDATE: it's now a Gist.

Today's snippet: generating a simple musical tone purely from JavaScript. I saw some online samples for doing it on a server (e. g. in PHP) and sending to the client, but that's a waste of bandwidth.

The ingredients are twofold:
  • An <audio> element with a source that's populated from a data: URI
  • An ArrayBuffer object with WAV data inside
There's a concise description of the WAV format here. My implementation limits the format to 16-bit mono PCM. The following JavaScript returns a data: URI with a WAV file with a sine wave that corresponds to a single tone:


function GenerateTone(frequency, duration, volume, rate)
{
    if (!volume)
        volume = 30000;
    if (!rate)
        rate = 8000;

    var nSamples = rate * duration,
        i, w = (2 * Math.PI * frequency) / rate,
        wav = new ArrayBuffer(44 + nSamples*2);
    (new Int32Array(wav, 0, 11)).set(
        [0x46464952, 36 + nSamples*2, 0x45564157,
        0x20746d66, 16, 0x10001, rate, rate*2, 0x100002,
        0x61746164, nSamples*2], 0);
    var samples = new Int16Array(wav, 44, nSamples);
    for (i = 0; i < nSamples; i++)
        samples[i] = volume * Math.sin(w * i);
    return "data:audio/wav;base64," + ToBase64(wav);
}

The parameters are:
  • frequency in Hz
  • duration in seconds
  • volume - max is 32768, min is 0
  • rate - in samples per second, default 8000 is enough for simple beeps
That's it. All that remains is feeding that data to an <audio> element. Assuming you have an <audio id="MyAudio"> element on the page:

var audio = document.getElementById("MyAudio");
audio.src = GenerateTone(349.23, 0.5); //The F note
audio.play();

The frequencies of piano notes can be found here. For high pitches you'd want a sample rate higher than 8000.

One missing bit in the code above is the ToBase64() function that takes an ArrayBuffer and returns its Base64 representation. There any many samples for this on the Net, and I expect it to become a part of DOM eventually, but for completeness' sake, here's one implementation I've been using:

function ToBase64(arrayBuffer)
{
    var s = "", a, b, c, d, chunk;
    var alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

    var bytes = new Uint8Array(arrayBuffer)
    var byteLength = bytes.byteLength;
    var byteRemainder = byteLength % 3;
    var mainLength = byteLength - byteRemainder;

    for (var i = 0; i < mainLength; i += 3)
    {
        chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
        a = (chunk & 16515072) >> 18;
        b = (chunk & 258048) >> 12;
        c = (chunk & 4032) >> 6;
        d = chunk & 63;
        s += alpha[a] + alpha[b] + alpha[c] + alpha[d];
    }

    if (byteRemainder == 1)
    {
        chunk = bytes[mainLength];
        a = (chunk & 252) >> 2;
        b = (chunk & 3) << 4;
        s += alpha[a] + alpha[b] + "==";
    }
    else if (byteRemainder == 2)
    {
        chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
        a = (chunk & 64512) >> 10;
        b = (chunk & 1008) >> 4;
        c = (chunk & 15) << 2;
        s += alpha[a] + alpha[b] + alpha[c] + "=";
    }
  
    return s;
}


This code will not work on big-endian machines. JavaScript's IntArrays use the endianness of the underlying CPU, while the WAV format assumes little-endian integers.