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