unrolled main mixing loop
This commit is contained in:
parent
efec2d7273
commit
5645739edb
2 changed files with 139 additions and 64 deletions
|
|
@ -7,7 +7,6 @@
|
||||||
code: <a href="http://github.com/a1k0n/jsxm/">github.com/a1k0n/jsxm</a>
|
code: <a href="http://github.com/a1k0n/jsxm/">github.com/a1k0n/jsxm</a>
|
||||||
tune: my dirty old kamel - <a href="http://zalza.bandcamp.com">Zalza</a>
|
tune: my dirty old kamel - <a href="http://zalza.bandcamp.com">Zalza</a>
|
||||||
todo:
|
todo:
|
||||||
- unroll mixer code loops
|
|
||||||
- multi-sample instruments
|
- multi-sample instruments
|
||||||
- missing XM effects (in rough order of priority):
|
- missing XM effects (in rough order of priority):
|
||||||
- Rxy - retrigger
|
- Rxy - retrigger
|
||||||
|
|
|
||||||
202
xm.js
202
xm.js
|
|
@ -262,7 +262,11 @@ function next_tick() {
|
||||||
if (cur_tick != 0 && ch.effectfn) {
|
if (cur_tick != 0 && ch.effectfn) {
|
||||||
ch.effectfn(ch);
|
ch.effectfn(ch);
|
||||||
}
|
}
|
||||||
if (inst === undefined) continue;
|
if (inst == undefined) continue;
|
||||||
|
if (ch.env_vol == undefined) {
|
||||||
|
console.log("channel", j, "env_vol defined but not inst?", ch);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
ch.volE = ch.env_vol.Tick(ch.release, 64);
|
ch.volE = ch.env_vol.Tick(ch.release, 64);
|
||||||
ch.panE = ch.env_pan.Tick(ch.release, 32);
|
ch.panE = ch.env_pan.Tick(ch.release, 32);
|
||||||
UpdateChannelPeriod(ch, ch.period + ch.periodoffset);
|
UpdateChannelPeriod(ch, ch.period + ch.periodoffset);
|
||||||
|
|
@ -299,7 +303,7 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) {
|
||||||
var inst = ch.inst;
|
var inst = ch.inst;
|
||||||
var samp, sample_end;
|
var samp, sample_end;
|
||||||
var loop = false;
|
var loop = false;
|
||||||
var looplen = 0;
|
var looplen = 0, loopstart = 0;
|
||||||
|
|
||||||
// nothing on this channel, just filter the last dc offset back down to zero
|
// nothing on this channel, just filter the last dc offset back down to zero
|
||||||
if (inst == undefined || ch.mute) {
|
if (inst == undefined || ch.mute) {
|
||||||
|
|
@ -310,8 +314,9 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) {
|
||||||
sample_end = inst.len;
|
sample_end = inst.len;
|
||||||
if ((inst.type & 3) == 1) { // todo: support pingpong
|
if ((inst.type & 3) == 1) { // todo: support pingpong
|
||||||
loop = true;
|
loop = true;
|
||||||
|
loopstart = inst.loop;
|
||||||
looplen = inst.looplen;
|
looplen = inst.looplen;
|
||||||
sample_end = looplen + inst.loop;
|
sample_end = loopstart + looplen;
|
||||||
}
|
}
|
||||||
var samplen = inst.len;
|
var samplen = inst.len;
|
||||||
var volE = ch.volE / 64.0; // current volume envelope
|
var volE = ch.volE / 64.0; // current volume envelope
|
||||||
|
|
@ -330,50 +335,125 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) {
|
||||||
var k = ch.off;
|
var k = ch.off;
|
||||||
var dk = ch.doff;
|
var dk = ch.doff;
|
||||||
var Vrms = 0;
|
var Vrms = 0;
|
||||||
for (var i = start; i < end; i++) {
|
var f0 = ch.filter[0], f1 = ch.filter[1], f2 = ch.filter[2];
|
||||||
|
var fs0 = ch.filterstate[0], fs1 = ch.filterstate[1], fs2 = ch.filterstate[2];
|
||||||
|
|
||||||
|
// we also low-pass filter volume changes with a simple one-zero,
|
||||||
|
// one-pole filter to avoid pops and clicks when volume changes.
|
||||||
|
var vL = popfilter_alpha * ch.vL + (1 - popfilter_alpha) * (volL + ch.vLprev) * 0.5;
|
||||||
|
var vR = popfilter_alpha * ch.vR + (1 - popfilter_alpha) * (volR + ch.vRprev) * 0.5;
|
||||||
|
var pf_8 = Math.pow(popfilter_alpha, 8);
|
||||||
|
ch.vLprev = volL;
|
||||||
|
ch.vRprev = volR;
|
||||||
|
|
||||||
|
// we can mix up to this many bytes before running into a sample end/loop
|
||||||
|
var i = start;
|
||||||
|
while (i < end) {
|
||||||
if (k >= sample_end) { // TODO: implement pingpong looping
|
if (k >= sample_end) { // TODO: implement pingpong looping
|
||||||
if (loop) {
|
if (loop) {
|
||||||
k %= looplen;
|
k = loopstart + (k - loopstart) % looplen;
|
||||||
} else {
|
} else {
|
||||||
// kill sample
|
// kill sample
|
||||||
ch.inst = undefined;
|
ch.inst = undefined;
|
||||||
// fill rest of buf with filtered dc offset using loop above
|
// fill rest of buf with filtered dc offset using loop above
|
||||||
return Vrms + MixSilenceIntoBuf(ch, i+1, end, dataL, dataR);
|
return Vrms + MixSilenceIntoBuf(ch, i, end, dataL, dataR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var s = samp[k|0];
|
var next_event = Math.min(end, i + (sample_end - k) / dk);
|
||||||
// TODO: robustify, then remove these NaN checks from the inner loops
|
// this is the inner loop of the player
|
||||||
if (isNaN(s)) {
|
|
||||||
console.log("NaN sample idx", samp.length, k|0);
|
// unrolled 8x
|
||||||
tempo = 10000;
|
for (; i + 8 < next_event; i+=8) {
|
||||||
break;
|
var s = samp[k|0];
|
||||||
|
var y = f0 * (s + fs0) + f1*fs1 + f2*fs2;
|
||||||
|
fs2 = fs1; fs1 = y; fs0 = s;
|
||||||
|
k += dk;
|
||||||
|
dataL[i] += vL * y;
|
||||||
|
dataR[i] += vR * y;
|
||||||
|
Vrms += (vL + vR) * y * y;
|
||||||
|
|
||||||
|
var s = samp[k|0];
|
||||||
|
var y = f0 * (s + fs0) + f1*fs1 + f2*fs2;
|
||||||
|
fs2 = fs1; fs1 = y; fs0 = s;
|
||||||
|
k += dk;
|
||||||
|
dataL[i+1] += vL * y;
|
||||||
|
dataR[i+1] += vR * y;
|
||||||
|
Vrms += (vL + vR) * y * y;
|
||||||
|
|
||||||
|
var s = samp[k|0];
|
||||||
|
var y = f0 * (s + fs0) + f1*fs1 + f2*fs2;
|
||||||
|
fs2 = fs1; fs1 = y; fs0 = s;
|
||||||
|
k += dk;
|
||||||
|
dataL[i+2] += vL * y;
|
||||||
|
dataR[i+2] += vR * y;
|
||||||
|
Vrms += (vL + vR) * y * y;
|
||||||
|
|
||||||
|
var s = samp[k|0];
|
||||||
|
var y = f0 * (s + fs0) + f1*fs1 + f2*fs2;
|
||||||
|
fs2 = fs1; fs1 = y; fs0 = s;
|
||||||
|
k += dk;
|
||||||
|
dataL[i+3] += vL * y;
|
||||||
|
dataR[i+3] += vR * y;
|
||||||
|
Vrms += (vL + vR) * y * y;
|
||||||
|
|
||||||
|
var s = samp[k|0];
|
||||||
|
var y = f0 * (s + fs0) + f1*fs1 + f2*fs2;
|
||||||
|
fs2 = fs1; fs1 = y; fs0 = s;
|
||||||
|
k += dk;
|
||||||
|
dataL[i+4] += vL * y;
|
||||||
|
dataR[i+4] += vR * y;
|
||||||
|
Vrms += (vL + vR) * y * y;
|
||||||
|
|
||||||
|
var s = samp[k|0];
|
||||||
|
var y = f0 * (s + fs0) + f1*fs1 + f2*fs2;
|
||||||
|
fs2 = fs1; fs1 = y; fs0 = s;
|
||||||
|
k += dk;
|
||||||
|
dataL[i+5] += vL * y;
|
||||||
|
dataR[i+5] += vR * y;
|
||||||
|
Vrms += (vL + vR) * y * y;
|
||||||
|
|
||||||
|
var s = samp[k|0];
|
||||||
|
var y = f0 * (s + fs0) + f1*fs1 + f2*fs2;
|
||||||
|
fs2 = fs1; fs1 = y; fs0 = s;
|
||||||
|
k += dk;
|
||||||
|
dataL[i+6] += vL * y;
|
||||||
|
dataR[i+6] += vR * y;
|
||||||
|
Vrms += (vL + vR) * y * y;
|
||||||
|
|
||||||
|
var s = samp[k|0];
|
||||||
|
var y = f0 * (s + fs0) + f1*fs1 + f2*fs2;
|
||||||
|
fs2 = fs1; fs1 = y; fs0 = s;
|
||||||
|
k += dk;
|
||||||
|
dataL[i+7] += vL * y;
|
||||||
|
dataR[i+7] += vR * y;
|
||||||
|
Vrms += (vL + vR) * y * y;
|
||||||
|
|
||||||
|
vL = pf_8 * vL + (1 - pf_8) * volL;
|
||||||
|
vR = pf_8 * vR + (1 - pf_8) * volR;
|
||||||
}
|
}
|
||||||
// we low-pass filter here since we are resampling some arbitrary
|
|
||||||
// frequency to f_smp; this is an anti-aliasing filter and is
|
for (; i < next_event; i++) {
|
||||||
// implemented as an IIR butterworth filter (usually we'd use an FIR
|
var s = samp[k|0];
|
||||||
// brick wall filter, but this is much simpler computationally and
|
// we low-pass filter here since we are resampling some arbitrary
|
||||||
// sounds fine)
|
// frequency to f_smp; this is an anti-aliasing filter and is
|
||||||
var si = ch.filter[0] * (s + ch.filterstate[0]) +
|
// implemented as an IIR butterworth filter (usually we'd use an FIR
|
||||||
ch.filter[1]*ch.filterstate[1] + ch.filter[2]*ch.filterstate[2];
|
// brick wall filter, but this is much simpler computationally and
|
||||||
if (isNaN(si)) {
|
// sounds fine)
|
||||||
console.log("NaN after filter sample idx", samp, k|0, ch.filter, ch.filterstate, s);
|
var y = f0 * (s + fs0) + f1*fs1 + f2*fs2;
|
||||||
break;
|
fs2 = fs1; fs1 = y; fs0 = s;
|
||||||
|
dataL[i] += vL * y;
|
||||||
|
dataR[i] += vR * y;
|
||||||
|
Vrms += (vL + vR) * y * y;
|
||||||
|
k += dk;
|
||||||
}
|
}
|
||||||
ch.filterstate[2] = ch.filterstate[1];
|
|
||||||
ch.filterstate[1] = si; ch.filterstate[0] = s;
|
|
||||||
// we also low-pass filter volume changes with a simple one-zero,
|
|
||||||
// one-pole filter to avoid pops and clicks when volume changes.
|
|
||||||
ch.vL = popfilter_alpha * ch.vL + (1 - popfilter_alpha) * (volL + ch.vLprev) * 0.5;
|
|
||||||
ch.vR = popfilter_alpha * ch.vR + (1 - popfilter_alpha) * (volR + ch.vRprev) * 0.5;
|
|
||||||
ch.vLprev = volL;
|
|
||||||
ch.vRprev = volR;
|
|
||||||
dataL[i] += ch.vL * si;
|
|
||||||
dataR[i] += ch.vR * si;
|
|
||||||
Vrms += (ch.vL + ch.vR) * si * si;
|
|
||||||
k += dk;
|
|
||||||
}
|
}
|
||||||
ch.off = k;
|
ch.off = k;
|
||||||
ch.doff = dk;
|
ch.doff = dk;
|
||||||
|
ch.filterstate[0] = fs0;
|
||||||
|
ch.filterstate[1] = fs1;
|
||||||
|
ch.filterstate[2] = fs2;
|
||||||
|
ch.vL = vL;
|
||||||
|
ch.vR = vR;
|
||||||
return Vrms * 0.5;
|
return Vrms * 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -383,15 +463,9 @@ function audio_cb(e) {
|
||||||
var dataL = e.outputBuffer.getChannelData(0);
|
var dataL = e.outputBuffer.getChannelData(0);
|
||||||
var dataR = e.outputBuffer.getChannelData(1);
|
var dataR = e.outputBuffer.getChannelData(1);
|
||||||
|
|
||||||
// backward compat w/ no array.fill
|
for (var i = 0; i < buflen; i++) {
|
||||||
if (dataL.fill === undefined) {
|
dataL[i] = 0;
|
||||||
for (var i = 0; i < buflen; i++) {
|
dataR[i] = 0;
|
||||||
dataL[i] = 0;
|
|
||||||
dataR[i] = 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dataL.fill(0);
|
|
||||||
dataR.fill(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var offset = 0;
|
var offset = 0;
|
||||||
|
|
@ -413,22 +487,24 @@ function audio_cb(e) {
|
||||||
buflen -= tickduration;
|
buflen -= tickduration;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update VU meters
|
window.requestAnimationFrame(function() {
|
||||||
var canvas = document.getElementById("vu");
|
// update VU meters
|
||||||
var ctx = canvas.getContext("2d");
|
var canvas = document.getElementById("vu");
|
||||||
ctx.fillStyle = '#000';
|
var ctx = canvas.getContext("2d");
|
||||||
ctx.fillRect(0, 0, 16 * nchan, 64);
|
ctx.fillStyle = '#000';
|
||||||
ctx.fillStyle = '#0f0';
|
ctx.fillRect(0, 0, 16 * nchan, 64);
|
||||||
for (var j = 0; j < nchan; j++) {
|
ctx.fillStyle = '#0f0';
|
||||||
var rms = VU[j] / e.outputBuffer.length;
|
for (var j = 0; j < nchan; j++) {
|
||||||
var y = -Math.log(rms)*10;
|
var rms = VU[j] / e.outputBuffer.length;
|
||||||
ctx.fillRect(j*16, y, 15, 64-y);
|
var y = -Math.log(rms)*10;
|
||||||
}
|
ctx.fillRect(j*16, y, 15, 64-y);
|
||||||
|
}
|
||||||
|
|
||||||
var debug = document.getElementById("debug");
|
var debug = document.getElementById("debug");
|
||||||
debug.innerHTML = 'pat ' + cur_pat + ' row ' + (cur_row-1);
|
debug.innerHTML = 'pat ' + cur_pat + ' row ' + (cur_row-1);
|
||||||
var pat = document.getElementById("pattern");
|
var pat = document.getElementById("pattern");
|
||||||
pat.innerHTML = patdisplay.join("\n");
|
pat.innerHTML = patdisplay.join("\n");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function eff_t0_0(ch, data) { // arpeggio
|
function eff_t0_0(ch, data) { // arpeggio
|
||||||
|
|
@ -692,7 +768,7 @@ function UnrollSampleLoop(inst) {
|
||||||
samp[i] = inst.sampledata[i];
|
samp[i] = inst.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 = inst.looplen - 1; k >= 0; k--) {
|
||||||
samp[i++] = inst.sampledata[inst.loop + k];
|
samp[i++] = inst.sampledata[inst.loop + k];
|
||||||
}
|
}
|
||||||
|
|
@ -702,9 +778,10 @@ function UnrollSampleLoop(inst) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log("unrolled sample loop", inst.number, "looplen", inst.looplen, "x", nloops, " = ", samplesiz);
|
||||||
inst.sampledata = samp;
|
inst.sampledata = samp;
|
||||||
inst.looplen = nloops * inst.looplen;
|
inst.looplen = nloops * inst.looplen;
|
||||||
inst.type &= ~2;
|
inst.type = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function playXM(arrayBuf) {
|
function playXM(arrayBuf) {
|
||||||
|
|
@ -725,8 +802,6 @@ function playXM(arrayBuf) {
|
||||||
for (var i = 0; i < nchan; i++) {
|
for (var i = 0; i < nchan; i++) {
|
||||||
channelinfo.push({
|
channelinfo.push({
|
||||||
filterstate: new Float32Array(3),
|
filterstate: new Float32Array(3),
|
||||||
popfilter: FilterCoeffs(200.0 / 44100.0),
|
|
||||||
popfilterstate: [new Float32Array(3), new Float32Array(3)],
|
|
||||||
vol: 0,
|
vol: 0,
|
||||||
pan: 128,
|
pan: 128,
|
||||||
vL: 0, vR: 0, // left right volume envelope followers (changes per sample)
|
vL: 0, vR: 0, // left right volume envelope followers (changes per sample)
|
||||||
|
|
@ -855,6 +930,7 @@ function playXM(arrayBuf) {
|
||||||
idx += totalsamples;
|
idx += totalsamples;
|
||||||
inst = {
|
inst = {
|
||||||
'name': instname,
|
'name': instname,
|
||||||
|
'number': i,
|
||||||
'len': samplen, 'loop': samploop,
|
'len': samplen, 'loop': samploop,
|
||||||
'looplen': samplooplen, 'note': sampnote, 'fine': sampfinetune,
|
'looplen': samplooplen, 'note': sampnote, 'fine': sampfinetune,
|
||||||
'pan': samppan, 'type': samptype, 'vol': sampvol,
|
'pan': samppan, 'type': samptype, 'vol': sampvol,
|
||||||
|
|
@ -869,7 +945,7 @@ function playXM(arrayBuf) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// unroll short loops and any pingpong loops
|
// unroll short loops and any pingpong loops
|
||||||
if ((inst.type & 1) && (inst.looplen < 2048 || (inst.type & 2))) {
|
if ((inst.type & 3) && (inst.looplen < 2048 || (inst.type & 2))) {
|
||||||
UnrollSampleLoop(inst);
|
UnrollSampleLoop(inst);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue