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..3784657 100644 --- a/index.html +++ b/index.html @@ -1,82 +1,29 @@ js xm - - - - - -
-
-
-
-
-
-
-
-

- - -

-
- -
- +
+
+
+
+
+
 code: github.com/a1k0n/jsxm
 todo:
- - missing XM effects:
-   - E3x, E6x, E7x, E9x, EDx, EEx
+ - file picker, upload more xms to a host we can access cross-origin
+ - make display even more faithfully FT2-like
+ - render instrument list
+ - oscilloscopes
+ - fix bugs!
+   - envelope loops - are they right or not now?
+ - multi-sample instruments
+ - missing XM effects (in rough order of priority):
+   - Rxy - retrigger
+   - E3x, E4x, E5x, E6x, E7x, E9x, EDx, EEx
    - 7xy - tremolo
-   - 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..322322f 100644 --- a/xm.js +++ b/xm.js @@ -1,44 +1,83 @@ -(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 _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 + 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 { + vol -= 0x10; + ctx.drawImage(fontimg, 8*(vol>>4), 4*8, 4, 8, dx, dy, 4, 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,104 +100,154 @@ 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(''); } -// Return 2-pole Butterworth lowpass filter coefficients for -// center frequncy f_c (relative to sampling frequency) -function filterCoeffs(f_c) { +var channelinfo = []; +var instruments = []; +var tempo = 4; + +// compute coefficients for a pole pair biquad section (with a single implied +// zero at z=-1); pole_r and _i are the real and imaginary components of the +// pole in the s (Laplace) plane, which are then scaled by w_c and projected +// into the z domain. +function SetBiquadCoeffs(s_pole_r, s_pole_i, f_c, poleno, filter) { + var w = 2 * Math.PI * f_c; + var e = Math.exp(s_pole_r * w); + var c = e * Math.cos(s_pole_i * w); + var s = e * Math.sin(s_pole_i * w); + var gain = (1 - 2*c + c*c + s*s) / 2; + filter[3*poleno + 0] = gain; + filter[3*poleno + 1] = 2*c; + filter[3*poleno + 2] = -c*c - s*s; +} + +// Return 4-pole lowpass filter coefficients for center frequncy f_c (relative +// to sampling frequency) +function SetFilterCoeffs(f_c, filter) { 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]; + + // This is the pole pair in the s plane we're going to use. + // It's a fairly resonant filter but it is designed to work together with the + // comb filter implied by using nearest-neighbor downsampling first, so the + // combination of the two has a nearly flat passband and a decently sharp + // cutoff for a 2-pole filter. + SetBiquadCoeffs(-0.47143406, 0.46783542, f_c, 0, filter); + SetBiquadCoeffs(-0.10882119, 0.91627743, f_c, 1, filter); } -function updateChannelPeriod(ch, period) { +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); + SetFilterCoeffs(ch.doff / 2, ch.filter); } -function periodForNote(ch, note) { - return 1920 - (note + ch.samp.note)*16 - ch.fine / 8.0; +function PeriodForNote(ch, note) { + return 1920 - note*16 - ch.inst.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; - // 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; + // update VU meters + var canvas = document.getElementById("vu"); + var ctx = canvas.getContext("2d"); + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, 16 * nchan, 64); + ctx.fillStyle = '#0f0'; + for (var j = 0; j < nchan; j++) { + var y = -Math.log(VU[j])*10; + ctx.fillRect(j*16, y, 15, 64-y); + } + + 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; + } } - - nextPat = player.xm.songpats[player.cur_songpos]; + 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; } - player.cur_pat = nextPat; + 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; + ch.update = false; 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) { - ch.samp = inst.samples[inst.samplemap[ch.note]]; - ch.vol = ch.samp.vol; - ch.pan = ch.samp.pan; - ch.fine = ch.samp.fine; - } + ch.pan = inst.pan; + ch.vol = inst.vol; } else { // console.log("invalid inst", r[i][1], instruments.length); } @@ -170,17 +259,11 @@ function nextRow() { ch.release = 1; triggernote = false; } else { - if (inst && inst.samplemap) { - var note = r[i][0]; + // assume linear frequency table (flags header & 1 == 1) + // is this true in kamel.xm? + if (inst != undefined) { + var note = r[i][0] + inst.note; ch.note = note; - ch.samp = inst.samples[inst.samplemap[ch.note]]; - if (triggernote) { - // if we were already triggering the note, reset vol/pan using - // (potentially) new sample - ch.pan = ch.samp.pan; - ch.vol = ch.samp.vol; - ch.fine = ch.samp.fine; - } triggernote = true; } } @@ -195,30 +278,19 @@ function nextRow() { } else if (v <= 0x50) { ch.vol = v - 0x10; } else if (v >= 0x60 && v < 0x70) { // volume slide down - ch.voleffectfn = function(ch) { + ch.voleffectfn = function() { ch.vol = Math.max(0, ch.vol - ch.voleffectdata); - }; + } } else if (v >= 0x70 && v < 0x80) { // volume slide up - ch.voleffectfn = function(ch) { + ch.voleffectfn = function() { 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)); } @@ -226,23 +298,22 @@ 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]; - if (eff_t0 && eff_t0(ch, ch.effectdata)) { + if (ch.effect < 16) { + ch.effectfn = effects_t1[ch.effect]; + if (effects_t0[ch.effect](ch, ch.effectdata)) { triggernote = false; } } else { - console.log("channel", i, "effect > 36", ch.effect); + console.log("channel", i, "effect > 16", ch.effect); } // 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 @@ -252,26 +323,20 @@ function nextRow() { // alone ch.envtick = 0; ch.release = 0; - ch.env_vol = new EnvelopeFollower(inst.env_vol); - ch.env_pan = new EnvelopeFollower(inst.env_pan); + ch.env_vol = new EnvelopeFollower(inst.env_vol, inst.fadeout); + ch.env_pan = new EnvelopeFollower(inst.env_pan, 0); } } } 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; - } + ch.period = PeriodForNote(ch, ch.note); } } } @@ -293,14 +358,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 +388,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); } } @@ -389,42 +448,44 @@ function MixSilenceIntoBuf(ch, start, end, dataL, dataR) { function MixChannelIntoBuf(ch, start, end, dataL, dataR) { var inst = ch.inst; - var instsamp = ch.samp; + var samp, sample_end; var loop = false; var looplen = 0, loopstart = 0; // nothing on this channel, just filter the last dc offset back down to zero - if (instsamp == undefined || inst == undefined || ch.mute) { + if (inst == undefined || ch.mute) { return MixSilenceIntoBuf(ch, start, end, dataL, dataR); } - var samp = instsamp.sampledata; - var sample_end = instsamp.len; - if ((instsamp.type & 3) == 1 && instsamp.looplen > 0) { + samp = inst.sampledata; + sample_end = inst.len; + if ((inst.type & 3) == 1) { // todo: support pingpong loop = true; - loopstart = instsamp.loop; - looplen = instsamp.looplen; + loopstart = inst.loop; + looplen = inst.looplen; sample_end = loopstart + looplen; } - var samplen = instsamp.len; + var samplen = inst.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); + console.log("NaN volume!?", volL, volR, volE, panE, ch.vol); return; } var k = ch.off; var dk = ch.doff; var Vrms = 0; - var f0 = ch.filter[0], f1 = ch.filter[1], f2 = ch.filter[2]; - var fs0 = ch.filterstate[0], fs1 = ch.filterstate[1], fs2 = ch.filterstate[2]; + var f0g = ch.filter[0], f01 = ch.filter[1], f02 = ch.filter[2]; + var f1g = ch.filter[3], f11 = ch.filter[4], f12 = ch.filter[5]; + var f0x = ch.filterstate[0], f0y1 = ch.filterstate[1], f0y2 = ch.filterstate[2]; + var f1x = ch.filterstate[3], f1y1 = ch.filterstate[4], f1y2 = ch.filterstate[5]; // we also low-pass filter volume changes with a simple one-zero, // one-pole filter to avoid pops and clicks when volume changes. @@ -438,9 +499,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 +516,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 + /* TODO after new filter // 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,114 +586,324 @@ 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 s = samp[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; - dataL[i] += vL * y; - dataR[i] += vR * y; - Vrms += (vL + vR) * y * y; + // implemented as an IIR filter (usually we'd use an FIR brick wall + // filter, but this is much simpler computationally and sounds fine) + // the filter is a cascade of two biquad sections here + var y0 = f0g * (s + f0x) + f01*f0y1 + f02*f0y2; + f0y2 = f0y1; f0y1 = y0; f0x = s; + var y1 = f1g * (y0 + f1x) + f11*f1y1 + f12*f1y2; + f1y2 = f1y1; f1y1 = y1; f1x = y0; + dataL[i] += vL * y1; + dataR[i] += vR * y1; + Vrms += (vL + vR) * y1 * y1; k += dk; + + vL = popfilter_alpha * vL + (1 - popfilter_alpha) * volL; + vR = popfilter_alpha * vR + (1 - popfilter_alpha) * volR; } } ch.off = k; - ch.filterstate[0] = fs0; - ch.filterstate[1] = fs1; - ch.filterstate[2] = fs2; + ch.filterstate[0] = f0x; + ch.filterstate[1] = f0y1; + ch.filterstate[2] = f0y2; + ch.filterstate[3] = f1x; + ch.filterstate[4] = f1y1; + ch.filterstate[5] = f1y2; ch.vL = vL; ch.vR = vR; return Vrms * 0.5; } 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 scopes = undefined; - for (j = 0; j < player.xm.nchan; j++) { - var scope; - if (tickduration >= 4*scopewidth) { - scope = new Float32Array(scopewidth); - for (k = 0; k < scopewidth; k++) { - scope[k] = -dataL[offset+k*4] - dataR[offset+k*4]; - } - } - + var tickduration = Math.min(buflen, ticklen - cur_ticksamp); + var VU = new Float32Array(nchan); + for (var j = 0; j < nchan; j++) { 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++) { - scope[k] += dataL[offset+k*4] + dataR[offset+k*4]; - } - 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, + 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 eff_t0_1(ch, data) { // pitch slide up + if (data != 0) { + ch.slideupspeed = data; + } +} + +function eff_t0_2(ch, data) { // pitch slide down + if (data != 0) { + ch.slidedownspeed = data; + } +} + +function eff_t0_3(ch, data) { // portamento + if (data != 0) { + ch.portaspeed = data; + } +} + +function eff_t0_4(ch, data) { // vibrato + if (data & 0x0f) { + ch.vibratodepth = data & 0x0f; + } + if (data >> 4) { + ch.vibratospeed = data >> 4; + } + eff_t1_4(ch, data); +} + +function eff_t0_8(ch, data) { // set panning + ch.pan = data; +} + +function eff_t0_9(ch, data) { // sample offset + ch.off = data * 256; +} + +function eff_t0_a(ch, data) { // volume slide + if (data) { + if (data & 0x0f) { + ch.volumeslide = -(data & 0x0f); + } else { + ch.volumeslide = data >> 4; } } } +function eff_t0_b(ch, data) { // song jump (untested) + if (data < songpats.length) { + cur_songpos = data + cur_pat = songpats[cur_songpos]; + } +} + +function eff_t0_c(ch, data) { // set volume + ch.vol = data & 0x3f; +} + +function eff_t0_d(ch, data) { // pattern jump + cur_songpos++; + if (cur_songpos >= songpats.length) + cur_songpos = song_looppos; + cur_pat = songpats[cur_songpos]; + cur_row = data; +} + +function eff_t0_e(ch, data) { // extended effects! + var eff = data >> 4; + data = data & 0x0f; + switch (eff) { + case 1: // fine porta up + ch.period -= data; + break; + case 2: // fine porta down + ch.period += data; + break; + case 8: // panning + ch.pan = data * 0x11; + break; + case 0x0a: // fine vol slide up (with memory) + if (data == 0 && ch.finevolup != undefined) + data = ch.finevolup; + ch.vol = Math.min(64, ch.vol + data); + ch.finevolup = data; + break; + case 0x0b: // fine vol slide down + if (data == 0 && ch.finevoldown != undefined) + data = ch.finevoldown; + ch.vol = Math.max(0, ch.vol - data); + ch.finevoldown = data; + break; + case 0x0c: // note cut handled in eff_t1_e + break; + default: + console.log("unimplemented extended effect E", ch.effect.toString(16)); + break; + } +} + +function eff_t0_f(ch, data) { // set tempo + if (data == 0) { + console.log("tempo 0?"); + return; + } else if(data < 0x20) { + tempo = data; + } else { + bpm = data; + } +} + +function eff_unimplemented_t0(ch, data) { + console.log("unimplemented effect", ch.effect.toString(16), data.toString(16)); +} + +var effects_t0 = [ // effect functions on tick 0 + eff_t1_0, // 1, arpeggio is processed on all ticks + eff_t0_1, + eff_t0_2, + eff_t0_3, + eff_t0_4, // 4 + eff_t0_a, // 5, same as A on first tick + eff_t0_a, // 6, same as A on first tick + eff_unimplemented_t0, // 7 + eff_t0_8, // 8 + eff_t0_9, // 9 + eff_t0_a, // a + eff_t0_b, // b + eff_t0_c, // c + eff_t0_d, // d + eff_t0_e, // e + eff_t0_f, // f +]; + +function eff_t1_0(ch) { // arpeggio + if (ch.effectdata != 0 && ch.inst != undefined) { + var arpeggio = [0, ch.effectdata>>4, ch.effectdata&15]; + var note = ch.note + arpeggio[cur_tick % 3]; + ch.period = PeriodForNote(ch, note); + } +} + +function eff_t1_1(ch) { // pitch slide up + if (ch.slideupspeed !== undefined) { + // is this limited? it appears not + ch.period -= ch.slideupspeed; + } +} + +function eff_t1_2(ch) { // pitch slide down + if (ch.slidedownspeed !== undefined) { + // 1728 is the period for C-1 + ch.period = Math.min(1728, ch.period + ch.slidedownspeed); + } +} + +function eff_t1_3(ch) { // portamento + if (ch.periodtarget !== undefined && ch.portaspeed !== undefined) { + if (ch.period > ch.periodtarget) { + ch.period = Math.max(ch.periodtarget, ch.period - ch.portaspeed); + } else { + ch.period = Math.min(ch.periodtarget, ch.period + ch.portaspeed); + } + } +} + +function eff_t1_4(ch) { // vibrato + ch.periodoffset = Math.sin(ch.vibratopos * Math.PI / 32) * ch.vibratodepth; + if (isNaN(ch.periodoffset)) { + console.log("vibrato periodoffset NaN?", ch.vibratopos, ch.vibratodepth); + ch.periodoffset = 0; + } + ch.vibratopos += ch.vibratospeed; + ch.vibratopos &= 63; +} + +function eff_t1_5(ch) { // portamento + volume slide + eff_t1_a(ch); + eff_t1_3(ch); +} + +function eff_t1_6(ch) { // vibrato + volume slide + eff_t1_a(ch); + eff_t1_4(ch); +} + +function eff_t1_a(ch) { // volume slide + if (ch.volumeslide !== undefined) { + ch.vol = Math.max(0, Math.min(64, ch.vol + ch.volumeslide)); + } +} + +function eff_t1_e(ch) { // note cut + switch (ch.effectdata >> 4) { + case 0x0c: + if (cur_tick == (ch.effectdata & 0x0f)) { + ch.vol = 0; + } + break; + } +} + +function eff_nop() {} +function eff_unimplemented() {} +var effects_t1 = [ // effect functions on tick 1+ + eff_t1_0, + eff_t1_1, + eff_t1_2, + eff_t1_3, + eff_t1_4, + eff_t1_5, // 5 + eff_t1_6, // 6 + eff_unimplemented, // 7 + eff_nop, // 8 + eff_nop, // 9 + eff_t1_a, // a + eff_nop, // b + eff_nop, // c + eff_nop, // d + eff_t1_e, // e + eff_nop, // f +]; + function ConvertSample(array, bits) { var len = array.length; var acc = 0; - var samp, b, k; - if (bits === 0) { // 8 bit sample - samp = new Float32Array(len); - for (k = 0; k < len; k++) { + if (bits == 0) { // 8 bit sample + var samp = new Float32Array(len); + 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); + 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; - acc = Math.max(-1, Math.min(1, acc + b / 32768.0)); - samp[k] = acc; + samp[k] = b / 32768.0; } return samp; } @@ -641,59 +911,54 @@ function ConvertSample(array, bits) { // optimization: unroll short sample loops so we can run our inner mixing loop // uninterrupted for as long as possible; this also handles pingpong loops. -function UnrollSampleLoop(samp) { - var nloops = ((2048 + samp.looplen - 1) / samp.looplen) | 0; - var pingpong = samp.type & 2; +function UnrollSampleLoop(inst) { + var nloops = ((2048 + inst.looplen - 1) / inst.looplen) | 0; + var pingpong = inst.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); - for (var i = 0; i < samp.loop; i++) { - data[i] = samp.sampledata[i]; + var samplesiz = inst.loop + nloops * inst.looplen; + var samp = new Float32Array(samplesiz); + for (var i = 0; i < inst.loop; i++) { + samp[i] = inst.sampledata[i]; } for (var j = 0; j < nloops; j++) { - var k; if ((j&1) && pingpong) { - for (k = samp.looplen - 1; k >= 0; k--) { - data[i++] = samp.sampledata[samp.loop + k]; + for (var k = inst.looplen - 1; k >= 0; k--) { + samp[i++] = inst.sampledata[inst.loop + k]; } } else { - for (k = 0; k < samp.looplen; k++) { - data[i++] = samp.sampledata[samp.loop + k]; + for (var k = 0; k < inst.looplen; k++) { + samp[i++] = inst.sampledata[inst.loop + k]; } } } - console.log("unrolled sample loop; looplen", samp.looplen, "x", nloops, " = ", samplesiz); - samp.sampledata = data; - samp.looplen = nloops * samp.looplen; - samp.type = 1; + console.log("unrolled sample loop", inst.number, "looplen", inst.looplen, "x", nloops, " = ", samplesiz); + inst.sampledata = samp; + inst.looplen = nloops * inst.looplen; + inst.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({ - number: i, - filterstate: new Float32Array(3), + var flags = dv.getUint16(0x4a, true); + tempo = dv.getUint16(0x4c, true); + bpm = dv.getUint16(0x4e, true); + document.getElementById('vu').width = 16 * nchan; + for (var i = 0; i < nchan; i++) { + channelinfo.push({ + filter: new Float32Array(6), + filterstate: new Float32Array(6), vol: 0, pan: 128, period: 1920 - 48*16, @@ -701,37 +966,34 @@ function load(arrayBuf) { vLprev: 0, vRprev: 0, 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,22 +1026,15 @@ function load(arrayBuf) { } pattern.push(row); } - player.xm.patterns.push(pattern); + patterns.push(pattern); } - player.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) { - var samplemap = new Uint8Array(arrayBuf, idx+33, 96); - var env_nvol = dv.getUint8(idx+225); var env_vol_type = dv.getUint8(idx+233); var env_vol_sustain = dv.getUint8(idx+227); @@ -792,83 +1047,77 @@ 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); - var sampvol = dv.getUint8(idx+12); - var sampfinetune = dv.getInt8(idx+13); - var samptype = dv.getUint8(idx+14); - var samppan = dv.getUint8(idx+15); - var sampnote = dv.getInt8(idx+16); - var sampname = getstring(dv, idx+18, 22); - var sampleoffset = totalsamples; - if (samplooplen === 0) { - samptype &= ~3; + if (j == 0) { + var samplen0 = samplen; // FIXME HACK HACK HACK + var samploop = dv.getUint32(idx+4, true); + var samplooplen = dv.getUint32(idx+8, true); + var sampvol = dv.getUint8(idx+12); + var sampfinetune = dv.getInt8(idx+13); + var samptype = dv.getUint8(idx+14); + var samppan = dv.getUint8(idx+15); + var sampnote = dv.getInt8(idx+16); + var sampname = getstring(dv, idx+18, 22); + var sampleoffset = idx + samphdrsiz; + console.log("sample %d: len %d name '%s' loop %d/%d vol %d", + j, samplen, sampname, samploop, samplooplen, sampvol); + console.log(" type %d note %s(%d) finetune %d pan %d", + samptype, prettify_note(sampnote + 12*4), sampnote, sampfinetune, samppan); + console.log(" vol env", env_vol, env_vol_sustain, + env_vol_loop_start, env_vol_loop_end, "type", env_vol_type, + "fadeout", vol_fadeout); + console.log(" pan env", env_pan, env_pan_sustain, + env_pan_loop_start, env_pan_loop_end, "type", env_pan_type); } - console.log("sample %d: len %d name '%s' loop %d/%d vol %d offset %s", - j, samplen, sampname, samploop, samplooplen, sampvol, sampleoffset.toString(16)); - console.log(" type %d note %s(%d) finetune %d pan %d", - samptype, prettify_note(sampnote + 12*4), sampnote, sampfinetune, samppan); - console.log(" vol env", env_vol, env_vol_sustain, - env_vol_loop_start, env_vol_loop_end, "type", env_vol_type, - "fadeout", vol_fadeout); - console.log(" pan env", env_pan, env_pan_sustain, - env_pan_loop_start, env_pan_loop_end, "type", env_pan_type); - var samp = { - 'len': samplen, 'loop': samploop, - 'looplen': samplooplen, 'note': sampnote, 'fine': sampfinetune, - 'pan': samppan, 'type': samptype, 'vol': sampvol, - 'fileoffset': sampleoffset - }; - // 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) { - 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); - } - } idx += totalsamples; - inst.samplemap = samplemap; - inst.samples = samps; + inst = { + 'name': instname, + 'number': i, + 'len': samplen0, 'loop': samploop, + 'looplen': samplooplen, 'note': sampnote, 'fine': sampfinetune, + 'pan': samppan, 'type': samptype, 'vol': sampvol, + 'fine': sampfinetune, + 'fadeout': vol_fadeout, + 'sampledata': ConvertSample(new Uint8Array(arrayBuf, sampleoffset, samplen0), samptype & 16), + }; + if (samptype & 16) { + inst.len /= 2; + inst.loop /= 2; + inst.looplen /= 2; + } + + // unroll short loops and any pingpong loops + if ((inst.type & 3) && (inst.looplen < 2048 || (inst.type & 2))) { + UnrollSampleLoop(inst); + } + if (env_vol_type) { // insert an automatic fadeout to 0 at the end of the envelope var env_end_tick = env_vol[env_vol.length-2]; + var fadeout_ticks = 65536.0 / inst.fadeout; if (!(env_vol_type & 2)) { // if there's no sustain point, create one env_vol_sustain = env_vol.length / 2; } - if (vol_fadeout > 0) { - var fadeout_ticks = 65536.0 / vol_fadeout; - env_vol.push(env_end_tick + fadeout_ticks); - env_vol.push(0); - } + env_vol.push(env_end_tick + fadeout_ticks); + env_vol.push(0); inst.env_vol = new Envelope( env_vol, env_vol_type, @@ -876,9 +1125,8 @@ function load(arrayBuf) { env_vol_loop_start, env_vol_loop_end); } else { - // no envelope, then just make a default full-volume envelope. - // i thought this would use fadeout, but apparently it doesn't. - inst.env_vol = new Envelope([0, 64, 1, 0], 2, 0, 0, 0); + // create a default envelope w/ fadeout + inst.env_vol = new Envelope([0, 64, fadeout_ticks, 0], 2, 0, 0, 0); } if (env_pan_type) { if (!(env_pan_type & 2)) { // if there's no sustain point, create one @@ -894,72 +1142,42 @@ 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); } - 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; + + // 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 deleted file mode 100644 index 27e906c..0000000 --- a/xmeffects.js +++ /dev/null @@ -1,346 +0,0 @@ -(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[player.cur_tick % 3]; - ch.period = player.periodForNote(ch, note); - } -} - -function eff_t0_1(ch, data) { // pitch slide up - if (data !== 0) { - ch.slideupspeed = data; - } -} - -function eff_t1_1(ch) { // pitch slide up - if (ch.slideupspeed !== undefined) { - // is this limited? it appears not - ch.period -= ch.slideupspeed; - } -} - -function eff_t0_2(ch, data) { // pitch slide down - if (data !== 0) { - ch.slidedownspeed = data; - } -} - -function eff_t1_2(ch) { // pitch slide down - if (ch.slidedownspeed !== undefined) { - // 1728 is the period for C-1 - ch.period = Math.min(1728, ch.period + ch.slidedownspeed); - } -} - -function eff_t0_3(ch, data) { // portamento - if (data !== 0) { - ch.portaspeed = data; - } -} - -function eff_t1_3(ch) { // portamento - if (ch.periodtarget !== undefined && ch.portaspeed !== undefined) { - if (ch.period > ch.periodtarget) { - ch.period = Math.max(ch.periodtarget, ch.period - ch.portaspeed); - } else { - ch.period = Math.min(ch.periodtarget, ch.period + ch.portaspeed); - } - } -} - -function eff_t0_4(ch, data) { // vibrato - if (data & 0x0f) { - ch.vibratodepth = (data & 0x0f) * 2; - } - if (data >> 4) { - ch.vibratospeed = data >> 4; - } - eff_t1_4(ch); -} - -function eff_t1_4(ch) { // vibrato - ch.periodoffset = getVibratoDelta(ch.vibratotype, ch.vibratopos) * ch.vibratodepth; - if (isNaN(ch.periodoffset)) { - console.log("vibrato periodoffset NaN?", - ch.vibratopos, ch.vibratospeed, 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; -} - -function eff_t1_5(ch) { // portamento + volume slide - eff_t1_a(ch); - eff_t1_3(ch); -} - -function eff_t1_6(ch) { // vibrato + volume slide - eff_t1_a(ch); - eff_t1_4(ch); -} - -function eff_t0_8(ch, data) { // set panning - ch.pan = data; -} - -function eff_t0_9(ch, data) { // sample offset - ch.off = data * 256; -} - -function eff_t0_a(ch, data) { // volume slide - if (data) { - ch.volumeslide = -(data & 0x0f) + (data >> 4); - } -} - -function eff_t1_a(ch) { // volume slide - if (ch.volumeslide !== undefined) { - ch.vol = Math.max(0, Math.min(64, ch.vol + ch.volumeslide)); - } -} - -function eff_t0_b(ch, data) { // song jump (untested) - if (data < player.xm.songpats.length) { - player.cur_songpos = data; - player.cur_pat = player.xm.songpats[player.cur_songpos]; - player.cur_row = -1; - } -} - -function eff_t0_c(ch, data) { // set volume - ch.vol = Math.min(64, data); -} - -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; -} - -function eff_t0_e(ch, data) { // extended effects! - var eff = data >> 4; - data = data & 0x0f; - switch (eff) { - case 1: // fine porta up - ch.period -= data; - break; - case 2: // fine porta down - ch.period += data; - break; - case 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) - data = ch.finevolup; - ch.vol = Math.min(64, ch.vol + data); - ch.finevolup = data; - break; - case 0x0b: // fine vol slide down - if (data === 0 && ch.finevoldown !== undefined) - data = ch.finevoldown; - ch.vol = Math.max(0, ch.vol - data); - ch.finevoldown = data; - break; - case 0x0c: // note cut handled in eff_t1_e - break; - default: - console.log("unimplemented extended effect E", ch.effectdata.toString(16)); - break; - } -} - -function eff_t1_e(ch) { // note cut - switch (ch.effectdata >> 4) { - case 0x0c: - if (player.cur_tick == (ch.effectdata & 0x0f)) { - ch.vol = 0; - } - break; - } -} - -function eff_t0_f(ch, data) { // set tempo - if (data === 0) { - console.log("tempo 0?"); - return; - } else if (data < 0x20) { - player.xm.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)); - } -} - -function eff_t0_r(ch, data) { // retrigger - if (data & 0x0f) ch.retrig = (ch.retrig & 0xf0) + (data & 0x0f); - if (data & 0xf0) ch.retrig = (ch.retrig & 0x0f) + (data & 0xf0); - - // retrigger volume table - switch (ch.retrig >> 4) { - case 1: ch.vol -= 1; break; - case 2: ch.vol -= 2; break; - case 3: ch.vol -= 4; break; - case 4: ch.vol -= 8; break; - case 5: ch.vol -= 16; break; - case 6: ch.vol *= 2; ch.vol /= 3; break; - case 7: ch.vol /= 2; break; - case 9: ch.vol += 1; break; - case 0x0a: ch.vol += 2; break; - case 0x0b: ch.vol += 4; break; - case 0x0c: ch.vol += 8; break; - case 0x0d: ch.vol += 16; break; - case 0x0e: ch.vol *= 3; ch.vol /= 2; break; - case 0x0f: ch.vol *= 2; break; - } - ch.vol = Math.min(64, Math.max(0, ch.vol)); -} - -function eff_t1_r(ch) { - if (player.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)); -} - -player.effects_t0 = [ // effect functions on tick 0 - eff_t1_0, // 1, arpeggio is processed on all ticks - eff_t0_1, - eff_t0_2, - eff_t0_3, - eff_t0_4, // 4 - eff_t0_a, // 5, same as A on first tick - eff_t0_a, // 6, same as A on first tick - eff_unimplemented_t0, // 7 - eff_t0_8, // 8 - eff_t0_9, // 9 - eff_t0_a, // a - eff_t0_b, // b - eff_t0_c, // c - eff_t0_d, // d - eff_t0_e, // e - eff_t0_f, // f - eff_t0_g, // g - eff_t0_h, // h - eff_unimplemented_t0, // i - eff_unimplemented_t0, // j - eff_unimplemented_t0, // k - eff_unimplemented_t0, // l - eff_unimplemented_t0, // m - eff_unimplemented_t0, // n - eff_unimplemented_t0, // o - eff_unimplemented_t0, // p - eff_unimplemented_t0, // q - eff_t0_r, // r - eff_unimplemented_t0, // s - eff_unimplemented_t0, // t - eff_unimplemented_t0, // u - eff_unimplemented_t0, // v - eff_unimplemented_t0, // w - eff_unimplemented_t0, // x - eff_unimplemented_t0, // y - eff_unimplemented_t0, // z -]; - -player.effects_t1 = [ // effect functions on tick 1+ - eff_t1_0, - eff_t1_1, - eff_t1_2, - eff_t1_3, - eff_t1_4, - eff_t1_5, // 5 - eff_t1_6, // 6 - eff_unimplemented, // 7 - null, // 8 - null, // 9 - eff_t1_a, // a - null, // b - null, // c - null, // d - eff_t1_e, // e - null, // f - null, // g - eff_t1_h, // h - eff_unimplemented, // i - eff_unimplemented, // j - eff_unimplemented, // k - eff_unimplemented, // l - eff_unimplemented, // m - eff_unimplemented, // n - eff_unimplemented, // o - eff_unimplemented, // p - eff_unimplemented, // q - eff_t1_r, // r - eff_unimplemented, // s - eff_unimplemented, // t - eff_unimplemented, // u - eff_unimplemented, // v - eff_unimplemented, // w - eff_unimplemented, // x - eff_unimplemented, // y - eff_unimplemented // z -]; - -})(window);