implemented multiple samples / instrument
This commit is contained in:
parent
22dd555228
commit
c4370f447c
2 changed files with 77 additions and 63 deletions
|
|
@ -17,7 +17,6 @@ todo:
|
||||||
- make display even more faithfully FT2-like
|
- make display even more faithfully FT2-like
|
||||||
- render instrument list in ft2 font
|
- render instrument list in ft2 font
|
||||||
- oscilloscope trigger on zero crossing or something
|
- oscilloscope trigger on zero crossing or something
|
||||||
- multi-sample instruments
|
|
||||||
- missing XM effects:
|
- missing XM effects:
|
||||||
- E3x, E4x, E5x, E6x, E7x, E9x, EDx, EEx
|
- E3x, E4x, E5x, E6x, E7x, E9x, EDx, EEx
|
||||||
- 7xy - tremolo
|
- 7xy - tremolo
|
||||||
|
|
|
||||||
139
xm.js
139
xm.js
|
|
@ -154,7 +154,7 @@ function UpdateChannelPeriod(ch, period) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function PeriodForNote(ch, note) {
|
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 = [];
|
var audio_events = [];
|
||||||
|
|
@ -252,8 +252,10 @@ function next_row() {
|
||||||
ch.inst = inst;
|
ch.inst = inst;
|
||||||
// retrigger unless overridden below
|
// retrigger unless overridden below
|
||||||
triggernote = true;
|
triggernote = true;
|
||||||
ch.pan = inst.pan;
|
if (ch.samp) {
|
||||||
ch.vol = inst.vol;
|
ch.vol = ch.samp.vol;
|
||||||
|
ch.pan = ch.samp.pan;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// console.log("invalid inst", r[i][1], instruments.length);
|
// console.log("invalid inst", r[i][1], instruments.length);
|
||||||
}
|
}
|
||||||
|
|
@ -266,9 +268,12 @@ function next_row() {
|
||||||
triggernote = false;
|
triggernote = false;
|
||||||
} else {
|
} else {
|
||||||
if (inst != undefined) {
|
if (inst != undefined) {
|
||||||
var note = r[i][0] + inst.note;
|
var note = r[i][0];
|
||||||
ch.note = note;
|
ch.note = note;
|
||||||
triggernote = true;
|
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) {
|
function MixChannelIntoBuf(ch, start, end, dataL, dataR) {
|
||||||
var inst = ch.inst;
|
var inst = ch.inst;
|
||||||
var samp, sample_end;
|
var instsamp = ch.samp;
|
||||||
var loop = false;
|
var loop = false;
|
||||||
var looplen = 0, loopstart = 0;
|
var looplen = 0, loopstart = 0;
|
||||||
|
|
||||||
|
|
@ -464,15 +469,15 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) {
|
||||||
return MixSilenceIntoBuf(ch, start, end, dataL, dataR);
|
return MixSilenceIntoBuf(ch, start, end, dataL, dataR);
|
||||||
}
|
}
|
||||||
|
|
||||||
samp = inst.sampledata;
|
var samp = instsamp.sampledata;
|
||||||
sample_end = inst.len;
|
var sample_end = instsamp.len;
|
||||||
if ((inst.type & 3) == 1) { // todo: support pingpong
|
if ((instsamp.type & 3) == 1) { // todo: support pingpong
|
||||||
loop = true;
|
loop = true;
|
||||||
loopstart = inst.loop;
|
loopstart = instsamp.loop;
|
||||||
looplen = inst.looplen;
|
looplen = instsamp.looplen;
|
||||||
sample_end = loopstart + looplen;
|
sample_end = loopstart + looplen;
|
||||||
}
|
}
|
||||||
var samplen = inst.len;
|
var samplen = instsamp.len;
|
||||||
var volE = ch.volE / 64.0; // current volume envelope
|
var volE = ch.volE / 64.0; // current volume envelope
|
||||||
var panE = 4*(ch.panE - 32); // current panning envelope
|
var panE = 4*(ch.panE - 32); // current panning envelope
|
||||||
var p = panE + ch.pan - 128; // final pan
|
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
|
// optimization: unroll short sample loops so we can run our inner mixing loop
|
||||||
// uninterrupted for as long as possible; this also handles pingpong loops.
|
// uninterrupted for as long as possible; this also handles pingpong loops.
|
||||||
function UnrollSampleLoop(inst) {
|
function UnrollSampleLoop(samp) {
|
||||||
var nloops = ((2048 + inst.looplen - 1) / inst.looplen) | 0;
|
var nloops = ((2048 + samp.looplen - 1) / samp.looplen) | 0;
|
||||||
var pingpong = inst.type & 2;
|
console.log(samp.looplen, nloops);
|
||||||
|
var pingpong = samp.type & 2;
|
||||||
if (pingpong) {
|
if (pingpong) {
|
||||||
// make sure we have an even number of loops if we are pingponging
|
// make sure we have an even number of loops if we are pingponging
|
||||||
nloops = (nloops + 1) & (~1);
|
nloops = (nloops + 1) & (~1);
|
||||||
}
|
}
|
||||||
var samplesiz = inst.loop + nloops * inst.looplen;
|
var samplesiz = samp.loop + nloops * samp.looplen;
|
||||||
var samp = new Float32Array(samplesiz);
|
var data = new Float32Array(samplesiz);
|
||||||
for (var i = 0; i < inst.loop; i++) {
|
for (var i = 0; i < samp.loop; i++) {
|
||||||
samp[i] = inst.sampledata[i];
|
data[i] = samp.sampledata[i];
|
||||||
}
|
}
|
||||||
for (var j = 0; j < nloops; j++) {
|
for (var j = 0; j < nloops; j++) {
|
||||||
if ((j&1) && pingpong) {
|
if ((j&1) && pingpong) {
|
||||||
for (var k = inst.looplen - 1; k >= 0; k--) {
|
for (var k = samp.looplen - 1; k >= 0; k--) {
|
||||||
samp[i++] = inst.sampledata[inst.loop + k];
|
data[i++] = samp.sampledata[samp.loop + k];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (var k = 0; k < inst.looplen; k++) {
|
for (var k = 0; k < samp.looplen; k++) {
|
||||||
samp[i++] = inst.sampledata[inst.loop + k];
|
data[i++] = samp.sampledata[samp.loop + k];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log("unrolled sample loop", inst.number, "looplen", inst.looplen, "x", nloops, " = ", samplesiz);
|
console.log("unrolled sample loop; looplen", samp.looplen, "x", nloops, " = ", samplesiz);
|
||||||
inst.sampledata = samp;
|
samp.sampledata = data;
|
||||||
inst.looplen = nloops * inst.looplen;
|
samp.looplen = nloops * samp.looplen;
|
||||||
inst.type = 1;
|
samp.type = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function playXM(arrayBuf) {
|
function playXM(arrayBuf) {
|
||||||
|
|
@ -829,6 +835,10 @@ function playXM(arrayBuf) {
|
||||||
var instname = getstring(dv, idx+0x4, 22);
|
var instname = getstring(dv, idx+0x4, 22);
|
||||||
var nsamp = dv.getUint16(idx+0x1b, true);
|
var nsamp = dv.getUint16(idx+0x1b, true);
|
||||||
if (nsamp > 0) {
|
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_nvol = dv.getUint8(idx+225);
|
||||||
var env_vol_type = dv.getUint8(idx+233);
|
var env_vol_type = dv.getUint8(idx+233);
|
||||||
var env_vol_sustain = dv.getUint8(idx+227);
|
var env_vol_sustain = dv.getUint8(idx+227);
|
||||||
|
|
@ -855,29 +865,48 @@ function playXM(arrayBuf) {
|
||||||
hdrsiz, i, instname, nsamp, samphdrsiz);
|
hdrsiz, i, instname, nsamp, samphdrsiz);
|
||||||
idx += hdrsiz;
|
idx += hdrsiz;
|
||||||
var totalsamples = 0;
|
var totalsamples = 0;
|
||||||
|
var samps = [];
|
||||||
for (var j = 0; j < nsamp; j++) {
|
for (var j = 0; j < nsamp; j++) {
|
||||||
var samplen = dv.getUint32(idx, true);
|
var samplen = dv.getUint32(idx, true);
|
||||||
if (j == 0) {
|
var samploop = dv.getUint32(idx+4, true);
|
||||||
var samplen0 = samplen; // FIXME HACK HACK HACK
|
var samplooplen = dv.getUint32(idx+8, true);
|
||||||
var samploop = dv.getUint32(idx+4, true);
|
var sampvol = dv.getUint8(idx+12);
|
||||||
var samplooplen = dv.getUint32(idx+8, true);
|
var sampfinetune = dv.getInt8(idx+13);
|
||||||
var sampvol = dv.getUint8(idx+12);
|
var samptype = dv.getUint8(idx+14);
|
||||||
var sampfinetune = dv.getInt8(idx+13);
|
var samppan = dv.getUint8(idx+15);
|
||||||
var samptype = dv.getUint8(idx+14);
|
var sampnote = dv.getInt8(idx+16);
|
||||||
var samppan = dv.getUint8(idx+15);
|
var sampname = getstring(dv, idx+18, 22);
|
||||||
var sampnote = dv.getInt8(idx+16);
|
var sampleoffset = idx + samphdrsiz + totalsamples;
|
||||||
var sampname = getstring(dv, idx+18, 22);
|
console.log("sample %d: len %d name '%s' loop %d/%d vol %d",
|
||||||
var sampleoffset = idx + samphdrsiz;
|
j, samplen, sampname, samploop, samplooplen, sampvol);
|
||||||
console.log("sample %d: len %d name '%s' loop %d/%d vol %d",
|
console.log(" type %d note %s(%d) finetune %d pan %d",
|
||||||
j, samplen, sampname, samploop, samplooplen, sampvol);
|
samptype, prettify_note(sampnote + 12*4), sampnote, sampfinetune, samppan);
|
||||||
console.log(" type %d note %s(%d) finetune %d pan %d",
|
console.log(" vol env", env_vol, env_vol_sustain,
|
||||||
samptype, prettify_note(sampnote + 12*4), sampnote, sampfinetune, samppan);
|
env_vol_loop_start, env_vol_loop_end, "type", env_vol_type,
|
||||||
console.log(" vol env", env_vol, env_vol_sustain,
|
"fadeout", vol_fadeout);
|
||||||
env_vol_loop_start, env_vol_loop_end, "type", env_vol_type,
|
console.log(" pan env", env_pan, env_pan_sustain,
|
||||||
"fadeout", vol_fadeout);
|
env_pan_loop_start, env_pan_loop_end, "type", env_pan_type);
|
||||||
console.log(" pan env", env_pan, env_pan_sustain,
|
var samp = {
|
||||||
env_pan_loop_start, env_pan_loop_end, "type", env_pan_type);
|
'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;
|
idx += samphdrsiz;
|
||||||
totalsamples += samplen;
|
totalsamples += samplen;
|
||||||
}
|
}
|
||||||
|
|
@ -885,24 +914,10 @@ function playXM(arrayBuf) {
|
||||||
inst = {
|
inst = {
|
||||||
'name': instname,
|
'name': instname,
|
||||||
'number': i,
|
'number': i,
|
||||||
'len': samplen0, 'loop': samploop,
|
|
||||||
'looplen': samplooplen, 'note': sampnote, 'fine': sampfinetune,
|
|
||||||
'pan': samppan, 'type': samptype, 'vol': sampvol,
|
|
||||||
'fine': sampfinetune,
|
|
||||||
'fadeout': vol_fadeout,
|
'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) {
|
if (env_vol_type) {
|
||||||
// insert an automatic fadeout to 0 at the end of the envelope
|
// insert an automatic fadeout to 0 at the end of the envelope
|
||||||
var env_end_tick = env_vol[env_vol.length-2];
|
var env_end_tick = env_vol[env_vol.length-2];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue