Some time ago, I read about data URI scheme while I was absorbing HTML5, I got an idea about using <audio> to generate wave data on the fly—not meaning streaming, this is not new idea, many people have done using <embed> and <audio> already. I did it on my own anyway.

This is just the first baby step, my goal is to make a code to play a 8-bit game music. Right now, I don’t know how to do next because I don’t know what synthesis method/math those games/console use, or where to get music sheets of games. I have no talent in music, you can’t expect me to compose one. There are some Flash play 8-bit music, but I don’t know where they started from, using existing wavefont or synthesis on their own? I want to do from the fundamental.

1   Generating

The following is what I have now, Chrome/Chromium (5.0.375.9) couldn’t play it normally at this moment. Firefox 3.6.3 can play it correctly.

Function:
Frequency: Hz / Duration: seconds /
First 600 data points

It is just simple math, view source for JavaScript. I didn’t really code well, it might look messy.

If you know 8-bit game music and you also know where I can get a cheatsheet of math they use and precise parameters for wavefonts, please drop me a message!

2   Source code

In 2011, I got a request of the permission of using the code in this page, I granted the person with the permission by re-licensing this code to New BSD. A few moments ago, another request came in via comment, so I decided to resolve all the issue once and for all. Licensing the code with the MIT License and clearly stating the licenses within this post, so anyone wants to use my code can grab and do whatever they like.


<script>
// Code for Single frequency wave generation using HTML5 audio
// Link: https://yjlv.blogspot.com/2010/04/single-frequency-wave-generation-using.html
//
// Copyright (c) 2010 Yu-Jie Lin
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// The base64_encoder function code is copied and modified from my old code, re-licensed to MIT License.
// Link: https://bitbucket.org/livibetter/llbbsc/src/399aa8c2de03494457622cea0c290867c8a672e4/GoogleGadget/HTMLTool/HTMLTool.xml?at=default#cl-468
//
// Copyright (c) 2008 Yu-Jie Lin
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

function base64_encoder(b) {
// http://tools.ietf.org/html/rfc4648
var map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("");
var r = "";
for (var i = 0; i < b.length; i += 3) {
var bin = 0x000000;
// FIXME with one loop
bin |= b[i] << 16;
if (i + 1 < b.length)
bin |= b[i + 1] << 8;
if (i + 2 < b.length)
bin |= b[i + 2];
// Converting
var mask = 0xFC0000;
for (var j = 0; j < 4; j++) {
r += map[((bin & (0xFC0000 >> (j * 6))) >> ((3 - j) * 6))];
}
}
var padding = (b.length % 3 > 0) ? "==".substring(b.length % 3 - 1, 2) : "";
return r.substring(0, r.length - padding.length) + padding;
}

// Make Little Endian Bytestream String
function mk_le_bs(data, size) {
s = '';
while (size > 0) {
s += String.fromCharCode(0xFF & data);
data >>= 8;
size--;
}
return s;
}

function square(t) {
var _ = Math.sin(t);
if (_ > 0) return 1.0;
if (_ < 0) return -1.0;
return 0.0;
}

function tri(t, T) {
var a = T / 2.0;
return 2.0 * Math.abs(2.0 * (t / a - Math.floor(t / a + 0.5))) - 1.0;
}

function saw(t, T) {
return 2.0 * (t / T - Math.floor(t / T + 0.5));
}

function play() {

var t_begin = new Date().getTime();
var WAVE_FORMAT_PCM = 0x0001;
var f = parseFloat($('#freq').val());
var T = 1.0 / f;
var sample_rate = 8000;
var sample_duration = 1.0 / 8000;

var samples_per_period = Math.floor(T * sample_rate);

data = [];
var _const1 = Math.floor(sample_rate * parseFloat($('#duration').val()));
var _const2 = 2.0 * Math.PI * f * sample_duration;

var wavefunc = $('input:radio[name=wavefunc]:checked').val();

for (var idx=0; idx<_const1; idx++) {
//data.push(Math.floor(255 * ((Math.sin(2.0 * Math.PI * f * idx * sample_duration) + 1.0) / 2.0)));
switch (wavefunc) {
case 'Sine':
data.push(Math.floor(127.5 * Math.sin(idx * _const2) + 127.5));
break;
case 'Square':
data.push(Math.floor(127.5 * square(idx * _const2) + 127.5));
break;
case 'Triangle':
data.push(Math.floor(127.5 * tri(idx * sample_duration, T) + 127.5));
break;
case 'Sawtooth':
data.push(Math.floor(127.5 * saw(idx * sample_duration, T) + 127.5));
break;
}
}

var wave = $('#wave')[0];
wave.width = wave.width;
var ctx = wave.getContext('2d');
ctx.beginPath();
ctx.moveTo = (0, 255 - data[0]);
for (var idx=1; idx<Math.min(data.length, wave.width); idx++) {
ctx.lineTo(idx, 255 - data[idx]);
}
ctx.strokeStyle = "#000";
ctx.stroke();
ctx.closePath();

var s = '';

pad = (data.length % 2 == 1) ? ' ': '';
// http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
s = 'RIFF' + mk_le_bs(4 + 24 + (8 + 1 * 1 * data.length + pad.length), 4) +
'WAVE' + 'fmt ' + mk_le_bs(16, 4) +
mk_le_bs(WAVE_FORMAT_PCM, 2) +
mk_le_bs(1, 2) +
mk_le_bs(sample_rate, 4) +
mk_le_bs(sample_rate * 1 * 1, 4) +
mk_le_bs(1 * 1, 2) +
mk_le_bs(8 * 1, 2) +
'data' + mk_le_bs(1 * 1 * data.length, 4)
;

bs = [];
for (var idx=0; idx<s.length; idx++) {
bs.push(s.charCodeAt(idx));
}
bs = bs.concat(data);
if (pad.length > 0)
bs.push(0x00);

s = 'data:audio/wav;base64,' + base64_encoder(bs);
var audio = new Audio(s);
audio.play();
}
</script>

<div style="text-align: center">
Function:
<label><input type="radio" name="wavefunc" value="Sine" checked/> Sine</label>
<label><input type="radio" name="wavefunc" value="Square"/> Square</label>
<label><input type="radio" name="wavefunc" value="Triangle"/> Triangle</label>
<label><input type="radio" name="wavefunc" value="Sawtooth"/> Sawtooth</label>
<br/>
Frequency: <input id="freq" value="261.626" size="10"/> Hz / Duration: <input id="duration" value="1.0" size="5"/> seconds / <input type="button" onclick="play()" value="Play!"/>
</div>
<div style='text-align:center'>
<div><canvas id="wave" width="600" height="256" style="border: 1px solid #333;"></canvas></div>
<div>First 600 data points</div>
</div>