diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 3c3629e..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 796d98f..0000000 --- a/.travis.yml +++ /dev/null @@ -1,3 +0,0 @@ -language: node_js -node_js: - - "0.12" diff --git a/README.md b/README.md index b6609c9..e559534 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,16 @@ # xm.js -[![Build Status](https://travis-ci.org/a1k0n/jsxm.svg?branch=master)](https://travis-ci.org/a1k0n/jsxm) - FastTracker 2 .XM player, written for fun. -[Demo](http://www.a1k0n.net/code/jsxm/) +[Demo](http://www.a1k0n.net/code/jsxm/) [warning: plays sound immediately on +load] -There is an XM player and a visualizer which are separate components. The -player API looks like this: +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. - - `XMPlayer.init` -> starts up audio context; it's available as - `XMPlayer.audioctx` - - `XMPlayer.load(ArrayBuffer)` -> returns `true` if loaded, otherwise - barfs randomly - - `XMPlayer.play()` -> starts playing - - `XMPlayer.pause()` -> obvious - - `XMPlayer.stop()` -> obvious; call this before loading a new one - -Loading trackview.js is optional; without it, the player won't do any -visualizations. Or, you can override the following to get callbacks: - - - `XMView.pushEvent(e)` -> push an audio event onto the queue. Called - once per tick (about 50Hz, controlled by song). `e` contains fields: - - `t` - audio timestamp - - `vu` - Float32Array of RMS power (volume) for each channel - - `scopes` - [Float32Array] of oscilloscope data, one array per - channel; `XMView.scope_width` contains # of samples to produce here - - `songpos` - position in the song (# patterns played) - - `pat` - pattern number currently playing - - `row` - row within pattern - - `XMView.pause()` - pause visualization - - `XMView.stop()` - stop/reset visualization - -The code which defines what the buttons do and downloads songs and so -forth is in `shell.js`. - -The player is fairly feature-complete, but is missing a bunch of effects. +It is fairly feature-complete, but is missing a bunch of effects. MIT license. diff --git a/index.html b/index.html index 3c6535e..3bc0bb7 100644 --- a/index.html +++ b/index.html @@ -1,82 +1,26 @@ js xm - - - - - + -
-
-
-
-
-
-
-
-

- - -

-
- -
- +
+
+
+
+
+
 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, E6x, E7x, E9x, EDx, EEx
+   - E3x, E4x, E5x, E6x, E7x, E9x, EDx, EEx
    - 7xy - tremolo
-   - Kxx, Lxx, Pxy, Txy
- - render pattern with the wider fonts for fewer channels
- - fix occasional rendering/audio hiccups when switching songs
+   - Gxx, Hxy, Kxx, Lxx, Pxy, Txy
 
