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