diff --git a/index.html b/index.html
index c27548e..3bc0bb7 100644
--- a/index.html
+++ b/index.html
@@ -17,7 +17,6 @@ todo:
- make display even more faithfully FT2-like
- render instrument list in ft2 font
- oscilloscope trigger on zero crossing or something
- - multi-sample instruments
- missing XM effects:
- E3x, E4x, E5x, E6x, E7x, E9x, EDx, EEx
- 7xy - tremolo
diff --git a/xm.js b/xm.js
index 6622528..b4e90fd 100644
--- a/xm.js
+++ b/xm.js
@@ -154,7 +154,7 @@ function UpdateChannelPeriod(ch, period) {
}
function PeriodForNote(ch, note) {
- return 1920 - note*16 - ch.inst.fine / 8.0;
+ return 1920 - (note + ch.samp.note)*16 - ch.samp.fine / 8.0;
}
var audio_events = [];
@@ -252,8 +252,10 @@ function next_row() {
ch.inst = inst;
// retrigger unless overridden below
triggernote = true;
- ch.pan = inst.pan;
- ch.vol = inst.vol;
+ if (ch.samp) {
+ ch.vol = ch.samp.vol;
+ ch.pan = ch.samp.pan;
+ }
} else {
// console.log("invalid inst", r[i][1], instruments.length);
}
@@ -266,9 +268,12 @@ function next_row() {
triggernote = false;
} else {
if (inst != undefined) {
- var note = r[i][0] + inst.note;
+ var note = r[i][0];
ch.note = note;
triggernote = true;
+ ch.samp = inst.samples[inst.samplemap[ch.note]];
+ ch.pan = ch.samp.pan;
+ ch.vol = ch.samp.vol;
}
}
}
@@ -455,7 +460,7 @@ function MixSilenceIntoBuf(ch, start, end, dataL, dataR) {
function MixChannelIntoBuf(ch, start, end, dataL, dataR) {
var inst = ch.inst;
- var samp, sample_end;
+ var instsamp = ch.samp;
var loop = false;
var looplen = 0, loopstart = 0;
@@ -464,15 +469,15 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) {
return MixSilenceIntoBuf(ch, start, end, dataL, dataR);
}
- samp = inst.sampledata;
- sample_end = inst.len;
- if ((inst.type & 3) == 1) { // todo: support pingpong
+ var samp = instsamp.sampledata;
+ var sample_end = instsamp.len;
+ if ((instsamp.type & 3) == 1) { // todo: support pingpong
loop = true;
- loopstart = inst.loop;
- looplen = inst.looplen;
+ loopstart = instsamp.loop;
+ looplen = instsamp.looplen;
sample_end = loopstart + looplen;
}
- var samplen = inst.len;
+ var samplen = instsamp.len;
var volE = ch.volE / 64.0; // current volume envelope
var panE = 4*(ch.panE - 32); // current panning envelope
var p = panE + ch.pan - 128; // final pan
@@ -703,33 +708,34 @@ function ConvertSample(array, bits) {
// optimization: unroll short sample loops so we can run our inner mixing loop
// uninterrupted for as long as possible; this also handles pingpong loops.
-function UnrollSampleLoop(inst) {
- var nloops = ((2048 + inst.looplen - 1) / inst.looplen) | 0;
- var pingpong = inst.type & 2;
+function UnrollSampleLoop(samp) {
+ var nloops = ((2048 + samp.looplen - 1) / samp.looplen) | 0;
+ console.log(samp.looplen, nloops);
+ var pingpong = samp.type & 2;
if (pingpong) {
// make sure we have an even number of loops if we are pingponging
nloops = (nloops + 1) & (~1);
}
- var samplesiz = inst.loop + nloops * inst.looplen;
- var samp = new Float32Array(samplesiz);
- for (var i = 0; i < inst.loop; i++) {
- samp[i] = inst.sampledata[i];
+ var samplesiz = samp.loop + nloops * samp.looplen;
+ var data = new Float32Array(samplesiz);
+ for (var i = 0; i < samp.loop; i++) {
+ data[i] = samp.sampledata[i];
}
for (var j = 0; j < nloops; j++) {
if ((j&1) && pingpong) {
- for (var k = inst.looplen - 1; k >= 0; k--) {
- samp[i++] = inst.sampledata[inst.loop + k];
+ for (var k = samp.looplen - 1; k >= 0; k--) {
+ data[i++] = samp.sampledata[samp.loop + k];
}
} else {
- for (var k = 0; k < inst.looplen; k++) {
- samp[i++] = inst.sampledata[inst.loop + k];
+ for (var k = 0; k < samp.looplen; k++) {
+ data[i++] = samp.sampledata[samp.loop + k];
}
}
}
- console.log("unrolled sample loop", inst.number, "looplen", inst.looplen, "x", nloops, " = ", samplesiz);
- inst.sampledata = samp;
- inst.looplen = nloops * inst.looplen;
- inst.type = 1;
+ console.log("unrolled sample loop; looplen", samp.looplen, "x", nloops, " = ", samplesiz);
+ samp.sampledata = data;
+ samp.looplen = nloops * samp.looplen;
+ samp.type = 1;
}
function playXM(arrayBuf) {
@@ -829,6 +835,10 @@ function playXM(arrayBuf) {
var instname = getstring(dv, idx+0x4, 22);
var nsamp = dv.getUint16(idx+0x1b, true);
if (nsamp > 0) {
+ // return a slice so we have a fresh copy and don't retain pointers to
+ // the original xm file arraybuf forever
+ var samplemap = new Uint8Array(arrayBuf, idx+33, 96).slice();
+
var env_nvol = dv.getUint8(idx+225);
var env_vol_type = dv.getUint8(idx+233);
var env_vol_sustain = dv.getUint8(idx+227);
@@ -855,29 +865,48 @@ function playXM(arrayBuf) {
hdrsiz, i, instname, nsamp, samphdrsiz);
idx += hdrsiz;
var totalsamples = 0;
+ var samps = [];
for (var j = 0; j < nsamp; j++) {
var samplen = dv.getUint32(idx, true);
- if (j == 0) {
- var samplen0 = samplen; // FIXME HACK HACK HACK
- 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.getInt8(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(%d) finetune %d pan %d",
- samptype, prettify_note(sampnote + 12*4), sampnote, sampfinetune, samppan);
- console.log(" vol env", env_vol, env_vol_sustain,
- env_vol_loop_start, env_vol_loop_end, "type", env_vol_type,
- "fadeout", vol_fadeout);
- console.log(" pan env", env_pan, env_pan_sustain,
- env_pan_loop_start, env_pan_loop_end, "type", env_pan_type);
+ 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.getInt8(idx+16);
+ var sampname = getstring(dv, idx+18, 22);
+ var sampleoffset = idx + samphdrsiz + totalsamples;
+ 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(%d) finetune %d pan %d",
+ samptype, prettify_note(sampnote + 12*4), sampnote, sampfinetune, samppan);
+ console.log(" vol env", env_vol, env_vol_sustain,
+ env_vol_loop_start, env_vol_loop_end, "type", env_vol_type,
+ "fadeout", vol_fadeout);
+ console.log(" pan env", env_pan, env_pan_sustain,
+ env_pan_loop_start, env_pan_loop_end, "type", env_pan_type);
+ var samp = {
+ 'len': samplen, 'loop': samploop,
+ 'looplen': samplooplen, 'note': sampnote, 'fine': sampfinetune,
+ 'pan': samppan, 'type': samptype, 'vol': sampvol,
+ 'fine': sampfinetune,
+ 'sampledata': ConvertSample(
+ new Uint8Array(arrayBuf, sampleoffset, samplen), samptype & 16),
+ };
+ // length / pointers are all specified in bytes; fixup for 16-bit samples
+ if (samptype & 16) {
+ samp.len /= 2;
+ samp.loop /= 2;
+ samp.looplen /= 2;
}
+
+ // unroll short loops and any pingpong loops
+ if ((samp.type & 3) && (samp.looplen < 2048 || (samp.type & 2))) {
+ UnrollSampleLoop(samp);
+ }
+ samps.push(samp);
+
idx += samphdrsiz;
totalsamples += samplen;
}
@@ -885,24 +914,10 @@ function playXM(arrayBuf) {
inst = {
'name': instname,
'number': i,
- 'len': samplen0, 'loop': samploop,
- 'looplen': samplooplen, 'note': sampnote, 'fine': sampfinetune,
- 'pan': samppan, 'type': samptype, 'vol': sampvol,
- 'fine': sampfinetune,
'fadeout': vol_fadeout,
- 'sampledata': ConvertSample(new Uint8Array(arrayBuf, sampleoffset, samplen0), samptype & 16),
+ 'samplemap': samplemap,
+ 'samples': samps,
};
- if (samptype & 16) {
- inst.len /= 2;
- inst.loop /= 2;
- inst.looplen /= 2;
- }
-
- // unroll short loops and any pingpong loops
- if ((inst.type & 3) && (inst.looplen < 2048 || (inst.type & 2))) {
- UnrollSampleLoop(inst);
- }
-
if (env_vol_type) {
// insert an automatic fadeout to 0 at the end of the envelope
var env_end_tick = env_vol[env_vol.length-2];