diff --git a/kamel.xm b/kamel.xm new file mode 100644 index 0000000..8766dff Binary files /dev/null and b/kamel.xm differ diff --git a/package.json b/package.json deleted file mode 100644 index e21fffb..0000000 --- a/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "jsxm", - "version": "1.0.0", - "description": "WebAudio FastTracker 2 .XM module player", - "keywords": "fasttracker xm webaudio", - "uri": "http://www.a1k0n.net/code/jsxm/", - "repository": "a1k0n/jsxm", - "author": "Andy Sloane (http://www.a1k0n.net/)", - "license": "MIT", - "contributors": [{ - "name": "Johan Hillerström", - "email": "progr@mmer.nu" - }], - "main": "index.html", - "window": { - "frame": true, - "toolbar": false, - "position": "center", - "resizable": true - }, - "scripts": { "test": "node test/all.js" }, - "devDependencies": { "test": ">=0.0.5" } -} diff --git a/shell.js b/shell.js deleted file mode 100644 index 8a96b70..0000000 --- a/shell.js +++ /dev/null @@ -1,111 +0,0 @@ -(function (window, document) { -if (!window.XMPlayer) { - window.XMPlayer = {}; -} -var XMPlayer = window.XMPlayer; -if (!window.XMView) { - window.XMView = {}; -} -var XMView = window.XMView; - -function loadXMAndInit(xmdata) { - if (!XMPlayer.load(xmdata)) { - return; - } - - XMView.init(); - - var playbutton = document.getElementById("playpause"); - playbutton.innerHTML='Play'; - playbutton.onclick = function() { - if (XMPlayer.playing) { - XMPlayer.pause(); - playbutton.innerHTML='Play'; - } else { - XMPlayer.play(); - playbutton.innerHTML='Pause'; - } - }; - playbutton.disabled = false; - - return XMPlayer.xm; -} - -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) { - loadXMAndInit(arrayBuffer); - } else { - console.log("unable to load", uri); - } - }; - xmReq.send(null); -} - -XMPlayer.allowDrop = function(ev) { - ev.stopPropagation(); - ev.preventDefault(); - var elem = document.getElementById("playercontainer"); - elem.className = (ev.type == "dragover" ? "draghover" : "playercontainer"); - return false; -}; - -XMPlayer.handleDrop = function(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) { - XMPlayer.stop(); - loadXMAndInit(e.target.result); - }; - reader.readAsArrayBuffer(files[0]); - return false; -}; - -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"; - XMPlayer.stop(); - 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"; - } - }; -} - -window.onload = function() { - XMPlayer.init(); - initFilelist(); - - var uri = location.hash.substr(1); - if (uri === "") { - uri = "kamel.xm"; - } - if (!uri.startsWith("http")) { - uri = baseuri + uri; - } - downloadXM(uri); -}; - -})(window, document); diff --git a/test/all.js b/test/all.js deleted file mode 100644 index e84bc0c..0000000 --- a/test/all.js +++ /dev/null @@ -1,130 +0,0 @@ -window = {}; - -require('../xm.js'); -require('../xmeffects.js'); -var XMPlayer = window.XMPlayer; - -testdata = require('./testdata.js'); - -// test TODO: -// - audio output sample positions after ticks -// - unit test for envelopes -// -// using assert passed to the test function that just logs failures -exports['test XM startup'] = function(assert) { - testdata.resetXMData(); - XMPlayer.nextTick(); - assert.equal(XMPlayer.cur_songpos, 0, 'advance to initial song position'); - assert.equal(XMPlayer.cur_pat, 0, 'advance to pattern 0'); - assert.equal(XMPlayer.cur_tick, 0, 'advance to tick 0'); - assert.equal(XMPlayer.cur_row, 0, 'advance to row 0'); -}; - -exports['test non-existing song position'] = function(assert) { - testdata.resetXMData(); - // 2 existing patterns, 1 non-existing - XMPlayer.xm.songpats = [0, 1, 2] - // 1 channel, 1 row, 2 blank patterns - XMPlayer.xm.patterns = [ - [[[-1, -1, -1, 0, 0]]], - [[[-1, -1, -1, 0, 0]]] - ]; - XMPlayer.xm.tempo = 1; - XMPlayer.nextTick(); - assert.equal(XMPlayer.cur_songpos, 0, 'advance to initial song position'); - assert.equal(XMPlayer.cur_pat, 0, 'advance to pattern 0'); - assert.equal(XMPlayer.cur_row, 0, 'advance to row 0'); - XMPlayer.nextTick(); - assert.equal(XMPlayer.cur_songpos, 1, 'advance to song position 1'); - assert.equal(XMPlayer.cur_pat, 1, 'advance to pattern 1'); - XMPlayer.nextTick(); - assert.equal(XMPlayer.cur_songpos, 0, 'return to song position 0'); - assert.equal(XMPlayer.cur_pat, 0, 'return to pattern 0'); -}; - -exports['test non-existing first song position'] = function(assert) { - testdata.resetXMData(); - // 2 existing patterns, 1 non-existing - XMPlayer.xm.songpats = [2, 0, 1] - // 1 channel, 1 row, 2 blank patterns - XMPlayer.xm.patterns = [ - [[[-1, -1, -1, 0, 0]]], - [[[-1, -1, -1, 0, 0]]] - ]; - XMPlayer.xm.tempo = 1; - XMPlayer.nextTick(); - assert.equal(XMPlayer.cur_songpos, 1, 'advance to song position 1'); - assert.equal(XMPlayer.cur_pat, 0, 'advance to pattern 0'); - XMPlayer.nextTick(); - assert.equal(XMPlayer.cur_songpos, 2, 'advance to song position 2'); - assert.equal(XMPlayer.cur_pat, 1, 'advance to pattern 1'); -}; - -exports['test multiple non-existing song positions'] = function(assert) { - testdata.resetXMData(); - // 2 existing patterns, 2 non-existing - XMPlayer.xm.songpats = [2, 0, 3, 1] - // 1 channel, 1 row, 2 blank patterns - XMPlayer.xm.patterns = [ - [[[-1, -1, -1, 0, 0]]], - [[[-1, -1, -1, 0, 0]]] - ]; - XMPlayer.xm.tempo = 1; - XMPlayer.nextTick(); - assert.equal(XMPlayer.cur_songpos, 1, 'advance to song position 1'); - assert.equal(XMPlayer.cur_pat, 0, 'advance to pattern 0'); - XMPlayer.nextTick(); - assert.equal(XMPlayer.cur_songpos, 3, 'advance to song position 3'); - assert.equal(XMPlayer.cur_pat, 1, 'advance to pattern 1'); -}; - -exports['test non-existing song position with loop'] = function(assert) { - testdata.resetXMData(); - // 2 existing patterns, 1 non-existing - XMPlayer.xm.songpats = [0, 1, 2] - // 1 channel, 1 row, 2 blank patterns - XMPlayer.xm.patterns = [ - [[[-1, -1, -1, 0, 0]]], - [[[-1, -1, -1, 0, 0]]] - ]; - XMPlayer.xm.song_looppos = 1; - XMPlayer.xm.tempo = 1; - XMPlayer.nextTick(); - assert.equal(XMPlayer.cur_songpos, 0, 'advance to initial song position'); - assert.equal(XMPlayer.cur_pat, 0, 'advance to pattern 0'); - assert.equal(XMPlayer.cur_row, 0, 'advance to row 0'); - XMPlayer.nextTick(); - assert.equal(XMPlayer.cur_songpos, 1, 'advance to song position 1'); - assert.equal(XMPlayer.cur_pat, 1, 'advance to pattern 1'); - XMPlayer.nextTick(); - assert.equal(XMPlayer.cur_songpos, 1, 'replay song position 1'); - assert.equal(XMPlayer.cur_pat, 1, 'replay pattern 1'); -}; - -exports['test non-existing song position with invalid loop'] = function(assert) { - testdata.resetXMData(); - // 2 existing patterns, 1 non-existing - XMPlayer.xm.songpats = [0, 1, 2] - // 1 channel, 1 row, 2 blank patterns - XMPlayer.xm.patterns = [ - [[[-1, -1, -1, 0, 0]]], - [[[-1, -1, -1, 0, 0]]] - ]; - XMPlayer.xm.song_looppos = 4; - XMPlayer.xm.tempo = 1; - XMPlayer.nextTick(); - assert.equal(XMPlayer.cur_songpos, 0, 'advance to initial song position'); - assert.equal(XMPlayer.cur_pat, 0, 'advance to pattern 0'); - assert.equal(XMPlayer.cur_row, 0, 'advance to row 0'); - XMPlayer.nextTick(); - assert.equal(XMPlayer.cur_songpos, 1, 'advance to song position 1'); - assert.equal(XMPlayer.cur_pat, 1, 'advance to pattern 1'); - XMPlayer.nextTick(); - assert.equal(XMPlayer.cur_songpos, 0, 'return to song position 0'); - assert.equal(XMPlayer.cur_pat, 0, 'return to pattern 0'); -}; - -exports['test instruments'] = require('./instrument.js'); -exports['test effects'] = require('./effects.js'); - -if (module == require.main) require('test').run(exports); diff --git a/test/effects.js b/test/effects.js deleted file mode 100644 index 4a84f07..0000000 --- a/test/effects.js +++ /dev/null @@ -1,506 +0,0 @@ -var XMPlayer = window.XMPlayer; - -// tests TODO: -// - bxx: song jump -// - rxy: retrigger w/ volume changes -// low priority TODO (trivial or already covered by other tests): -// - 5xy: porta+vol -// - 6xy: vibrato+vol -// - 8xx: panning -// - 9xx: sample offset - -exports['test 0xy arpeggio'] = function(assert) { - var xm = testdata.resetXMData(); - // [pat][row][channel] - xm.patterns[0][0][0] = [48, 1, -1, 0, 0x4f]; // C-4 1 -- 04f - XMPlayer.xm.tempo = 4; - XMPlayer.nextTick(); - var ch = xm.channelinfo[0]; - assert.equal(ch.note, 48, 'note 0'); - assert.equal(ch.period, 1152, 'row 0 tick 0 period 0'); - XMPlayer.nextTick(); - assert.equal(ch.period, 1152 - 16*4, 'row 0 tick 1 period 4'); - XMPlayer.nextTick(); - assert.equal(ch.period, 1152 - 16*15, 'row 0 tick 2 period f'); - XMPlayer.nextTick(); - assert.equal(ch.period, 1152 - 16*0, 'row 0 tick 3 period 0'); - XMPlayer.nextTick(); - assert.equal(ch.period, 1152 - 16*0, 'row 1 tick 0 period 0'); -}; - -exports['test 1xx slide up'] = function(assert) { - var xm = testdata.resetXMData(); - // [pat][row][channel] - xm.patterns[0][0][0] = [48, 1, -1, 1, 0x01]; // C-4 1 -- 101 - xm.patterns[0][1][0] = [-1, -1, -1, 1, 0x00]; // --- -- -- 100 - XMPlayer.xm.tempo = 3; - XMPlayer.nextTick(); - var ch = xm.channelinfo[0]; - assert.equal(ch.period, 1152, 'row 0 tick 0 period 0'); - XMPlayer.nextTick(); - assert.equal(ch.period, 1152 - 1, 'row 0 tick 1 period -1'); - XMPlayer.nextTick(); - assert.equal(ch.period, 1152 - 2, 'row 0 tick 2 period -2'); - XMPlayer.nextTick(); - assert.equal(ch.period, 1152 - 2, 'row 1 tick 0 period -2'); - XMPlayer.nextTick(); - assert.equal(ch.period, 1152 - 3, 'row 1 tick 1 period -3'); -}; - -exports['test 2xx slide down'] = function(assert) { - var xm = testdata.resetXMData(); - // [pat][row][channel] - xm.patterns[0][0][0] = [48, 1, -1, 2, 0x01]; // C-4 1 -- 201 - xm.patterns[0][1][0] = [-1, -1, -1, 2, 0x00]; // --- -- -- 200 - XMPlayer.xm.tempo = 3; - XMPlayer.nextTick(); - var ch = xm.channelinfo[0]; - assert.equal(ch.period, 1152, 'row 0 tick 0 period 0'); - XMPlayer.nextTick(); - assert.equal(ch.period, 1152 + 1, 'row 0 tick 1 period +1'); - XMPlayer.nextTick(); - assert.equal(ch.period, 1152 + 2, 'row 0 tick 2 period +2'); - XMPlayer.nextTick(); - assert.equal(ch.period, 1152 + 2, 'row 1 tick 0 period +2'); - XMPlayer.nextTick(); - assert.equal(ch.period, 1152 + 3, 'row 1 tick 1 period +3'); -}; - -exports['test 3xx portamento'] = function(assert) { - var xm = testdata.resetXMData(); - // [pat][row][channel] - xm.patterns[0][0][0] = [48, 1, -1, 0, 0x00]; // C-4 1 -- 000 - xm.patterns[0][1][0] = [49, 1, -1, 3, 0x09]; // C#4 1 -- 309 - XMPlayer.xm.tempo = 3; - XMPlayer.nextTick(); // row 0 tick 0 - var ch = xm.channelinfo[0]; - assert.equal(ch.period, 1152, 'row 0 tick 0 period 0'); - XMPlayer.nextTick(); // row 0 tick 1 - XMPlayer.nextTick(); // row 0 tick 2 - XMPlayer.nextTick(); // row 1 tick 0 - assert.equal(ch.period, 1152, 'row 1 tick 0 period 0'); - XMPlayer.nextTick(); - assert.equal(ch.period, 1152 - 9, 'row 1 tick 1 period -9'); - XMPlayer.nextTick(); - assert.equal(ch.period, 1152 - 16, 'row 1 tick 2 period -16'); -}; - -exports['test 4xy vibrato - sine'] = function(assert) { - var xm = testdata.resetXMData(); - // vibrato 4xy: speed x, depth y - // full cycle is 64/speed - xm.patterns[0] = [ - [[48, 1, -1, 4, 0x81]], // C-4 1 -- 481 - [[-1, -1, -1, 4, 0x02]], // --- -- -- 402 - [[-1, -1, -1, 4, 0x10]], // --- -- -- 410 - [[-1, -1, -1, 4, 0x00]], // --- -- -- 400 - [[-1, -1, -1, 0, 0x00]], // --- -- -- --- - no vibrato - [[-1, -1, -1, 4, 0x00]], // --- -- -- 400 - resume vibrato @ pos 0 - [[-1, -1, 0xb2, 0, 0x00]], // --- -- V2 --- - volume effect vibrato - ]; - // I should really be testing ch.doff directly - XMPlayer.xm.tempo = 3; - var ch = xm.channelinfo[0]; - XMPlayer.nextTick(); // row 0 tick 0 - var p0 = ch.doff; - assert.equal(ch.periodoffset, 0, 'row 0 tick 0 periodoffset=0'); - XMPlayer.nextTick(); // row 0 tick 1 - // compute logical period p from actual play frequency - var p = 16*12 * Math.log(p0 / ch.doff) / Math.log(2); - assert.equal(p.toFixed(3), "0.000", 'row 0 tick 1 period +0'); - XMPlayer.nextTick(); // row 0 tick 2 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "1.414", 'row 0 tick 2 period +1.414'); - XMPlayer.nextTick(); // row 1 tick 0 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "4.000", 'row 1 tick 0 period +4.000'); - XMPlayer.nextTick(); // row 1 tick 1 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "4.000", 'row 1 tick 1 period +4.000'); - XMPlayer.nextTick(); // row 1 tick 2 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "2.828", 'row 1 tick 2 period 2.828'); - XMPlayer.nextTick(); // row 2 tick 0 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "0.000", 'row 2 tick 0 period +0'); - XMPlayer.nextTick(); // row 2 tick 1 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "0.000", 'row 2 tick 1 period +0'); - XMPlayer.nextTick(); // row 2 tick 2 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-0.392", 'row 2 tick 2 period -0.392'); - XMPlayer.nextTick(); // row 3 tick 0 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-0.780", 'row 3 tick 0 period -0.780'); - XMPlayer.nextTick(); // row 3 tick 1 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-0.780", 'row 3 tick 1 period -0.780'); - XMPlayer.nextTick(); // row 3 tick 2 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-1.161", 'row 3 tick 2 period -1.161'); - XMPlayer.nextTick(); // row 4 tick 0 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "0.000", 'row 4 tick 0 period 0 - no vibrato'); - XMPlayer.nextTick(); // row 4 tick 1 - XMPlayer.nextTick(); // row 4 tick 2 - // I actually don't know whether vibrato is supposed to reset when the effect - // goes away or whether it should resume. Resuming is simpler to implement so - // that's what I'm assuming here... - XMPlayer.nextTick(); // row 5 tick 0 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-1.531", 'row 5 tick 0 period -1.531 - vibrato resume'); - XMPlayer.nextTick(); // row 5 tick 1 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-1.531", 'row 5 tick 1 period -1.531'); - XMPlayer.nextTick(); // row 5 tick 2 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-1.886", 'row 5 tick 2 period -1.886'); - XMPlayer.nextTick(); // row 6 tick 0 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-1.111", 'row 6 tick 0 period -1.111'); -}; - -exports['test 4xy vibrato - saw'] = function(assert) { - var xm = testdata.resetXMData(); - // vibrato 4xy: speed x, depth y - // full cycle is 64/speed - xm.patterns[0] = [ - [[48, 1, -1, 14, 0x41]], // C-4 1 -- E41 - 1 = saw (ramp-down) - [[-1, -1, -1, 4, 0x81]], // --- -- -- 481 - [[-1, -1, -1, 4, 0x00]], // --- -- -- 400 - [[-1, -1, -1, 0, 0x00]], // --- -- -- --- - no vibrato - [[-1, -1, -1, 4, 0x00]], // --- -- -- 400 - resume vibrato @ pos 0 - ]; - XMPlayer.xm.tempo = 4; - var ch = xm.channelinfo[0]; - assert.equal(ch.vibratotype, 0, 'initial vibratotype 0'); - XMPlayer.nextTick(); // row 0 tick 0 - var p0 = ch.doff; - assert.equal(ch.vibratotype, 1, 'row 0 tick 0 vibratotype=1'); - XMPlayer.nextTick(); // row 0 tick 1 - XMPlayer.nextTick(); // row 0 tick 2 - XMPlayer.nextTick(); // row 0 tick 3 - XMPlayer.nextTick(); // row 1 tick 0 - assert.equal(ch.periodoffset, 0, 'row 1 tick 0 periodoffset=0'); - XMPlayer.nextTick(); // row 1 tick 1 - // compute logical period p from actual play frequency - var p = 16*12 * Math.log(p0 / ch.doff) / Math.log(2); - assert.equal(p.toFixed(3), "0.000", 'row 1 tick 1 period +0'); - XMPlayer.nextTick(); // row 1 tick 2 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "0.500", 'row 1 tick 2 period +0.500'); - XMPlayer.nextTick(); // row 1 tick 3 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "1.000", 'row 1 tick 3 period +1.000'); - XMPlayer.nextTick(); // row 2 tick 0 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "1.500", 'row 2 tick 0 period +1.500'); - XMPlayer.nextTick(); // row 2 tick 1 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "1.500", 'row 2 tick 1 period +1.500'); - XMPlayer.nextTick(); // row 2 tick 2 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-2.000", 'row 2 tick 2 period -2.000'); - XMPlayer.nextTick(); // row 2 tick 3 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-1.500", 'row 2 tick 3 period -1.500'); - XMPlayer.nextTick(); // row 3 tick 0 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "0.000", 'row 3 tick 0 period 0 - no vibrato'); - XMPlayer.nextTick(); // row 3 tick 1 - XMPlayer.nextTick(); // row 3 tick 2 - XMPlayer.nextTick(); // row 3 tick 3 - XMPlayer.nextTick(); // row 4 tick 0 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-1.000", 'row 4 tick 0 period -1.000 - vibrato resume'); - XMPlayer.nextTick(); // row 4 tick 1 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-1.000", 'row 4 tick 1 period -1.000'); - XMPlayer.nextTick(); // row 4 tick 2 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-0.500", 'row 4 tick 2 period -0.500'); - XMPlayer.nextTick(); // row 4 tick 3 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "0.000", 'row 4 tick 3 period +0'); -}; - -exports['test 4xy vibrato - square'] = function(assert) { - var xm = testdata.resetXMData(); - // vibrato 4xy: speed x, depth y - // full cycle is 64/speed - xm.patterns[0] = [ - [[48, 1, -1, 14, 0x42]], // C-4 1 -- E42 - 2 = square - [[-1, -1, -1, 4, 0x81]], // --- -- -- 481 - [[-1, -1, -1, 4, 0x02]], // --- -- -- 402 - [[-1, -1, -1, 4, 0x10]], // --- -- -- 410 - [[-1, -1, -1, 4, 0x03]], // --- -- -- 403 - [[-1, -1, -1, 0, 0x00]], // --- -- -- --- - no vibrato - [[-1, -1, -1, 4, 0x00]], // --- -- -- 400 - resume vibrato @ pos 0 - ]; - XMPlayer.xm.tempo = 3; - var ch = xm.channelinfo[0]; - assert.equal(ch.vibratotype, 0, 'initial vibratotype 0'); - XMPlayer.nextTick(); // row 0 tick 0 - var p0 = ch.doff; - assert.equal(ch.vibratotype, 2, 'row 0 tick 0 vibratotype=2'); - XMPlayer.nextTick(); // row 0 tick 1 - XMPlayer.nextTick(); // row 0 tick 2 - XMPlayer.nextTick(); // row 1 tick 0 - assert.equal(ch.periodoffset, 2, 'row 1 tick 0 periodoffset=2'); - XMPlayer.nextTick(); // row 1 tick 1 - // compute logical period p from actual play frequency - var p = 16*12 * Math.log(p0 / ch.doff) / Math.log(2); - assert.equal(p.toFixed(3), "2.000", 'row 1 tick 1 period +2.000'); - XMPlayer.nextTick(); // row 1 tick 2 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "2.000", 'row 1 tick 2 period +2.000'); - XMPlayer.nextTick(); // row 2 tick 0 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "4.000", 'row 2 tick 0 period +4.000'); - XMPlayer.nextTick(); // row 2 tick 1 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "4.000", 'row 2 tick 1 period +4.000'); - XMPlayer.nextTick(); // row 2 tick 2 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "4.000", 'row 2 tick 2 period +4.000'); - XMPlayer.nextTick(); // row 3 tick 0 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-4.000", 'row 3 tick 0 period -4.000'); - XMPlayer.nextTick(); // row 3 tick 1 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-4.000", 'row 3 tick 1 period -4.000'); - XMPlayer.nextTick(); // row 3 tick 2 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-4.000", 'row 3 tick 2 period -4.000'); - XMPlayer.nextTick(); // row 4 tick 0 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-6.000", 'row 4 tick 0 period -6.000'); - XMPlayer.nextTick(); // row 4 tick 1 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-6.000", 'row 4 tick 1 period -6.000'); - XMPlayer.nextTick(); // row 4 tick 2 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-6.000", 'row 4 tick 2 period -6.000'); - XMPlayer.nextTick(); // row 4 tick 0 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "0.000", 'row 4 tick 0 period 0 - no vibrato'); - XMPlayer.nextTick(); // row 4 tick 1 - XMPlayer.nextTick(); // row 4 tick 2 - XMPlayer.nextTick(); // row 5 tick 0 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-6.000", 'row 5 tick 0 period -6.000 - vibrato resume'); - XMPlayer.nextTick(); // row 5 tick 1 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-6.000", 'row 5 tick 1 period -6.000'); - XMPlayer.nextTick(); // row 5 tick 2 - p = -16*12 * Math.log(ch.doff / p0) / Math.log(2); - assert.equal(p.toFixed(3), "-6.000", 'row 5 tick 2 period -6.000'); -}; - -exports['test Axy volume slide'] = function(assert) { - var xm = testdata.resetXMData(); - XMPlayer.xm.tempo = 6; - - xm.patterns[0] = [ - [[48, 1, -1, 10, 0x0f]], // C-4 1 -- A0f (slide down) - [[-1, -1, -1, 10, 0x90]], // --- -- -- A90 (slide up) - [[-1, -1, -1, 10, 0x00]], // --- -- -- A00 (continue same) - [[-1, -1, 0x30, 10, 0x11]], // --- -- 20 A11 (invalid, do nothing) - ]; - var ch = xm.channelinfo[0]; - XMPlayer.nextTick(); - assert.equal(ch.vol, 64, 'row 0 tick 0 vol 64'); - XMPlayer.nextTick(); - assert.equal(ch.vol, 49, 'row 0 tick 1 vol 49'); - XMPlayer.nextTick(); - assert.equal(ch.vol, 34, 'row 0 tick 2 vol 34'); - XMPlayer.nextTick(); - assert.equal(ch.vol, 19, 'row 0 tick 3 vol 19'); - XMPlayer.nextTick(); - assert.equal(ch.vol, 4, 'row 0 tick 4 vol 4'); - XMPlayer.nextTick(); - assert.equal(ch.vol, 0, 'row 0 tick 5 vol 0'); - XMPlayer.nextTick(); - assert.equal(ch.vol, 0, 'row 1 tick 0 vol 0'); - XMPlayer.nextTick(); - assert.equal(ch.vol, 9, 'row 1 tick 1 vol 9'); - XMPlayer.nextTick(); // row 1 tick 2 - XMPlayer.nextTick(); // tick 3 - XMPlayer.nextTick(); // tick 4 - XMPlayer.nextTick(); // tick 5 - assert.equal(ch.vol, 45, 'row 1 tick 5 vol 45'); - XMPlayer.nextTick(); // row 2 tick 0 - assert.equal(ch.vol, 45, 'row 2 tick 0 vol 45'); - XMPlayer.nextTick(); // row 2 tick 1 - assert.equal(ch.vol, 54, 'row 2 tick 1 vol 54'); - XMPlayer.nextTick(); // tick 2 - XMPlayer.nextTick(); // tick 3 - assert.equal(ch.vol, 64, 'row 2 tick 3 vol 64'); - XMPlayer.nextTick(); // tick 4 - XMPlayer.nextTick(); // tick 5 - XMPlayer.nextTick(); // row 3 tick 0 - assert.equal(ch.vol, 32, 'row 3 tick 0 vol 32'); - XMPlayer.nextTick(); // tick 1 - assert.equal(ch.vol, 32, 'row 3 tick 1 vol 32'); - XMPlayer.nextTick(); // tick 2 - assert.equal(ch.vol, 32, 'row 3 tick 2 vol 32'); -}; - -exports['test Dxx pattern jump'] = function(assert) { - var xm = testdata.resetXMData(); - xm.tempo = 2; - xm.patterns[0] = [ - [[-1, -1, -1, 0x0, 0x00]], // 00 --- -- -- 000 - [[-1, -1, -1, 0x0, 0x00]], // 01 --- -- -- 000 - [[-1, -1, -1, 0x0, 0x00]], // 02 --- -- -- 000 - [[-1, -1, -1, 0x0, 0x00]], // 03 --- -- -- 000 - [[-1, -1, -1, 0x0, 0x00]], // 04 --- -- -- 000 - [[-1, -1, -1, 0x0, 0x00]], // 05 --- -- -- 000 - [[-1, -1, -1, 0x0, 0x00]], // 06 --- -- -- 000 - [[-1, -1, -1, 0x0, 0x00]], // 07 --- -- -- 000 - [[-1, -1, -1, 0x0, 0x00]], // 08 --- -- -- 000 - [[-1, -1, -1, 0x0, 0x00]], // 09 --- -- -- 000 - [[49, 1, -1, 0x0, 0x00]], // 0a C#4 1 -- 000 - [[-1, -1, -1, 0x0, 0x00]], // 0b --- -- -- 000 - ]; - // argument is BCD, so 0x10 actually means 10 (decimal) - xm.patterns[1] = [ - [[-1, -1, -1, 0x0, 0x00]], // --- -- -- 000 - [[48, 1, -1, 0xd, 0x10]], // C-4 1 -- D10 - [[-1, -1, -1, 0x0, 0x00]], // --- -- -- 000 - ]; - xm.songpats = [1, 0]; - XMPlayer.nextTick(); // play first, empty row (pattern 1) - XMPlayer.nextTick(); - assert.equal(XMPlayer.cur_songpos, 0, "songpos 0"); - assert.equal(XMPlayer.cur_pat, 1, "pattern 1"); - assert.equal(XMPlayer.cur_row, 0, "row 0"); - - XMPlayer.nextTick(); // play C-4 1 -- D01 in pattern 1 - XMPlayer.nextTick(); // we will jump to pattern 0 row 1 after this - - assert.equal(xm.channelinfo[0].period, 1152, "play C-4 on Dxx row"); - - XMPlayer.nextTick(); // play row 0x0a C#4 1 -- 000 in pattern 0 - - assert.equal(xm.channelinfo[0].period, 1136, "play C#4 on following row"); - assert.equal(XMPlayer.cur_songpos, 1, "songpos 0"); - assert.equal(XMPlayer.cur_pat, 0, "pattern 1"); - assert.equal(XMPlayer.cur_row, 10, "row 10"); -}; - -exports['test Gxx global volume'] = function(assert) { - var xm = testdata.resetXMData(); - xm.patterns = [ - [ - [[48, 1, -1, 16, 0x40]], // C-4 1 -- G40 - [[48, 1, -1, 16, 0x2B]], // C-4 1 -- G2B - // test out of bounds volume - [[48, 1, -1, 16, 0x80]] // C-4 1 -- G80 - ] - ]; - XMPlayer.xm.tempo = 1; - XMPlayer.nextTick(); - // volume gets multiplied by 2 to match - // the initial max global volume of 128 - assert.equal(XMPlayer.xm.global_volume, 0x40*2, 'global volume set to 0x40'); - XMPlayer.nextTick(); - assert.equal(XMPlayer.xm.global_volume, 0x2B*2, 'global volume set to 0x2B'); - XMPlayer.nextTick(); - assert.equal(XMPlayer.xm.global_volume, 0x40*2, 'global volume set to 0x40'); -}; - -exports['test Hxy global volume slide'] = function(assert) { - var xm = testdata.resetXMData(); - xm.tempo = 6; - xm.global_volume = 128; - - xm.patterns[0] = [ - [[48, 1, -1, 17, 0x0f]], // C-4 1 -- H0f (slide down) - [[-1, -1, -1, 17, 0x90]], // --- -- -- H90 (slide up) - [[-1, -1, -1, 17, 0x00]], // --- -- -- H00 (continue same) - [[-1, -1, 0x30, 17, 0x11]], // --- -- 30 H11 (invalid, do nothing) - ]; - XMPlayer.nextTick(); - assert.equal(xm.global_volume, 128, 'row 0 tick 0 vol 128'); - XMPlayer.nextTick(); - assert.equal(xm.global_volume, 98, 'row 0 tick 1 vol 98'); - XMPlayer.nextTick(); - assert.equal(xm.global_volume, 68, 'row 0 tick 2 vol 68'); - XMPlayer.nextTick(); - assert.equal(xm.global_volume, 38, 'row 0 tick 3 vol 38'); - XMPlayer.nextTick(); - assert.equal(xm.global_volume, 8, 'row 0 tick 4 vol 8'); - XMPlayer.nextTick(); - assert.equal(xm.global_volume, 0, 'row 0 tick 5 vol 0'); - XMPlayer.nextTick(); - assert.equal(xm.global_volume, 0, 'row 1 tick 0 vol 0'); - XMPlayer.nextTick(); - assert.equal(xm.global_volume, 18, 'row 1 tick 1 vol 18'); - XMPlayer.nextTick(); // row 1 tick 2 - XMPlayer.nextTick(); // tick 3 - XMPlayer.nextTick(); // tick 4 - XMPlayer.nextTick(); // tick 5 - assert.equal(xm.global_volume, 90, 'row 1 tick 5 vol 90'); - XMPlayer.nextTick(); // row 2 tick 0 - assert.equal(xm.global_volume, 90, 'row 2 tick 0 vol 90'); - XMPlayer.nextTick(); // row 2 tick 1 - assert.equal(xm.global_volume, 108, 'row 2 tick 1 vol 108'); - XMPlayer.nextTick(); // tick 2 - XMPlayer.nextTick(); // tick 3 - assert.equal(xm.global_volume, 128, 'row 2 tick 3 vol 128'); - XMPlayer.nextTick(); // tick 4 - XMPlayer.nextTick(); // tick 5 - XMPlayer.nextTick(); // row 3 tick 0 - assert.equal(xm.global_volume, 128, 'row 3 tick 0 vol 128'); - XMPlayer.nextTick(); // tick 1 - assert.equal(xm.global_volume, 128, 'row 3 tick 1 vol 128'); - XMPlayer.nextTick(); // tick 2 - assert.equal(xm.global_volume, 128, 'row 3 tick 2 vol 128'); -}; - -exports['test E4x set vibrato waveform'] = function(assert) { - var xm = testdata.resetXMData(); - xm.patterns = [ - [ - [[48, 1, -1, 0, 0x00]], // C-4 1 -- --- (default waveform - sine) - [[48, 1, -1, 14, 0x41]], // C-4 1 -- E41 (saw, ramp-down) - [[48, 1, -1, 14, 0x42]], // C-4 1 -- E42 (square) - [[48, 1, -1, 14, 0x43]] // C-4 1 -- E43 (random) - ] - ]; - xm.tempo = 1; - var ch = xm.channelinfo[0]; - XMPlayer.nextTick(); - assert.equal(ch.vibratotype, 0, 'row 0 tick 0 vibratotype=0'); - XMPlayer.nextTick(); - assert.equal(ch.vibratotype, 1, 'row 1 tick 0 vibratotype=1'); - XMPlayer.nextTick(); - assert.equal(ch.vibratotype, 2, 'row 2 tick 0 vibratotype=2'); - XMPlayer.nextTick(); - assert.equal(ch.vibratotype, 3, 'row 3 tick 0 vibratotype=3'); -}; - -exports['test E5x finetune override'] = function(assert) { - var xm = testdata.resetXMData(); - // set an initial finetune so we know we're overriding it... - xm.instruments[0].samples[0].fine = -4; - xm.patterns = [ - [ - [[48, 1, -1, 0, 0x00]], // C-4 1 -- 000 (sample finetune -4) - [[48, 1, -1, 14, 0x50]], // C-4 1 -- E50 (finetune -128) - [[48, 1, -1, 14, 0x5f]] // C-4 1 -- E5f (finetune +127) - ] - ]; - xm.tempo = 1; - var ch = xm.channelinfo[0]; - XMPlayer.nextTick(); - var f0 = ch.doff; - XMPlayer.nextTick(); - // compare frequency relative to original finetune -4 note - var f1 = 12 * 128 * Math.log(ch.doff / f0) / Math.log(2); - assert.equal(f1.toFixed(2), "-124.00", "E50 finetune -128"); - XMPlayer.nextTick(); - var f2 = 12 * 128 * Math.log(ch.doff / f0) / Math.log(2); - assert.equal(f2.toFixed(2), "131.00", "E5f finetune +127"); -}; diff --git a/test/instrument.js b/test/instrument.js deleted file mode 100644 index 433b2a4..0000000 --- a/test/instrument.js +++ /dev/null @@ -1,57 +0,0 @@ -var XMPlayer = window.XMPlayer; - -// TODO: -// - test volume envelope (sustain, release) -// - test panning envelope - -exports['test note on'] = function(assert) { - var xm = testdata.resetXMData(); - // [pat][row][channel] - xm.patterns[0][0][0] = [48, 1, 0x10 + 0x33, 0, 0]; // C-4 1 33 000 - XMPlayer.nextRow(); - var ch = xm.channelinfo[0]; - assert.equal(ch.note, 48, 'set note'); - assert.equal(ch.period, 1152, 'channel period'); - assert.equal(ch.vol, 0x33, 'channel volume'); - assert.equal(ch.samp, xm.instruments[0].samples[0], 'set sample'); - assert.equal(ch.off, 0, 'trigger offset'); -}; - -exports['test instrument trigger'] = function(assert) { - var xm = testdata.resetXMData(); - // [pat][row][channel] - xm.patterns[0][0][0] = [48, 1, 0x10 + 0x33, 0, 0]; // C-4 1 33 000 - xm.patterns[0][1][0] = [-1, 1, -1, 0, 0]; // --- 1 -- 000 - var ch = xm.channelinfo[0]; - XMPlayer.nextRow(); - ch.pan = 1; // forcibly override panning - ch.off = 100; // and sample offset - assert.equal(XMPlayer.cur_row, 0, 'row 0'); - XMPlayer.nextRow(); - assert.equal(XMPlayer.cur_row, 1, 'row 1'); - assert.equal(ch.note, 48, 'note same after inst trigger'); - assert.equal(ch.period, 1152, 'period same after inst trigger'); - assert.equal(ch.vol, 64, 'vol reset after inst trigger'); - assert.equal(ch.pan, 128, 'pan reset after inst trigger'); - assert.equal(ch.samp, xm.instruments[0].samples[0], 'set sample'); - assert.equal(ch.off, 0, 'set offset=0'); -}; - -exports['test note trigger'] = function(assert) { - var xm = testdata.resetXMData(); - // [pat][row][channel] - xm.patterns[0][0][0] = [48, 1, 0x10 + 0x33, 0, 0]; // C-4 1 33 000 - xm.patterns[0][1][0] = [49, -1, -1, 0, 0]; // C#4 -- -- 000 - var ch = xm.channelinfo[0]; - XMPlayer.nextRow(); - ch.pan = 1; // forcibly override panning - ch.off = 100; // and sample offset - ch.vibratopos = 1; // and vibrato position - XMPlayer.nextRow(); - assert.equal(ch.note, 49, 'note updated after inst trigger'); - assert.equal(ch.period, 1136, 'period updated after note trigger'); - assert.equal(ch.vol, 0x33, 'vol not reset after note trigger'); - assert.equal(ch.pan, 1, 'pan not reset after note trigger'); - assert.equal(ch.off, 0, 'set offset=0'); - assert.equal(ch.vibratopos, 0, 'set vibrato pos=0'); -}; diff --git a/test/testdata.js b/test/testdata.js deleted file mode 100644 index c8dd629..0000000 --- a/test/testdata.js +++ /dev/null @@ -1,58 +0,0 @@ -var XMPlayer = window.XMPlayer; - -// set up basic blank single-channel, single-pattern XM -exports.resetXMData = function() { - var xm = {}; - window.XMPlayer.xm = xm; - xm.channelinfo = []; - xm.songname = "test song"; - xm.song_looppos = 0; - xm.nchan = 1; - xm.flags = 1; - xm.tempo = 3; - xm.bpm = 125; - xm.channelinfo.push({ - number: 0, - filterstate: new Float32Array(3), - vol: 0, - pan: 128, - period: 1920 - 48*16, - vL: 0, vR: 0, - vLprev: 0, vRprev: 0, - mute: 0, - volE: 0, panE: 0, - retrig: 0, - vibratopos: 0, - vibratodepth: 1, - vibratospeed: 1, - vibratotype: 0, - }); - xm.songpats = [0]; - // 1 channel, 2 row blank pattern - xm.patterns = [ - [ - [[-1, -1, -1, 0, 0]], - [[-1, -1, -1, 0, 0]] - ] - ]; - xm.instruments = []; - xm.instruments.push({ - 'name': "test instrument", - 'number': 0, - 'samples': [ - { 'len': 256, 'loop': 0, - 'looplen': 256, 'note': 0, 'fine': 0, - 'pan': 128, 'type': 0, 'vol': 64, - 'sampledata': new Float32Array(256) - } - ], - 'samplemap': new Uint8Array(96), - 'env_vol': new XMPlayer.Envelope([0, 64, 1, 0], 2, 0, 0, 0), - 'env_pan': new XMPlayer.Envelope([0, 32], 0, 0, 0, 0) - }); - // reset song position - XMPlayer.cur_songpos = -1; - XMPlayer.cur_pat = -1; - XMPlayer.cur_tick = 64; - return xm; -}; diff --git a/trackview.js b/trackview.js deleted file mode 100644 index 52046bf..0000000 --- a/trackview.js +++ /dev/null @@ -1,348 +0,0 @@ -(function (window, document) { -if (!window.XMPlayer) { - window.XMPlayer = {}; -} -var player = window.XMPlayer; - -if (!window.XMView) { - window.XMView = {}; -} -var view = window.XMView; - -var _pattern_cellwidth = 16 + 4 + 8 + 4 + 8 + 16 + 4; -var _scope_width = _pattern_cellwidth - 1; -var _pattern_border = 20; - -view.init = init; -view.pushEvent = pushEvent; -view.pause = pause; -view.resume = resume; -view.stop = stop; -view.scope_width = _scope_width; - -// Load font (ripped from FastTracker 2) -var fontimg = new window.Image(); -fontimg.src = "ft2font.png"; - -var _fontmap_notes = [8*5, 8*22, 8*28]; -var pat_canvas_patnum; - -var audio_events; -var paused_events; -var shown_row; - -// canvas to render patterns onto -var pat_canvas = document.createElement('canvas'); - -// 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]; - -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) { - 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 + 56; - var width = _fontwidths[n]; - ctx.drawImage(fontimg, sx, sy, width, 10, dx, dy, width, 10); - dx += width + 1; - } - return dx - dx0; -} - -function getTextSize(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; -} - -function RenderPattern(canv, pattern) { - // a pattern consists of NxM cells which look like - // N-O II VV EFF - var cellwidth = _pattern_cellwidth; - canv.width = pattern[0].length * cellwidth + _pattern_border; - canv.height = pattern.length * 8; - var ctx = canv.getContext('2d'); - ctx.fillcolor='#000'; - ctx.fillRect(0, 0, canv.width, canv.height); - for (var j = 0; j < pattern.length; j++) { - var row = pattern[j]; - var dy = j * 8; - // render row number - ctx.drawImage(fontimg, 8*(j>>4), 0, 8, 8, 2, dy, 8, 8); - ctx.drawImage(fontimg, 8*(j&15), 0, 8, 8, 10, dy, 8, 8); - - for (var i = 0; i < row.length; i++) { - var dx = i*cellwidth + 2 + _pattern_border; - var data = row[i]; - - // render note - var note = data[0]; - if (note < 0) { - // no note = ... - ctx.drawImage(fontimg, 0, 8*5, 16, 8, dx, dy, 16, 8); - } else { - var octave = (note/12)|0; - var note_fontrow = _fontmap_notes[(octave/3)|0]; - note = (note % (12*3))|0; - ctx.drawImage(fontimg, 16+16*note, note_fontrow, 16, 8, dx, dy, 16, 8); - } - dx += 20; - - // render instrument - var inst = data[1]; - if (inst != -1) { // no instrument = render nothing - 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; - - // render volume - var vol = data[2]; - if (vol < 0x10) { - // no volume = .. - ctx.drawImage(fontimg, 312, 0, 8, 8, dx, dy, 8, 8); - } else { - 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; - - // render effect - var eff = data[3]; - var effdata = data[4]; - if (eff !== 0 || effdata !== 0) { - // draw effect with tiny font (4px space + effect type 0..9a..z) - ctx.drawImage(fontimg, 8*eff + 16*8, 4*8, 8, 8, dx, dy, 8, 8); - dx += 8; - // (hexadecimal 4-width font) - ctx.drawImage(fontimg, 8*(effdata>>4), 4*8, 4, 8, dx, dy, 4, 8); - ctx.drawImage(fontimg, 8*(effdata&15), 4*8, 4, 8, dx+4, dy, 4, 8); - } else { - // no effect = ... - ctx.drawImage(fontimg, 0, 8*5, 16, 8, dx+2, dy, 16, 8); - } - } - } -} - -function redrawScreen() { - var e; - var t = player.audioctx.currentTime; - while (audio_events.length > 0 && audio_events[0].t < t) { - e = audio_events.shift(); - } - if (!e) { - if (player.playing) { - window.requestAnimationFrame(redrawScreen); - } - return; - } - var VU = e.vu; - var scopes = e.scopes; - var ctx; - - if (e.scopes !== undefined) { - // update VU meters & oscilliscopes - var canvas = document.getElementById("vu"); - 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 < player.xm.nchan; j++) { - var x = _pattern_border + j * _pattern_cellwidth; - // render channel number - drawText(''+j, x, 1, ctx); - - // 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]; - if (scope) { - ctx.beginPath(); - for (var k = 0; k < _scope_width; k++) { - ctx.lineTo(x + 1 + k, 32 - 16 * scope[k]); - } - ctx.stroke(); - } - } - } - - if (e.row != shown_row || e.pat != pat_canvas_patnum) { - if (e.pat != pat_canvas_patnum) { - var p = player.xm.patterns[e.pat]; - if (p) { - RenderPattern(pat_canvas, player.xm.patterns[e.pat]); - pat_canvas_patnum = e.pat; - } - } - - var gfx = document.getElementById("gfxpattern"); - ctx = gfx.getContext('2d'); - ctx.fillStyle = '#000'; - ctx.fillRect(0, 0, gfx.width, gfx.height); - ctx.fillStyle = '#2a5684'; - ctx.fillRect(0, gfx.height/2 - 4, gfx.width, 8); - ctx.globalCompositeOperation = 'lighten'; - ctx.drawImage(pat_canvas, 0, gfx.height / 2 - 4 - 8*(e.row)); - ctx.globalCompositeOperation = 'source-over'; - shown_row = e.row; - } - - if (player.playing) { - window.requestAnimationFrame(redrawScreen); - } -} - -function init() { - var title = document.getElementById("title"); - // make title element fit text exactly, then render it - title.width = getTextSize(player.xm.songname, _bigfontwidths); - var ctx = title.getContext('2d'); - 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 = ((player.xm.instruments.length + 7) / 8) | 0; - for (var i = 0; i < instrcols; i++) { - var canvas = document.createElement('canvas'); - 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, 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 = player.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 = player.xm.instruments[j].name; - ctx.globalCompositeOperation = 'lighten'; - drawText(n, 1, y, ctx); - if (name !== '') { - drawText(player.xm.instruments[j].name, 20, y, ctx); - hasname++; - } - ctx.globalCompositeOperation = 'source-over'; - } - if (hasname || hasdata) { - instrlist.appendChild(canvas); - } - } - - document.getElementById('vu').width = _pattern_border + - _pattern_cellwidth * player.xm.nchan; - var gfxpattern = document.getElementById("gfxpattern"); - gfxpattern.width = _pattern_cellwidth * player.xm.nchan + _pattern_border; - - // generate a fake audio event to render the initial paused screen - var scopes = []; - for (i = 0; i < player.xm.nchan; i++) { - scopes.push(new Float32Array(_scope_width)); - } - - // reset display - shown_row = undefined; - pat_canvas_patnum = undefined; - - audio_events = []; - paused_events = []; - audio_events.push({ - t: 0, row: 0, pat: player.xm.songpats[0], - vu: new Float32Array(player.xm.nchan), - scopes: scopes - }); - redrawScreen(); -} - -function pushEvent(e) { - audio_events.push(e); - if (audio_events.length == 1) { - requestAnimationFrame(redrawScreen); - } -} - -function pause() { - // grab all the audio events - var t = player.audioctx.currentTime; - while (audio_events.length > 0) { - var e = audio_events.shift(); - e.t -= t; - paused_events.push(e); - } -} - -function resume() { - var t = player.audioctx.currentTime; - while (paused_events.length > 0) { - var e = paused_events.shift(); - e.t += t; - audio_events.push(e); - } - requestAnimationFrame(redrawScreen); -} - -function stop() { - audio_events = []; - paused_events = []; -} - -})(window, document); diff --git a/xm.js b/xm.js index 5f815b5..cc943e0 100644 --- a/xm.js +++ b/xm.js @@ -1,44 +1,85 @@ -(function (window) { -if (!window.XMPlayer) { - window.XMPlayer = {}; -} -var player = window.XMPlayer; +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 +}; -if (!window.XMView) { - window.XMView = {}; -} -var XMView = window.XMView; - -player.periodForNote = periodForNote; -player.prettify_effect = prettify_effect; -player.init = init; -player.load = load; -player.play = play; -player.pause = pause; -player.stop = stop; -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 -player.xm.global_volume = player.max_global_volume = 128; - -// exposed for testing -player.nextTick = nextTick; -player.nextRow = nextRow; -player.Envelope = Envelope; - -// for pretty-printing notes -var _note_names = [ - "C-", "C#", "D-", "D#", "E-", "F-", - "F#", "G-", "G#", "A-", "A#", "B-"]; +fontimg.src = "ft2font.png"; +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 -// per-sample exponential moving average for volume changes (to prevent pops -// and clicks); evaluated every 8 samples -var popfilter_alpha = 0.9837; +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) { + // a pattern consists of NxM cells which look like + // N-O II VV EFF + var cellwidth = _pattern_cellwidth; + canv.width = pattern[0].length * cellwidth + _pattern_border; + canv.height = pattern.length * 8; + var ctx = canv.getContext('2d'); + ctx.fillcolor='#000'; + ctx.fillRect(0, 0, canv.width, canv.height); + for (var j = 0; j < pattern.length; j++) { + var row = pattern[j]; + var dy = j * 8; + // render row number + ctx.drawImage(fontimg, 8*(j>>4), 0, 8, 8, 2, dy, 8, 8); + ctx.drawImage(fontimg, 8*(j&15), 0, 8, 8, 10, dy, 8, 8); + + for (var i = 0; i < row.length; i++) { + var dx = i*cellwidth + 2 + _pattern_border; + var data = row[i]; + + // render note + var note = data[0]; + if (note < 0) { + ctx.drawImage(fontimg, 0, 8*5, 16, 8, dx, dy, 16, 8); + } else { + var octave = (note/12)|0; + var note_fontrow = _fontmap_notes[(octave/3)|0]; + note = (note % (12*3))|0; + ctx.drawImage(fontimg, 16+16*note, note_fontrow, 16, 8, dx, dy, 16, 8); + } + dx += 20; + + // render instrument + var inst = data[1]; + if (inst != -1) { // no instrument = render nothing + 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; + + // render volume + var vol = data[2]; + if (vol < 0x10) { + ctx.drawImage(fontimg, 312, 0, 8, 8, dx, dy, 8, 8); + } else { + 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; + + // render effect + var eff = data[3]; + var effdata = data[4]; + ctx.drawImage(fontimg, 8*eff + 16*8, 4*8, 8, 8, dx, dy, 8, 8); + dx += 8; + ctx.drawImage(fontimg, 8*(effdata>>4), 4*8, 4, 8, dx, dy, 4, 8); + ctx.drawImage(fontimg, 8*(effdata&15), 4*8, 4, 8, dx+4, dy, 4, 8); + } + } +} function prettify_note(note) { if (note < 0) return "---"; @@ -61,103 +102,159 @@ function prettify_effect(t, p) { if (t >= 10) t = String.fromCharCode(55 + t); if (p < 16) p = '0' + p.toString(16); else p = p.toString(16); - return t + p; + return t + p } function prettify_notedata(data) { - return (prettify_note(data[0]) + " " + prettify_number(data[1]) + " " + - prettify_volume(data[2]) + " " + - prettify_effect(data[3], data[4])); + return (prettify_note(data[0]) + " " + prettify_number(data[1]) + " " + + prettify_volume(data[2]) + " " + + prettify_effect(data[3], data[4])); } function getstring(dv, offset, len) { var str = []; for (var i = offset; i < offset+len; i++) { var c = dv.getUint8(i); - if (c === 0) break; + if (c == 0) break; str.push(String.fromCharCode(c)); } 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) { +function FilterCoeffs(f_c) { 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); - var gain = (1 - 2*c + e*e) / 2; - return [gain, 2*c, -e*e]; + var s = e * Math.sin(wct); + var gain = (1 - 2*c + c*c + s*s); + return [gain, 2*c, -c*c - s*s]; } -function updateChannelPeriod(ch, period) { +popfilter = FilterCoeffs(200.0 / 44100.0); +popfilter_alpha = 0.9837; + +function UpdateChannelPeriod(ch, period) { var freq = 8363 * Math.pow(2, (1152.0 - period) / 192.0); if (isNaN(freq)) { console.log("invalid period!", period); return; } ch.doff = freq / f_smp; - ch.filter = filterCoeffs(ch.doff / 2); + ch.filter = FilterCoeffs(ch.doff / 2); } -function periodForNote(ch, note) { - return 1920 - (note + ch.samp.note)*16 - ch.fine / 8.0; +function PeriodForNote(ch, note) { + return 1920 - (note + ch.samp.note)*16 - ch.samp.fine / 8.0; } -function setCurrentPattern() { - var nextPat = player.xm.songpats[player.cur_songpos]; +var audio_events = []; +var shown_row = undefined; +function RedrawScreen() { + var e; + var t = audioctx.currentTime; + do { + e = audio_events.shift(); + } while(e.t < t && audio_events.length > 0); + if (e == undefined) return; + var VU = e.vu; + var scopes = e.scopes; - // check for out of range pattern index - while (nextPat >= player.xm.patterns.length) { - if (player.cur_songpos + 1 < player.xm.songpats.length) { - // first try skipping the position - player.cur_songpos++; - } else if ((player.cur_songpos === player.xm.song_looppos && player.cur_songpos !== 0) - || player.xm.song_looppos >= player.xm.songpats.length) { - // if we allready tried song_looppos or if song_looppos - // is out of range, go to the first position - player.cur_songpos = 0; - } else { - // try going to song_looppos - player.cur_songpos = player.xm.song_looppos; + 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(); } - - nextPat = player.xm.songpats[player.cur_songpos]; } - player.cur_pat = nextPat; + var debug = document.getElementById("debug"); + debug.innerHTML = 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]; + if (p != undefined) { + RenderPattern(pat_canvas, patterns[e.pat]); + pat_canvas_patnum = e.pat; + } + } + var ctx = gfx.getContext('2d'); + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, gfx.width, gfx.height); + ctx.fillStyle = '#2a5684'; + ctx.fillRect(0, gfx.height/2 - 4, gfx.width, 8); + ctx.globalCompositeOperation = 'lighten'; + ctx.drawImage(pat_canvas, 0, gfx.height / 2 - 4 - 8*(e.row)); + ctx.globalCompositeOperation = 'source-over'; + shown_row = e.row; + } + + if (audio_events.length > 0) { + var next_event = audio_events[0].t; + setTimeout(RedrawScreen, 1000*(next_event - audioctx.currentTime)); + } } -function nextRow() { - player.cur_row++; - if (player.cur_pat == -1 || player.cur_row >= player.xm.patterns[player.cur_pat].length) { - player.cur_row = 0; - player.cur_songpos++; - if (player.cur_songpos >= player.xm.songpats.length) - player.cur_songpos = player.xm.song_looppos; - setCurrentPattern(); +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) { + cur_row = 0; + cur_songpos++; + if (cur_songpos >= songpats.length) + cur_songpos = song_looppos; + cur_pat = songpats[cur_songpos]; } - var p = player.xm.patterns[player.cur_pat]; - var r = p[player.cur_row]; + var p = patterns[cur_pat]; + var r = p[cur_row]; + cur_row++; for (var i = 0; i < r.length; i++) { - var ch = player.xm.channelinfo[i]; + var ch = channelinfo[i]; var inst = ch.inst; var triggernote = false; // instrument trigger if (r[i][1] != -1) { - inst = player.xm.instruments[r[i][1] - 1]; - if (inst && inst.samplemap) { + inst = instruments[r[i][1] - 1]; + if (inst != undefined) { ch.inst = inst; // retrigger unless overridden below triggernote = true; - if (ch.note && inst.samplemap) { + if (ch.note) { ch.samp = inst.samples[inst.samplemap[ch.note]]; ch.vol = ch.samp.vol; ch.pan = ch.samp.pan; - ch.fine = ch.samp.fine; } } else { // console.log("invalid inst", r[i][1], instruments.length); @@ -170,7 +267,7 @@ function nextRow() { ch.release = 1; triggernote = false; } else { - if (inst && inst.samplemap) { + if (inst != undefined) { var note = r[i][0]; ch.note = note; ch.samp = inst.samples[inst.samplemap[ch.note]]; @@ -179,7 +276,6 @@ function nextRow() { // (potentially) new sample ch.pan = ch.samp.pan; ch.vol = ch.samp.vol; - ch.fine = ch.samp.fine; } triggernote = true; } @@ -197,28 +293,17 @@ function nextRow() { } else if (v >= 0x60 && v < 0x70) { // volume slide down 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) { ch.vol = Math.min(64, ch.vol + ch.voleffectdata); - }; + } } else if (v >= 0x80 && v < 0x90) { // fine volume slide down ch.vol = Math.max(0, ch.vol - (v & 0x0f)); } else if (v >= 0x90 && v < 0xa0) { // fine volume slide up ch.vol = Math.min(64, ch.vol + (v & 0x0f)); - } else if (v >= 0xa0 && v < 0xb0) { // vibrato speed - ch.vibratospeed = v & 0x0f; - } else if (v >= 0xb0 && v < 0xc0) { // vibrato w/ depth - ch.vibratodepth = v & 0x0f; - ch.voleffectfn = player.effects_t1[4]; // use vibrato effect directly - player.effects_t1[4](ch); // and also call it on tick 0 } else if (v >= 0xc0 && v < 0xd0) { // set panning ch.pan = (v & 0x0f) * 0x11; - } else if (v >= 0xf0 && v <= 0xff) { // portamento - if (v & 0x0f) { - ch.portaspeed = (v & 0x0f) << 4; - } - ch.voleffectfn = player.effects_t1[3]; // just run 3x0 } else { console.log("channel", i, "volume effect", v.toString(16)); } @@ -227,8 +312,8 @@ function nextRow() { ch.effect = r[i][3]; ch.effectdata = r[i][4]; if (ch.effect < 36) { - ch.effectfn = player.effects_t1[ch.effect]; - var eff_t0 = player.effects_t0[ch.effect]; + ch.effectfn = effects_t1[ch.effect]; + var eff_t0 = effects_t0[ch.effect]; if (eff_t0 && eff_t0(ch, ch.effectdata)) { triggernote = false; } @@ -237,12 +322,12 @@ function nextRow() { } // special handling for portamentos: don't trigger the note - if (ch.effect == 3 || ch.effect == 5 || r[i][2] >= 0xf0) { + if (ch.effect == 3 || ch.effect == 5) { if (r[i][0] != -1) { - ch.periodtarget = periodForNote(ch, ch.note); + ch.periodtarget = PeriodForNote(ch, ch.note); } triggernote = false; - if (inst && inst.samplemap) { + if (inst != undefined) { if (ch.env_vol == undefined) { // note wasn't already playing; we basically have to ignore the // portamento and just trigger @@ -259,18 +344,14 @@ function nextRow() { } if (triggernote) { - // there's gotta be a less hacky way to handle offset commands... - if (ch.effect != 9) ch.off = 0; + ch.off = 0; ch.release = 0; ch.envtick = 0; + ch.vibratopos = 0; ch.env_vol = new EnvelopeFollower(inst.env_vol); ch.env_pan = new EnvelopeFollower(inst.env_pan); - if (ch.note) { - ch.period = periodForNote(ch, ch.note); - } - // waveforms 0-3 are retriggered on new notes while 4-7 are continuous - if (ch.vibratotype < 4) { - ch.vibratopos = 0; + if (ch.note != undefined) { + ch.period = PeriodForNote(ch, ch.note); } } } @@ -293,14 +374,14 @@ Envelope.prototype.Get = function(ticks) { y0 = env[i+1]; if (ticks < env[i]) { var x0 = env[i-2]; - y0 = env[i-1]; + var y0 = env[i-1]; var dx = env[i] - x0; var dy = env[i+1] - y0; return y0 + (ticks - x0) * dy / dx; } } return y0; -}; +} function EnvelopeFollower(env) { this.env = env; @@ -323,41 +404,35 @@ EnvelopeFollower.prototype.Tick = function(release) { } } return value; -}; +} -function nextTick() { - player.cur_tick++; - var j, ch; - for (j = 0; j < player.xm.nchan; j++) { - ch = player.xm.channelinfo[j]; - ch.periodoffset = 0; +function next_tick() { + cur_tick++; + if (cur_tick >= tempo) { + cur_tick = 0; + next_row(); } - if (player.cur_tick >= player.xm.tempo) { - player.cur_tick = 0; - nextRow(); - } - for (j = 0; j < player.xm.nchan; j++) { - ch = player.xm.channelinfo[j]; + for (var j = 0; j < nchan; j++) { + var ch = channelinfo[j]; var inst = ch.inst; - if (player.cur_tick !== 0) { + ch.periodoffset = 0; + if (cur_tick != 0) { if(ch.voleffectfn) ch.voleffectfn(ch); if(ch.effectfn) ch.effectfn(ch); } if (isNaN(ch.period)) { - console.log(prettify_notedata( - player.xm.patterns[player.cur_pat][player.cur_row][j]), + console.log(prettify_notedata(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( - player.xm.patterns[player.cur_pat][player.cur_row][j]), + if (inst == undefined) continue; + if (ch.env_vol == undefined) { + console.log(prettify_notedata(patterns[cur_pat][cur_row-1][j]), "set channel", j, "env_vol to undefined, but note is playing"); continue; } ch.volE = ch.env_vol.Tick(ch.release); ch.panE = ch.env_pan.Tick(ch.release); - updateChannelPeriod(ch, ch.period + ch.periodoffset); + UpdateChannelPeriod(ch, ch.period + ch.periodoffset); } } @@ -400,21 +475,20 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) { var samp = instsamp.sampledata; var sample_end = instsamp.len; - if ((instsamp.type & 3) == 1 && instsamp.looplen > 0) { + if ((instsamp.type & 3) == 1) { // todo: support pingpong loop = true; loopstart = instsamp.loop; 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 - var volL = player.xm.global_volume * volE * (128 - p) * ch.vol / (64 * 128 * 128); - var volR = player.xm.global_volume * volE * (128 + p) * ch.vol / (64 * 128 * 128); + var volL = volE * (128 - p) * ch.vol / 8192.0; + var volR = volE * (128 + p) * ch.vol / 8192.0; if (volL < 0) volL = 0; if (volR < 0) volR = 0; - if (volR === 0 && volL === 0) + if (volR == 0 && volL == 0) return; if (isNaN(volR) || isNaN(volL)) { console.log("NaN volume!?", ch.number, volL, volR, volE, panE, ch.vol); @@ -438,9 +512,8 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) { var i = start; var failsafe = 100; while (i < end) { - if (failsafe-- === 0) { - console.log("failsafe in mixing loop! channel", ch.number, k, sample_end, - loopstart, looplen, dk); + if (failsafe-- == 0) { + console.log("failsafe in mixing loop!", k, sample_end, loopstart, looplen, dk); break; } if (k >= sample_end) { // TODO: implement pingpong looping @@ -456,67 +529,67 @@ 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 - var s, y; for (; i + 7 < next_event; i+=8) { - s = samp[k|0]; - y = f0 * (s + fs0) + f1*fs1 + f2*fs2; + var s = samp[k|0]; + var y = f0 * (s + fs0) + f1*fs1 + f2*fs2; fs2 = fs1; fs1 = y; fs0 = s; k += dk; dataL[i] += vL * y; dataR[i] += vR * y; Vrms += (vL + vR) * y * y; - s = samp[k|0]; - y = f0 * (s + fs0) + f1*fs1 + f2*fs2; + var s = samp[k|0]; + var y = f0 * (s + fs0) + f1*fs1 + f2*fs2; fs2 = fs1; fs1 = y; fs0 = s; k += dk; dataL[i+1] += vL * y; dataR[i+1] += vR * y; Vrms += (vL + vR) * y * y; - s = samp[k|0]; - y = f0 * (s + fs0) + f1*fs1 + f2*fs2; + var s = samp[k|0]; + var y = f0 * (s + fs0) + f1*fs1 + f2*fs2; fs2 = fs1; fs1 = y; fs0 = s; k += dk; dataL[i+2] += vL * y; dataR[i+2] += vR * y; Vrms += (vL + vR) * y * y; - s = samp[k|0]; - y = f0 * (s + fs0) + f1*fs1 + f2*fs2; + var s = samp[k|0]; + var y = f0 * (s + fs0) + f1*fs1 + f2*fs2; fs2 = fs1; fs1 = y; fs0 = s; k += dk; dataL[i+3] += vL * y; dataR[i+3] += vR * y; Vrms += (vL + vR) * y * y; - s = samp[k|0]; - y = f0 * (s + fs0) + f1*fs1 + f2*fs2; + var s = samp[k|0]; + var y = f0 * (s + fs0) + f1*fs1 + f2*fs2; fs2 = fs1; fs1 = y; fs0 = s; k += dk; dataL[i+4] += vL * y; dataR[i+4] += vR * y; Vrms += (vL + vR) * y * y; - s = samp[k|0]; - y = f0 * (s + fs0) + f1*fs1 + f2*fs2; + var s = samp[k|0]; + var y = f0 * (s + fs0) + f1*fs1 + f2*fs2; fs2 = fs1; fs1 = y; fs0 = s; k += dk; dataL[i+5] += vL * y; dataR[i+5] += vR * y; Vrms += (vL + vR) * y * y; - s = samp[k|0]; - y = f0 * (s + fs0) + f1*fs1 + f2*fs2; + var s = samp[k|0]; + var y = f0 * (s + fs0) + f1*fs1 + f2*fs2; fs2 = fs1; fs1 = y; fs0 = s; k += dk; dataL[i+6] += vL * y; dataR[i+6] += vR * y; Vrms += (vL + vR) * y * y; - s = samp[k|0]; - y = f0 * (s + fs0) + f1*fs1 + f2*fs2; + var s = samp[k|0]; + var y = f0 * (s + fs0) + f1*fs1 + f2*fs2; fs2 = fs1; fs1 = y; fs0 = s; k += dk; dataL[i+7] += vL * y; @@ -526,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++) { - 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) - 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; @@ -552,115 +630,112 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) { } function audio_cb(e) { - f_smp = player.audioctx.sampleRate; - var time_sound_started; + f_smp = audioctx.sampleRate; + var time_sound_started = undefined; var buflen = e.outputBuffer.length; var dataL = e.outputBuffer.getChannelData(0); var dataR = e.outputBuffer.getChannelData(1); - var i, j, k; - for (i = 0; i < buflen; i++) { + for (var i = 0; i < buflen; i++) { dataL[i] = 0; dataR[i] = 0; } var offset = 0; - var ticklen = 0|(f_smp * 2.5 / player.xm.bpm); - var scopewidth = XMView.scope_width; + var ticklen = 0|(f_smp * 2.5 / bpm); while(buflen > 0) { - if (player.cur_pat == -1 || player.cur_ticksamp >= ticklen) { - nextTick(f_smp); - player.cur_ticksamp -= ticklen; + if (cur_pat == -1 || cur_ticksamp >= ticklen) { + next_tick(f_smp); + cur_ticksamp -= ticklen; } - var tickduration = Math.min(buflen, ticklen - player.cur_ticksamp); - var VU = new Float32Array(player.xm.nchan); + var tickduration = Math.min(buflen, ticklen - cur_ticksamp); + var VU = new Float32Array(nchan); var scopes = undefined; - for (j = 0; j < player.xm.nchan; j++) { + for (var j = 0; j < nchan; j++) { var scope; - if (tickduration >= 4*scopewidth) { - scope = new Float32Array(scopewidth); - for (k = 0; k < scopewidth; k++) { + if (tickduration >= 4*_scope_width) { + scope = new Float32Array(_scope_width); + for (var k = 0; k < _scope_width; k++) { scope[k] = -dataL[offset+k*4] - dataR[offset+k*4]; } } VU[j] = MixChannelIntoBuf( - player.xm.channelinfo[j], offset, offset + tickduration, dataL, dataR) / + channelinfo[j], offset, offset + tickduration, dataL, dataR) / tickduration; - if (tickduration >= 4*scopewidth) { - for (k = 0; k < scopewidth; k++) { + if (tickduration >= 4*_scope_width) { + for (var k = 0; k < _scope_width; k++) { scope[k] += dataL[offset+k*4] + dataR[offset+k*4]; } - if (scopes === undefined) scopes = []; + if (scopes == undefined) scopes = []; scopes.push(scope); } } - if (XMView.pushEvent) { - XMView.pushEvent({ - t: e.playbackTime + (0.0 + offset) / f_smp, - vu: VU, - scopes: scopes, - songpos: player.cur_songpos, - pat: player.cur_pat, - row: player.cur_row - }); - offset += tickduration; - player.cur_ticksamp += tickduration; - buflen -= tickduration; + 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 + }); + if (audio_events.length == 1) { + requestAnimationFrame(RedrawScreen); } + offset += tickduration; + cur_ticksamp += tickduration; + buflen -= tickduration; } } function ConvertSample(array, bits) { var len = array.length; var acc = 0; - var samp, b, k; - if (bits === 0) { // 8 bit sample - samp = new Float32Array(len); - for (k = 0; k < len; k++) { + var samp = new Float32Array(len+1); + if (bits == 0) { // 8 bit sample + for (var k = 0; k < len; k++) { acc += array[k]; - b = acc&255; + var b = acc&255; if (b & 128) b = b-256; samp[k] = b / 128.0; } - return samp; } else { len /= 2; - samp = new Float32Array(len); - for (k = 0; k < len; k++) { - b = array[k*2] + (array[k*2 + 1] << 8); + 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; - acc = Math.max(-1, Math.min(1, acc + b / 32768.0)); - samp[k] = acc; + 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 // 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 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]; } for (var j = 0; j < nloops; j++) { - var k; if ((j&1) && pingpong) { - for (k = samp.looplen - 1; k >= 0; k--) { + for (var k = samp.looplen - 1; k >= 0; k--) { data[i++] = samp.sampledata[samp.loop + k]; } } else { - for (k = 0; k < samp.looplen; k++) { + for (var k = 0; k < samp.looplen; k++) { data[i++] = samp.sampledata[samp.loop + k]; } } @@ -671,27 +746,23 @@ function UnrollSampleLoop(samp) { samp.type = 1; } -function load(arrayBuf) { +function playXM(arrayBuf) { var dv = new DataView(arrayBuf); - player.xm = {}; + window.dv = dv; - player.xm.songname = getstring(dv, 17, 20); + songname = getstring(dv, 17, 20); var hlen = dv.getUint32(0x3c, true) + 0x3c; var songlen = dv.getUint16(0x40, true); - player.xm.song_looppos = dv.getUint16(0x42, true); - player.xm.nchan = dv.getUint16(0x44, true); + song_looppos = dv.getUint16(0x42, true); + nchan = dv.getUint16(0x44, true); var npat = dv.getUint16(0x46, true); var ninst = dv.getUint16(0x48, true); - player.xm.flags = dv.getUint16(0x4a, true); - player.xm.tempo = dv.getUint16(0x4c, true); - player.xm.bpm = dv.getUint16(0x4e, true); - player.xm.channelinfo = []; - player.xm.global_volume = player.max_global_volume; - - var i, j, k; - - for (i = 0; i < player.xm.nchan; i++) { - player.xm.channelinfo.push({ + 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({ number: i, filterstate: new Float32Array(3), vol: 0, @@ -702,36 +773,34 @@ function load(arrayBuf) { mute: 0, volE: 0, panE: 0, retrig: 0, - vibratopos: 0, vibratodepth: 1, vibratospeed: 1, - vibratotype: 0, - }); + }) } console.log("header len " + hlen); - 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); + 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); - player.xm.songpats = []; - for (i = 0; i < songlen; i++) { - player.xm.songpats.push(dv.getUint8(0x50 + i)); + songpats = []; + for (var i = 0; i < songlen; i++) { + songpats.push(dv.getUint8(0x50 + i)); } - console.log("song patterns: ", player.xm.songpats); + console.log("song patterns: ", songpats); var idx = hlen; - player.xm.patterns = []; - for (i = 0; i < npat; i++) { + patterns = []; + for (var i = 0; i < npat; i++) { var pattern = []; var patheaderlen = dv.getUint32(idx, true); var patrows = dv.getUint16(idx + 5, true); var patsize = dv.getUint16(idx + 7, true); console.log("pattern %d: %d bytes, %d rows", i, patsize, patrows); idx += 9; - for (j = 0; patsize > 0 && j < patrows; j++) { + for (var j = 0; patsize > 0 && j < patrows; j++) { row = []; - for (k = 0; k < player.xm.nchan; k++) { + for (var k = 0; k < nchan; k++) { var byte0 = dv.getUint8(idx); idx++; var note = -1, inst = -1, vol = -1, efftype = 0, effparam = 0; if (byte0 & 0x80) { @@ -764,21 +833,19 @@ function load(arrayBuf) { } pattern.push(row); } - player.xm.patterns.push(pattern); + patterns.push(pattern); } - player.xm.instruments = []; + var namelist = []; // 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) { - var samplemap = new Uint8Array(arrayBuf, idx+33, 96); + // 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); @@ -792,22 +859,22 @@ function load(arrayBuf) { var env_pan_loop_end = dv.getUint8(idx+232); var vol_fadeout = dv.getUint16(idx+239, true); var env_vol = []; - for (j = 0; j < env_nvol*2; j++) { + for (var j = 0; j < env_nvol*2; j++) { env_vol.push(dv.getUint16(idx+129+j*2, true)); } var env_pan = []; - for (j = 0; j < env_npan*2; j++) { + for (var j = 0; j < env_npan*2; j++) { env_pan.push(dv.getUint16(idx+177+j*2, true)); } // FIXME: ignoring keymaps for now and assuming 1 sample / instrument // var keymap = getarray(dv, idx+0x21); var samphdrsiz = dv.getUint32(idx+0x1d, true); - console.log("hdrsiz %d; instrument %s: '%s' %d samples, samphdrsiz %d", - hdrsiz, (i+1).toString(16), instname, nsamp, samphdrsiz); + console.log("hdrsiz %d; instrument %d: '%s' %d samples, samphdrsiz %d", + hdrsiz, i, instname, nsamp, samphdrsiz); idx += hdrsiz; var totalsamples = 0; var samps = []; - for (j = 0; j < nsamp; j++) { + for (var j = 0; j < nsamp; j++) { var samplen = dv.getUint32(idx, true); var samploop = dv.getUint32(idx+4, true); var samplooplen = dv.getUint32(idx+8, true); @@ -817,12 +884,9 @@ function load(arrayBuf) { var samppan = dv.getUint8(idx+15); var sampnote = dv.getInt8(idx+16); var sampname = getstring(dv, idx+18, 22); - var sampleoffset = totalsamples; - if (samplooplen === 0) { - samptype &= ~3; - } - console.log("sample %d: len %d name '%s' loop %d/%d vol %d offset %s", - j, samplen, sampname, samploop, samplooplen, sampvol, sampleoffset.toString(16)); + 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, @@ -834,38 +898,48 @@ function load(arrayBuf) { 'len': samplen, 'loop': samploop, 'looplen': samplooplen, 'note': sampnote, 'fine': sampfinetune, 'pan': samppan, 'type': samptype, 'vol': sampvol, - 'fileoffset': sampleoffset + 'fine': sampfinetune, + 'sampledata': ConvertSample( + new Uint8Array(arrayBuf, sampleoffset, samplen), + samptype & 16), }; // length / pointers are all specified in bytes; fixup for 16-bit samples - samps.push(samp); - idx += samphdrsiz; - totalsamples += samplen; - } - for (j = 0; j < nsamp; j++) { - var samp = samps[j]; - samp.sampledata = ConvertSample( - new Uint8Array(arrayBuf, idx + samp.fileoffset, samp.len), samp.type & 16); - if (samp.type & 16) { + 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); + 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); + + idx += samphdrsiz; + totalsamples += samplen; } idx += totalsamples; - inst.samplemap = samplemap; - inst.samples = samps; + inst = { + 'name': instname, + 'number': i, + 'fadeout': vol_fadeout, + 'samplemap': samplemap, + '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 (vol_fadeout > 0) { - var fadeout_ticks = 65536.0 / vol_fadeout; + if (inst.fadeout > 0) { + var fadeout_ticks = 65536.0 / inst.fadeout; env_vol.push(env_end_tick + fadeout_ticks); env_vol.push(0); } @@ -894,72 +968,49 @@ function load(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); } - player.xm.instruments.push(inst); + + var n = (i+1).toString(16); + if (i < 15) n = ' ' + n; + namelist.push(n + " " + instname); } - console.log("loaded \"" + player.xm.songname + "\""); - return true; -} - -var jsNode, gainNode; -function init() { - if (!player.audioctx) { - var audioContext = window.AudioContext || window.webkitAudioContext; - player.audioctx = new audioContext(); - gainNode = player.audioctx.createGain(); - gainNode.gain.value = 0.1; // master volume - } - if (player.audioctx.createScriptProcessor === undefined) { - jsNode = player.audioctx.createJavaScriptNode(16384, 0, 2); - } else { - jsNode = player.audioctx.createScriptProcessor(16384, 0, 2); - } + 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(player.audioctx.destination); + 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); } -player.playing = false; -function play() { - if (!player.playing) { - // put paused events back into action, if any - if (XMView.resume) XMView.resume(); - // start playing - jsNode.connect(gainNode); - - // hack to get iOS to play anything - var temp_osc = player.audioctx.createOscillator(); - temp_osc.connect(player.audioctx.destination); - if (temp_osc.noteOn) temp_osc.start = temp_osc.noteOn; - temp_osc.start(0); - temp_osc.stop(); - temp_osc.disconnect(); +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); } - player.playing = true; } - -function pause() { - if (player.playing) { - jsNode.disconnect(gainNode); - if (XMView.pause) XMView.pause(); - } - player.playing = false; -} - -function stop() { - if (player.playing) { - jsNode.disconnect(gainNode); - player.playing = false; - } - player.cur_pat = -1; - player.cur_row = 64; - player.cur_songpos = -1; - player.cur_ticksamp = 0; - player.xm.global_volume = player.max_global_volume; - if (XMView.stop) XMView.stop(); - init(); -} -})(window); +xmReq.send(null); diff --git a/xmeffects.js b/xmeffects.js index 27e906c..4c25716 100644 --- a/xmeffects.js +++ b/xmeffects.js @@ -1,19 +1,13 @@ -(function (window) { -if (!window.XMPlayer) { - window.XMPlayer = {}; -} -var player = window.XMPlayer; - function eff_t1_0(ch) { // arpeggio - if (ch.effectdata !== 0 && ch.inst !== undefined) { + if (ch.effectdata != 0 && ch.inst != undefined) { var arpeggio = [0, ch.effectdata>>4, ch.effectdata&15]; - var note = ch.note + arpeggio[player.cur_tick % 3]; - ch.period = player.periodForNote(ch, note); + 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) { + if (data != 0) { ch.slideupspeed = data; } } @@ -26,7 +20,7 @@ function eff_t1_1(ch) { // pitch slide up } function eff_t0_2(ch, data) { // pitch slide down - if (data !== 0) { + if (data != 0) { ch.slidedownspeed = data; } } @@ -39,7 +33,7 @@ function eff_t1_2(ch) { // pitch slide down } function eff_t0_3(ch, data) { // portamento - if (data !== 0) { + if (data != 0) { ch.portaspeed = data; } } @@ -56,44 +50,22 @@ function eff_t1_3(ch) { // portamento function eff_t0_4(ch, data) { // vibrato if (data & 0x0f) { - ch.vibratodepth = (data & 0x0f) * 2; + ch.vibratodepth = data & 0x0f; } if (data >> 4) { ch.vibratospeed = data >> 4; } - eff_t1_4(ch); + eff_t1_4(ch, data); } function eff_t1_4(ch) { // vibrato - ch.periodoffset = getVibratoDelta(ch.vibratotype, ch.vibratopos) * ch.vibratodepth; + ch.periodoffset = Math.sin(ch.vibratopos * Math.PI / 32) * ch.vibratodepth; if (isNaN(ch.periodoffset)) { - console.log("vibrato periodoffset NaN?", - ch.vibratopos, ch.vibratospeed, ch.vibratodepth); + console.log("vibrato periodoffset NaN?", ch.vibratopos, ch.vibratodepth); ch.periodoffset = 0; } - // only updates on non-first ticks - if (player.cur_tick > 0) { - ch.vibratopos += ch.vibratospeed; - ch.vibratopos &= 63; - } -} - -function getVibratoDelta(type, x) { - var delta = 0; - switch (type & 0x03) { - case 1: // sawtooth (ramp-down) - delta = ((1 + x * 2 / 64) % 2) - 1; - break; - case 2: // square - case 3: // random (in FT2 these two are the same) - delta = x < 32 ? 1 : -1; - break; - case 0: - default: // sine - delta = Math.sin(x * Math.PI / 32); - break; - } - return delta; + ch.vibratopos += ch.vibratospeed; + ch.vibratopos &= 63; } function eff_t1_5(ch) { // portamento + volume slide @@ -116,7 +88,11 @@ function eff_t0_9(ch, data) { // sample offset function eff_t0_a(ch, data) { // volume slide if (data) { - ch.volumeslide = -(data & 0x0f) + (data >> 4); + if (data & 0x0f) { + ch.volumeslide = -(data & 0x0f); + } else { + ch.volumeslide = data >> 4; + } } } @@ -127,10 +103,9 @@ function eff_t1_a(ch) { // volume slide } function eff_t0_b(ch, data) { // song jump (untested) - if (data < player.xm.songpats.length) { - player.cur_songpos = data; - player.cur_pat = player.xm.songpats[player.cur_songpos]; - player.cur_row = -1; + if (data < songpats.length) { + cur_songpos = data + cur_pat = songpats[cur_songpos]; } } @@ -139,11 +114,11 @@ function eff_t0_c(ch, data) { // set volume } function eff_t0_d(ch, data) { // pattern jump - 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]; - player.cur_row = (data >> 4) * 10 + (data & 0x0f) - 1; + 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! @@ -156,23 +131,17 @@ function eff_t0_e(ch, data) { // extended effects! case 2: // fine porta down ch.period += data; break; - case 4: // set vibrato waveform - ch.vibratotype = data & 0x07; - break; - case 5: // finetune - ch.fine = (data<<4) + data - 128; - break; case 8: // panning ch.pan = data * 0x11; break; case 0x0a: // fine vol slide up (with memory) - if (data === 0 && ch.finevolup !== undefined) + 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) + if (data == 0 && ch.finevoldown != undefined) data = ch.finevoldown; ch.vol = Math.max(0, ch.vol - data); ch.finevoldown = data; @@ -180,7 +149,7 @@ function eff_t0_e(ch, data) { // extended effects! case 0x0c: // note cut handled in eff_t1_e break; default: - console.log("unimplemented extended effect E", ch.effectdata.toString(16)); + console.log("unimplemented extended effect E", ch.effect.toString(16)); break; } } @@ -188,7 +157,7 @@ function eff_t0_e(ch, data) { // extended effects! function eff_t1_e(ch) { // note cut switch (ch.effectdata >> 4) { case 0x0c: - if (player.cur_tick == (ch.effectdata & 0x0f)) { + if (cur_tick == (ch.effectdata & 0x0f)) { ch.vol = 0; } break; @@ -196,37 +165,13 @@ function eff_t1_e(ch) { // note cut } function eff_t0_f(ch, data) { // set tempo - if (data === 0) { + if (data == 0) { console.log("tempo 0?"); return; - } else if (data < 0x20) { - player.xm.tempo = data; + } else if(data < 0x20) { + tempo = data; } else { - player.xm.bpm = data; - } -} - -function eff_t0_g(ch, data) { // set global volume - if (data <= 0x40) { - // volume gets multiplied by 2 to match - // the initial max global volume of 128 - player.xm.global_volume = Math.max(0, data * 2); - } else { - player.xm.global_volume = player.max_global_volume; - } -} - -function eff_t0_h(ch, data) { // global volume slide - if (data) { - // same as Axy but multiplied by 2 - player.xm.global_volumeslide = (-(data & 0x0f) + (data >> 4)) * 2; - } -} - -function eff_t1_h(ch) { // global volume slide - if (player.xm.global_volumeslide !== undefined) { - player.xm.global_volume = Math.max(0, Math.min(player.max_global_volume, - player.xm.global_volume + player.xm.global_volumeslide)); + bpm = data; } } @@ -255,17 +200,17 @@ function eff_t0_r(ch, data) { // retrigger } function eff_t1_r(ch) { - if (player.cur_tick % (ch.retrig & 0x0f) === 0) { + if (cur_tick % (ch.retrig & 0x0f) == 0) { ch.off = 0; } } function eff_unimplemented() {} function eff_unimplemented_t0(ch, data) { - console.log("unimplemented effect", player.prettify_effect(ch.effect, data)); + console.log("unimplemented effect", prettify_effect(ch.effect, data)); } -player.effects_t0 = [ // effect functions on tick 0 +var effects_t0 = [ // effect functions on tick 0 eff_t1_0, // 1, arpeggio is processed on all ticks eff_t0_1, eff_t0_2, @@ -282,8 +227,8 @@ player.effects_t0 = [ // effect functions on tick 0 eff_t0_d, // d eff_t0_e, // e eff_t0_f, // f - eff_t0_g, // g - eff_t0_h, // h + eff_unimplemented_t0, // g + eff_unimplemented_t0, // h eff_unimplemented_t0, // i eff_unimplemented_t0, // j eff_unimplemented_t0, // k @@ -304,7 +249,7 @@ player.effects_t0 = [ // effect functions on tick 0 eff_unimplemented_t0, // z ]; -player.effects_t1 = [ // effect functions on tick 1+ +var effects_t1 = [ // effect functions on tick 1+ eff_t1_0, eff_t1_1, eff_t1_2, @@ -321,8 +266,8 @@ player.effects_t1 = [ // effect functions on tick 1+ null, // d eff_t1_e, // e null, // f - null, // g - eff_t1_h, // h + eff_unimplemented, // g + eff_unimplemented, // h eff_unimplemented, // i eff_unimplemented, // j eff_unimplemented, // k @@ -342,5 +287,3 @@ player.effects_t1 = [ // effect functions on tick 1+ eff_unimplemented, // y eff_unimplemented // z ]; - -})(window);