diff --git a/index.html b/index.html index 2bf0923..115cee1 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,8 @@
todo:
- - pan envelopes
+ - instrument fadeout
+ - pingpong loops
- volume effects
- 8x
- others?
diff --git a/xm.js b/xm.js
index 65dda1c..7c62d88 100644
--- a/xm.js
+++ b/xm.js
@@ -46,22 +46,6 @@ var channelinfo = [];
var instruments = [];
var tempo = 4;
-function GetEnvelope(env, ticks) {
- // TODO: optimize, maybe
- var y0;
- for (var i = 0; i < env.length; i += 2) {
- y0 = env[i+1];
- if (ticks < env[i]) {
- var x0 = env[i-2];
- var y0 = env[i-1];
- var dx = env[i] - x0;
- var dy = env[i+1] - y0;
- return y0 + (ticks - x0) * dy / dx;
- }
- }
- return y0;
-}
-
// Return 2-pole Butterworth lowpass filter coefficients for
// center frequncy f_c (relative to sampling frequency)
function FilterCoeffs(f_c) {
@@ -178,6 +162,8 @@ function next_row() {
// alone
ch.envtick = 0;
ch.release = 0;
+ ch.env_vol = new EnvelopeFollower(inst.env_vol);
+ ch.env_pan = new EnvelopeFollower(inst.env_pan);
}
}
@@ -186,6 +172,8 @@ function next_row() {
ch.release = 0;
ch.envtick = 0;
ch.vibratopos = 0;
+ ch.env_vol = new EnvelopeFollower(inst.env_vol);
+ ch.env_pan = new EnvelopeFollower(inst.env_pan);
UpdateChannelNote(ch, note);
}
}
@@ -200,6 +188,59 @@ function next_row() {
pat.innerHTML = patdisplay.join("\n");
}
+function Envelope(points, type, sustain, loopstart, loopend) {
+ this.points = points;
+ this.type = type;
+ this.sustain = sustain;
+ this.loopstart = loopstart;
+ this.loopend = loopend;
+}
+
+Envelope.prototype.Get = function(ticks) {
+ // TODO: optimize follower with ptr
+ // or even do binary search here
+ var y0;
+ var env = this.points;
+ for (var i = 0; i < env.length; i += 2) {
+ y0 = env[i+1];
+ if (ticks < env[i]) {
+ var x0 = env[i-2];
+ var y0 = env[i-1];
+ var dx = env[i] - x0;
+ var dy = env[i+1] - y0;
+ return y0 + (ticks - x0) * dy / dx;
+ }
+ }
+ return y0;
+}
+
+function EnvelopeFollower(env) {
+ this.env = env;
+ this.tick = 0;
+}
+
+EnvelopeFollower.prototype.Tick = function(release) {
+ if (this.env === undefined) {
+ return 64;
+ }
+ var value = this.env.Get(this.tick);
+ if (this.env.type & 1) { // sustain?
+ // if we're sustaining a note, stop advancing the tick counter
+ if (!release &&
+ this.tick >= this.env.points[this.env.sustain*2]) {
+ return this.env.points[this.env.sustain*2 + 1];
+ }
+ }
+ this.tick++;
+ if (this.env.type & 2) { // envelope loop?
+ if (!release &&
+ this.tick > this.env.loopend) {
+ this.tick -= this.env.loopend - this.env.loopstart;
+ }
+ }
+ return value;
+}
+
function next_tick() {
cur_tick++;
if (cur_tick >= tempo) {
@@ -213,20 +254,8 @@ function next_tick() {
ch.effectfn(ch);
}
if (inst === undefined) continue;
- if (inst.env_vol !== undefined) {
- ch.env_vol = GetEnvelope(inst.env_vol, ch.envtick);
- if (inst.env_vol_type & 2) { // sustain loop?
- // if we're sustaining a note, leave env_vol alone
- if (ch.release == 0) {
- if (ch.envtick >= inst.env_vol[inst.env_vol_sustain*2]) {
- continue;
- }
- }
- }
- ch.envtick++;
- } else {
- ch.env_vol = 64;
- }
+ ch.volE = ch.env_vol.Tick(ch.release);
+ ch.panE = ch.env_pan.Tick(ch.release);
}
}
@@ -263,10 +292,11 @@ function audio_cb(e) {
sample_end = looplen + inst.loop;
}
var samplen = inst.len;
- var env_vol = ch.env_vol / 64.0;
- var p = ch.pan - 128;
- var volL = env_vol * (128 - p) * ch.vol / 8192.0;
- var volR = env_vol * (128 + p) * ch.vol / 8192.0;
+ 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)
@@ -277,8 +307,17 @@ function audio_cb(e) {
for (var i = offset; i < offset+tickduration; i++) {
if (ch.mute) break;
var s = samp[k|0];
- 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 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;
@@ -292,12 +331,19 @@ function audio_cb(e) {
} else {
// kill sample
ch.inst = undefined;
- // ramp down with the pop filter
+ // 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;
+ 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;
}
@@ -568,10 +614,19 @@ function playXM(arrayBuf) {
var env_vol_sustain = dv.getUint8(idx+227);
var env_vol_loop_start = dv.getUint8(idx+228);
var env_vol_loop_end = dv.getUint8(idx+229);
+ var env_npan = dv.getUint8(idx+226);
+ var env_pan_type = dv.getUint8(idx+234);
+ var env_pan_sustain = dv.getUint8(idx+230);
+ var env_pan_loop_start = dv.getUint8(idx+231);
+ var env_pan_loop_end = dv.getUint8(idx+232);
env_vol = [];
for (var j = 0; j < env_nvol*2; j++) {
env_vol.push(dv.getUint16(idx+129+j*2, true));
}
+ env_pan = [];
+ for (var j = 0; j < env_npan*2; j++) {
+ env_pan.push(dv.getUint16(idx+177+j*2, true));
+ }
if (nsamp > 0) {
// FIXME: ignoring keymaps for now and assuming 1 sample / instrument
// var keymap = getarray(dv, idx+0x21);
@@ -607,11 +662,20 @@ function playXM(arrayBuf) {
'sampledata': ConvertSample(new Uint8Array(arrayBuf, sampleoffset, samplen), samptype & 4),
};
if (env_vol_type) {
- inst.env_vol = env_vol;
- inst.env_vol_type = env_vol_type;
- inst.env_vol_sustain = env_vol_sustain;
- inst.env_vol_loop_start = env_vol_loop_start;
- inst.env_vol_loop_end = env_vol_loop_end;
+ inst.env_vol = new Envelope(
+ env_vol,
+ env_vol_type,
+ env_vol_sustain,
+ env_vol_loop_start,
+ env_vol_loop_end);
+ }
+ if (env_pan_type) {
+ inst.env_pan = new Envelope(
+ env_pan,
+ env_pan_type,
+ env_pan_sustain,
+ env_pan_loop_start,
+ env_pan_loop_end);
}
instruments.push(inst);
} else {