Then the obvious question: How do you play back the generated waveform in a browser? A few options exist:
- Use the Mozilla Audio Data API.
- Use the Web Audio API.
- Use the HTML 5 audio element with a fat data URI.
- Support all of the above.
Since option 3 is the only API that’s currently widely supported (and option 4 would make the program larger and more complex) I decided to go with the data URI solution. It basically goes like this:
- Convert the wave data to a string-of-bytes in proper RIFF WAVE format (i.e. prepend a header and ensure little endian byte ordering).
- Convert the WAVE formatted string to base64 encoding using the btoa() function.
- Prepend a data URI header: “data:audio/wav;base64,”
And yes, it works!
What I found out, though, was that the synth ate huge amounts of memory (between 1.5 and 4 GB, depending on browser!). The main reason was that I was using regular ECMAScript arrays for the wave data. There are some differences in how browsers deal with such arrays, but they all have in common that they use more memory per element than the size of the actual data (in ECMAScript, every array element is essentially an object). Add to that the fact that numbers in ECMAScript are treated as doubles (i.e. 8 bytes per number)…
The solution to the problem spells typed arrays. Unfortunately, typed arrays are not that common (yet). The current driving force for typed arrays seems to be WebGL, which is just starting to emerge. But there is one kind of typed array available in browsers that support the canvas element: the CanvasPixelArray.
It may seem like an awkward thing to do, but by using the createImageData(w,h) method (on a 2D canvas context), you get a byte array with w*h*4 elements. This byte array is packed (N array elements cost N bytes of RAM). There were two issues, though:
- I wanted 16-bit sound data, so I had to do manual packing and unpacking of the elements (store one sound sample in two array elements). This required some more code, but on the whole turned out to be faster than regular arrays anyway.
- Some browsers (hrrr-o-mmm-e) don’t support image sizes exceeding 32767 x 32767, meaning that I couldn’t request an N x 1 buffer. The straight forward solution was to use a ceil(sqrt(N)) x ceil(sqrt(N)) buffer. Since the array has linear access, this didn’t introduce any more complexity (just some wasted memory).
You can try out the result for yourself here.