refactored mixing code a bit

removed all doubt about handling note-end pops
This commit is contained in:
Andy Sloane 2015-10-27 08:48:29 -07:00
commit c2fe03797e

167
xm.js
View file

@ -261,6 +261,90 @@ function next_tick() {
}
}
// This function gradually brings the channel back down to zero if it isn't
// already to avoid clicks and pops when samples end.
function MixSilenceIntoBuf(ch, start, end, dataL, dataR) {
var s = ch.filterstate[1];
for (var i = start; i < end; i++) {
if (Math.abs(s) < 1.526e-5) // == 1/65536.0
break;
dataL[i] += s * ch.vL;
dataR[i] += s * ch.vR;
s *= popfilter_alpha;
}
ch.filterstate[1] = s;
ch.filterstate[2] = s;
return 0;
}
function MixChannelIntoBuf(ch, start, end, dataL, dataR) {
var inst = ch.inst;
var samp, sample_end;
var loop = false;
var looplen = 0;
// nothing on this channel, just filter the last dc offset back down to zero
if (inst === undefined || ch.mute) {
return MixSilenceIntoBuf(ch, start, end, dataL, dataR);
}
samp = inst.sampledata;
sample_end = inst.len;
if ((inst.type & 3) == 1) { // todo: support pingpong
loop = true;
looplen = inst.looplen;
sample_end = looplen + inst.loop;
}
var samplen = inst.len;
var volE = ch.volE / 64.0;
var panE = (ch.panE - 32);
var p = panE + ch.pan - 128;
var volL = volE * (128 - p) * ch.vol / 8192.0;
var volR = volE * (128 + p) * ch.vol / 8192.0;
if (volL < 0) volL = 0;
if (volR < 0) volR = 0;
if (volR == 0 && volL == 0)
return;
var k = ch.off;
var dk = ch.doff;
var Vrms = 0;
for (var i = start; i < end; i++) {
var s = samp[k|0];
// we low-pass filter here since we are resampling some arbitrary
// frequency to f_smp; this is an anti-aliasing filter and is
// implemented as an IIR butterworth filter (usually we'd use an FIR
// brick wall filter, but this is much simpler computationally and
// sounds fine)
var si = ch.filter[0] * (s + ch.filterstate[0]) +
ch.filter[1]*ch.filterstate[1] + ch.filter[2]*ch.filterstate[2];
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;
if (k >= sample_end) { // TODO: implement pingpong looping
if (loop) {
k -= looplen;
} else {
// kill sample
ch.inst = undefined;
// fill rest of buf with filtered dc offset using loop above
return Vrms + MixSilenceIntoBuf(ch, i+1, end, dataL, dataR);
}
}
}
ch.off = k;
ch.doff = dk;
return Vrms;
}
function audio_cb(e) {
f_smp = audioctx.sampleRate;
var buflen = e.outputBuffer.length;
@ -289,86 +373,8 @@ function audio_cb(e) {
}
var tickduration = Math.min(buflen, ticklen - cur_ticksamp);
for (var j = 0; j < nchan; j++) {
var ch = channelinfo[j];
var inst = ch.inst;
var samp, sample_end;
var loop = false;
var looplen = 0;
if (inst === undefined) {
continue;
}
samp = inst.sampledata;
sample_end = inst.len;
if ((inst.type & 3) == 1) { // todo: support pingpong
loop = true;
looplen = inst.looplen;
sample_end = looplen + inst.loop;
}
var samplen = inst.len;
var volE = ch.volE / 64.0;
var panE = (ch.panE - 32);
var p = panE + ch.pan - 128;
var volL = volE * (128 - p) * ch.vol / 8192.0;
var volR = volE * (128 + p) * ch.vol / 8192.0;
if (volL < 0) volL = 0;
if (volR < 0) volR = 0;
if (volR == 0 && volL == 0)
continue;
var k = ch.off;
var dk = ch.doff;
var Vrms = 0;
// console.log(j, offset, ch);
for (var i = offset; i < offset+tickduration; i++) {
if (ch.mute) break;
var s = samp[k|0];
// we low-pass filter here since we are resampling some arbitrary
// frequency to f_smp; this is an anti-aliasing filter and is
// implemented as an IIR butterworth filter (usually we'd use an FIR
// brick wall filter, but this is much simpler computationally and
// sounds fine)
var si = ch.filter[0] * (s + ch.filterstate[0]) +
ch.filter[1]*ch.filterstate[1] + ch.filter[2]*ch.filterstate[2];
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;
if (k >= sample_end) { // TODO: implement pingpong looping
if (loop) {
k -= looplen;
} else {
// kill sample
ch.inst = undefined;
// ramp down to zero with the pop filter
// if the sample ends right before the end of the tick (or the end
// of the buffer), we could still get a pop but *usually* it's
// hidden behind other changes in the tick... there's only so much
// we can do here, anyway. i guess we could hold the dc offset...
var rampend = Math.min(offset+tickduration, i+200);
for (i++; i < rampend; i++) {
// fill rest of buffer with filtered silence to avoid a pop
var si = popfilter[0] * (ch.filterstate[0]) +
popfilter[1]*ch.filterstate[1] +
popfilter[2]*ch.filterstate[2];
ch.filterstate[2] = ch.filterstate[1];
ch.filterstate[1] = si; ch.filterstate[0] = 0;
dataL[i] += ch.vL * si;
dataR[i] += ch.vR * si;
}
break;
}
}
}
ch.off = k;
ch.doff = dk;
VU[j] += Vrms;
VU[j] += MixChannelIntoBuf(
channelinfo[j], offset, offset + tickduration, dataL, dataR);
}
offset += tickduration;
cur_ticksamp += tickduration;
@ -568,6 +574,7 @@ function playXM(arrayBuf) {
vL: 0, vR: 0, // left right volume envelope followers (changes per sample)
vLprev: 0, vRprev: 0,
mute: 0,
volE: 0, panE: 0
})
}
console.log("header len " + hlen);