From 32b09bdf63ae33e8b879d74eba8d77e5f869c7a1 Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Sat, 31 Oct 2015 09:27:38 -0700 Subject: [PATCH 01/78] various envelope/effect fixes Dubmood's .xms seem to expose a lot of bugs... --- xm.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/xm.js b/xm.js index e9374a8..9f36d0e 100644 --- a/xm.js +++ b/xm.js @@ -309,8 +309,8 @@ function next_row() { // alone ch.envtick = 0; ch.release = 0; - ch.env_vol = new EnvelopeFollower(inst.env_vol, inst.fadeout); - ch.env_pan = new EnvelopeFollower(inst.env_pan, 0); + ch.env_vol = new EnvelopeFollower(inst.env_vol); + ch.env_pan = new EnvelopeFollower(inst.env_pan); } } } @@ -691,7 +691,7 @@ function eff_t0_b(ch, data) { // song jump (untested) } function eff_t0_c(ch, data) { // set volume - ch.vol = data & 0x3f; + ch.vol = Math.min(64, data); } function eff_t0_d(ch, data) { // pattern jump @@ -1085,12 +1085,14 @@ function playXM(arrayBuf) { if (env_vol_type) { // insert an automatic fadeout to 0 at the end of the envelope var env_end_tick = env_vol[env_vol.length-2]; - var fadeout_ticks = 65536.0 / inst.fadeout; if (!(env_vol_type & 2)) { // if there's no sustain point, create one env_vol_sustain = env_vol.length / 2; } - env_vol.push(env_end_tick + fadeout_ticks); - env_vol.push(0); + if (inst.fadeout > 0) { + var fadeout_ticks = 65536.0 / inst.fadeout; + env_vol.push(env_end_tick + fadeout_ticks); + env_vol.push(0); + } inst.env_vol = new Envelope( env_vol, env_vol_type, @@ -1098,8 +1100,9 @@ function playXM(arrayBuf) { env_vol_loop_start, env_vol_loop_end); } else { - // create a default envelope w/ fadeout - inst.env_vol = new Envelope([0, 64, fadeout_ticks, 0], 2, 0, 0, 0); + // no envelope, then just make a default full-volume envelope. + // i thought this would use fadeout, but apparently it doesn't. + inst.env_vol = new Envelope([0, 64, 1, 0], 2, 0, 0, 0); } if (env_pan_type) { if (!(env_pan_type & 2)) { // if there's no sustain point, create one From 5e141535ab65d1dddbcabb7ea402d754c02882d7 Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Sat, 31 Oct 2015 09:33:59 -0700 Subject: [PATCH 02/78] fix some filter weirdness --- xm.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/xm.js b/xm.js index 9f36d0e..5ba1c84 100644 --- a/xm.js +++ b/xm.js @@ -126,11 +126,9 @@ var tempo = 4; // Return 2-pole Butterworth lowpass filter coefficients for // center frequncy f_c (relative to sampling frequency) function FilterCoeffs(f_c) { - // if (f_c > 0.5) { // we can't lowpass above the nyquist frequency... - // return [1, 0, 0]; - // } - // what happens instead is the filter wraps around to an alias frequency, - // and that also works OK, though it isn't strictly right... FIXME + if (f_c > 0.5) { // we can't lowpass above the nyquist frequency... + f_c = 0.5; + } var wct = Math.sqrt(2) * Math.PI * f_c; var e = Math.exp(-wct); var c = e * Math.cos(wct); From 49005f0ae0ec1c64c5ad2623b549c71874433180 Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Sat, 31 Oct 2015 10:52:46 -0700 Subject: [PATCH 03/78] so many weird uses of effects --- xm.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/xm.js b/xm.js index 5ba1c84..d79ac66 100644 --- a/xm.js +++ b/xm.js @@ -320,7 +320,9 @@ function next_row() { ch.vibratopos = 0; ch.env_vol = new EnvelopeFollower(inst.env_vol); ch.env_pan = new EnvelopeFollower(inst.env_pan); - ch.period = PeriodForNote(ch, ch.note); + if (ch.note != undefined) { + ch.period = PeriodForNote(ch, ch.note); + } } } } From a3b94c51b9b92bd1e11892d75f6115441f31985b Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Sat, 31 Oct 2015 11:17:22 -0700 Subject: [PATCH 04/78] oh man, DUH --- xm.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/xm.js b/xm.js index d79ac66..ed1416d 100644 --- a/xm.js +++ b/xm.js @@ -221,7 +221,6 @@ function next_row() { for (var i = 0; i < r.length; i++) { var ch = channelinfo[i]; var inst = ch.inst; - ch.update = false; var triggernote = false; // instrument trigger if (r[i][1] != -1) { @@ -243,8 +242,6 @@ function next_row() { ch.release = 1; triggernote = false; } else { - // assume linear frequency table (flags header & 1 == 1) - // is this true in kamel.xm? if (inst != undefined) { var note = r[i][0] + inst.note; ch.note = note; @@ -262,11 +259,11 @@ function next_row() { } else if (v <= 0x50) { ch.vol = v - 0x10; } else if (v >= 0x60 && v < 0x70) { // volume slide down - ch.voleffectfn = function() { + ch.voleffectfn = function(ch) { ch.vol = Math.max(0, ch.vol - ch.voleffectdata); } } else if (v >= 0x70 && v < 0x80) { // volume slide up - ch.voleffectfn = function() { + ch.voleffectfn = function(ch) { ch.vol = Math.min(64, ch.vol + ch.voleffectdata); } } else if (v >= 0x80 && v < 0x90) { // fine volume slide down @@ -462,7 +459,7 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) { if (volR == 0 && volL == 0) return; if (isNaN(volR) || isNaN(volL)) { - console.log("NaN volume!?", volL, volR, volE, panE, ch.vol); + console.log("NaN volume!?", ch.number, volL, volR, volE, panE, ch.vol); return; } var k = ch.off; @@ -931,6 +928,7 @@ function playXM(arrayBuf) { document.getElementById('vu').width = 16 * nchan; for (var i = 0; i < nchan; i++) { channelinfo.push({ + number: i, filterstate: new Float32Array(3), vol: 0, pan: 128, From 6dee1787f3988911f444f9a4d2473e1057327e18 Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Sat, 31 Oct 2015 11:39:04 -0700 Subject: [PATCH 05/78] show the instruments --- index.html | 4 ++-- xm.js | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index 3784657..6b798bd 100644 --- a/index.html +++ b/index.html @@ -7,14 +7,14 @@
 
-
+
 
 code: github.com/a1k0n/jsxm
 todo:
  - file picker, upload more xms to a host we can access cross-origin
  - make display even more faithfully FT2-like
- - render instrument list
+ - render instrument list in ft2 font
  - oscilloscopes
  - fix bugs!
    - envelope loops - are they right or not now?
diff --git a/xm.js b/xm.js
index ed1416d..66eb132 100644
--- a/xm.js
+++ b/xm.js
@@ -52,7 +52,9 @@ function RenderPattern(canv, pattern) {
       // render instrument
       var inst = data[1];
       if (inst != -1) {  // no instrument = render nothing
-        ctx.drawImage(fontimg, 8*(inst>>4), 4*8, 4, 8, dx, dy, 4, 8);
+        if (inst > 15) {
+          ctx.drawImage(fontimg, 8*(inst>>4), 4*8, 4, 8, dx, dy, 4, 8);
+        }
         ctx.drawImage(fontimg, 8*(inst&15), 4*8, 4, 8, dx+4, dy, 4, 8);
       }
       dx += 12;
@@ -1137,6 +1139,17 @@ function playXM(arrayBuf) {
   var gfxpattern = document.getElementById("gfxpattern");
   gfxpattern.width = _pattern_cellwidth * nchan + _pattern_border;
 
+  var instrlist = document.getElementById("instruments");
+  var list = [];
+  for (var i = 0; i < ninst; i++) {
+    var inst = instruments[i];
+    if (inst == null) continue;
+    var n = (i+1).toString(16);
+    if (i < 15) n = ' ' + n;
+    list.push(n + " " + inst.name);
+  }
+  instrlist.innerHTML = list.join("\n");
+
   // start playing
   gainNode.connect(audioctx.destination);
 }

From 3daeeee96bbe5d7fa2e04133ba62631b182504f1 Mon Sep 17 00:00:00 2001
From: Andy Sloane 
Date: Sun, 1 Nov 2015 07:42:52 -0800
Subject: [PATCH 06/78] implement retrigger, move effects into separate js

---
 index.html   |   2 +-
 xm.js        | 229 +---------------------------------------
 xmeffects.js | 289 +++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 295 insertions(+), 225 deletions(-)
 create mode 100644 xmeffects.js

diff --git a/index.html b/index.html
index 6b798bd..6d0b4e5 100644
--- a/index.html
+++ b/index.html
@@ -1,6 +1,7 @@
 
   js xm
   
+  
 
 
   
@@ -20,7 +21,6 @@ todo: - envelope loops - are they right or not now? - multi-sample instruments - missing XM effects (in rough order of priority): - - Rxy - retrigger - E3x, E4x, E5x, E6x, E7x, E9x, EDx, EEx - 7xy - tremolo - Gxx, Hxy, Kxx, Lxx, Pxy, Txy diff --git a/xm.js b/xm.js index 66eb132..ebd2b1a 100644 --- a/xm.js +++ b/xm.js @@ -281,13 +281,14 @@ function next_row() { ch.effect = r[i][3]; ch.effectdata = r[i][4]; - if (ch.effect < 16) { + if (ch.effect < 36) { ch.effectfn = effects_t1[ch.effect]; - if (effects_t0[ch.effect](ch, ch.effectdata)) { + var eff_t0 = effects_t0[ch.effect]; + if (eff_t0 && eff_t0(ch, ch.effectdata)) { triggernote = false; } } else { - console.log("channel", i, "effect > 16", ch.effect); + console.log("channel", i, "effect > 36", ch.effect); } // special handling for portamentos: don't trigger the note @@ -636,227 +637,6 @@ function audio_cb(e) { } } -function eff_t0_1(ch, data) { // pitch slide up - if (data != 0) { - ch.slideupspeed = data; - } -} - -function eff_t0_2(ch, data) { // pitch slide down - if (data != 0) { - ch.slidedownspeed = data; - } -} - -function eff_t0_3(ch, data) { // portamento - if (data != 0) { - ch.portaspeed = data; - } -} - -function eff_t0_4(ch, data) { // vibrato - if (data & 0x0f) { - ch.vibratodepth = data & 0x0f; - } - if (data >> 4) { - ch.vibratospeed = data >> 4; - } - eff_t1_4(ch, data); -} - -function eff_t0_8(ch, data) { // set panning - ch.pan = data; -} - -function eff_t0_9(ch, data) { // sample offset - ch.off = data * 256; -} - -function eff_t0_a(ch, data) { // volume slide - if (data) { - if (data & 0x0f) { - ch.volumeslide = -(data & 0x0f); - } else { - ch.volumeslide = data >> 4; - } - } -} - -function eff_t0_b(ch, data) { // song jump (untested) - if (data < songpats.length) { - cur_songpos = data - cur_pat = songpats[cur_songpos]; - } -} - -function eff_t0_c(ch, data) { // set volume - ch.vol = Math.min(64, data); -} - -function eff_t0_d(ch, data) { // pattern jump - cur_songpos++; - if (cur_songpos >= songpats.length) - cur_songpos = song_looppos; - cur_pat = songpats[cur_songpos]; - cur_row = data; -} - -function eff_t0_e(ch, data) { // extended effects! - var eff = data >> 4; - data = data & 0x0f; - switch (eff) { - case 1: // fine porta up - ch.period -= data; - break; - case 2: // fine porta down - ch.period += data; - break; - case 8: // panning - ch.pan = data * 0x11; - break; - case 0x0a: // fine vol slide up (with memory) - if (data == 0 && ch.finevolup != undefined) - data = ch.finevolup; - ch.vol = Math.min(64, ch.vol + data); - ch.finevolup = data; - break; - case 0x0b: // fine vol slide down - if (data == 0 && ch.finevoldown != undefined) - data = ch.finevoldown; - ch.vol = Math.max(0, ch.vol - data); - ch.finevoldown = data; - break; - case 0x0c: // note cut handled in eff_t1_e - break; - default: - console.log("unimplemented extended effect E", ch.effect.toString(16)); - break; - } -} - -function eff_t0_f(ch, data) { // set tempo - if (data == 0) { - console.log("tempo 0?"); - return; - } else if(data < 0x20) { - tempo = data; - } else { - bpm = data; - } -} - -function eff_unimplemented_t0(ch, data) { - console.log("unimplemented effect", ch.effect.toString(16), data.toString(16)); -} - -var effects_t0 = [ // effect functions on tick 0 - eff_t1_0, // 1, arpeggio is processed on all ticks - eff_t0_1, - eff_t0_2, - eff_t0_3, - 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_8, // 8 - eff_t0_9, // 9 - eff_t0_a, // a - eff_t0_b, // b - eff_t0_c, // c - eff_t0_d, // d - eff_t0_e, // e - eff_t0_f, // f -]; - -function eff_t1_0(ch) { // arpeggio - if (ch.effectdata != 0 && ch.inst != undefined) { - var arpeggio = [0, ch.effectdata>>4, ch.effectdata&15]; - var note = ch.note + arpeggio[cur_tick % 3]; - ch.period = PeriodForNote(ch, note); - } -} - -function eff_t1_1(ch) { // pitch slide up - if (ch.slideupspeed !== undefined) { - // is this limited? it appears not - ch.period -= ch.slideupspeed; - } -} - -function eff_t1_2(ch) { // pitch slide down - if (ch.slidedownspeed !== undefined) { - // 1728 is the period for C-1 - ch.period = Math.min(1728, ch.period + ch.slidedownspeed); - } -} - -function eff_t1_3(ch) { // portamento - if (ch.periodtarget !== undefined && ch.portaspeed !== undefined) { - if (ch.period > ch.periodtarget) { - ch.period = Math.max(ch.periodtarget, ch.period - ch.portaspeed); - } else { - ch.period = Math.min(ch.periodtarget, ch.period + ch.portaspeed); - } - } -} - -function eff_t1_4(ch) { // vibrato - ch.periodoffset = Math.sin(ch.vibratopos * Math.PI / 32) * ch.vibratodepth; - if (isNaN(ch.periodoffset)) { - console.log("vibrato periodoffset NaN?", ch.vibratopos, ch.vibratodepth); - ch.periodoffset = 0; - } - ch.vibratopos += ch.vibratospeed; - ch.vibratopos &= 63; -} - -function eff_t1_5(ch) { // portamento + volume slide - eff_t1_a(ch); - eff_t1_3(ch); -} - -function eff_t1_6(ch) { // vibrato + volume slide - eff_t1_a(ch); - eff_t1_4(ch); -} - -function eff_t1_a(ch) { // volume slide - if (ch.volumeslide !== undefined) { - ch.vol = Math.max(0, Math.min(64, ch.vol + ch.volumeslide)); - } -} - -function eff_t1_e(ch) { // note cut - switch (ch.effectdata >> 4) { - case 0x0c: - if (cur_tick == (ch.effectdata & 0x0f)) { - ch.vol = 0; - } - break; - } -} - -function eff_nop() {} -function eff_unimplemented() {} -var effects_t1 = [ // effect functions on tick 1+ - eff_t1_0, - eff_t1_1, - eff_t1_2, - eff_t1_3, - eff_t1_4, - eff_t1_5, // 5 - eff_t1_6, // 6 - eff_unimplemented, // 7 - eff_nop, // 8 - eff_nop, // 9 - eff_t1_a, // a - eff_nop, // b - eff_nop, // c - eff_nop, // d - eff_t1_e, // e - eff_nop, // f -]; - function ConvertSample(array, bits) { var len = array.length; var acc = 0; @@ -939,6 +719,7 @@ function playXM(arrayBuf) { vLprev: 0, vRprev: 0, mute: 0, volE: 0, panE: 0, + retrig: 0, vibratodepth: 1, vibratospeed: 1, }) diff --git a/xmeffects.js b/xmeffects.js new file mode 100644 index 0000000..30c1998 --- /dev/null +++ b/xmeffects.js @@ -0,0 +1,289 @@ +function eff_t1_0(ch) { // arpeggio + if (ch.effectdata != 0 && ch.inst != undefined) { + var arpeggio = [0, ch.effectdata>>4, ch.effectdata&15]; + var note = ch.note + arpeggio[cur_tick % 3]; + ch.period = PeriodForNote(ch, note); + } +} + +function eff_t0_1(ch, data) { // pitch slide up + if (data != 0) { + ch.slideupspeed = data; + } +} + +function eff_t1_1(ch) { // pitch slide up + if (ch.slideupspeed !== undefined) { + // is this limited? it appears not + ch.period -= ch.slideupspeed; + } +} + +function eff_t0_2(ch, data) { // pitch slide down + if (data != 0) { + ch.slidedownspeed = data; + } +} + +function eff_t1_2(ch) { // pitch slide down + if (ch.slidedownspeed !== undefined) { + // 1728 is the period for C-1 + ch.period = Math.min(1728, ch.period + ch.slidedownspeed); + } +} + +function eff_t0_3(ch, data) { // portamento + if (data != 0) { + ch.portaspeed = data; + } +} + +function eff_t1_3(ch) { // portamento + if (ch.periodtarget !== undefined && ch.portaspeed !== undefined) { + if (ch.period > ch.periodtarget) { + ch.period = Math.max(ch.periodtarget, ch.period - ch.portaspeed); + } else { + ch.period = Math.min(ch.periodtarget, ch.period + ch.portaspeed); + } + } +} + +function eff_t0_4(ch, data) { // vibrato + if (data & 0x0f) { + ch.vibratodepth = data & 0x0f; + } + if (data >> 4) { + ch.vibratospeed = data >> 4; + } + eff_t1_4(ch, data); +} + +function eff_t1_4(ch) { // vibrato + ch.periodoffset = Math.sin(ch.vibratopos * Math.PI / 32) * ch.vibratodepth; + if (isNaN(ch.periodoffset)) { + console.log("vibrato periodoffset NaN?", ch.vibratopos, ch.vibratodepth); + ch.periodoffset = 0; + } + ch.vibratopos += ch.vibratospeed; + ch.vibratopos &= 63; +} + +function eff_t1_5(ch) { // portamento + volume slide + eff_t1_a(ch); + eff_t1_3(ch); +} + +function eff_t1_6(ch) { // vibrato + volume slide + eff_t1_a(ch); + eff_t1_4(ch); +} + +function eff_t0_8(ch, data) { // set panning + ch.pan = data; +} + +function eff_t0_9(ch, data) { // sample offset + ch.off = data * 256; +} + +function eff_t0_a(ch, data) { // volume slide + if (data) { + if (data & 0x0f) { + ch.volumeslide = -(data & 0x0f); + } else { + ch.volumeslide = data >> 4; + } + } +} + +function eff_t1_a(ch) { // volume slide + if (ch.volumeslide !== undefined) { + ch.vol = Math.max(0, Math.min(64, ch.vol + ch.volumeslide)); + } +} + +function eff_t0_b(ch, data) { // song jump (untested) + if (data < songpats.length) { + cur_songpos = data + cur_pat = songpats[cur_songpos]; + } +} + +function eff_t0_c(ch, data) { // set volume + ch.vol = Math.min(64, data); +} + +function eff_t0_d(ch, data) { // pattern jump + cur_songpos++; + if (cur_songpos >= songpats.length) + cur_songpos = song_looppos; + cur_pat = songpats[cur_songpos]; + cur_row = data; +} + +function eff_t0_e(ch, data) { // extended effects! + var eff = data >> 4; + data = data & 0x0f; + switch (eff) { + case 1: // fine porta up + ch.period -= data; + break; + case 2: // fine porta down + ch.period += data; + break; + case 8: // panning + ch.pan = data * 0x11; + break; + case 0x0a: // fine vol slide up (with memory) + if (data == 0 && ch.finevolup != undefined) + data = ch.finevolup; + ch.vol = Math.min(64, ch.vol + data); + ch.finevolup = data; + break; + case 0x0b: // fine vol slide down + if (data == 0 && ch.finevoldown != undefined) + data = ch.finevoldown; + ch.vol = Math.max(0, ch.vol - data); + ch.finevoldown = data; + break; + case 0x0c: // note cut handled in eff_t1_e + break; + default: + console.log("unimplemented extended effect E", ch.effect.toString(16)); + break; + } +} + +function eff_t1_e(ch) { // note cut + switch (ch.effectdata >> 4) { + case 0x0c: + if (cur_tick == (ch.effectdata & 0x0f)) { + ch.vol = 0; + } + break; + } +} + +function eff_t0_f(ch, data) { // set tempo + if (data == 0) { + console.log("tempo 0?"); + return; + } else if(data < 0x20) { + tempo = data; + } else { + bpm = data; + } +} + +function eff_t0_r(ch, data) { // retrigger + if (data & 0x0f) ch.retrig = (ch.retrig & 0xf0) + (data & 0x0f); + if (data & 0xf0) ch.retrig = (ch.retrig & 0x0f) + (data & 0xf0); + + // retrigger volume table + switch (ch.retrig >> 4) { + case 1: ch.vol -= 1; break; + case 2: ch.vol -= 2; break; + case 3: ch.vol -= 4; break; + case 4: ch.vol -= 8; break; + case 5: ch.vol -= 16; break; + case 6: ch.vol *= 2; ch.vol /= 3; break; + case 7: ch.vol /= 2; break; + case 9: ch.vol += 1; break; + case 0x0a: ch.vol += 2; break; + case 0x0b: ch.vol += 4; break; + case 0x0c: ch.vol += 8; break; + case 0x0d: ch.vol += 16; break; + case 0x0e: ch.vol *= 3; ch.vol /= 2; break; + case 0x0f: ch.vol *= 2; break; + } + ch.vol = Math.min(64, Math.max(0, ch.vol)); +} + +function eff_t1_r(ch) { + if (cur_tick % (ch.retrig & 0x0f) == 0) { + ch.off = 0; + } +} + +function eff_unimplemented() {} +function eff_unimplemented_t0(ch, data) { + console.log("unimplemented effect", ch.effect.toString(16), data.toString(16)); +} + +var effects_t0 = [ // effect functions on tick 0 + eff_t1_0, // 1, arpeggio is processed on all ticks + eff_t0_1, + eff_t0_2, + eff_t0_3, + 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_8, // 8 + eff_t0_9, // 9 + eff_t0_a, // a + eff_t0_b, // b + eff_t0_c, // c + eff_t0_d, // d + eff_t0_e, // e + eff_t0_f, // f + eff_unimplemented_t0, // g + eff_unimplemented_t0, // h + eff_unimplemented_t0, // i + eff_unimplemented_t0, // j + eff_unimplemented_t0, // k + eff_unimplemented_t0, // l + eff_unimplemented_t0, // m + eff_unimplemented_t0, // n + eff_unimplemented_t0, // o + eff_unimplemented_t0, // p + eff_unimplemented_t0, // q + eff_t0_r, // r + eff_unimplemented_t0, // s + eff_unimplemented_t0, // t + eff_unimplemented_t0, // u + eff_unimplemented_t0, // v + eff_unimplemented_t0, // w + eff_unimplemented_t0, // x + eff_unimplemented_t0, // y + eff_unimplemented_t0, // z +]; + +var effects_t1 = [ // effect functions on tick 1+ + eff_t1_0, + eff_t1_1, + eff_t1_2, + eff_t1_3, + eff_t1_4, + eff_t1_5, // 5 + eff_t1_6, // 6 + eff_unimplemented, // 7 + null, // 8 + null, // 9 + eff_t1_a, // a + null, // b + null, // c + null, // d + eff_t1_e, // e + null, // f + eff_unimplemented, // g + eff_unimplemented, // h + eff_unimplemented, // i + eff_unimplemented, // j + eff_unimplemented, // k + eff_unimplemented, // l + eff_unimplemented, // m + eff_unimplemented, // n + eff_unimplemented, // o + eff_unimplemented, // p + eff_unimplemented, // q + eff_t1_r, // r + eff_unimplemented, // s + eff_unimplemented, // t + eff_unimplemented, // u + eff_unimplemented, // v + eff_unimplemented, // w + eff_unimplemented, // x + eff_unimplemented, // y + eff_unimplemented // z +]; From 114240ca9f6e2eced284dfd5c4f30996663544f3 Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Sun, 1 Nov 2015 09:26:03 -0800 Subject: [PATCH 07/78] oscilloscopes. couldn't really think of a better way to do it, but it looks pretty awesome. you can really see the filter effect too. --- index.html | 6 ++---- xm.js | 59 +++++++++++++++++++++++++++++++++++++++++++--------- xmeffects.js | 2 +- 3 files changed, 52 insertions(+), 15 deletions(-) diff --git a/index.html b/index.html index 6d0b4e5..c27548e 100644 --- a/index.html +++ b/index.html @@ -16,11 +16,9 @@ todo: - file picker, upload more xms to a host we can access cross-origin - make display even more faithfully FT2-like - render instrument list in ft2 font - - oscilloscopes - - fix bugs! - - envelope loops - are they right or not now? + - oscilloscope trigger on zero crossing or something - multi-sample instruments - - missing XM effects (in rough order of priority): + - missing XM effects: - E3x, E4x, E5x, E6x, E7x, E9x, EDx, EEx - 7xy - tremolo - Gxx, Hxy, Kxx, Lxx, Pxy, Txy diff --git a/xm.js b/xm.js index ebd2b1a..450dabf 100644 --- a/xm.js +++ b/xm.js @@ -15,6 +15,7 @@ audioContext = window.AudioContext || window.webkitAudioContext; var _fontmap_notes = [8*5, 8*22, 8*28]; var _pattern_cellwidth = 16 + 4 + 8 + 4 + 8 + 16 + 4; +var _scope_width = _pattern_cellwidth - 1; var _pattern_border = 20; var pat_canvas_patnum; function RenderPattern(canv, pattern) { @@ -166,16 +167,36 @@ function RedrawScreen() { } while(e.t < t && audio_events.length > 0); if (e == undefined) return; var VU = e.vu; + var scopes = e.scopes; - // update VU meters - var canvas = document.getElementById("vu"); - var ctx = canvas.getContext("2d"); - ctx.fillStyle = '#000'; - ctx.fillRect(0, 0, 16 * nchan, 64); - ctx.fillStyle = '#0f0'; - for (var j = 0; j < nchan; j++) { - var y = -Math.log(VU[j])*10; - ctx.fillRect(j*16, y, 15, 64-y); + if (e.scopes != undefined) { + // update VU meters & oscilliscopes + var canvas = document.getElementById("vu"); + var ctx = canvas.getContext("2d"); + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, canvas.width, 64); + ctx.fillStyle = '#0f0'; + ctx.strokeStyle = '#55acff'; + for (var j = 0; j < nchan; j++) { + var x = _pattern_border + j * _pattern_cellwidth; + // render channel number + if (j >= 10) { + ctx.drawImage(fontimg, 8*((j/10)|0), 4*8, 4, 8, x, 1, 4, 8); + } + ctx.drawImage(fontimg, 8*(j%10), 4*8, 4, 8, x+4, 1, 4, 8); + + // volume in dB as a green bar + var vu_y = -Math.log(VU[j])*10; + ctx.fillRect(x, vu_y, 2, 64-vu_y); + + // oscilloscope + var scope = scopes[j]; + ctx.beginPath(); + for (var k = 0; k < _scope_width; k++) { + ctx.lineTo(x + 1 + k, 32 - 16 * scope[k]); + } + ctx.stroke(); + } } var debug = document.getElementById("debug"); @@ -616,14 +637,32 @@ function audio_cb(e) { } var tickduration = Math.min(buflen, ticklen - cur_ticksamp); var VU = new Float32Array(nchan); + var scopes = undefined; for (var j = 0; j < nchan; j++) { + var scope; + if (tickduration >= _scope_width) { + scope = new Float32Array(_scope_width); + for (var k = 0; k < _scope_width; k++) { + scope[k] = -dataL[offset+k] - dataR[offset+k]; + } + } + VU[j] = MixChannelIntoBuf( channelinfo[j], offset, offset + tickduration, dataL, dataR) / tickduration; + + if (tickduration >= _scope_width) { + for (var k = 0; k < _scope_width; k++) { + scope[k] += dataL[offset+k] + dataR[offset+k]; + } + if (scopes == undefined) scopes = []; + scopes.push(scope); + } } audio_events.push({ t: e.playbackTime + (0.0 + offset) / f_smp, vu: VU, + scopes: scopes, songpos: cur_songpos, pat: cur_pat, row: cur_row - 1 @@ -707,7 +746,7 @@ function playXM(arrayBuf) { var flags = dv.getUint16(0x4a, true); tempo = dv.getUint16(0x4c, true); bpm = dv.getUint16(0x4e, true); - document.getElementById('vu').width = 16 * nchan; + document.getElementById('vu').width = _pattern_border + _pattern_cellwidth * nchan; for (var i = 0; i < nchan; i++) { channelinfo.push({ number: i, diff --git a/xmeffects.js b/xmeffects.js index 30c1998..4c25716 100644 --- a/xmeffects.js +++ b/xmeffects.js @@ -207,7 +207,7 @@ function eff_t1_r(ch) { function eff_unimplemented() {} function eff_unimplemented_t0(ch, data) { - console.log("unimplemented effect", ch.effect.toString(16), data.toString(16)); + console.log("unimplemented effect", prettify_effect(ch.effect, data)); } var effects_t0 = [ // effect functions on tick 0 From d68e8e8225a77df04f3c137d8abcb97c08da5238 Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Sun, 1 Nov 2015 21:26:03 -0800 Subject: [PATCH 08/78] show all instrument names --- xm.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/xm.js b/xm.js index 450dabf..123d1b6 100644 --- a/xm.js +++ b/xm.js @@ -822,6 +822,7 @@ function playXM(arrayBuf) { patterns.push(pattern); } + var namelist = []; // now load instruments for (i = 0; i < ninst; i++) { var hdrsiz = dv.getUint32(idx, true); @@ -944,6 +945,10 @@ function playXM(arrayBuf) { console.log("empty instrument", i, hdrsiz, idx); instruments.push(null); } + + var n = (i+1).toString(16); + if (i < 15) n = ' ' + n; + namelist.push(n + " " + instname); } audioctx = new audioContext(); @@ -960,15 +965,7 @@ function playXM(arrayBuf) { gfxpattern.width = _pattern_cellwidth * nchan + _pattern_border; var instrlist = document.getElementById("instruments"); - var list = []; - for (var i = 0; i < ninst; i++) { - var inst = instruments[i]; - if (inst == null) continue; - var n = (i+1).toString(16); - if (i < 15) n = ' ' + n; - list.push(n + " " + inst.name); - } - instrlist.innerHTML = list.join("\n"); + instrlist.innerHTML = namelist.join("\n"); // start playing gainNode.connect(audioctx.destination); From 22dd555228f523904cce6b31a4d76a9952b92c96 Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Sun, 1 Nov 2015 21:50:49 -0800 Subject: [PATCH 09/78] zoom scopes out 4x; looks way better --- xm.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/xm.js b/xm.js index 123d1b6..6622528 100644 --- a/xm.js +++ b/xm.js @@ -640,10 +640,10 @@ function audio_cb(e) { var scopes = undefined; for (var j = 0; j < nchan; j++) { var scope; - if (tickduration >= _scope_width) { + if (tickduration >= 4*_scope_width) { scope = new Float32Array(_scope_width); for (var k = 0; k < _scope_width; k++) { - scope[k] = -dataL[offset+k] - dataR[offset+k]; + scope[k] = -dataL[offset+k*4] - dataR[offset+k*4]; } } @@ -651,9 +651,9 @@ function audio_cb(e) { channelinfo[j], offset, offset + tickduration, dataL, dataR) / tickduration; - if (tickduration >= _scope_width) { + if (tickduration >= 4*_scope_width) { for (var k = 0; k < _scope_width; k++) { - scope[k] += dataL[offset+k] + dataR[offset+k]; + scope[k] += dataL[offset+k*4] + dataR[offset+k*4]; } if (scopes == undefined) scopes = []; scopes.push(scope); From c4370f447c356e07edcd493ed61b91e7779ddcc2 Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Mon, 2 Nov 2015 09:55:04 -0800 Subject: [PATCH 10/78] implemented multiple samples / instrument --- index.html | 1 - xm.js | 139 +++++++++++++++++++++++++++++------------------------ 2 files changed, 77 insertions(+), 63 deletions(-) diff --git a/index.html b/index.html index c27548e..3bc0bb7 100644 --- a/index.html +++ b/index.html @@ -17,7 +17,6 @@ todo: - make display even more faithfully FT2-like - render instrument list in ft2 font - oscilloscope trigger on zero crossing or something - - multi-sample instruments - missing XM effects: - E3x, E4x, E5x, E6x, E7x, E9x, EDx, EEx - 7xy - tremolo diff --git a/xm.js b/xm.js index 6622528..b4e90fd 100644 --- a/xm.js +++ b/xm.js @@ -154,7 +154,7 @@ function UpdateChannelPeriod(ch, period) { } function PeriodForNote(ch, note) { - return 1920 - note*16 - ch.inst.fine / 8.0; + return 1920 - (note + ch.samp.note)*16 - ch.samp.fine / 8.0; } var audio_events = []; @@ -252,8 +252,10 @@ function next_row() { ch.inst = inst; // retrigger unless overridden below triggernote = true; - ch.pan = inst.pan; - ch.vol = inst.vol; + if (ch.samp) { + ch.vol = ch.samp.vol; + ch.pan = ch.samp.pan; + } } else { // console.log("invalid inst", r[i][1], instruments.length); } @@ -266,9 +268,12 @@ function next_row() { triggernote = false; } else { if (inst != undefined) { - var note = r[i][0] + inst.note; + var note = r[i][0]; ch.note = note; triggernote = true; + ch.samp = inst.samples[inst.samplemap[ch.note]]; + ch.pan = ch.samp.pan; + ch.vol = ch.samp.vol; } } } @@ -455,7 +460,7 @@ function MixSilenceIntoBuf(ch, start, end, dataL, dataR) { function MixChannelIntoBuf(ch, start, end, dataL, dataR) { var inst = ch.inst; - var samp, sample_end; + var instsamp = ch.samp; var loop = false; var looplen = 0, loopstart = 0; @@ -464,15 +469,15 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) { return MixSilenceIntoBuf(ch, start, end, dataL, dataR); } - samp = inst.sampledata; - sample_end = inst.len; - if ((inst.type & 3) == 1) { // todo: support pingpong + var samp = instsamp.sampledata; + var sample_end = instsamp.len; + if ((instsamp.type & 3) == 1) { // todo: support pingpong loop = true; - loopstart = inst.loop; - looplen = inst.looplen; + loopstart = instsamp.loop; + looplen = instsamp.looplen; sample_end = loopstart + looplen; } - var samplen = inst.len; + var samplen = instsamp.len; 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 @@ -703,33 +708,34 @@ function ConvertSample(array, bits) { // optimization: unroll short sample loops so we can run our inner mixing loop // uninterrupted for as long as possible; this also handles pingpong loops. -function UnrollSampleLoop(inst) { - var nloops = ((2048 + inst.looplen - 1) / inst.looplen) | 0; - var pingpong = inst.type & 2; +function UnrollSampleLoop(samp) { + var nloops = ((2048 + samp.looplen - 1) / samp.looplen) | 0; + console.log(samp.looplen, nloops); + var pingpong = samp.type & 2; if (pingpong) { // make sure we have an even number of loops if we are pingponging nloops = (nloops + 1) & (~1); } - var samplesiz = inst.loop + nloops * inst.looplen; - var samp = new Float32Array(samplesiz); - for (var i = 0; i < inst.loop; i++) { - samp[i] = inst.sampledata[i]; + var samplesiz = samp.loop + nloops * samp.looplen; + var data = new Float32Array(samplesiz); + for (var i = 0; i < samp.loop; i++) { + data[i] = samp.sampledata[i]; } for (var j = 0; j < nloops; j++) { if ((j&1) && pingpong) { - for (var k = inst.looplen - 1; k >= 0; k--) { - samp[i++] = inst.sampledata[inst.loop + k]; + for (var k = samp.looplen - 1; k >= 0; k--) { + data[i++] = samp.sampledata[samp.loop + k]; } } else { - for (var k = 0; k < inst.looplen; k++) { - samp[i++] = inst.sampledata[inst.loop + k]; + for (var k = 0; k < samp.looplen; k++) { + data[i++] = samp.sampledata[samp.loop + k]; } } } - console.log("unrolled sample loop", inst.number, "looplen", inst.looplen, "x", nloops, " = ", samplesiz); - inst.sampledata = samp; - inst.looplen = nloops * inst.looplen; - inst.type = 1; + console.log("unrolled sample loop; looplen", samp.looplen, "x", nloops, " = ", samplesiz); + samp.sampledata = data; + samp.looplen = nloops * samp.looplen; + samp.type = 1; } function playXM(arrayBuf) { @@ -829,6 +835,10 @@ function playXM(arrayBuf) { var instname = getstring(dv, idx+0x4, 22); var nsamp = dv.getUint16(idx+0x1b, true); if (nsamp > 0) { + // return a slice so we have a fresh copy and don't retain pointers to + // the original xm file arraybuf forever + var samplemap = new Uint8Array(arrayBuf, idx+33, 96).slice(); + var env_nvol = dv.getUint8(idx+225); var env_vol_type = dv.getUint8(idx+233); var env_vol_sustain = dv.getUint8(idx+227); @@ -855,29 +865,48 @@ function playXM(arrayBuf) { hdrsiz, i, instname, nsamp, samphdrsiz); idx += hdrsiz; var totalsamples = 0; + var samps = []; for (var j = 0; j < nsamp; j++) { var samplen = dv.getUint32(idx, true); - if (j == 0) { - var samplen0 = samplen; // FIXME HACK HACK HACK - var samploop = dv.getUint32(idx+4, true); - var samplooplen = dv.getUint32(idx+8, true); - var sampvol = dv.getUint8(idx+12); - var sampfinetune = dv.getInt8(idx+13); - var samptype = dv.getUint8(idx+14); - var samppan = dv.getUint8(idx+15); - var sampnote = dv.getInt8(idx+16); - var sampname = getstring(dv, idx+18, 22); - var sampleoffset = idx + samphdrsiz; - console.log("sample %d: len %d name '%s' loop %d/%d vol %d", - j, samplen, sampname, samploop, samplooplen, sampvol); - console.log(" type %d note %s(%d) finetune %d pan %d", - samptype, prettify_note(sampnote + 12*4), sampnote, sampfinetune, samppan); - console.log(" vol env", env_vol, env_vol_sustain, - env_vol_loop_start, env_vol_loop_end, "type", env_vol_type, - "fadeout", vol_fadeout); - console.log(" pan env", env_pan, env_pan_sustain, - env_pan_loop_start, env_pan_loop_end, "type", env_pan_type); + var samploop = dv.getUint32(idx+4, true); + var samplooplen = dv.getUint32(idx+8, true); + var sampvol = dv.getUint8(idx+12); + var sampfinetune = dv.getInt8(idx+13); + var samptype = dv.getUint8(idx+14); + var samppan = dv.getUint8(idx+15); + var sampnote = dv.getInt8(idx+16); + var sampname = getstring(dv, idx+18, 22); + var sampleoffset = idx + samphdrsiz + totalsamples; + console.log("sample %d: len %d name '%s' loop %d/%d vol %d", + j, samplen, sampname, samploop, samplooplen, sampvol); + console.log(" type %d note %s(%d) finetune %d pan %d", + samptype, prettify_note(sampnote + 12*4), sampnote, sampfinetune, samppan); + console.log(" vol env", env_vol, env_vol_sustain, + env_vol_loop_start, env_vol_loop_end, "type", env_vol_type, + "fadeout", vol_fadeout); + console.log(" pan env", env_pan, env_pan_sustain, + env_pan_loop_start, env_pan_loop_end, "type", env_pan_type); + var samp = { + 'len': samplen, 'loop': samploop, + 'looplen': samplooplen, 'note': sampnote, 'fine': sampfinetune, + 'pan': samppan, 'type': samptype, 'vol': sampvol, + 'fine': sampfinetune, + 'sampledata': ConvertSample( + new Uint8Array(arrayBuf, sampleoffset, samplen), samptype & 16), + }; + // length / pointers are all specified in bytes; fixup for 16-bit samples + if (samptype & 16) { + samp.len /= 2; + samp.loop /= 2; + samp.looplen /= 2; } + + // unroll short loops and any pingpong loops + if ((samp.type & 3) && (samp.looplen < 2048 || (samp.type & 2))) { + UnrollSampleLoop(samp); + } + samps.push(samp); + idx += samphdrsiz; totalsamples += samplen; } @@ -885,24 +914,10 @@ function playXM(arrayBuf) { inst = { 'name': instname, 'number': i, - 'len': samplen0, 'loop': samploop, - 'looplen': samplooplen, 'note': sampnote, 'fine': sampfinetune, - 'pan': samppan, 'type': samptype, 'vol': sampvol, - 'fine': sampfinetune, 'fadeout': vol_fadeout, - 'sampledata': ConvertSample(new Uint8Array(arrayBuf, sampleoffset, samplen0), samptype & 16), + 'samplemap': samplemap, + 'samples': samps, }; - if (samptype & 16) { - inst.len /= 2; - inst.loop /= 2; - inst.looplen /= 2; - } - - // unroll short loops and any pingpong loops - if ((inst.type & 3) && (inst.looplen < 2048 || (inst.type & 2))) { - UnrollSampleLoop(inst); - } - if (env_vol_type) { // insert an automatic fadeout to 0 at the end of the envelope var env_end_tick = env_vol[env_vol.length-2]; From 27d0a0b1c354a43f09b4f8a9b4ce5975464abb7f Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Mon, 2 Nov 2015 16:51:02 -0800 Subject: [PATCH 11/78] render volume effect column with the icons --- xm.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/xm.js b/xm.js index b4e90fd..cf3b5c0 100644 --- a/xm.js +++ b/xm.js @@ -65,8 +65,7 @@ function RenderPattern(canv, pattern) { if (vol < 0x10) { ctx.drawImage(fontimg, 312, 0, 8, 8, dx, dy, 8, 8); } else { - vol -= 0x10; - ctx.drawImage(fontimg, 8*(vol>>4), 4*8, 4, 8, dx, dy, 4, 8); + ctx.drawImage(fontimg, 8*(vol>>4) + 56*8, 4*8, 8, 8, dx, dy, 8, 8); ctx.drawImage(fontimg, 8*(vol&15), 4*8, 4, 8, dx+4, dy, 4, 8); } dx += 8; From 06ad3ed8a5aecca7fe34d1e6bcabdaa8b1b27de4 Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Mon, 2 Nov 2015 22:29:34 -0800 Subject: [PATCH 12/78] fix instrument trigger glitch --- xm.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xm.js b/xm.js index cf3b5c0..a2ec4b5 100644 --- a/xm.js +++ b/xm.js @@ -251,7 +251,8 @@ function next_row() { ch.inst = inst; // retrigger unless overridden below triggernote = true; - if (ch.samp) { + if (ch.note) { + ch.samp = inst.samples[inst.samplemap[ch.note]]; ch.vol = ch.samp.vol; ch.pan = ch.samp.pan; } From 07cd6e81118807e8e277b166fa947f641b5f4775 Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Mon, 2 Nov 2015 22:50:46 -0800 Subject: [PATCH 13/78] another corner case, another broken .xm --- xm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xm.js b/xm.js index a2ec4b5..e7d3f6b 100644 --- a/xm.js +++ b/xm.js @@ -465,7 +465,7 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) { var looplen = 0, loopstart = 0; // nothing on this channel, just filter the last dc offset back down to zero - if (inst == undefined || ch.mute) { + if (instsamp == undefined || inst == undefined || ch.mute) { return MixSilenceIntoBuf(ch, start, end, dataL, dataR); } From 5967da7353056435bb1b06eb5142f7f555ae8eaf Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Tue, 3 Nov 2015 07:18:12 -0800 Subject: [PATCH 14/78] only reset pan/vol on instrument, not note --- xm.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/xm.js b/xm.js index e7d3f6b..de89689 100644 --- a/xm.js +++ b/xm.js @@ -270,10 +270,14 @@ function next_row() { if (inst != undefined) { var note = r[i][0]; ch.note = note; - triggernote = true; ch.samp = inst.samples[inst.samplemap[ch.note]]; - ch.pan = ch.samp.pan; - ch.vol = ch.samp.vol; + if (triggernote) { + // if we were already triggering the note, reset vol/pan using + // (potentially) new sample + ch.pan = ch.samp.pan; + ch.vol = ch.samp.vol; + } + triggernote = true; } } } From ea6edaf2e4ba148f3f6f2d54a008f0c014946ae2 Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Tue, 3 Nov 2015 08:42:09 -0800 Subject: [PATCH 15/78] add play/pause function --- index.html | 5 +- xm.js | 248 +++++++++++++++++++++++++++++++++-------------------- 2 files changed, 157 insertions(+), 96 deletions(-) diff --git a/index.html b/index.html index 3bc0bb7..20a54db 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,8 @@ - + +
@@ -14,9 +15,7 @@
 code: github.com/a1k0n/jsxm
 todo:
  - file picker, upload more xms to a host we can access cross-origin
- - make display even more faithfully FT2-like
  - render instrument list in ft2 font
- - oscilloscope trigger on zero crossing or something
  - missing XM effects:
    - E3x, E4x, E5x, E6x, E7x, E9x, EDx, EEx
    - 7xy - tremolo
diff --git a/xm.js b/xm.js
index de89689..0f93491 100644
--- a/xm.js
+++ b/xm.js
@@ -1,18 +1,17 @@
-var audioctx;
-var songname = '';
-var fontimg = new Image();
-var pat_canvas = document.createElement('canvas');
-fontimg.onload = function() {
-  // FIXME: don't attempt to render until this loads
-};
+var xm;  // contains all song data
+var audioctx;  // AudioContext
 
+// Load font (ripped from FastTracker 2)
+var fontimg = new Image();
 fontimg.src = "ft2font.png";
 
+// canvas to render patterns onto
+var pat_canvas = document.createElement('canvas');
+
+// 
 var _note_names = ["C-", "C#", "D-", "D#", "E-", "F-", "F#", "G-", "G#", "A-", "A#", "B-"];
 var f_smp = 44100;  // updated by play callback, default value here
 
-audioContext = window.AudioContext || window.webkitAudioContext;
-
 var _fontmap_notes = [8*5, 8*22, 8*28];
 var _pattern_cellwidth = 16 + 4 + 8 + 4 + 8 + 16 + 4;
 var _scope_width = _pattern_cellwidth - 1;
@@ -121,10 +120,6 @@ function getstring(dv, offset, len) {
   return str.join('');
 }
 
-var channelinfo = [];
-var instruments = [];
-var tempo = 4;
-
 // Return 2-pole Butterworth lowpass filter coefficients for
 // center frequncy f_c (relative to sampling frequency)
 function FilterCoeffs(f_c) {
@@ -159,6 +154,8 @@ function PeriodForNote(ch, note) {
 var audio_events = [];
 var shown_row = undefined;
 function RedrawScreen() {
+  if (audio_events.length == 0) return;
+
   var e;
   var t = audioctx.currentTime;
   do {
@@ -176,7 +173,7 @@ function RedrawScreen() {
     ctx.fillRect(0, 0, canvas.width, 64);
     ctx.fillStyle = '#0f0';
     ctx.strokeStyle = '#55acff';
-    for (var j = 0; j < nchan; j++) {
+    for (var j = 0; j < xm.nchan; j++) {
       var x = _pattern_border + j * _pattern_cellwidth;
       // render channel number
       if (j >= 10) {
@@ -199,14 +196,14 @@ function RedrawScreen() {
   }
 
   var debug = document.getElementById("debug");
-  debug.innerHTML = songname + '
pat ' + e.pat + ' row ' + (e.row); + debug.innerHTML = xm.songname + '
pat ' + e.pat + ' row ' + (e.row); if (e.row != shown_row) { var gfx = document.getElementById("gfxpattern"); if (e.pat != pat_canvas_patnum) { - var p = patterns[e.pat]; + var p = xm.patterns[e.pat]; if (p != undefined) { - RenderPattern(pat_canvas, patterns[e.pat]); + RenderPattern(pat_canvas, xm.patterns[e.pat]); pat_canvas_patnum = e.pat; } } @@ -230,23 +227,23 @@ function RedrawScreen() { var cur_songpos = -1, cur_pat = -1, cur_row = 64, cur_ticksamp = 0; var cur_tick = 6; function next_row() { - if (cur_pat == -1 || cur_row >= patterns[cur_pat].length) { + if (cur_pat == -1 || cur_row >= xm.patterns[cur_pat].length) { cur_row = 0; cur_songpos++; - if (cur_songpos >= songpats.length) - cur_songpos = song_looppos; - cur_pat = songpats[cur_songpos]; + if (cur_songpos >= xm.songpats.length) + cur_songpos = xm.song_looppos; + cur_pat = xm.songpats[cur_songpos]; } - var p = patterns[cur_pat]; + var p = xm.patterns[cur_pat]; var r = p[cur_row]; cur_row++; for (var i = 0; i < r.length; i++) { - var ch = channelinfo[i]; + var ch = xm.channelinfo[i]; var inst = ch.inst; var triggernote = false; // instrument trigger if (r[i][1] != -1) { - inst = instruments[r[i][1] - 1]; + inst = xm.instruments[r[i][1] - 1]; if (inst != undefined) { ch.inst = inst; // retrigger unless overridden below @@ -408,12 +405,12 @@ EnvelopeFollower.prototype.Tick = function(release) { function next_tick() { cur_tick++; - if (cur_tick >= tempo) { + if (cur_tick >= xm.tempo) { cur_tick = 0; next_row(); } - for (var j = 0; j < nchan; j++) { - var ch = channelinfo[j]; + for (var j = 0; j < xm.nchan; j++) { + var ch = xm.channelinfo[j]; var inst = ch.inst; ch.periodoffset = 0; if (cur_tick != 0) { @@ -421,12 +418,12 @@ function next_tick() { if(ch.effectfn) ch.effectfn(ch); } if (isNaN(ch.period)) { - console.log(prettify_notedata(patterns[cur_pat][cur_row-1][j]), + console.log(prettify_notedata(xm.patterns[cur_pat][cur_row-1][j]), "set channel", j, "period to NaN"); } if (inst == undefined) continue; if (ch.env_vol == undefined) { - console.log(prettify_notedata(patterns[cur_pat][cur_row-1][j]), + console.log(prettify_notedata(xm.patterns[cur_pat][cur_row-1][j]), "set channel", j, "env_vol to undefined, but note is playing"); continue; } @@ -637,7 +634,7 @@ function audio_cb(e) { } var offset = 0; - var ticklen = 0|(f_smp * 2.5 / bpm); + var ticklen = 0|(f_smp * 2.5 / xm.bpm); while(buflen > 0) { if (cur_pat == -1 || cur_ticksamp >= ticklen) { @@ -645,9 +642,9 @@ function audio_cb(e) { cur_ticksamp -= ticklen; } var tickduration = Math.min(buflen, ticklen - cur_ticksamp); - var VU = new Float32Array(nchan); + var VU = new Float32Array(xm.nchan); var scopes = undefined; - for (var j = 0; j < nchan; j++) { + for (var j = 0; j < xm.nchan; j++) { var scope; if (tickduration >= 4*_scope_width) { scope = new Float32Array(_scope_width); @@ -657,7 +654,7 @@ function audio_cb(e) { } VU[j] = MixChannelIntoBuf( - channelinfo[j], offset, offset + tickduration, dataL, dataR) / + xm.channelinfo[j], offset, offset + tickduration, dataL, dataR) / tickduration; if (tickduration >= 4*_scope_width) { @@ -742,23 +739,24 @@ function UnrollSampleLoop(samp) { samp.type = 1; } -function playXM(arrayBuf) { +function loadXM(arrayBuf) { var dv = new DataView(arrayBuf); - window.dv = dv; + var xm = {}; - songname = getstring(dv, 17, 20); + xm.songname = getstring(dv, 17, 20); var hlen = dv.getUint32(0x3c, true) + 0x3c; var songlen = dv.getUint16(0x40, true); - song_looppos = dv.getUint16(0x42, true); - nchan = dv.getUint16(0x44, true); + xm.song_looppos = dv.getUint16(0x42, true); + xm.nchan = dv.getUint16(0x44, true); var npat = dv.getUint16(0x46, true); var ninst = dv.getUint16(0x48, true); - var flags = dv.getUint16(0x4a, true); - tempo = dv.getUint16(0x4c, true); - bpm = dv.getUint16(0x4e, true); - document.getElementById('vu').width = _pattern_border + _pattern_cellwidth * nchan; - for (var i = 0; i < nchan; i++) { - channelinfo.push({ + xm.flags = dv.getUint16(0x4a, true); + xm.tempo = dv.getUint16(0x4c, true); + xm.bpm = dv.getUint16(0x4e, true); + xm.channelinfo = []; + + for (var i = 0; i < xm.nchan; i++) { + xm.channelinfo.push({ number: i, filterstate: new Float32Array(3), vol: 0, @@ -775,18 +773,18 @@ function playXM(arrayBuf) { } console.log("header len " + hlen); - console.log("songlen %d, %d channels, %d patterns, %d instruments", songlen, nchan, npat, ninst); - console.log("loop @%d", song_looppos); - console.log("flags=%d tempo %d bpm %d", flags, tempo, bpm); + console.log("songlen %d, %d channels, %d patterns, %d instruments", songlen, xm.nchan, npat, ninst); + console.log("loop @%d", xm.song_looppos); + console.log("flags=%d tempo %d bpm %d", xm.flags, xm.tempo, xm.bpm); - songpats = []; + xm.songpats = []; for (var i = 0; i < songlen; i++) { - songpats.push(dv.getUint8(0x50 + i)); + xm.songpats.push(dv.getUint8(0x50 + i)); } - console.log("song patterns: ", songpats); + console.log("song patterns: ", xm.songpats); var idx = hlen; - patterns = []; + xm.patterns = []; for (var i = 0; i < npat; i++) { var pattern = []; var patheaderlen = dv.getUint32(idx, true); @@ -796,7 +794,7 @@ function playXM(arrayBuf) { idx += 9; for (var j = 0; patsize > 0 && j < patrows; j++) { row = []; - for (var k = 0; k < nchan; k++) { + for (var k = 0; k < xm.nchan; k++) { var byte0 = dv.getUint8(idx); idx++; var note = -1, inst = -1, vol = -1, efftype = 0, effparam = 0; if (byte0 & 0x80) { @@ -829,15 +827,19 @@ function playXM(arrayBuf) { } pattern.push(row); } - patterns.push(pattern); + xm.patterns.push(pattern); } - var namelist = []; + xm.instruments = []; // now load instruments for (i = 0; i < ninst; i++) { var hdrsiz = dv.getUint32(idx, true); var instname = getstring(dv, idx+0x4, 22); var nsamp = dv.getUint16(idx+0x1b, true); + var inst = { + 'name': instname, + 'number': i, + }; if (nsamp > 0) { // return a slice so we have a fresh copy and don't retain pointers to // the original xm file arraybuf forever @@ -915,21 +917,16 @@ function playXM(arrayBuf) { totalsamples += samplen; } idx += totalsamples; - inst = { - 'name': instname, - 'number': i, - 'fadeout': vol_fadeout, - 'samplemap': samplemap, - 'samples': samps, - }; + inst.samplemap = samplemap; + inst.samples = samps; if (env_vol_type) { // insert an automatic fadeout to 0 at the end of the envelope var env_end_tick = env_vol[env_vol.length-2]; if (!(env_vol_type & 2)) { // if there's no sustain point, create one env_vol_sustain = env_vol.length / 2; } - if (inst.fadeout > 0) { - var fadeout_ticks = 65536.0 / inst.fadeout; + if (vol_fadeout > 0) { + var fadeout_ticks = 65536.0 / vol_fadeout; env_vol.push(env_end_tick + fadeout_ticks); env_vol.push(0); } @@ -958,49 +955,114 @@ function playXM(arrayBuf) { // create a default empty envelope inst.env_pan = new Envelope([0, 32], 0, 0, 0, 0); } - instruments.push(inst); } else { idx += hdrsiz; console.log("empty instrument", i, hdrsiz, idx); - instruments.push(null); } - - var n = (i+1).toString(16); - if (i < 15) n = ' ' + n; - namelist.push(n + " " + instname); + xm.instruments.push(inst); } + var debug = document.getElementById("debug"); + console.log("loaded \"" + xm.songname + "\""); + debug.innerHTML = xm.songname; + + var instrlist = document.getElementById("instruments"); + var namelist = []; + for (var j = 0; j < xm.instruments.length; j++) { + var n = (j+1).toString(16); + if (j < 15) n = ' ' + n; + namelist.push(n + " " + xm.instruments[j].name); + } + instrlist.innerHTML = namelist.join("\n"); + + return xm; +} + +var playing = false; +var jsNode, gainNode; +var paused_events = []; +function InitAudio() { + audioContext = window.AudioContext || window.webkitAudioContext; audioctx = new audioContext(); gainNode = audioctx.createGain(); gainNode.gain.value = 0.1; // master volume jsNode = audioctx.createScriptProcessor(16384, 0, 2); jsNode.onaudioprocess = audio_cb; - jsNode.connect(gainNode); - - var debug = document.getElementById("debug"); - console.log("loaded \"" + songname + "\""); - debug.innerHTML = songname; - var gfxpattern = document.getElementById("gfxpattern"); - gfxpattern.width = _pattern_cellwidth * nchan + _pattern_border; - - var instrlist = document.getElementById("instruments"); - instrlist.innerHTML = namelist.join("\n"); - - // start playing gainNode.connect(audioctx.destination); } -var xmReq = new XMLHttpRequest(); -var uri = location.search.substr(1); -if (uri == "") { - uri = "kamel.xm"; -} -xmReq.open("GET", uri, true); -xmReq.responseType = "arraybuffer"; -xmReq.onload = function (xmEvent) { - var arrayBuffer = xmReq.response; - if (arrayBuffer) { - playXM(arrayBuffer); +function PlayXM() { + if (!playing) { + // put paused events back into action, if any + var t = audioctx.currentTime; + while (paused_events.length > 0) { + var e = paused_events.shift(); + e.t += t; + audio_events.push(e); + } + // start playing + jsNode.connect(gainNode); + requestAnimationFrame(RedrawScreen); } + playing = true; } -xmReq.send(null); + +function PauseXM() { + if (playing) { + jsNode.disconnect(gainNode); + // grab all the audio events + var t = audioctx.currentTime; + while (audio_events.length > 0) { + var e = audio_events.shift(); + e.t -= t; + paused_events.push(e); + } + } + playing = false; +} + +function main() { + InitAudio(); + var xmReq = new XMLHttpRequest(); + var uri = location.search.substr(1); + if (uri == "") { + uri = "kamel.xm"; + } + xmReq.open("GET", uri, true); + xmReq.responseType = "arraybuffer"; + xmReq.onload = function (xmEvent) { + var arrayBuffer = xmReq.response; + if (arrayBuffer) { + xm = loadXM(arrayBuffer); + } else { + console.log("unable to load", uri); + } + document.getElementById('vu').width = _pattern_border + _pattern_cellwidth * xm.nchan; + var gfxpattern = document.getElementById("gfxpattern"); + gfxpattern.width = _pattern_cellwidth * xm.nchan + _pattern_border; + var playbutton = document.getElementById('playpause'); + playbutton.onclick = function() { + if (playing) { + PauseXM(); + playbutton.innerHTML='Play'; + } else { + PlayXM(); + playbutton.innerHTML='Pause'; + } + } + playbutton.disabled = false; + // generate a fake audio event to render the initial paused screen + var scopes = []; + for (var i = 0; i < xm.nchan; i++) { + scopes.push(new Float32Array(_scope_width)); + } + audio_events.push({ + t: 0, row: 0, pat: xm.songpats[0], + vu: new Float32Array(xm.nchan), + scopes: scopes + }); + RedrawScreen(); + } + xmReq.send(null); +} + From fd52b21e3ff75622e80230b00d361dfa74da70ab Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Tue, 3 Nov 2015 09:50:19 -0800 Subject: [PATCH 16/78] only instruments with samplemaps can play by refactoring the xm loader i inadvertently broke this --- xm.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/xm.js b/xm.js index 0f93491..05ddf84 100644 --- a/xm.js +++ b/xm.js @@ -244,11 +244,11 @@ function next_row() { // instrument trigger if (r[i][1] != -1) { inst = xm.instruments[r[i][1] - 1]; - if (inst != undefined) { + if (inst && inst.samplemap) { ch.inst = inst; // retrigger unless overridden below triggernote = true; - if (ch.note) { + if (ch.note && inst.samplemap) { ch.samp = inst.samples[inst.samplemap[ch.note]]; ch.vol = ch.samp.vol; ch.pan = ch.samp.pan; @@ -264,7 +264,7 @@ function next_row() { ch.release = 1; triggernote = false; } else { - if (inst != undefined) { + if (inst && inst.samplemap) { var note = r[i][0]; ch.note = note; ch.samp = inst.samples[inst.samplemap[ch.note]]; @@ -324,7 +324,7 @@ function next_row() { ch.periodtarget = PeriodForNote(ch, ch.note); } triggernote = false; - if (inst != undefined) { + if (inst && inst.samplemap) { if (ch.env_vol == undefined) { // note wasn't already playing; we basically have to ignore the // portamento and just trigger From 3c0a865824a1c2437439df7ff4cb220b264528c2 Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Tue, 3 Nov 2015 09:57:40 -0800 Subject: [PATCH 17/78] update the effects code after moving stuff into xm --- xmeffects.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/xmeffects.js b/xmeffects.js index 4c25716..a936bba 100644 --- a/xmeffects.js +++ b/xmeffects.js @@ -103,9 +103,9 @@ function eff_t1_a(ch) { // volume slide } function eff_t0_b(ch, data) { // song jump (untested) - if (data < songpats.length) { + if (data < xm.songpats.length) { cur_songpos = data - cur_pat = songpats[cur_songpos]; + cur_pat = xm.songpats[cur_songpos]; } } @@ -115,9 +115,9 @@ function eff_t0_c(ch, data) { // set volume function eff_t0_d(ch, data) { // pattern jump cur_songpos++; - if (cur_songpos >= songpats.length) - cur_songpos = song_looppos; - cur_pat = songpats[cur_songpos]; + if (cur_songpos >= xm.songpats.length) + cur_songpos = xm.song_looppos; + cur_pat = xm.songpats[cur_songpos]; cur_row = data; } From f95bd1e169619ab218a29195f78730d9e9bbe04b Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Tue, 3 Nov 2015 17:55:12 -0800 Subject: [PATCH 18/78] whoops, more refactoring bugs i should really write some tests. --- xmeffects.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xmeffects.js b/xmeffects.js index a936bba..38515d9 100644 --- a/xmeffects.js +++ b/xmeffects.js @@ -169,9 +169,9 @@ function eff_t0_f(ch, data) { // set tempo console.log("tempo 0?"); return; } else if(data < 0x20) { - tempo = data; + xm.tempo = data; } else { - bpm = data; + xm.bpm = data; } } From b859f27109a9b92712bf2389cfdad8002f0b6201 Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Tue, 3 Nov 2015 18:16:00 -0800 Subject: [PATCH 19/78] disable looping if looplen = 0... so much pathological crap in XM files... --- xm.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/xm.js b/xm.js index 05ddf84..a135eea 100644 --- a/xm.js +++ b/xm.js @@ -472,7 +472,7 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) { var samp = instsamp.sampledata; var sample_end = instsamp.len; - if ((instsamp.type & 3) == 1) { // todo: support pingpong + if ((instsamp.type & 3) == 1 && instsamp.looplen > 0) { loop = true; loopstart = instsamp.loop; looplen = instsamp.looplen; @@ -511,7 +511,8 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) { var failsafe = 100; while (i < end) { if (failsafe-- == 0) { - console.log("failsafe in mixing loop!", k, sample_end, loopstart, looplen, dk); + console.log("failsafe in mixing loop! channel", ch.number, k, sample_end, + loopstart, looplen, dk); break; } if (k >= sample_end) { // TODO: implement pingpong looping From 54809c2b8187294ec9f5d55c0b79ec4bdfeb24c1 Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Wed, 4 Nov 2015 05:50:29 -0800 Subject: [PATCH 20/78] bugfix for 9xx offset effect --- xm.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xm.js b/xm.js index a135eea..492a50f 100644 --- a/xm.js +++ b/xm.js @@ -341,7 +341,8 @@ function next_row() { } if (triggernote) { - ch.off = 0; + // there's gotta be a less hacky way to handle offset commands... + if (ch.effect != 9) ch.off = 0; ch.release = 0; ch.envtick = 0; ch.vibratopos = 0; From f092f847f7ee789ccdea61c8ec0208405f24a2e8 Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Wed, 4 Nov 2015 06:22:49 -0800 Subject: [PATCH 21/78] instrument list rendering with FT2 font. --- index.html | 11 ++++---- xm.js | 79 ++++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 74 insertions(+), 16 deletions(-) diff --git a/index.html b/index.html index 20a54db..5935e53 100644 --- a/index.html +++ b/index.html @@ -4,13 +4,14 @@ -
-
-
+
+
+
+
+
+
 
-
-
 code: github.com/a1k0n/jsxm
 todo:
diff --git a/xm.js b/xm.js
index 492a50f..7a0f867 100644
--- a/xm.js
+++ b/xm.js
@@ -8,10 +8,37 @@ fontimg.src = "ft2font.png";
 // canvas to render patterns onto
 var pat_canvas = document.createElement('canvas');
 
-// 
+// for pretty-printing notes
 var _note_names = ["C-", "C#", "D-", "D#", "E-", "F-", "F#", "G-", "G#", "A-", "A#", "B-"];
+
 var f_smp = 44100;  // updated by play callback, default value here
 
+// pixel widths of each character in the proportional font
+var _fontwidths = [
+  4, 7, 3, 6, 6, 6, 6, 5, 4, 5, 5, 5, 5, 5, 7, 7,
+  5, 5, 5, 6, 6, 6, 6, 6, 6, 7, 6, 7, 7, 7, 7, 7,
+  4, 2, 5, 7, 7, 7, 7, 3, 4, 4, 6, 6, 3, 6, 2, 7,
+  6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 2, 3, 6, 6, 6, 7,
+  7, 7, 7, 7, 7, 7, 7, 7, 7, 2, 7, 7, 7, 8, 8, 7,
+  7, 7, 7, 7, 8, 7, 7, 8, 8, 8, 7, 4, 7, 4, 4, 5,
+  3, 6, 6, 6, 6, 6, 4, 6, 6, 2, 4, 6, 2, 8, 6, 6,
+  6, 6, 4, 6, 4, 6, 7, 8, 7, 6, 6, 4, 2, 4, 4, 4];
+
+// draw FT2 proportional font text to a drawing context
+// returns width rendered
+function DrawText(text, dx, dy, ctx) {
+  var dx0 = dx;
+  for (var i = 0; i < text.length; i++) {
+    var n = text.charCodeAt(i);
+    var sx = (n&63)*8;
+    var sy = (n>>6)*10 + 7*8;
+    var width = _fontwidths[n];
+    ctx.drawImage(fontimg, sx, sy, width, 10, dx, dy, width, 10);
+    dx += width + 1;
+  }
+  return dx - dx0;
+}
+
 var _fontmap_notes = [8*5, 8*22, 8*28];
 var _pattern_cellwidth = 16 + 4 + 8 + 4 + 8 + 16 + 4;
 var _scope_width = _pattern_cellwidth - 1;
@@ -176,10 +203,7 @@ function RedrawScreen() {
     for (var j = 0; j < xm.nchan; j++) {
       var x = _pattern_border + j * _pattern_cellwidth;
       // render channel number
-      if (j >= 10) {
-        ctx.drawImage(fontimg, 8*((j/10)|0), 4*8, 4, 8, x, 1, 4, 8);
-      }
-      ctx.drawImage(fontimg, 8*(j%10), 4*8, 4, 8, x+4, 1, 4, 8);
+      DrawText(j, x, 1, ctx);
 
       // volume in dB as a green bar
       var vu_y = -Math.log(VU[j])*10;
@@ -969,13 +993,46 @@ function loadXM(arrayBuf) {
   debug.innerHTML = xm.songname;
 
   var instrlist = document.getElementById("instruments");
-  var namelist = [];
-  for (var j = 0; j < xm.instruments.length; j++) {
-    var n = (j+1).toString(16);
-    if (j < 15) n = ' ' + n;
-    namelist.push(n + " " + xm.instruments[j].name);
+  var instrcols = ((xm.instruments.length + 7) / 8) | 0;
+  for (var i = 0; i < instrcols; i++) {
+    var canvas = document.createElement('canvas');
+    var ctx = canvas.getContext('2d');
+    var instrcolumnwidth = 8*22;
+    canvas.width = instrcolumnwidth;
+    canvas.height = 8 * 10;
+    ctx.fillStyle = '#000';
+    ctx.fillRect(0, 0, canvas.width, canvas.height);
+    var hasname = 0, hasdata = 0;
+    for (var j = 8*i; j < Math.min(8*i+8, xm.instruments.length); j++) {
+      var y = 10*(j - 8*i);
+      var n = (j+1).toString(16);
+      if (j < 15) n = '0' + n;
+      var data = xm.instruments[j].samples;
+      if (data) {
+        var len = data[0].len;
+        data = data[0].sampledata;
+        var scale = Math.ceil(len / instrcolumnwidth);
+        ctx.strokeStyle = '#55acff';
+        ctx.beginPath();
+        for (var k = 0; k < Math.min(len / scale, instrcolumnwidth - 20); k++) {
+          ctx.lineTo(k + 20, y + data[k*scale] * 4 + 4);
+        }
+        ctx.stroke();
+        hasdata++;
+      }
+      var name = xm.instruments[j].name;
+      ctx.globalCompositeOperation = 'lighten';
+      DrawText(n, 1, y, ctx);
+      if (name != '') {
+        DrawText(xm.instruments[j].name, 20, y, ctx);
+        hasname++;
+      }
+      ctx.globalCompositeOperation = 'source-over';
+    }
+    if (hasname || hasdata) {
+      instrlist.appendChild(canvas);
+    }
   }
-  instrlist.innerHTML = namelist.join("\n");
 
   return xm;
 }

From a5477320632fd780d516421869e9d8c5c0b5ef4d Mon Sep 17 00:00:00 2001
From: Andy Sloane 
Date: Wed, 4 Nov 2015 07:19:44 -0800
Subject: [PATCH 22/78] render a nice big title and center stuff

---
 index.html | 14 ++++++++++----
 xm.js      | 47 ++++++++++++++++++++++++++++++++++++++++-------
 2 files changed, 50 insertions(+), 11 deletions(-)

diff --git a/index.html b/index.html
index 5935e53..c0d7a18 100644
--- a/index.html
+++ b/index.html
@@ -2,16 +2,22 @@
   js xm
   
   
+  
 
 
   
-
-
+
+
+
-
-
 code: github.com/a1k0n/jsxm
 todo:
diff --git a/xm.js b/xm.js
index 7a0f867..490f098 100644
--- a/xm.js
+++ b/xm.js
@@ -24,6 +24,16 @@ var _fontwidths = [
   3, 6, 6, 6, 6, 6, 4, 6, 6, 2, 4, 6, 2, 8, 6, 6,
   6, 6, 4, 6, 4, 6, 7, 8, 7, 6, 6, 4, 2, 4, 4, 4];
 
+var _bigfontwidths = [
+   4, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
+  15, 15, 15, 15, 15, 15, 15, 15, 13, 13, 13, 15, 15, 15, 15, 15,
+   4,  5, 12, 16, 15, 15, 16,  5,  8,  8, 13, 10,  6, 10,  5, 12,
+  15, 15, 15, 15, 15, 15, 15, 15, 15, 15,  5,  6, 12, 10, 12, 15,
+  14, 15, 15, 15, 15, 15, 15, 15, 15,  5, 13, 15, 14, 15, 15, 15,
+  15, 16, 15, 15, 15, 15, 15, 15, 15, 15, 16,  7, 12,  7, 13, 15,
+   5, 13, 13, 13, 13, 13, 11, 13, 13,  5,  9, 13,  5, 16, 13, 13,
+  13, 13, 12, 13, 11, 13, 13, 16, 15, 13, 15,  9,  2,  9, 15, 15];
+
 // draw FT2 proportional font text to a drawing context
 // returns width rendered
 function DrawText(text, dx, dy, ctx) {
@@ -31,7 +41,7 @@ function DrawText(text, dx, dy, ctx) {
   for (var i = 0; i < text.length; i++) {
     var n = text.charCodeAt(i);
     var sx = (n&63)*8;
-    var sy = (n>>6)*10 + 7*8;
+    var sy = (n>>6)*10 + 56;
     var width = _fontwidths[n];
     ctx.drawImage(fontimg, sx, sy, width, 10, dx, dy, width, 10);
     dx += width + 1;
@@ -39,6 +49,28 @@ function DrawText(text, dx, dy, ctx) {
   return dx - dx0;
 }
 
+function TextSize(text, widthtable) {
+  var width = 0;
+  for (var i = 0; i < text.length; i++) {
+    var n = text.charCodeAt(i);
+    width += widthtable[n] + 1;
+  }
+  return width;
+}
+
+function DrawBigText(text, dx, dy, ctx) {
+  var dx0 = dx;
+  for (var i = 0; i < text.length; i++) {
+    var n = text.charCodeAt(i);
+    var sx = (n&31)*16;
+    var sy = (n>>5)*20 + 96;
+    var width = _bigfontwidths[n];
+    ctx.drawImage(fontimg, sx, sy, width, 20, dx, dy, width, 20);
+    dx += width + 1;
+  }
+  return dx - dx0;
+}
+
 var _fontmap_notes = [8*5, 8*22, 8*28];
 var _pattern_cellwidth = 16 + 4 + 8 + 4 + 8 + 16 + 4;
 var _scope_width = _pattern_cellwidth - 1;
@@ -203,7 +235,7 @@ function RedrawScreen() {
     for (var j = 0; j < xm.nchan; j++) {
       var x = _pattern_border + j * _pattern_cellwidth;
       // render channel number
-      DrawText(j, x, 1, ctx);
+      DrawText(''+j, x, 1, ctx);
 
       // volume in dB as a green bar
       var vu_y = -Math.log(VU[j])*10;
@@ -219,9 +251,6 @@ function RedrawScreen() {
     }
   }
 
-  var debug = document.getElementById("debug");
-  debug.innerHTML = xm.songname + '
pat ' + e.pat + ' row ' + (e.row); - if (e.row != shown_row) { var gfx = document.getElementById("gfxpattern"); if (e.pat != pat_canvas_patnum) { @@ -988,9 +1017,13 @@ function loadXM(arrayBuf) { xm.instruments.push(inst); } - var debug = document.getElementById("debug"); console.log("loaded \"" + xm.songname + "\""); - debug.innerHTML = xm.songname; + + var title = document.getElementById("title"); + // make title element fit text exactly, then render it + title.width = TextSize(xm.songname, _bigfontwidths); + var ctx = title.getContext('2d'); + DrawBigText(xm.songname, 0, 1, ctx); var instrlist = document.getElementById("instruments"); var instrcols = ((xm.instruments.length + 7) / 8) | 0; From 7d6481ad7355751ebfc2d65409389ccbabaa1975 Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Wed, 4 Nov 2015 10:21:04 -0800 Subject: [PATCH 23/78] file picker! --- index.html | 20 ++++++++++---- xm.js | 76 +++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 79 insertions(+), 17 deletions(-) diff --git a/index.html b/index.html index c0d7a18..ef7e8e1 100644 --- a/index.html +++ b/index.html @@ -2,12 +2,17 @@ js xm + @@ -16,17 +21,22 @@ canvas.centered {
-
+
+

+ + +

+
+
 code: github.com/a1k0n/jsxm
 todo:
- - file picker, upload more xms to a host we can access cross-origin
- - render instrument list in ft2 font
  - missing XM effects:
    - E3x, E4x, E5x, E6x, E7x, E9x, EDx, EEx
    - 7xy - tremolo
    - Gxx, Hxy, Kxx, Lxx, Pxy, Txy
+ - render pattern with the wider fonts for fewer channels
 
diff --git a/xm.js b/xm.js index 490f098..13b8a7b 100644 --- a/xm.js +++ b/xm.js @@ -794,7 +794,7 @@ function UnrollSampleLoop(samp) { samp.type = 1; } -function loadXM(arrayBuf) { +function LoadXM(arrayBuf) { var dv = new DataView(arrayBuf); var xm = {}; @@ -1026,6 +1026,10 @@ function loadXM(arrayBuf) { DrawBigText(xm.songname, 0, 1, ctx); var instrlist = document.getElementById("instruments"); + // clear instrument list if not already clear + while (instrlist.childNodes.length) { + instrlist.removeChild(instrlist.childNodes[0]); + } var instrcols = ((xm.instruments.length + 7) / 8) | 0; for (var i = 0; i < instrcols; i++) { var canvas = document.createElement('canvas'); @@ -1074,10 +1078,12 @@ var playing = false; var jsNode, gainNode; var paused_events = []; function InitAudio() { - audioContext = window.AudioContext || window.webkitAudioContext; - audioctx = new audioContext(); - gainNode = audioctx.createGain(); - gainNode.gain.value = 0.1; // master volume + if (audioctx == undefined) { + audioContext = window.AudioContext || window.webkitAudioContext; + audioctx = new audioContext(); + gainNode = audioctx.createGain(); + gainNode.gain.value = 0.1; // master volume + } jsNode = audioctx.createScriptProcessor(16384, 0, 2); jsNode.onaudioprocess = audio_cb; gainNode.connect(audioctx.destination); @@ -1113,19 +1119,25 @@ function PauseXM() { playing = false; } -function main() { - InitAudio(); - var xmReq = new XMLHttpRequest(); - var uri = location.search.substr(1); - if (uri == "") { - uri = "kamel.xm"; +function StopXM() { + if (playing) { + jsNode.disconnect(gainNode); + playing = false; + audio_events = []; + paused_events = []; } + cur_songpos = -1, cur_pat = -1, cur_row = 64, cur_ticksamp = 0; + InitAudio(); +} + +function DownloadXM(uri) { + var xmReq = new XMLHttpRequest(); xmReq.open("GET", uri, true); xmReq.responseType = "arraybuffer"; xmReq.onload = function (xmEvent) { var arrayBuffer = xmReq.response; if (arrayBuffer) { - xm = loadXM(arrayBuffer); + xm = LoadXM(arrayBuffer); } else { console.log("unable to load", uri); } @@ -1133,6 +1145,7 @@ function main() { var gfxpattern = document.getElementById("gfxpattern"); gfxpattern.width = _pattern_cellwidth * xm.nchan + _pattern_border; var playbutton = document.getElementById('playpause'); + playbutton.innerHTML='Play'; playbutton.onclick = function() { if (playing) { PauseXM(); @@ -1148,6 +1161,11 @@ function main() { for (var i = 0; i < xm.nchan; i++) { scopes.push(new Float32Array(_scope_width)); } + + // reset display + shown_row = undefined; + pat_canvas_patnum = undefined; + audio_events.push({ t: 0, row: 0, pat: xm.songpats[0], vu: new Float32Array(xm.nchan), @@ -1158,3 +1176,37 @@ function main() { xmReq.send(null); } +function InitFilelist() { + var el = document.getElementById('filelist'); + xmuris.forEach(function(entry) { + var a = document.createElement('a'); + a.text = entry[0]; + a.href = '#'+entry[1]; + a.onclick = function() { + el.style.display = "none"; + StopXM(); + DownloadXM(baseuri + entry[1]); + } + el.appendChild(a); + el.appendChild(document.createElement('br')); + }); + var loadbutton = document.getElementById('loadbutton'); + loadbutton.onclick = function() { + if (el.style.display == "none") { + el.style.display = "block"; + } else { + el.style.display = "none"; + } + } +} + +function main() { + InitAudio(); + InitFilelist(); + var uri = location.hash.substr(1); + if (uri == "") { + uri = "kamel.xm"; + } + DownloadXM(baseuri + uri); +} + From 541a9d721caccf90b1b54c8ce21f4f35900ed99f Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Wed, 4 Nov 2015 10:21:27 -0800 Subject: [PATCH 24/78] no longer storing xms in git --- kamel.xm | Bin 41995 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 kamel.xm diff --git a/kamel.xm b/kamel.xm deleted file mode 100644 index 8766dff138bb3be3759b244db30f3850a5b5d1ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41995 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 From 5aa5c06ab0aa9977bb3f087b53707156bf01a33b Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Wed, 4 Nov 2015 10:23:54 -0800 Subject: [PATCH 25/78] update readme --- README.md | 9 ++++----- index.html | 1 + 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e559534..d5bf329 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,13 @@ FastTracker 2 .XM player, written for fun. -[Demo](http://www.a1k0n.net/code/jsxm/) [warning: plays sound immediately on -load] +[Demo](http://www.a1k0n.net/code/jsxm/) By default it plays "my dirty old kamel" by Alexander Bulér aka [Zalza](http://zalza.bandcamp.com/). I love this tune, and wrote this code just -to play it. It can also load other .XMs by loading index.html?_tune-uri.xm_, -though it either needs to be local to your domain or cross-origin resource -sharing must be enabled for that to work. +to play it. It can also load some other tunes hosted on my website with a file +picker, and it should play just about any .XM file now if checked out and run +locally. It is fairly feature-complete, but is missing a bunch of effects. diff --git a/index.html b/index.html index ef7e8e1..6f95e78 100644 --- a/index.html +++ b/index.html @@ -37,6 +37,7 @@ todo: - 7xy - tremolo - Gxx, Hxy, Kxx, Lxx, Pxy, Txy - render pattern with the wider fonts for fewer channels + - drag-n-drop .xm files onto player
From 2cf8840665a4cfc5063d137f1c8d6159c32b2109 Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Tue, 10 Nov 2015 20:30:41 -0800 Subject: [PATCH 26/78] Contact with the real world Many, many fixes found after posting this on my website. - drag n drop supported - plays on OSX and iOS safari now - random other hiccups fixed there seems to be some remaining issues cleaning up when loading a new song. --- index.html | 53 ++++++++++++++++--- xm.js | 150 +++++++++++++++++++++++++++++++++++------------------ 2 files changed, 144 insertions(+), 59 deletions(-) diff --git a/index.html b/index.html index 6f95e78..19da337 100644 --- a/index.html +++ b/index.html @@ -13,22 +13,59 @@ #filelist a { color: #fff; } +.playercontainer { + background:#000; +} + +.draghover { + background:#000; + opacity: 0.5; +} + +.hscroll { + overflow: auto; + margin-bottom: 14px; +} + +.hscroll::-webkit-scrollbar { + -webkit-appearance: none; +} + +.hscroll::-webkit-scrollbar:horizontal { + height: 11px; +} + +.hscroll::-webkit-scrollbar-thumb { + border-radius: 2px; + border: 1px solid #93C3E9; /* should match background, can't be transparent */ + background-color: #A0D4FD; +} + +.hscroll::-webkit-scrollbar-track { + background-color: #333; + border-radius: 8px; +} + - -
-
-
-
+ +
+
+
+
+
+

- - + +

+
 code: github.com/a1k0n/jsxm
 todo:
@@ -37,7 +74,7 @@ todo:
    - 7xy - tremolo
    - Gxx, Hxy, Kxx, Lxx, Pxy, Txy
  - render pattern with the wider fonts for fewer channels
- - drag-n-drop .xm files onto player
+ - fix occasional rendering/audio hiccups when switching songs
 
diff --git a/xm.js b/xm.js index 13b8a7b..6bd8e26 100644 --- a/xm.js +++ b/xm.js @@ -243,11 +243,13 @@ function RedrawScreen() { // oscilloscope var scope = scopes[j]; - ctx.beginPath(); - for (var k = 0; k < _scope_width; k++) { - ctx.lineTo(x + 1 + k, 32 - 16 * scope[k]); + if (scope != undefined) { + ctx.beginPath(); + for (var k = 0; k < _scope_width; k++) { + ctx.lineTo(x + 1 + k, 32 - 16 * scope[k]); + } + ctx.stroke(); } - ctx.stroke(); } } @@ -753,10 +755,10 @@ function ConvertSample(array, bits) { len /= 2; var samp = new Float32Array(len); for (var k = 0; k < len; k++) { - acc += array[k*2] + (array[k*2 + 1] << 8); - var b = acc&65535; + var b = array[k*2] + (array[k*2 + 1] << 8); if (b & 32768) b = b-65536; - samp[k] = b / 32768.0; + acc = Math.max(-1, Math.min(1, acc + b / 32768.0)); + samp[k] = acc; } return samp; } @@ -766,7 +768,6 @@ function ConvertSample(array, bits) { // uninterrupted for as long as possible; this also handles pingpong loops. function UnrollSampleLoop(samp) { var nloops = ((2048 + samp.looplen - 1) / samp.looplen) | 0; - console.log(samp.looplen, nloops); var pingpong = samp.type & 2; if (pingpong) { // make sure we have an even number of loops if we are pingponging @@ -896,9 +897,7 @@ function LoadXM(arrayBuf) { 'number': i, }; if (nsamp > 0) { - // return a slice so we have a fresh copy and don't retain pointers to - // the original xm file arraybuf forever - var samplemap = new Uint8Array(arrayBuf, idx+33, 96).slice(); + var samplemap = new Uint8Array(arrayBuf, idx+33, 96); var env_nvol = dv.getUint8(idx+225); var env_vol_type = dv.getUint8(idx+233); @@ -1074,21 +1073,25 @@ function LoadXM(arrayBuf) { return xm; } -var playing = false; var jsNode, gainNode; -var paused_events = []; function InitAudio() { if (audioctx == undefined) { - audioContext = window.AudioContext || window.webkitAudioContext; + var audioContext = window.AudioContext || window.webkitAudioContext; audioctx = new audioContext(); gainNode = audioctx.createGain(); gainNode.gain.value = 0.1; // master volume } - jsNode = audioctx.createScriptProcessor(16384, 0, 2); + if (audioctx.createScriptProcessor == undefined) { + jsNode = audioctx.createJavaScriptNode(16384, 0, 2); + } else { + jsNode = audioctx.createScriptProcessor(16384, 0, 2); + } jsNode.onaudioprocess = audio_cb; gainNode.connect(audioctx.destination); } +var playing = false; +var paused_events = []; function PlayXM() { if (!playing) { // put paused events back into action, if any @@ -1100,6 +1103,17 @@ function PlayXM() { } // start playing jsNode.connect(gainNode); + + // hack to get iOS to play anything + var temp_osc = audioctx.createOscillator(); + temp_osc.connect(audioctx.destination); + if (temp_osc.noteOn) temp_osc.start = temp_osc.noteOn; + temp_osc.frequency.value = 1; + temp_osc.start(0); + window.setTimeout(10, function() { + temp_osc.disconnect(); + }); + requestAnimationFrame(RedrawScreen); } playing = true; @@ -1108,7 +1122,7 @@ function PlayXM() { function PauseXM() { if (playing) { jsNode.disconnect(gainNode); - // grab all the audio events + // grab all the audio events var t = audioctx.currentTime; while (audio_events.length > 0) { var e = audio_events.shift(); @@ -1130,6 +1144,45 @@ function StopXM() { InitAudio(); } +function LoadXMAndInit(xmdata) { + xm = LoadXM(xmdata); + if (!xm) return; + + document.getElementById('vu').width = _pattern_border + _pattern_cellwidth * xm.nchan; + var gfxpattern = document.getElementById("gfxpattern"); + gfxpattern.width = _pattern_cellwidth * xm.nchan + _pattern_border; + var playbutton = document.getElementById('playpause'); + playbutton.innerHTML='Play'; + playbutton.onclick = function() { + if (playing) { + PauseXM(); + playbutton.innerHTML='Play'; + } else { + PlayXM(); + playbutton.innerHTML='Pause'; + } + } + playbutton.disabled = false; + // generate a fake audio event to render the initial paused screen + var scopes = []; + for (var i = 0; i < xm.nchan; i++) { + scopes.push(new Float32Array(_scope_width)); + } + + // reset display + shown_row = undefined; + pat_canvas_patnum = undefined; + + audio_events.push({ + t: 0, row: 0, pat: xm.songpats[0], + vu: new Float32Array(xm.nchan), + scopes: scopes + }); + RedrawScreen(); + + return xm; +} + function DownloadXM(uri) { var xmReq = new XMLHttpRequest(); xmReq.open("GET", uri, true); @@ -1137,45 +1190,37 @@ function DownloadXM(uri) { xmReq.onload = function (xmEvent) { var arrayBuffer = xmReq.response; if (arrayBuffer) { - xm = LoadXM(arrayBuffer); + xm = LoadXMAndInit(arrayBuffer); } else { console.log("unable to load", uri); } - document.getElementById('vu').width = _pattern_border + _pattern_cellwidth * xm.nchan; - var gfxpattern = document.getElementById("gfxpattern"); - gfxpattern.width = _pattern_cellwidth * xm.nchan + _pattern_border; - var playbutton = document.getElementById('playpause'); - playbutton.innerHTML='Play'; - playbutton.onclick = function() { - if (playing) { - PauseXM(); - playbutton.innerHTML='Play'; - } else { - PlayXM(); - playbutton.innerHTML='Pause'; - } - } - playbutton.disabled = false; - // generate a fake audio event to render the initial paused screen - var scopes = []; - for (var i = 0; i < xm.nchan; i++) { - scopes.push(new Float32Array(_scope_width)); - } - - // reset display - shown_row = undefined; - pat_canvas_patnum = undefined; - - audio_events.push({ - t: 0, row: 0, pat: xm.songpats[0], - vu: new Float32Array(xm.nchan), - scopes: scopes - }); - RedrawScreen(); } xmReq.send(null); } +function allowDrop(ev) { + ev.stopPropagation(); + ev.preventDefault(); + var elem = document.getElementById("playercontainer"); + elem.className = (ev.type == "dragover" ? "draghover" : "playercontainer"); + return false; +} + +function handleDrop(e) { + console.log(e); + e.preventDefault(); + var elem = document.getElementById("playercontainer"); + elem.className = "playercontainer"; + var files = e.target.files || e.dataTransfer.files; + if (files.length < 1) return false; + var reader = new FileReader(); + reader.onload = function(e) { + LoadXMAndInit(e.target.result); + } + reader.readAsArrayBuffer(files[0]); + return false; +} + function InitFilelist() { var el = document.getElementById('filelist'); xmuris.forEach(function(entry) { @@ -1200,13 +1245,16 @@ function InitFilelist() { } } -function main() { +window.onload = function() { InitAudio(); InitFilelist(); + var uri = location.hash.substr(1); if (uri == "") { uri = "kamel.xm"; } - DownloadXM(baseuri + uri); + if (!uri.startsWith("http")) { + uri = baseuri + uri; + } + DownloadXM(uri); } - From 41e8713b7499eef99fce69401eb13e627cc87225 Mon Sep 17 00:00:00 2001 From: luckcolors Date: Wed, 11 Nov 2015 22:28:21 +0100 Subject: [PATCH 27/78] First commit for desktop app Nw.js --- package.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 package.json diff --git a/package.json b/package.json new file mode 100644 index 0000000..cbcc2e9 --- /dev/null +++ b/package.json @@ -0,0 +1,4 @@ +{ + "name": "jsxm", + "main": "index.html" +} From 14d88e9a101740261befb5bbc1cbffc8495fc7ff Mon Sep 17 00:00:00 2001 From: luckcolors Date: Wed, 11 Nov 2015 22:49:25 +0100 Subject: [PATCH 28/78] Removed toolbar. --- package.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index cbcc2e9..96863a7 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,11 @@ { "name": "jsxm", - "main": "index.html" + "version": "1", + "main": "index.html", + "window": { + "frame": true, + "toolbar": false, + "position": "center", + "resizable": true + } } From a37555ed5882379255f402154c0b0b9467bb7ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Hillerstr=C3=B6m?= Date: Thu, 12 Nov 2015 00:00:43 +0100 Subject: [PATCH 29/78] Reorder to load without any issues --- index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 19da337..b83e85e 100644 --- a/index.html +++ b/index.html @@ -1,8 +1,8 @@ js xm - - + + -
+
diff --git a/xm.js b/xm.js index 6bd8e26..59465d7 100644 --- a/xm.js +++ b/xm.js @@ -1,4 +1,19 @@ -var xm; // contains all song data +(function (window, document) { +if (!window.XMPlayer) { + window.XMPlayer = {}; +} +var player = window.XMPlayer; +player.allowDrop = allowDrop; +player.handleDrop = handleDrop; +player.PeriodForNote = PeriodForNote; +player.prettify_effect = prettify_effect; +player.cur_songpos = -1; +player.cur_pat = -1; +player.cur_row = 64; +player.cur_ticksamp = 0; +player.cur_tick = 6; +player.xm = {}; // contains all song data + var audioctx; // AudioContext // Load font (ripped from FastTracker 2) @@ -232,7 +247,7 @@ function RedrawScreen() { ctx.fillRect(0, 0, canvas.width, 64); ctx.fillStyle = '#0f0'; ctx.strokeStyle = '#55acff'; - for (var j = 0; j < xm.nchan; j++) { + for (var j = 0; j < player.xm.nchan; j++) { var x = _pattern_border + j * _pattern_cellwidth; // render channel number DrawText(''+j, x, 1, ctx); @@ -256,9 +271,9 @@ function RedrawScreen() { if (e.row != shown_row) { var gfx = document.getElementById("gfxpattern"); if (e.pat != pat_canvas_patnum) { - var p = xm.patterns[e.pat]; + var p = player.xm.patterns[e.pat]; if (p != undefined) { - RenderPattern(pat_canvas, xm.patterns[e.pat]); + RenderPattern(pat_canvas, player.xm.patterns[e.pat]); pat_canvas_patnum = e.pat; } } @@ -279,26 +294,24 @@ function RedrawScreen() { } } -var cur_songpos = -1, cur_pat = -1, cur_row = 64, cur_ticksamp = 0; -var cur_tick = 6; function next_row() { - if (cur_pat == -1 || cur_row >= xm.patterns[cur_pat].length) { - cur_row = 0; - cur_songpos++; - if (cur_songpos >= xm.songpats.length) - cur_songpos = xm.song_looppos; - cur_pat = xm.songpats[cur_songpos]; + if (player.cur_pat == -1 || player.cur_row >= player.xm.patterns[player.cur_pat].length) { + player.cur_row = 0; + player.cur_songpos += 1; + if (player.cur_songpos >= player.xm.songpats.length) + player.cur_songpos = player.xm.song_looppos; + player.cur_pat = player.xm.songpats[player.cur_songpos]; } - var p = xm.patterns[cur_pat]; - var r = p[cur_row]; - cur_row++; + var p = player.xm.patterns[player.cur_pat]; + var r = p[player.cur_row]; + player.cur_row += 1; for (var i = 0; i < r.length; i++) { - var ch = xm.channelinfo[i]; + var ch = player.xm.channelinfo[i]; var inst = ch.inst; var triggernote = false; // instrument trigger if (r[i][1] != -1) { - inst = xm.instruments[r[i][1] - 1]; + inst = player.xm.instruments[r[i][1] - 1]; if (inst && inst.samplemap) { ch.inst = inst; // retrigger unless overridden below @@ -364,8 +377,8 @@ function next_row() { ch.effect = r[i][3]; ch.effectdata = r[i][4]; if (ch.effect < 36) { - ch.effectfn = effects_t1[ch.effect]; - var eff_t0 = effects_t0[ch.effect]; + ch.effectfn = player.effects_t1[ch.effect]; + var eff_t0 = player.effects_t0[ch.effect]; if (eff_t0 && eff_t0(ch, ch.effectdata)) { triggernote = false; } @@ -460,26 +473,26 @@ EnvelopeFollower.prototype.Tick = function(release) { } function next_tick() { - cur_tick++; - if (cur_tick >= xm.tempo) { - cur_tick = 0; + player.cur_tick += 1; + if (player.cur_tick >= player.xm.tempo) { + player.cur_tick = 0; next_row(); } - for (var j = 0; j < xm.nchan; j++) { - var ch = xm.channelinfo[j]; + for (var j = 0; j < player.xm.nchan; j++) { + var ch = player.xm.channelinfo[j]; var inst = ch.inst; ch.periodoffset = 0; - if (cur_tick != 0) { + if (player.cur_tick != 0) { if(ch.voleffectfn) ch.voleffectfn(ch); if(ch.effectfn) ch.effectfn(ch); } if (isNaN(ch.period)) { - console.log(prettify_notedata(xm.patterns[cur_pat][cur_row-1][j]), + console.log(prettify_notedata(player.xm.patterns[player.cur_pat][player.cur_row-1][j]), "set channel", j, "period to NaN"); } if (inst == undefined) continue; if (ch.env_vol == undefined) { - console.log(prettify_notedata(xm.patterns[cur_pat][cur_row-1][j]), + console.log(prettify_notedata(player.xm.patterns[player.cur_pat][player.cur_row-1][j]), "set channel", j, "env_vol to undefined, but note is playing"); continue; } @@ -691,17 +704,17 @@ function audio_cb(e) { } var offset = 0; - var ticklen = 0|(f_smp * 2.5 / xm.bpm); + var ticklen = 0|(f_smp * 2.5 / player.xm.bpm); while(buflen > 0) { - if (cur_pat == -1 || cur_ticksamp >= ticklen) { + if (player.cur_pat == -1 || player.cur_ticksamp >= ticklen) { next_tick(f_smp); - cur_ticksamp -= ticklen; + player.cur_ticksamp -= ticklen; } - var tickduration = Math.min(buflen, ticklen - cur_ticksamp); - var VU = new Float32Array(xm.nchan); + var tickduration = Math.min(buflen, ticklen - player.cur_ticksamp); + var VU = new Float32Array(player.xm.nchan); var scopes = undefined; - for (var j = 0; j < xm.nchan; j++) { + for (var j = 0; j < player.xm.nchan; j++) { var scope; if (tickduration >= 4*_scope_width) { scope = new Float32Array(_scope_width); @@ -711,7 +724,7 @@ function audio_cb(e) { } VU[j] = MixChannelIntoBuf( - xm.channelinfo[j], offset, offset + tickduration, dataL, dataR) / + player.xm.channelinfo[j], offset, offset + tickduration, dataL, dataR) / tickduration; if (tickduration >= 4*_scope_width) { @@ -726,15 +739,15 @@ function audio_cb(e) { t: e.playbackTime + (0.0 + offset) / f_smp, vu: VU, scopes: scopes, - songpos: cur_songpos, - pat: cur_pat, - row: cur_row - 1 + songpos: player.cur_songpos, + pat: player.cur_pat, + row: player.cur_row - 1 }); if (audio_events.length == 1) { requestAnimationFrame(RedrawScreen); } offset += tickduration; - cur_ticksamp += tickduration; + player.cur_ticksamp += tickduration; buflen -= tickduration; } } @@ -797,22 +810,22 @@ function UnrollSampleLoop(samp) { function LoadXM(arrayBuf) { var dv = new DataView(arrayBuf); - var xm = {}; + player.xm = {}; - xm.songname = getstring(dv, 17, 20); + player.xm.songname = getstring(dv, 17, 20); var hlen = dv.getUint32(0x3c, true) + 0x3c; var songlen = dv.getUint16(0x40, true); - xm.song_looppos = dv.getUint16(0x42, true); - xm.nchan = dv.getUint16(0x44, true); + player.xm.song_looppos = dv.getUint16(0x42, true); + player.xm.nchan = dv.getUint16(0x44, true); var npat = dv.getUint16(0x46, true); var ninst = dv.getUint16(0x48, true); - xm.flags = dv.getUint16(0x4a, true); - xm.tempo = dv.getUint16(0x4c, true); - xm.bpm = dv.getUint16(0x4e, true); - xm.channelinfo = []; + player.xm.flags = dv.getUint16(0x4a, true); + player.xm.tempo = dv.getUint16(0x4c, true); + player.xm.bpm = dv.getUint16(0x4e, true); + player.xm.channelinfo = []; - for (var i = 0; i < xm.nchan; i++) { - xm.channelinfo.push({ + for (var i = 0; i < player.xm.nchan; i++) { + player.xm.channelinfo.push({ number: i, filterstate: new Float32Array(3), vol: 0, @@ -829,18 +842,18 @@ function LoadXM(arrayBuf) { } console.log("header len " + hlen); - console.log("songlen %d, %d channels, %d patterns, %d instruments", songlen, xm.nchan, npat, ninst); - console.log("loop @%d", xm.song_looppos); - console.log("flags=%d tempo %d bpm %d", xm.flags, xm.tempo, xm.bpm); + console.log("songlen %d, %d channels, %d patterns, %d instruments", songlen, player.xm.nchan, npat, ninst); + console.log("loop @%d", player.xm.song_looppos); + console.log("flags=%d tempo %d bpm %d", player.xm.flags, player.xm.tempo, player.xm.bpm); - xm.songpats = []; + player.xm.songpats = []; for (var i = 0; i < songlen; i++) { - xm.songpats.push(dv.getUint8(0x50 + i)); + player.xm.songpats.push(dv.getUint8(0x50 + i)); } - console.log("song patterns: ", xm.songpats); + console.log("song patterns: ", player.xm.songpats); var idx = hlen; - xm.patterns = []; + player.xm.patterns = []; for (var i = 0; i < npat; i++) { var pattern = []; var patheaderlen = dv.getUint32(idx, true); @@ -850,7 +863,7 @@ function LoadXM(arrayBuf) { idx += 9; for (var j = 0; patsize > 0 && j < patrows; j++) { row = []; - for (var k = 0; k < xm.nchan; k++) { + for (var k = 0; k < player.xm.nchan; k++) { var byte0 = dv.getUint8(idx); idx++; var note = -1, inst = -1, vol = -1, efftype = 0, effparam = 0; if (byte0 & 0x80) { @@ -883,10 +896,10 @@ function LoadXM(arrayBuf) { } pattern.push(row); } - xm.patterns.push(pattern); + player.xm.patterns.push(pattern); } - xm.instruments = []; + player.xm.instruments = []; // now load instruments for (i = 0; i < ninst; i++) { var hdrsiz = dv.getUint32(idx, true); @@ -1013,23 +1026,23 @@ function LoadXM(arrayBuf) { idx += hdrsiz; console.log("empty instrument", i, hdrsiz, idx); } - xm.instruments.push(inst); + player.xm.instruments.push(inst); } - console.log("loaded \"" + xm.songname + "\""); + console.log("loaded \"" + player.xm.songname + "\""); var title = document.getElementById("title"); // make title element fit text exactly, then render it - title.width = TextSize(xm.songname, _bigfontwidths); + title.width = TextSize(player.xm.songname, _bigfontwidths); var ctx = title.getContext('2d'); - DrawBigText(xm.songname, 0, 1, ctx); + DrawBigText(player.xm.songname, 0, 1, ctx); var instrlist = document.getElementById("instruments"); // clear instrument list if not already clear while (instrlist.childNodes.length) { instrlist.removeChild(instrlist.childNodes[0]); } - var instrcols = ((xm.instruments.length + 7) / 8) | 0; + var instrcols = ((player.xm.instruments.length + 7) / 8) | 0; for (var i = 0; i < instrcols; i++) { var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); @@ -1039,11 +1052,11 @@ function LoadXM(arrayBuf) { ctx.fillStyle = '#000'; ctx.fillRect(0, 0, canvas.width, canvas.height); var hasname = 0, hasdata = 0; - for (var j = 8*i; j < Math.min(8*i+8, xm.instruments.length); j++) { + for (var j = 8*i; j < Math.min(8*i+8, player.xm.instruments.length); j++) { var y = 10*(j - 8*i); var n = (j+1).toString(16); if (j < 15) n = '0' + n; - var data = xm.instruments[j].samples; + var data = player.xm.instruments[j].samples; if (data) { var len = data[0].len; data = data[0].sampledata; @@ -1056,11 +1069,11 @@ function LoadXM(arrayBuf) { ctx.stroke(); hasdata++; } - var name = xm.instruments[j].name; + var name = player.xm.instruments[j].name; ctx.globalCompositeOperation = 'lighten'; DrawText(n, 1, y, ctx); if (name != '') { - DrawText(xm.instruments[j].name, 20, y, ctx); + DrawText(player.xm.instruments[j].name, 20, y, ctx); hasname++; } ctx.globalCompositeOperation = 'source-over'; @@ -1070,7 +1083,7 @@ function LoadXM(arrayBuf) { } } - return xm; + return player.xm; } var jsNode, gainNode; @@ -1110,7 +1123,7 @@ function PlayXM() { if (temp_osc.noteOn) temp_osc.start = temp_osc.noteOn; temp_osc.frequency.value = 1; temp_osc.start(0); - window.setTimeout(10, function() { + setTimeout(10, function() { temp_osc.disconnect(); }); @@ -1140,17 +1153,17 @@ function StopXM() { audio_events = []; paused_events = []; } - cur_songpos = -1, cur_pat = -1, cur_row = 64, cur_ticksamp = 0; + player.cur_songpos = -1, player.cur_pat = -1, player.cur_row = 64, player.cur_ticksamp = 0; InitAudio(); } function LoadXMAndInit(xmdata) { - xm = LoadXM(xmdata); - if (!xm) return; + player.xm = LoadXM(xmdata); + if (!player.xm) return; - document.getElementById('vu').width = _pattern_border + _pattern_cellwidth * xm.nchan; + document.getElementById('vu').width = _pattern_border + _pattern_cellwidth * player.xm.nchan; var gfxpattern = document.getElementById("gfxpattern"); - gfxpattern.width = _pattern_cellwidth * xm.nchan + _pattern_border; + gfxpattern.width = _pattern_cellwidth * player.xm.nchan + _pattern_border; var playbutton = document.getElementById('playpause'); playbutton.innerHTML='Play'; playbutton.onclick = function() { @@ -1165,7 +1178,7 @@ function LoadXMAndInit(xmdata) { playbutton.disabled = false; // generate a fake audio event to render the initial paused screen var scopes = []; - for (var i = 0; i < xm.nchan; i++) { + for (var i = 0; i < player.xm.nchan; i++) { scopes.push(new Float32Array(_scope_width)); } @@ -1174,13 +1187,13 @@ function LoadXMAndInit(xmdata) { pat_canvas_patnum = undefined; audio_events.push({ - t: 0, row: 0, pat: xm.songpats[0], - vu: new Float32Array(xm.nchan), + t: 0, row: 0, pat: player.xm.songpats[0], + vu: new Float32Array(player.xm.nchan), scopes: scopes }); RedrawScreen(); - return xm; + return player.xm; } function DownloadXM(uri) { @@ -1190,7 +1203,7 @@ function DownloadXM(uri) { xmReq.onload = function (xmEvent) { var arrayBuffer = xmReq.response; if (arrayBuffer) { - xm = LoadXMAndInit(arrayBuffer); + player.xm = LoadXMAndInit(arrayBuffer); } else { console.log("unable to load", uri); } @@ -1258,3 +1271,4 @@ window.onload = function() { } DownloadXM(uri); } +})(window, document); \ No newline at end of file diff --git a/xmeffects.js b/xmeffects.js index 38515d9..66819ba 100644 --- a/xmeffects.js +++ b/xmeffects.js @@ -1,8 +1,14 @@ +(function (window) { +if (!window.XMPlayer) { + window.XMPlayer = {}; +} +var player = window.XMPlayer; + function eff_t1_0(ch) { // arpeggio if (ch.effectdata != 0 && ch.inst != undefined) { var arpeggio = [0, ch.effectdata>>4, ch.effectdata&15]; - var note = ch.note + arpeggio[cur_tick % 3]; - ch.period = PeriodForNote(ch, note); + var note = ch.note + arpeggio[player.cur_tick % 3]; + ch.period = player.PeriodForNote(ch, note); } } @@ -103,9 +109,9 @@ function eff_t1_a(ch) { // volume slide } function eff_t0_b(ch, data) { // song jump (untested) - if (data < xm.songpats.length) { - cur_songpos = data - cur_pat = xm.songpats[cur_songpos]; + if (data < player.xm.songpats.length) { + player.cur_songpos = data; + player.cur_pat = player.xm.songpats[player.cur_songpos]; } } @@ -114,11 +120,11 @@ function eff_t0_c(ch, data) { // set volume } function eff_t0_d(ch, data) { // pattern jump - cur_songpos++; - if (cur_songpos >= xm.songpats.length) - cur_songpos = xm.song_looppos; - cur_pat = xm.songpats[cur_songpos]; - cur_row = data; + player.cur_songpos += 1; + if (player.cur_songpos >= player.xm.songpats.length) + player.cur_songpos = player.xm.song_looppos; + player.cur_pat = player.xm.songpats[player.cur_songpos]; + player.cur_row = data; } function eff_t0_e(ch, data) { // extended effects! @@ -157,7 +163,7 @@ function eff_t0_e(ch, data) { // extended effects! function eff_t1_e(ch) { // note cut switch (ch.effectdata >> 4) { case 0x0c: - if (cur_tick == (ch.effectdata & 0x0f)) { + if (player.cur_tick == (ch.effectdata & 0x0f)) { ch.vol = 0; } break; @@ -168,10 +174,10 @@ function eff_t0_f(ch, data) { // set tempo if (data == 0) { console.log("tempo 0?"); return; - } else if(data < 0x20) { - xm.tempo = data; + } else if (data < 0x20) { + player.xm.tempo = data; } else { - xm.bpm = data; + player.xm.bpm = data; } } @@ -200,17 +206,17 @@ function eff_t0_r(ch, data) { // retrigger } function eff_t1_r(ch) { - if (cur_tick % (ch.retrig & 0x0f) == 0) { + if (player.cur_tick % (ch.retrig & 0x0f) == 0) { ch.off = 0; } } function eff_unimplemented() {} function eff_unimplemented_t0(ch, data) { - console.log("unimplemented effect", prettify_effect(ch.effect, data)); + console.log("unimplemented effect", player.prettify_effect(ch.effect, data)); } -var effects_t0 = [ // effect functions on tick 0 +player.effects_t0 = [ // effect functions on tick 0 eff_t1_0, // 1, arpeggio is processed on all ticks eff_t0_1, eff_t0_2, @@ -249,7 +255,7 @@ var effects_t0 = [ // effect functions on tick 0 eff_unimplemented_t0, // z ]; -var effects_t1 = [ // effect functions on tick 1+ +player.effects_t1 = [ // effect functions on tick 1+ eff_t1_0, eff_t1_1, eff_t1_2, @@ -287,3 +293,4 @@ var effects_t1 = [ // effect functions on tick 1+ eff_unimplemented, // y eff_unimplemented // z ]; +})(window || {}); \ No newline at end of file From 7aa67803ed2407ee9ec32ab3c9435ec194225eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Hillerstr=C3=B6m?= Date: Thu, 12 Nov 2015 01:04:59 +0100 Subject: [PATCH 31/78] Use postfix increment operator --- xm.js | 6 +++--- xmeffects.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/xm.js b/xm.js index 59465d7..8830798 100644 --- a/xm.js +++ b/xm.js @@ -297,14 +297,14 @@ function RedrawScreen() { function next_row() { if (player.cur_pat == -1 || player.cur_row >= player.xm.patterns[player.cur_pat].length) { player.cur_row = 0; - player.cur_songpos += 1; + player.cur_songpos++; if (player.cur_songpos >= player.xm.songpats.length) player.cur_songpos = player.xm.song_looppos; player.cur_pat = player.xm.songpats[player.cur_songpos]; } var p = player.xm.patterns[player.cur_pat]; var r = p[player.cur_row]; - player.cur_row += 1; + player.cur_row++; for (var i = 0; i < r.length; i++) { var ch = player.xm.channelinfo[i]; var inst = ch.inst; @@ -473,7 +473,7 @@ EnvelopeFollower.prototype.Tick = function(release) { } function next_tick() { - player.cur_tick += 1; + player.cur_tick++; if (player.cur_tick >= player.xm.tempo) { player.cur_tick = 0; next_row(); diff --git a/xmeffects.js b/xmeffects.js index 66819ba..1838981 100644 --- a/xmeffects.js +++ b/xmeffects.js @@ -120,7 +120,7 @@ function eff_t0_c(ch, data) { // set volume } function eff_t0_d(ch, data) { // pattern jump - player.cur_songpos += 1; + player.cur_songpos++; if (player.cur_songpos >= player.xm.songpats.length) player.cur_songpos = player.xm.song_looppos; player.cur_pat = player.xm.songpats[player.cur_songpos]; From 664686f199c53fda732f29909cebc880073efb09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Hillerstr=C3=B6m?= Date: Thu, 12 Nov 2015 01:05:59 +0100 Subject: [PATCH 32/78] Add newlines --- xm.js | 2 +- xmeffects.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/xm.js b/xm.js index 8830798..73adf5c 100644 --- a/xm.js +++ b/xm.js @@ -1271,4 +1271,4 @@ window.onload = function() { } DownloadXM(uri); } -})(window, document); \ No newline at end of file +})(window, document); diff --git a/xmeffects.js b/xmeffects.js index 1838981..bc9a9aa 100644 --- a/xmeffects.js +++ b/xmeffects.js @@ -293,4 +293,4 @@ player.effects_t1 = [ // effect functions on tick 1+ eff_unimplemented, // y eff_unimplemented // z ]; -})(window || {}); \ No newline at end of file +})(window || {}); From 1667ab502b073638edbf81398e05127d36f81c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Hillerstr=C3=B6m?= Date: Thu, 12 Nov 2015 01:09:20 +0100 Subject: [PATCH 33/78] Add window fallback --- xm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xm.js b/xm.js index 73adf5c..c55ac4f 100644 --- a/xm.js +++ b/xm.js @@ -1271,4 +1271,4 @@ window.onload = function() { } DownloadXM(uri); } -})(window, document); +})(window || {}, document); From 8e114a16da95a5fea14e75a32180d32dcb7d79a3 Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Wed, 11 Nov 2015 18:00:21 -0800 Subject: [PATCH 34/78] Split viewer code into trackview.js Also disabled iOS hack, as it seems to be causing issues, and fixed a minor drag-n-drop bug (wouldn't load if track was already playing past length of newly loaded track). To play without a viewer, you still have to stub it out. Not sure the best way to handle that case yet. --- index.html | 1 + trackview.js | 334 ++++++++++++++++++++++++++++++++ xm.js | 525 +++++++++++++-------------------------------------- xmeffects.js | 3 +- 4 files changed, 465 insertions(+), 398 deletions(-) create mode 100644 trackview.js diff --git a/index.html b/index.html index e54e71d..1370a34 100644 --- a/index.html +++ b/index.html @@ -2,6 +2,7 @@ js xm +