Compare commits

..
Author SHA1 Message Date
Andy Sloane
ded244dc1f new 4-pole resampling filter
can't tell whether i like it better or not, yet. it sounds brighter,
since it cuts off less of the low end of the passband, and it also
rejects the stopband a lot better.
2015-10-31 08:56:22 -07:00
14 changed files with 668 additions and 2110 deletions

1
.gitignore vendored
View file

@ -1 +0,0 @@
node_modules

View file

@ -1,3 +0,0 @@
language: node_js
node_js:
- "0.12"

View file

@ -1,40 +1,16 @@
# xm.js # 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. 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 By default it plays "my dirty old kamel" by Alexander Bulér aka
player API looks like this: [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 It is fairly feature-complete, but is missing a bunch of effects.
`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.
MIT license. MIT license.

View file

@ -1,82 +1,29 @@
<head> <head>
<title>js xm</title> <title>js xm</title>
<script src="http://a1k0n-pub.s3-website-us-west-1.amazonaws.com/xm/xmlist.js"></script>
<script src="xmeffects.js"></script>
<script src="trackview.js"></script>
<script src="xm.js"></script> <script src="xm.js"></script>
<script src="shell.js"></script>
<style>
.centered {
display: block;
margin-left: auto;
margin-right: auto;
}
#filelist a { color: #fff; }
.playercontainer {
background:#000;
}
.draghover {
background:#000;
opacity: 0.5;
}
.hscroll {
overflow: auto;
margin-bottom: 14px;
}
.hscroll::-webkit-scrollbar {
-webkit-appearance: none;
}
.hscroll::-webkit-scrollbar:horizontal {
height: 11px;
}
.hscroll::-webkit-scrollbar-thumb {
border-radius: 2px;
border: 1px solid #93C3E9; /* should match background, can't be transparent */
background-color: #A0D4FD;
}
.hscroll::-webkit-scrollbar-track {
background-color: #333;
border-radius: 8px;
}
</style>
</head> </head>
<body> <body>
<div id="playercontainer" class="playercontainer" ondrop="XMPlayer.handleDrop(event)" <div><canvas id='vu' width=224 height=64></canvas></div>
ondragover="XMPlayer.allowDrop(event)" ondragleave="XMPlayer.allowDrop(event)"> <div><canvas id='gfxpattern' width=640 height=200></canvas></div>
<div><canvas class="centered" id='title' width="640" height="22"></canvas></div> <pre id='debug'>
<div class="hscroll"> </pre>
<div><canvas class="centered" id='vu' width="224" height="64"></canvas></div> <pre id='pattern'>
<div><canvas class="centered" id='gfxpattern' width="640" height="504"></canvas></div> </pre>
</div>
<div id='instruments'></div>
<div>
<p style="text-align: center">
<button id="playpause" disabled="true" style="width: 100px; background: #ccc;">Play</button>
<button id="loadbutton" style="width: 100px; background: #ccc">Load</button>
</p>
</div>
<div style="display: none" id='filelist'></div>
</div>
<pre> <pre>
code: <a href="http://github.com/a1k0n/jsxm/">github.com/a1k0n/jsxm</a> code: <a href="http://github.com/a1k0n/jsxm/">github.com/a1k0n/jsxm</a>
todo: todo:
- missing XM effects: - file picker, upload more xms to a host we can access cross-origin
- E3x, E6x, E7x, E9x, EDx, EEx - 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 - 7xy - tremolo
- Kxx, Lxx, Pxy, Txy - Gxx, Hxy, Kxx, Lxx, Pxy, Txy
- render pattern with the wider fonts for fewer channels
- fix occasional rendering/audio hiccups when switching songs
</pre> </pre>
</body> </body>

BIN
kamel.xm Normal file

Binary file not shown.

View file

@ -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 <andy@a1k0n.net> (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" }
}

111
shell.js
View file

@ -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);

View file

@ -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);

506
test/effects.js vendored
View file

@ -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");
};

View file

@ -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');
};

View file

@ -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;
};

View file

@ -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);

1074
xm.js

File diff suppressed because it is too large Load diff

View file

@ -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);