diff --git a/index.html b/index.html
index 3c6535e..dcaedaa 100644
--- a/index.html
+++ b/index.html
@@ -72,8 +72,7 @@
code: github.com/a1k0n/jsxm
todo:
- missing XM effects:
- - E3x, E6x, E7x, E9x, EDx, EEx
- - 7xy - tremolo
+ - E3x, E6x, E9x, EDx, EEx
- Kxx, Lxx, Pxy, Txy
- render pattern with the wider fonts for fewer channels
- fix occasional rendering/audio hiccups when switching songs
diff --git a/test/effects.js b/test/effects.js
index 93e5809..56cd48c 100644
--- a/test/effects.js
+++ b/test/effects.js
@@ -505,6 +505,29 @@ exports['test E5x finetune override'] = function(assert) {
assert.equal(f2.toFixed(2), "131.00", "E5f finetune +127");
};
+exports['test E7x set tremolo waveform'] = function(assert) {
+ var xm = testdata.resetXMData();
+ xm.patterns = [
+ [
+ [[48, 1, -1, 0, 0x00]], // C-4 1 -- --- (default waveform - sine)
+ [[48, 1, -1, 14, 0x71]], // C-4 1 -- E71 (saw, ramp-down)
+ [[48, 1, -1, 14, 0x72]], // C-4 1 -- E72 (square)
+ [[48, 1, -1, 14, 0x73]] // C-4 1 -- E73 (random)
+ ]
+ ];
+ xm.tempo = 1;
+ var ch = xm.channelinfo[0];
+ XMPlayer.nextTick();
+ assert.equal(ch.tremolotype, 0, 'row 0 tick 0 tremolotype=0');
+ XMPlayer.nextTick();
+ assert.equal(ch.tremolotype, 1, 'row 1 tick 0 tremolotype=1');
+ XMPlayer.nextTick();
+ assert.equal(ch.tremolotype, 2, 'row 2 tick 0 tremolotype=2');
+ XMPlayer.nextTick();
+ assert.equal(ch.tremolotype, 3, 'row 3 tick 0 tremolotype=3');
+};
+
+
exports['test E60 loop twice with set beginning'] = function(assert) {
var xm = testdata.resetXMData(2);
diff --git a/test/testdata.js b/test/testdata.js
index 97696fb..ed438b9 100644
--- a/test/testdata.js
+++ b/test/testdata.js
@@ -28,6 +28,7 @@ exports.resetXMData = function(channels) {
vibratodepth: 1,
vibratospeed: 1,
vibratotype: 0,
+ tremolotype: 0,
});
}
xm.songpats = [0];
diff --git a/xm.js b/xm.js
index 169c42b..054aac1 100644
--- a/xm.js
+++ b/xm.js
@@ -335,6 +335,7 @@ function nextTick() {
for (j = 0; j < player.xm.nchan; j++) {
ch = player.xm.channelinfo[j];
ch.periodoffset = 0;
+ ch.voloffset = 0;
}
if (player.cur_tick >= player.xm.tempo) {
player.cur_tick = 0;
@@ -414,8 +415,8 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) {
var volE = ch.volE / 64.0; // current volume envelope
var panE = 4*(ch.panE - 32); // current panning envelope
var p = panE + ch.pan - 128; // final pan
- var volL = player.xm.global_volume * volE * (128 - p) * ch.vol / (64 * 128 * 128);
- var volR = player.xm.global_volume * volE * (128 + p) * ch.vol / (64 * 128 * 128);
+ var volL = player.xm.global_volume * volE * (128 - p) * (ch.vol + ch.voloffset) / (64 * 128 * 128);
+ var volR = player.xm.global_volume * volE * (128 + p) * (ch.vol + ch.voloffset) / (64 * 128 * 128);
if (volL < 0) volL = 0;
if (volR < 0) volR = 0;
if (volR === 0 && volL === 0)
@@ -710,6 +711,10 @@ function load(arrayBuf) {
vibratodepth: 1,
vibratospeed: 1,
vibratotype: 0,
+ tremolopos: 0,
+ tremolodepth: 1,
+ tremolospeed: 1,
+ tremolotype: 0,
});
}
console.log("header len " + hlen);
diff --git a/xmeffects.js b/xmeffects.js
index 711963a..7c47525 100644
--- a/xmeffects.js
+++ b/xmeffects.js
@@ -106,6 +106,50 @@ function eff_t1_6(ch) { // vibrato + volume slide
eff_t1_4(ch);
}
+function eff_t0_7(ch, data) { // tremolo
+ if (data & 0x0f) {
+ ch.tremolodepth = (data & 0x0f) * 2;
+ }
+ if (data >> 4) {
+ ch.tremolospeed = data >> 4;
+ }
+ eff_t1_7(ch);
+}
+
+function eff_t1_7(ch) { // tremolo
+ ch.voloffset =
+ getTremoloDelta(ch.tremolotype, ch.tremolopos) * ch.tremolodepth;
+ if (isNaN(ch.voloffset)) {
+ console.log("tremolo voloffset NaN?",
+ ch.tremolopos, ch.tremolospeed, ch.tremolodepth);
+ ch.voloffset = 0;
+ }
+ // only updates on non-first ticks
+ if (player.cur_tick > 0) {
+ ch.tremolopos += ch.tremolospeed;
+ ch.tremolopos &= 63;
+ }
+}
+
+function getTremoloDelta(type, x) {
+ var delta = 0;
+ switch (type & 0x03) {
+ case 1: // sawtooth (ramp-down)
+ delta = ((1 + x * 2 / 64) % 2) - 1;
+ break;
+ case 2: // square
+ case 3: // random (in FT2 these two are the same)
+ delta = x < 32 ? 1 : -1;
+ break;
+ case 0:
+ default: // sine
+ delta = Math.sin(x * Math.PI / 32);
+ break;
+ }
+ return delta;
+}
+
+
function eff_t0_8(ch, data) { // set panning
ch.pan = data;
}
@@ -179,6 +223,9 @@ function eff_t0_e(ch, data) { // extended effects!
}
}
break;
+ case 7: // set tremolo waveform
+ ch.tremolotype = data & 0x07;
+ break;
case 8: // panning
ch.pan = data * 0x11;
break;
@@ -290,7 +337,7 @@ player.effects_t0 = [ // effect functions on tick 0
eff_t0_4, // 4
eff_t0_a, // 5, same as A on first tick
eff_t0_a, // 6, same as A on first tick
- eff_unimplemented_t0, // 7
+ eff_t0_7, // 7
eff_t0_8, // 8
eff_t0_9, // 9
eff_t0_a, // a
@@ -329,7 +376,7 @@ player.effects_t1 = [ // effect functions on tick 1+
eff_t1_4,
eff_t1_5, // 5
eff_t1_6, // 6
- eff_unimplemented, // 7
+ eff_t1_7, // 7
null, // 8
null, // 9
eff_t1_a, // a