From d5c99e21e229089718ab7b6a0d7ecf7d7d121745 Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Mon, 2 Nov 2015 20:48:38 -0800 Subject: [PATCH 01/65] experimenting: linear interpolation --- xm.js | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/xm.js b/xm.js index de89689..cc943e0 100644 --- a/xm.js +++ b/xm.js @@ -135,7 +135,7 @@ function FilterCoeffs(f_c) { var e = Math.exp(-wct); var c = e * Math.cos(wct); var s = e * Math.sin(wct); - var gain = (1 - 2*c + c*c + s*s) / 2; + var gain = (1 - 2*c + c*c + s*s); return [gain, 2*c, -c*c - s*s]; } @@ -481,7 +481,6 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) { looplen = instsamp.looplen; sample_end = loopstart + looplen; } - 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 @@ -530,6 +529,7 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) { var next_event = Math.max(1, Math.min(end, i + (sample_end - k) / dk)); // this is the inner loop of the player + /* // unrolled 8x for (; i + 7 < next_event; i+=8) { var s = samp[k|0]; @@ -599,20 +599,25 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) { vL = pf_8 * vL + (1 - pf_8) * volL; vR = pf_8 * vR + (1 - pf_8) * volR; } + */ for (; i < next_event; i++) { - var s = samp[k|0]; + var s0 = samp[k|0]; + var s1 = samp[1+k|0]; + var t = k - (k|0); // we low-pass filter here since we are resampling some arbitrary // frequency to f_smp; this is an anti-aliasing filter and is // implemented as an IIR butterworth filter (usually we'd use an FIR // brick wall filter, but this is much simpler computationally and // sounds fine) - var y = f0 * (s + fs0) + f1*fs1 + f2*fs2; - fs2 = fs1; fs1 = y; fs0 = s; + var y = f0 * (t*s1 + (1-t) * s0) + f1*fs1 + f2*fs2; + fs2 = fs1; fs1 = y; dataL[i] += vL * y; dataR[i] += vR * y; Vrms += (vL + vR) * y * y; k += dk; + vL = popfilter_alpha * vL + (1 - popfilter_alpha) * volL; + vR = popfilter_alpha * vR + (1 - popfilter_alpha) * volR; } } ch.off = k; @@ -688,26 +693,25 @@ function audio_cb(e) { function ConvertSample(array, bits) { var len = array.length; var acc = 0; + var samp = new Float32Array(len+1); if (bits == 0) { // 8 bit sample - var samp = new Float32Array(len); for (var k = 0; k < len; k++) { acc += array[k]; var b = acc&255; if (b & 128) b = b-256; samp[k] = b / 128.0; } - return samp; } else { 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; if (b & 32768) b = b-65536; samp[k] = b / 32768.0; } - return samp; } + samp[len] = samp[len-1]; + return samp; } // optimization: unroll short sample loops so we can run our inner mixing loop @@ -721,7 +725,7 @@ function UnrollSampleLoop(samp) { nloops = (nloops + 1) & (~1); } var samplesiz = samp.loop + nloops * samp.looplen; - var data = new Float32Array(samplesiz); + var data = new Float32Array(samplesiz + 1); for (var i = 0; i < samp.loop; i++) { data[i] = samp.sampledata[i]; } @@ -896,7 +900,8 @@ function playXM(arrayBuf) { 'pan': samppan, 'type': samptype, 'vol': sampvol, 'fine': sampfinetune, 'sampledata': ConvertSample( - new Uint8Array(arrayBuf, sampleoffset, samplen), samptype & 16), + new Uint8Array(arrayBuf, sampleoffset, samplen), + samptype & 16), }; // length / pointers are all specified in bytes; fixup for 16-bit samples if (samptype & 16) { @@ -906,8 +911,13 @@ function playXM(arrayBuf) { } // unroll short loops and any pingpong loops - if ((samp.type & 3) && (samp.looplen < 2048 || (samp.type & 2))) { - UnrollSampleLoop(samp); + if (samp.type & 3) { + if ((samp.looplen < 2048 || (samp.type & 2))) { + UnrollSampleLoop(samp); + } + // overwrite loop boundary sample (we always have at least one + // extra sample for interpolation) + samp.sampledata[samp.loop + samp.looplen] = samp.sampledata[samp.loop]; } samps.push(samp); From ea6edaf2e4ba148f3f6f2d54a008f0c014946ae2 Mon Sep 17 00:00:00 2001 From: Andy Sloane Date: Tue, 3 Nov 2015 08:42:09 -0800 Subject: [PATCH 02/65] 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 03/65] 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 04/65] 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 05/65] 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 06/65] 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 07/65] 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 08/65] 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 09/65] 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 10/65] 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 11/65] 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 12/65] 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 13/65] 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 14/65] 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 15/65] 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 16/65] 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 18/65] 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 19/65] 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 20/65] 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 21/65] 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 +