refactored mixing code a bit
removed all doubt about handling note-end pops
This commit is contained in:
parent
343da0f3e9
commit
c2fe03797e
1 changed files with 87 additions and 80 deletions
167
xm.js
167
xm.js
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue