Compare commits

...
Sign in to create a new pull request.
Author SHA1 Message Date
Andy Sloane
219479ecc6 addendum to previous commit; duh 2016-05-31 22:15:46 -07:00
Andy Sloane
f6113fc6c4 simple optimization to filter
just basic algebra, but I should have noticed this sooner!
2016-05-31 18:52:33 -07:00
Andy Sloane
ea423577f0 [#4] Re-enable iOS hack
Before it was running a 1Hz oscillator for 10ms which caused a huge DC
offset when it was done.. Turns out it works if you just immediately
stop and disconnect it, without causing DC offsets or other problems.
2016-04-22 12:12:50 -07:00
Andy Sloane
27d9435d83 [#24] Implement M0 volume effect (portamento)
Written so Mx is equivalent to 3x0; I think if both are present, it
might not do exactly what FT2 does.
2016-04-19 16:34:20 -07:00
Johan Hillerström
7153fa7f3c Merge pull request #21 from hillerstorm/implement_E4x
Implement E4x and address some vibrato issues
2015-11-21 20:25:35 +01:00
Johan Hillerström
e3d159e7ff Add contributors to package.json 2015-11-21 20:22:00 +01:00
Johan Hillerström
1d8b91c8ea Remove E4x from missing effects 2015-11-21 20:21:39 +01:00
Johan Hillerström
9b959532fb Add tests for E4x, E41 and E42 2015-11-21 20:19:30 +01:00
Johan Hillerström
677c157c71 Fix failing vibrato test 2015-11-21 19:54:54 +01:00
Johan Hillerström
e39f601038 Double vibrato depth to act like FT2 2015-11-21 15:50:58 +01:00
Johan Hillerström
b81d675b66 Test for resetting vibrato pos on note retrigger 2015-11-21 15:50:48 +01:00
Johan Hillerström
413bc4b538 Retrigger waveforms 0-3 2015-11-21 15:50:35 +01:00
Johan Hillerström
5af28d860e Update vibratopos according to the spec 2015-11-21 14:58:33 +01:00
Johan Hillerström
0e47b13d83 Implement all vibrato waveforms 2015-11-21 14:58:32 +01:00
Johan Hillerström
fb103e61bc Read vibrato waveform type 2015-11-21 14:58:32 +01:00
Andy Sloane
65a2ea2863 [#5] implement volume effect vibrato 2015-11-20 17:43:48 -08:00
Andy Sloane
8b3f318f60 [#19] handle loops of length 0
also more robust way to find sample offsets to load in the file
2015-11-20 17:42:41 -08:00
Andy Sloane
2679587654 update screen if pattern changes without row changing
this is solely for strobe - CELL II, pretty much...
2015-11-20 12:09:57 -08:00
Andy Sloane
34e975ef55 changed my mind: render 000 effect as ...
also make the track view much taller in the standalone player
2015-11-19 20:47:28 -08:00
Andy Sloane
89e6b9c554 fix Dxx effect
argument is BCD, so 0x10 actually means 10 decimal
2015-11-19 20:24:00 -08:00
Andy Sloane
bf138a245c re-sort Dxx test 2015-11-16 19:21:51 -08:00
Andy Sloane
21e3f073d9 add test for Dxx effect 2015-11-16 19:20:29 -08:00
Andy Sloane
e347807abf Change cur_row to actually mean current row
It used to really mean "next row", and resulted in showing the wrong row
in the pattern view (row -1 instead of the last row).

Fixes #14.
2015-11-16 18:59:46 -08:00
Andy Sloane
215f72c19d Fix samples for instruments with >1 sample
The sample offset was incorrectly computed, and was including the sample
headers for all samples after the first one into the first sample's
data, causing various glitches at the beginning of the sample.

Fixes #10.
2015-11-16 18:47:41 -08:00
Andy Sloane
bb5555f5a1 Merge pull request #13 from hillerstorm/implement_Hxy_effect
Implement Hxy effect
2015-11-14 20:28:14 -08:00
Johan Hillerström
be1c0df30d Remove Hxy from missing effects 2015-11-15 00:38:55 +01:00
Johan Hillerström
007f0db8f7 Add test case for Hxy 2015-11-15 00:37:46 +01:00
Johan Hillerström
0f3888cfff Implement the Hxy effect 2015-11-15 00:37:35 +01:00
Andy Sloane
0e82964315 add license field to package.json
I was certain I had already added it, but somehow it went missing.
2015-11-14 09:38:43 -08:00
Andy Sloane
c35bb07fac fix some wrong comments here and there 2015-11-14 09:37:15 -08:00
Andy Sloane
05216f5ce0 implement E5x finetune override
nf_chill.xm just needs EDx now
2015-11-14 09:29:08 -08:00
Andy Sloane
9a410aa535 Merge remote-tracking branch 'hillerstorm/implement_Gxx_effect' 2015-11-14 08:39:27 -08:00
Andy Sloane
0f86a8d2c5 Merge pull request #11 from hillerstorm/fix_out_of_range_pattern_error
Fix non-existing pattern error
2015-11-14 08:35:04 -08:00
Andy Sloane
660729010b jshint cleanup of xmeffects 2015-11-14 08:30:35 -08:00
Andy Sloane
267ecbeffb test for effect Axy 2015-11-14 08:27:17 -08:00
Johan Hillerström
8fe05fb89d Remove Gxx from missing effects 2015-11-14 13:07:51 +01:00
Johan Hillerström
88584b9cd0 Add test case for Gxx 2015-11-14 13:04:43 +01:00
Johan Hillerström
eff3fd1ed6 Implement the Gxx effect 2015-11-14 13:04:29 +01:00
Johan Hillerström
b04e6c00b0 Introduce a global volume variable 2015-11-14 13:04:16 +01:00
Johan Hillerström
fc6be2b246 Add tests for non-existing song positions 2015-11-14 12:35:28 +01:00
Johan Hillerström
1f2b0ba419 Add safeguards when setting cur_pat 2015-11-14 12:35:18 +01:00
Andy Sloane
570aa33a20 added test for vibrato... and fixed vibrato
naturally when i actually tested it, it turned out to be subtly broken

also reorg'd the tests a bit, sorry @hillerstorm.
2015-11-13 22:06:50 -08:00
Andy Sloane
be463856db Add round 1 of tests 2015-11-12 10:57:02 -08:00
Andy Sloane
6a5fe5e024 Remove XM suffix from XMPlayer calls 2015-11-12 08:56:33 -08:00
Andy Sloane
ce5fd027a3 add build status and API docs to README 2015-11-11 22:49:40 -08:00
Andy Sloane
9bf35716f3 add travis testing 2015-11-11 22:32:56 -08:00
Andy Sloane
7ca90f9b98 really basic test runner
npm test now does... something somewhat useful.

now to write a bunch of tests.
2015-11-11 22:28:49 -08:00
Andy Sloane
8c37c8d96b Separate html "shell" javascript; player now standalone
Closes #3.

The code which defines what the buttons do and downloads songs and so
forth are now completely separated from the player, which has the
following API:

 - `XMPlayer.initAudio` -> starts up audio context; it's available as
   `XMPlayer.audioctx`
 - `XMPlayer.loadXM(ArrayBuffer)` -> returns `true` if loaded, otherwise
   barfs randomly
 - `XMPlayer.playXM()` -> starts playing
 - `XMPlayer.pauseXM()` -> obvious
 - `XMPlayer.stopXM()` -> obvious; call this before loading a new one

Loading trackview.js is now 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
2015-11-11 21:19:41 -08:00
Andy Sloane
8e114a16da Split viewer code into trackview.js
Also disabled iOS hack, as it seems to be causing issues, and fixed a
minor drag-n-drop bug (wouldn't load if track was already playing past
length of newly loaded track).

To play without a viewer, you still have to stub it out. Not sure the
best way to handle that case yet.
2015-11-11 18:01:15 -08:00
Andy Sloane
4b6e75332e Merge pull request #9 from hillerstorm/namespacing_and_global_leakage
Namespacing and global leakage
2015-11-11 16:12:14 -08:00
Johan Hillerström
1667ab502b Add window fallback 2015-11-12 01:09:20 +01:00
Johan Hillerström
664686f199 Add newlines 2015-11-12 01:05:59 +01:00
Johan Hillerström
7aa67803ed Use postfix increment operator 2015-11-12 01:04:59 +01:00
Andy Sloane
e34970c05f Merge pull request #8 from luckcolors/master
Added nw.js package.json file for building desktop app.
2015-11-11 15:49:23 -08:00
Johan Hillerström
ea36dceb55 Wrap in IIFE and namespace any shared info 2015-11-12 00:08:32 +01:00
Johan Hillerström
a37555ed58 Reorder to load without any issues 2015-11-12 00:00:43 +01:00
luckcolors
14d88e9a10 Removed toolbar. 2015-11-11 22:49:25 +01:00
luckcolors
41e8713b74 First commit for desktop app Nw.js 2015-11-11 22:28:21 +01:00
Andy Sloane
2cf8840665 Contact with the real world
Many, many fixes found after posting this on my website.

 - drag n drop supported
 - plays on OSX and iOS safari now
 - random other hiccups fixed

there seems to be some remaining issues cleaning up when loading a new
song.
2015-11-10 20:30:41 -08:00
Andy Sloane
5aa5c06ab0 update readme 2015-11-04 10:23:54 -08:00
Andy Sloane
541a9d721c no longer storing xms in git 2015-11-04 10:21:27 -08:00
Andy Sloane
7d6481ad73 file picker! 2015-11-04 10:21:04 -08:00
Andy Sloane
a547732063 render a nice big title and center stuff 2015-11-04 07:19:44 -08:00
Andy Sloane
f092f847f7 instrument list rendering with FT2 font. 2015-11-04 06:22:49 -08:00
Andy Sloane
54809c2b81 bugfix for 9xx offset effect 2015-11-04 05:50:29 -08:00
Andy Sloane
b859f27109 disable looping if looplen = 0...
so much pathological crap in XM files...
2015-11-03 18:16:00 -08:00
Andy Sloane
f95bd1e169 whoops, more refactoring bugs
i should really write some tests.
2015-11-03 17:55:12 -08:00
Andy Sloane
3c0a865824 update the effects code after moving stuff into xm 2015-11-03 09:57:40 -08:00
Andy Sloane
fd52b21e3f only instruments with samplemaps can play
by refactoring the xm loader i inadvertently broke this
2015-11-03 09:50:19 -08:00
Andy Sloane
ea6edaf2e4 add play/pause function 2015-11-03 09:36:00 -08:00
Andy Sloane
5967da7353 only reset pan/vol on instrument, not note 2015-11-03 07:18:30 -08:00
Andy Sloane
07cd6e8111 another corner case, another broken .xm 2015-11-02 22:51:04 -08:00
Andy Sloane
06ad3ed8a5 fix instrument trigger glitch 2015-11-02 22:29:59 -08:00
Andy Sloane
27d0a0b1c3 render volume effect column with the icons 2015-11-02 16:51:02 -08:00
Andy Sloane
c4370f447c implemented multiple samples / instrument 2015-11-02 13:46:23 -08:00
Andy Sloane
22dd555228 zoom scopes out 4x; looks way better 2015-11-02 13:46:22 -08:00
Andy Sloane
d68e8e8225 show all instrument names 2015-11-01 21:26:03 -08:00
Andy Sloane
114240ca9f oscilloscopes.
couldn't really think of a better way to do it, but it looks pretty
awesome. you can really see the filter effect too.
2015-11-01 09:26:03 -08:00
Andy Sloane
3daeeee96b implement retrigger, move effects into separate js 2015-11-01 07:42:52 -08:00
Andy Sloane
6dee1787f3 show the instruments 2015-10-31 11:39:49 -07:00
Andy Sloane
a3b94c51b9 oh man, DUH 2015-10-31 11:17:22 -07:00
Andy Sloane
49005f0ae0 so many weird uses of effects 2015-10-31 10:52:46 -07:00
Andy Sloane
5e141535ab fix some filter weirdness 2015-10-31 09:33:59 -07:00
Andy Sloane
32b09bdf63 various envelope/effect fixes
Dubmood's .xms seem to expose a lot of bugs...
2015-10-31 09:27:38 -07:00
14 changed files with 2095 additions and 626 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules

3
.travis.yml Normal file
View file

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

View file

@ -1,16 +1,40 @@
# xm.js
[![Build Status](https://travis-ci.org/a1k0n/jsxm.svg?branch=master)](https://travis-ci.org/a1k0n/jsxm)
FastTracker 2 .XM player, written for fun.
[Demo](http://www.a1k0n.net/code/jsxm/) [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.

View file

@ -1,29 +1,82 @@
<head>
<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="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='pattern'>
</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
- 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
- missing XM effects:
- 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

Binary file not shown.

23
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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);

969
xm.js

File diff suppressed because it is too large Load diff

346
xmeffects.js Normal file
View file

@ -0,0 +1,346 @@
(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);