diff --git a/index.html b/index.html index 3e1123e..60eb037 100644 --- a/index.html +++ b/index.html @@ -1,132 +1,13 @@
todo: - - load xm using XHR + - load instruments + - load samples + - open audio and start generating
diff --git a/xm.js b/xm.js new file mode 100644 index 0000000..48fe5eb --- /dev/null +++ b/xm.js @@ -0,0 +1,242 @@ +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);