242 lines
7.2 KiB
JavaScript
242 lines
7.2 KiB
JavaScript
var _note_names = ["C-", "C#", "D-", "D#", "E-", "F-", "F#", "G-", "G#", "A-", "A#", "B-"];
|
|
function prettify_note(note) {
|
|
if (note <= 0) return "---";
|
|
if (note == 97) return "^^^";
|
|
note += 11;
|
|
return _note_names[note%12] + ~~(note/12);
|
|
}
|
|
|
|
function prettify_number(num) {
|
|
if (num == -1) return "--";
|
|
if (num < 10) return "0" + num;
|
|
return num;
|
|
}
|
|
|
|
function prettify_effect(t, p) {
|
|
if (t == -1) t = "-"; else t = t.toString(16);
|
|
if (p == -1) p = "--";
|
|
else if (p < 16) p = '0' + p.toString(16);
|
|
else p = p.toString(16);
|
|
return "" + t + p
|
|
}
|
|
|
|
function prettify_notedata(note, inst, vol, efftype, effparam) {
|
|
return (prettify_note(note) + " " + prettify_number(inst) + " "
|
|
+ prettify_number(vol) + " "
|
|
+ prettify_effect(efftype, effparam));
|
|
}
|
|
|
|
function getstring(dv, offset, len) {
|
|
var str = []
|
|
for (var i = offset; i < offset+len; i++) {
|
|
var c = dv.getUint8(i);
|
|
if (c == 0) break;
|
|
str.push(String.fromCharCode(c));
|
|
}
|
|
return str.join('');
|
|
}
|
|
|
|
channelinfo = []
|
|
|
|
cur_songpos = -1, cur_pat = -1, cur_row = 64, cur_ticksamp = 0;
|
|
function next_row(f_smp) {
|
|
if (cur_row >= 64) {
|
|
cur_row = 0;
|
|
cur_songpos++;
|
|
if (cur_songpos >= songpats.length)
|
|
cur_songpos = song_looppos;
|
|
cur_pat = songpats[cur_songpos];
|
|
}
|
|
var p = patterns[cur_pat];
|
|
var r = p[cur_row];
|
|
cur_row++;
|
|
for (var i = 0; i < cur_row.length; i++) {
|
|
// instrument trigger
|
|
if (r[i][1]) channelinfo[i].inst = r[i][1];
|
|
// note trigger
|
|
if (r[i][0]) {
|
|
if (r[i][0] == 97) {
|
|
// release note, FIXME once envelopes are implemented
|
|
} else {
|
|
// assume linear frequency table (flags header & 1 == 1)
|
|
var inst = channelinfo[i].inst;
|
|
var period = 7680 - r[i][0]*64 - instruments[inst].fine*0.5;
|
|
var freq = 8363 * ((4608 - period) / 768);
|
|
channelinfo[i].doff = freq / f_smp;
|
|
channelinfo[i].off = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function next_tick(f_smp) {
|
|
// blah
|
|
}
|
|
|
|
function audio_cb(e) {
|
|
var f_smp = audioctx.sampleRate;
|
|
var buflen = e.outputBuffer.length;
|
|
var dataL = e.outputBuffer.getChannelData(0);
|
|
var dataR = e.outputBuffer.getChannelData(1);
|
|
var offset = 0;
|
|
var ticklen = f_smp * 2.5 / bpm;
|
|
|
|
while(buflen > 0) {
|
|
if (cur_ticksamp >= ticklen) {
|
|
next_tick(f_smp);
|
|
cur_ticksamp -= ticklen;
|
|
}
|
|
var tickduration = Math.min(buflen, ticklen - cur_ticksamp);
|
|
for (var i = 0; i < tickduration; i++) {
|
|
for (var j = 0; j < nchan; j++) {
|
|
}
|
|
}
|
|
|
|
cur_ticksamp += tickduration;
|
|
buflen -= tickduration;
|
|
}
|
|
console.log("buflen %d f_smp %d", buflen, f_smp);
|
|
}
|
|
|
|
function playXM(arrayBuf) {
|
|
var dv = new DataView(arrayBuf);
|
|
window.dv = dv;
|
|
|
|
var name = getstring(dv, 17, 20);
|
|
var hlen = dv.getUint32(0x3c, true) + 0x3c;
|
|
var songlen = dv.getUint16(0x40, true);
|
|
song_looppos = dv.getUint16(0x42, true);
|
|
nchan = dv.getUint16(0x44, true);
|
|
var npat = dv.getUint16(0x46, true);
|
|
var ninst = dv.getUint16(0x48, true);
|
|
var flags = dv.getUint16(0x4a, true);
|
|
tempo = dv.getUint16(0x4c, true);
|
|
bpm = dv.getUint16(0x4e, true);
|
|
for (var i = 0; i < nchan; i++) {
|
|
channelinfo.push({})
|
|
}
|
|
console.log("header len " + hlen);
|
|
|
|
console.log("songlen %d, %d channels, %d patterns, %d instruments", songlen, nchan, npat, ninst);
|
|
console.log("loop @%d", song_looppos);
|
|
console.log("flags=%d tempo %d bpm %d", flags, tempo, bpm);
|
|
|
|
songpats = [];
|
|
for (var i = 0; i < songlen; i++) {
|
|
songpats.push(dv.getUint8(0x50 + i));
|
|
}
|
|
console.log("song patterns: ", songpats);
|
|
|
|
var idx = hlen;
|
|
patterns = [];
|
|
for (var i = 0; i < npat; i++) {
|
|
var pattern = []
|
|
var patheaderlen = dv.getUint32(idx, true);
|
|
var patrows = dv.getUint16(idx + 5, true);
|
|
var patsize = dv.getUint16(idx + 7, true);
|
|
console.log("pattern %d: %d bytes, %d rows", i, patsize, patrows);
|
|
idx += 9;
|
|
for (var j = 0; j < patrows; j++) {
|
|
row = [];
|
|
pretty_row = [];
|
|
for (var k = 0; k < nchan; k++) {
|
|
var byte0 = dv.getUint8(idx); idx++;
|
|
var note = -1, inst = -1, vol = -1, efftype = -1, effparam = -1;
|
|
if (byte0 & 0x80) {
|
|
if (byte0 & 0x01) {
|
|
note = dv.getUint8(idx); idx++;
|
|
}
|
|
if (byte0 & 0x02) {
|
|
inst = dv.getUint8(idx); idx++;
|
|
}
|
|
if (byte0 & 0x04) {
|
|
vol = dv.getUint8(idx); idx++;
|
|
}
|
|
if (byte0 & 0x08) {
|
|
efftype = dv.getUint8(idx); idx++;
|
|
}
|
|
if (byte0 & 0x10) {
|
|
effparam = dv.getUint8(idx); idx++;
|
|
}
|
|
} else {
|
|
note = byte0;
|
|
inst = dv.getUint8(idx); idx++;
|
|
vol = dv.getUint8(idx); idx++;
|
|
efftype = dv.getUint8(idx); idx++;
|
|
effparam = dv.getUint8(idx); idx++;
|
|
}
|
|
pretty_row.push(prettify_notedata(note, inst, vol, efftype, effparam));
|
|
row.push([note, inst, vol, efftype, effparam]);
|
|
}
|
|
if (i == 0)
|
|
console.log(pretty_row.join(" "));
|
|
pattern.push(row);
|
|
}
|
|
patterns.push(pattern);
|
|
}
|
|
|
|
instruments = []
|
|
// now load instruments
|
|
for (i = 0; i < ninst; i++) {
|
|
var hdrsiz = dv.getUint32(idx, true);
|
|
var instname = getstring(dv, idx+0x4, 22);
|
|
var nsamp = dv.getUint16(idx+0x1b, true);
|
|
if (nsamp > 0) {
|
|
// FIXME: ignoring keymaps for now and assuming 1 sample / instrument
|
|
// var keymap = getarray(dv, idx+0x21);
|
|
var samphdrsiz = dv.getUint32(idx+0x1d, true);
|
|
console.log("hdrsiz %d; instrument %d: '%s' %d samples, samphdrsiz %d",
|
|
hdrsiz, i, instname, nsamp, samphdrsiz);
|
|
idx += hdrsiz;
|
|
for (var j = 0; j < nsamp; j++) {
|
|
var samplen = dv.getUint32(idx, true);
|
|
var samploop = dv.getUint32(idx+4, true);
|
|
var samplooplen = dv.getUint32(idx+8, true);
|
|
var sampvol = dv.getUint8(idx+12);
|
|
var sampfinetune = dv.getInt8(idx+13);
|
|
var samptype = dv.getUint8(idx+14);
|
|
var samppan = dv.getUint8(idx+15);
|
|
var sampnote = dv.getUint8(idx+16);
|
|
var sampname = getstring(dv, idx+18, 22);
|
|
var sampleoffset = idx + samphdrsiz;
|
|
console.log("sample %d: len %d name '%s' loop %d/%d vol %d",
|
|
j, samplen, sampname, samploop, samplooplen, sampvol);
|
|
console.log(" type %d note %s finetune %d pan %d",
|
|
samptype, prettify_note(sampnote), sampfinetune, samppan);
|
|
idx += samplen + samphdrsiz;
|
|
}
|
|
instruments.push({
|
|
'name': instname,
|
|
'off': sampleoffset, 'len': samplen, 'loop': samploop,
|
|
'looplen': samplooplen, 'note': sampnote, 'fine': sampfinetune,
|
|
'pan': samppan})
|
|
} else {
|
|
instruments.push(null);
|
|
}
|
|
}
|
|
|
|
audioctx = new webkitAudioContext();
|
|
gainNode = audioctx.createGain();
|
|
gainNode.gain.value = 1.0; // master volume
|
|
jsNode = audioctx.createScriptProcessor(4096, 0, 2);
|
|
jsNode.onaudioprocess = audio_cb;
|
|
// jsNode.connect(gainNode);
|
|
|
|
var debug = document.getElementById("debug");
|
|
console.log("loaded \"" + name + "\"");
|
|
debug.innerHTML = name;
|
|
|
|
// start playing
|
|
gainNode.connect(audioctx.destination);
|
|
}
|
|
|
|
var xmReq = new XMLHttpRequest();
|
|
xmReq.open("GET", "kamel.xm", true);
|
|
xmReq.responseType = "arraybuffer";
|
|
xmReq.onload = function (xmEvent) {
|
|
var arrayBuffer = xmReq.response;
|
|
if (arrayBuffer) {
|
|
playXM(arrayBuffer);
|
|
}
|
|
}
|
|
xmReq.send(null);
|