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) {
|
function audio_cb(e) {
|
||||||
f_smp = audioctx.sampleRate;
|
f_smp = audioctx.sampleRate;
|
||||||
var buflen = e.outputBuffer.length;
|
var buflen = e.outputBuffer.length;
|
||||||
|
|
@ -289,86 +373,8 @@ function audio_cb(e) {
|
||||||
}
|
}
|
||||||
var tickduration = Math.min(buflen, ticklen - cur_ticksamp);
|
var tickduration = Math.min(buflen, ticklen - cur_ticksamp);
|
||||||
for (var j = 0; j < nchan; j++) {
|
for (var j = 0; j < nchan; j++) {
|
||||||
var ch = channelinfo[j];
|
VU[j] += MixChannelIntoBuf(
|
||||||
var inst = ch.inst;
|
channelinfo[j], offset, offset + tickduration, dataL, dataR);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
offset += tickduration;
|
offset += tickduration;
|
||||||
cur_ticksamp += tickduration;
|
cur_ticksamp += tickduration;
|
||||||
|
|
@ -568,6 +574,7 @@ function playXM(arrayBuf) {
|
||||||
vL: 0, vR: 0, // left right volume envelope followers (changes per sample)
|
vL: 0, vR: 0, // left right volume envelope followers (changes per sample)
|
||||||
vLprev: 0, vRprev: 0,
|
vLprev: 0, vRprev: 0,
|
||||||
mute: 0,
|
mute: 0,
|
||||||
|
volE: 0, panE: 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
console.log("header len " + hlen);
|
console.log("header len " + hlen);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue