Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
219479ecc6 | ||
|
|
f6113fc6c4 | ||
|
|
ea423577f0 | ||
|
|
27d9435d83 | ||
|
|
7153fa7f3c | ||
|
|
e3d159e7ff | ||
|
|
1d8b91c8ea | ||
|
|
9b959532fb | ||
|
|
677c157c71 | ||
|
|
e39f601038 | ||
|
|
b81d675b66 | ||
|
|
413bc4b538 | ||
|
|
5af28d860e | ||
|
|
0e47b13d83 | ||
|
|
fb103e61bc | ||
|
|
65a2ea2863 | ||
|
|
8b3f318f60 | ||
|
|
2679587654 | ||
|
|
34e975ef55 | ||
|
|
89e6b9c554 | ||
|
|
bf138a245c | ||
|
|
21e3f073d9 | ||
|
|
e347807abf | ||
|
|
215f72c19d | ||
|
|
bb5555f5a1 | ||
|
|
be1c0df30d | ||
|
|
007f0db8f7 | ||
|
|
0f3888cfff | ||
|
|
0e82964315 | ||
|
|
c35bb07fac | ||
|
|
05216f5ce0 | ||
|
|
9a410aa535 | ||
|
|
0f86a8d2c5 | ||
|
|
660729010b | ||
|
|
267ecbeffb | ||
|
|
8fe05fb89d | ||
|
|
88584b9cd0 | ||
|
|
eff3fd1ed6 | ||
|
|
b04e6c00b0 | ||
|
|
fc6be2b246 | ||
|
|
1f2b0ba419 | ||
|
|
570aa33a20 | ||
|
|
be463856db | ||
|
|
6a5fe5e024 | ||
|
|
ce5fd027a3 | ||
|
|
9bf35716f3 | ||
|
|
7ca90f9b98 | ||
|
|
8c37c8d96b | ||
|
|
8e114a16da | ||
|
|
4b6e75332e | ||
|
|
1667ab502b | ||
|
|
664686f199 | ||
|
|
7aa67803ed | ||
|
|
e34970c05f | ||
|
|
ea36dceb55 | ||
|
|
a37555ed58 | ||
|
|
14d88e9a10 | ||
|
|
41e8713b74 | ||
|
|
2cf8840665 | ||
|
|
5aa5c06ab0 | ||
|
|
541a9d721c | ||
|
|
7d6481ad73 | ||
|
|
a547732063 | ||
|
|
f092f847f7 | ||
|
|
54809c2b81 | ||
|
|
b859f27109 | ||
|
|
f95bd1e169 | ||
|
|
3c0a865824 | ||
|
|
fd52b21e3f | ||
|
|
ea6edaf2e4 |
14 changed files with 1765 additions and 442 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules
|
||||
3
.travis.yml
Normal file
3
.travis.yml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- "0.12"
|
||||
40
README.md
40
README.md
|
|
@ -1,16 +1,40 @@
|
|||
# xm.js
|
||||
|
||||
[](https://travis-ci.org/a1k0n/jsxm)
|
||||
|
||||
FastTracker 2 .XM player, written for fun.
|
||||
|
||||
[Demo](http://www.a1k0n.net/code/jsxm/) [warning: plays sound immediately on
|
||||
load]
|
||||
[Demo](http://www.a1k0n.net/code/jsxm/)
|
||||
|
||||
By default it plays "my dirty old kamel" by Alexander Bulér aka
|
||||
[Zalza](http://zalza.bandcamp.com/). I love this tune, and wrote this code just
|
||||
to play it. It can also load other .XMs by loading index.html?_tune-uri.xm_,
|
||||
though it either needs to be local to your domain or cross-origin resource
|
||||
sharing must be enabled for that to work.
|
||||
There is an XM player and a visualizer which are separate components. The
|
||||
player API looks like this:
|
||||
|
||||
It is fairly feature-complete, but is missing a bunch of effects.
|
||||
- `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.
|
||||
|
||||
MIT license.
|
||||
|
|
|
|||
82
index.html
82
index.html
|
|
@ -1,26 +1,82 @@
|
|||
<head>
|
||||
<title>js xm</title>
|
||||
<script src="xm.js"></script>
|
||||
<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="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>
|
||||
<body>
|
||||
<div><canvas id='vu' width=224 height=64></canvas></div>
|
||||
<div><canvas id='gfxpattern' width=640 height=200></canvas></div>
|
||||
<pre id='debug'>
|
||||
</pre>
|
||||
<pre id='instruments'>
|
||||
</pre>
|
||||
<div id="playercontainer" class="playercontainer" ondrop="XMPlayer.handleDrop(event)"
|
||||
ondragover="XMPlayer.allowDrop(event)" ondragleave="XMPlayer.allowDrop(event)">
|
||||
<div><canvas class="centered" id='title' width="640" height="22"></canvas></div>
|
||||
<div class="hscroll">
|
||||
<div><canvas class="centered" id='vu' width="224" height="64"></canvas></div>
|
||||
<div><canvas class="centered" id='gfxpattern' width="640" height="504"></canvas></div>
|
||||
</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>
|
||||
code: <a href="http://github.com/a1k0n/jsxm/">github.com/a1k0n/jsxm</a>
|
||||
todo:
|
||||
- file picker, upload more xms to a host we can access cross-origin
|
||||
- make display even more faithfully FT2-like
|
||||
- render instrument list in ft2 font
|
||||
- oscilloscope trigger on zero crossing or something
|
||||
- missing XM effects:
|
||||
- E3x, E4x, E5x, E6x, E7x, E9x, EDx, EEx
|
||||
- E3x, E6x, E7x, E9x, EDx, EEx
|
||||
- 7xy - tremolo
|
||||
- Gxx, Hxy, Kxx, Lxx, Pxy, Txy
|
||||
- Kxx, Lxx, Pxy, Txy
|
||||
- render pattern with the wider fonts for fewer channels
|
||||
- fix occasional rendering/audio hiccups when switching songs
|
||||
</pre>
|
||||
</body>
|
||||
|
||||
|
|
|
|||
BIN
kamel.xm
BIN
kamel.xm
Binary file not shown.
23
package.json
Normal file
23
package.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"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
Normal file
111
shell.js
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
(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);
|
||||
130
test/all.js
Normal file
130
test/all.js
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
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
Normal file
506
test/effects.js
vendored
Normal file
|
|
@ -0,0 +1,506 @@
|
|||
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");
|
||||
};
|
||||
57
test/instrument.js
Normal file
57
test/instrument.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
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');
|
||||
};
|
||||
58
test/testdata.js
Normal file
58
test/testdata.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
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;
|
||||
};
|
||||
348
trackview.js
Normal file
348
trackview.js
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
(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);
|
||||
139
xmeffects.js
139
xmeffects.js
|
|
@ -1,13 +1,19 @@
|
|||
(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[cur_tick % 3];
|
||||
ch.period = PeriodForNote(ch, note);
|
||||
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) {
|
||||
if (data !== 0) {
|
||||
ch.slideupspeed = data;
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +26,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;
|
||||
}
|
||||
}
|
||||
|
|
@ -33,7 +39,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;
|
||||
}
|
||||
}
|
||||
|
|
@ -50,22 +56,44 @@ function eff_t1_3(ch) { // portamento
|
|||
|
||||
function eff_t0_4(ch, data) { // vibrato
|
||||
if (data & 0x0f) {
|
||||
ch.vibratodepth = data & 0x0f;
|
||||
ch.vibratodepth = (data & 0x0f) * 2;
|
||||
}
|
||||
if (data >> 4) {
|
||||
ch.vibratospeed = data >> 4;
|
||||
}
|
||||
eff_t1_4(ch, data);
|
||||
eff_t1_4(ch);
|
||||
}
|
||||
|
||||
function eff_t1_4(ch) { // vibrato
|
||||
ch.periodoffset = Math.sin(ch.vibratopos * Math.PI / 32) * ch.vibratodepth;
|
||||
ch.periodoffset = getVibratoDelta(ch.vibratotype, ch.vibratopos) * ch.vibratodepth;
|
||||
if (isNaN(ch.periodoffset)) {
|
||||
console.log("vibrato periodoffset NaN?", ch.vibratopos, ch.vibratodepth);
|
||||
console.log("vibrato periodoffset NaN?",
|
||||
ch.vibratopos, ch.vibratospeed, ch.vibratodepth);
|
||||
ch.periodoffset = 0;
|
||||
}
|
||||
ch.vibratopos += ch.vibratospeed;
|
||||
ch.vibratopos &= 63;
|
||||
// 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
|
||||
|
|
@ -88,11 +116,7 @@ function eff_t0_9(ch, data) { // sample offset
|
|||
|
||||
function eff_t0_a(ch, data) { // volume slide
|
||||
if (data) {
|
||||
if (data & 0x0f) {
|
||||
ch.volumeslide = -(data & 0x0f);
|
||||
} else {
|
||||
ch.volumeslide = data >> 4;
|
||||
}
|
||||
ch.volumeslide = -(data & 0x0f) + (data >> 4);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,9 +127,10 @@ function eff_t1_a(ch) { // volume slide
|
|||
}
|
||||
|
||||
function eff_t0_b(ch, data) { // song jump (untested)
|
||||
if (data < songpats.length) {
|
||||
cur_songpos = data
|
||||
cur_pat = songpats[cur_songpos];
|
||||
if (data < player.xm.songpats.length) {
|
||||
player.cur_songpos = data;
|
||||
player.cur_pat = player.xm.songpats[player.cur_songpos];
|
||||
player.cur_row = -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -114,11 +139,11 @@ function eff_t0_c(ch, data) { // set volume
|
|||
}
|
||||
|
||||
function eff_t0_d(ch, data) { // pattern jump
|
||||
cur_songpos++;
|
||||
if (cur_songpos >= songpats.length)
|
||||
cur_songpos = song_looppos;
|
||||
cur_pat = songpats[cur_songpos];
|
||||
cur_row = data;
|
||||
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!
|
||||
|
|
@ -131,17 +156,23 @@ 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;
|
||||
|
|
@ -149,7 +180,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.effect.toString(16));
|
||||
console.log("unimplemented extended effect E", ch.effectdata.toString(16));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -157,7 +188,7 @@ function eff_t0_e(ch, data) { // extended effects!
|
|||
function eff_t1_e(ch) { // note cut
|
||||
switch (ch.effectdata >> 4) {
|
||||
case 0x0c:
|
||||
if (cur_tick == (ch.effectdata & 0x0f)) {
|
||||
if (player.cur_tick == (ch.effectdata & 0x0f)) {
|
||||
ch.vol = 0;
|
||||
}
|
||||
break;
|
||||
|
|
@ -165,13 +196,37 @@ 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) {
|
||||
tempo = data;
|
||||
} else if (data < 0x20) {
|
||||
player.xm.tempo = data;
|
||||
} else {
|
||||
bpm = data;
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -200,17 +255,17 @@ function eff_t0_r(ch, data) { // retrigger
|
|||
}
|
||||
|
||||
function eff_t1_r(ch) {
|
||||
if (cur_tick % (ch.retrig & 0x0f) == 0) {
|
||||
if (player.cur_tick % (ch.retrig & 0x0f) === 0) {
|
||||
ch.off = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function eff_unimplemented() {}
|
||||
function eff_unimplemented_t0(ch, data) {
|
||||
console.log("unimplemented effect", prettify_effect(ch.effect, data));
|
||||
console.log("unimplemented effect", player.prettify_effect(ch.effect, data));
|
||||
}
|
||||
|
||||
var effects_t0 = [ // effect functions on tick 0
|
||||
player.effects_t0 = [ // effect functions on tick 0
|
||||
eff_t1_0, // 1, arpeggio is processed on all ticks
|
||||
eff_t0_1,
|
||||
eff_t0_2,
|
||||
|
|
@ -227,8 +282,8 @@ var effects_t0 = [ // effect functions on tick 0
|
|||
eff_t0_d, // d
|
||||
eff_t0_e, // e
|
||||
eff_t0_f, // f
|
||||
eff_unimplemented_t0, // g
|
||||
eff_unimplemented_t0, // h
|
||||
eff_t0_g, // g
|
||||
eff_t0_h, // h
|
||||
eff_unimplemented_t0, // i
|
||||
eff_unimplemented_t0, // j
|
||||
eff_unimplemented_t0, // k
|
||||
|
|
@ -249,7 +304,7 @@ var effects_t0 = [ // effect functions on tick 0
|
|||
eff_unimplemented_t0, // z
|
||||
];
|
||||
|
||||
var effects_t1 = [ // effect functions on tick 1+
|
||||
player.effects_t1 = [ // effect functions on tick 1+
|
||||
eff_t1_0,
|
||||
eff_t1_1,
|
||||
eff_t1_2,
|
||||
|
|
@ -266,8 +321,8 @@ var effects_t1 = [ // effect functions on tick 1+
|
|||
null, // d
|
||||
eff_t1_e, // e
|
||||
null, // f
|
||||
eff_unimplemented, // g
|
||||
eff_unimplemented, // h
|
||||
null, // g
|
||||
eff_t1_h, // h
|
||||
eff_unimplemented, // i
|
||||
eff_unimplemented, // j
|
||||
eff_unimplemented, // k
|
||||
|
|
@ -287,3 +342,5 @@ var effects_t1 = [ // effect functions on tick 1+
|
|||
eff_unimplemented, // y
|
||||
eff_unimplemented // z
|
||||
];
|
||||
|
||||
})(window);
|
||||
|
|
|
|||
Loading…
Reference in a new issue