Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ded244dc1f |
1 changed files with 56 additions and 29 deletions
85
xm.js
85
xm.js
|
|
@ -123,23 +123,37 @@ var channelinfo = [];
|
||||||
var instruments = [];
|
var instruments = [];
|
||||||
var tempo = 4;
|
var tempo = 4;
|
||||||
|
|
||||||
// Return 2-pole Butterworth lowpass filter coefficients for
|
// compute coefficients for a pole pair biquad section (with a single implied
|
||||||
// center frequncy f_c (relative to sampling frequency)
|
// zero at z=-1); pole_r and _i are the real and imaginary components of the
|
||||||
function FilterCoeffs(f_c) {
|
// pole in the s (Laplace) plane, which are then scaled by w_c and projected
|
||||||
// if (f_c > 0.5) { // we can't lowpass above the nyquist frequency...
|
// into the z domain.
|
||||||
// return [1, 0, 0];
|
function SetBiquadCoeffs(s_pole_r, s_pole_i, f_c, poleno, filter) {
|
||||||
// }
|
var w = 2 * Math.PI * f_c;
|
||||||
// what happens instead is the filter wraps around to an alias frequency,
|
var e = Math.exp(s_pole_r * w);
|
||||||
// and that also works OK, though it isn't strictly right... FIXME
|
var c = e * Math.cos(s_pole_i * w);
|
||||||
var wct = Math.sqrt(2) * Math.PI * f_c;
|
var s = e * Math.sin(s_pole_i * w);
|
||||||
var e = Math.exp(-wct);
|
|
||||||
var c = e * Math.cos(wct);
|
|
||||||
var s = e * Math.sin(wct);
|
|
||||||
var gain = (1 - 2*c + c*c + s*s) / 2;
|
var gain = (1 - 2*c + c*c + s*s) / 2;
|
||||||
return [gain, 2*c, -c*c - s*s];
|
filter[3*poleno + 0] = gain;
|
||||||
|
filter[3*poleno + 1] = 2*c;
|
||||||
|
filter[3*poleno + 2] = -c*c - s*s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return 4-pole lowpass filter coefficients for center frequncy f_c (relative
|
||||||
|
// to sampling frequency)
|
||||||
|
function SetFilterCoeffs(f_c, filter) {
|
||||||
|
if (f_c > 0.5) { // we can't lowpass above the nyquist frequency...
|
||||||
|
f_c = 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the pole pair in the s plane we're going to use.
|
||||||
|
// It's a fairly resonant filter but it is designed to work together with the
|
||||||
|
// comb filter implied by using nearest-neighbor downsampling first, so the
|
||||||
|
// combination of the two has a nearly flat passband and a decently sharp
|
||||||
|
// cutoff for a 2-pole filter.
|
||||||
|
SetBiquadCoeffs(-0.47143406, 0.46783542, f_c, 0, filter);
|
||||||
|
SetBiquadCoeffs(-0.10882119, 0.91627743, f_c, 1, filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
popfilter = FilterCoeffs(200.0 / 44100.0);
|
|
||||||
popfilter_alpha = 0.9837;
|
popfilter_alpha = 0.9837;
|
||||||
|
|
||||||
function UpdateChannelPeriod(ch, period) {
|
function UpdateChannelPeriod(ch, period) {
|
||||||
|
|
@ -149,7 +163,7 @@ function UpdateChannelPeriod(ch, period) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ch.doff = freq / f_smp;
|
ch.doff = freq / f_smp;
|
||||||
ch.filter = FilterCoeffs(ch.doff / 2);
|
SetFilterCoeffs(ch.doff / 2, ch.filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PeriodForNote(ch, note) {
|
function PeriodForNote(ch, note) {
|
||||||
|
|
@ -468,8 +482,10 @@ 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;
|
||||||
var f0 = ch.filter[0], f1 = ch.filter[1], f2 = ch.filter[2];
|
var f0g = ch.filter[0], f01 = ch.filter[1], f02 = ch.filter[2];
|
||||||
var fs0 = ch.filterstate[0], fs1 = ch.filterstate[1], fs2 = ch.filterstate[2];
|
var f1g = ch.filter[3], f11 = ch.filter[4], f12 = ch.filter[5];
|
||||||
|
var f0x = ch.filterstate[0], f0y1 = ch.filterstate[1], f0y2 = ch.filterstate[2];
|
||||||
|
var f1x = ch.filterstate[3], f1y1 = ch.filterstate[4], f1y2 = ch.filterstate[5];
|
||||||
|
|
||||||
// we also low-pass filter volume changes with a simple one-zero,
|
// we also low-pass filter volume changes with a simple one-zero,
|
||||||
// one-pole filter to avoid pops and clicks when volume changes.
|
// one-pole filter to avoid pops and clicks when volume changes.
|
||||||
|
|
@ -500,6 +516,7 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) {
|
||||||
var next_event = Math.max(1, Math.min(end, i + (sample_end - k) / dk));
|
var next_event = Math.max(1, Math.min(end, i + (sample_end - k) / dk));
|
||||||
// this is the inner loop of the player
|
// this is the inner loop of the player
|
||||||
|
|
||||||
|
/* TODO after new filter
|
||||||
// unrolled 8x
|
// unrolled 8x
|
||||||
for (; i + 7 < next_event; i+=8) {
|
for (; i + 7 < next_event; i+=8) {
|
||||||
var s = samp[k|0];
|
var s = samp[k|0];
|
||||||
|
|
@ -569,26 +586,35 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) {
|
||||||
vL = pf_8 * vL + (1 - pf_8) * volL;
|
vL = pf_8 * vL + (1 - pf_8) * volL;
|
||||||
vR = pf_8 * vR + (1 - pf_8) * volR;
|
vR = pf_8 * vR + (1 - pf_8) * volR;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
for (; i < next_event; i++) {
|
for (; i < next_event; i++) {
|
||||||
var s = samp[k|0];
|
var s = samp[k|0];
|
||||||
// we low-pass filter here since we are resampling some arbitrary
|
// we low-pass filter here since we are resampling some arbitrary
|
||||||
// frequency to f_smp; this is an anti-aliasing filter and is
|
// frequency to f_smp; this is an anti-aliasing filter and is
|
||||||
// implemented as an IIR butterworth filter (usually we'd use an FIR
|
// implemented as an IIR filter (usually we'd use an FIR brick wall
|
||||||
// brick wall filter, but this is much simpler computationally and
|
// filter, but this is much simpler computationally and sounds fine)
|
||||||
// sounds fine)
|
// the filter is a cascade of two biquad sections here
|
||||||
var y = f0 * (s + fs0) + f1*fs1 + f2*fs2;
|
var y0 = f0g * (s + f0x) + f01*f0y1 + f02*f0y2;
|
||||||
fs2 = fs1; fs1 = y; fs0 = s;
|
f0y2 = f0y1; f0y1 = y0; f0x = s;
|
||||||
dataL[i] += vL * y;
|
var y1 = f1g * (y0 + f1x) + f11*f1y1 + f12*f1y2;
|
||||||
dataR[i] += vR * y;
|
f1y2 = f1y1; f1y1 = y1; f1x = y0;
|
||||||
Vrms += (vL + vR) * y * y;
|
dataL[i] += vL * y1;
|
||||||
|
dataR[i] += vR * y1;
|
||||||
|
Vrms += (vL + vR) * y1 * y1;
|
||||||
k += dk;
|
k += dk;
|
||||||
|
|
||||||
|
vL = popfilter_alpha * vL + (1 - popfilter_alpha) * volL;
|
||||||
|
vR = popfilter_alpha * vR + (1 - popfilter_alpha) * volR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ch.off = k;
|
ch.off = k;
|
||||||
ch.filterstate[0] = fs0;
|
ch.filterstate[0] = f0x;
|
||||||
ch.filterstate[1] = fs1;
|
ch.filterstate[1] = f0y1;
|
||||||
ch.filterstate[2] = fs2;
|
ch.filterstate[2] = f0y2;
|
||||||
|
ch.filterstate[3] = f1x;
|
||||||
|
ch.filterstate[4] = f1y1;
|
||||||
|
ch.filterstate[5] = f1y2;
|
||||||
ch.vL = vL;
|
ch.vL = vL;
|
||||||
ch.vR = vR;
|
ch.vR = vR;
|
||||||
return Vrms * 0.5;
|
return Vrms * 0.5;
|
||||||
|
|
@ -931,7 +957,8 @@ function playXM(arrayBuf) {
|
||||||
document.getElementById('vu').width = 16 * nchan;
|
document.getElementById('vu').width = 16 * nchan;
|
||||||
for (var i = 0; i < nchan; i++) {
|
for (var i = 0; i < nchan; i++) {
|
||||||
channelinfo.push({
|
channelinfo.push({
|
||||||
filterstate: new Float32Array(3),
|
filter: new Float32Array(6),
|
||||||
|
filterstate: new Float32Array(6),
|
||||||
vol: 0,
|
vol: 0,
|
||||||
pan: 128,
|
pan: 128,
|
||||||
period: 1920 - 48*16,
|
period: 1920 - 48*16,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue