really basic test runner
npm test now does... something somewhat useful. now to write a bunch of tests.
This commit is contained in:
parent
8c37c8d96b
commit
7ca90f9b98
5 changed files with 116 additions and 20 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
||||||
12
package.json
12
package.json
|
|
@ -1,11 +1,19 @@
|
||||||
{
|
{
|
||||||
"name": "jsxm",
|
"name": "jsxm",
|
||||||
"version": "1",
|
"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/)",
|
||||||
|
|
||||||
"main": "index.html",
|
"main": "index.html",
|
||||||
"window": {
|
"window": {
|
||||||
"frame": true,
|
"frame": true,
|
||||||
"toolbar": false,
|
"toolbar": false,
|
||||||
"position": "center",
|
"position": "center",
|
||||||
"resizable": true
|
"resizable": true
|
||||||
}
|
},
|
||||||
|
"scripts": { "test": "node test/all.js" },
|
||||||
|
"devDependencies": { "test": ">=0.0.5" }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
82
test/all.js
Normal file
82
test/all.js
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
window = {};
|
||||||
|
|
||||||
|
require('../xm.js', window);
|
||||||
|
require('../xmeffects.js', window);
|
||||||
|
|
||||||
|
var XMPlayer = window.XMPlayer;
|
||||||
|
|
||||||
|
// set up basic blank single-channel, single-pattern XM
|
||||||
|
function ResetXMData() {
|
||||||
|
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,
|
||||||
|
vibratodepth: 1,
|
||||||
|
vibratospeed: 1,
|
||||||
|
});
|
||||||
|
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 = xm.tempo;
|
||||||
|
return xm;
|
||||||
|
}
|
||||||
|
|
||||||
|
// using assert passed to the test function that just logs failures
|
||||||
|
exports['test XM startup'] = function(assert) {
|
||||||
|
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, 1, 'advance to row 1');
|
||||||
|
};
|
||||||
|
|
||||||
|
exports['test note on'] = function(assert) {
|
||||||
|
var xm = ResetXMData();
|
||||||
|
// [row][column][channel]
|
||||||
|
xm.patterns[0][0][0] = [48, 1, 0x10 + 33, 0, 0]; // C-4 1 40 000
|
||||||
|
XMPlayer.nextTick();
|
||||||
|
var ch = xm.channelinfo[0];
|
||||||
|
assert.equal(ch.note, 48, 'set note');
|
||||||
|
assert.equal(ch.period, 1152, 'channel period');
|
||||||
|
assert.equal(ch.vol, 33, 'channel volume');
|
||||||
|
assert.equal(ch.samp, xm.instruments[0].samples[0], 'set sample');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (module == require.main) require('test').run(exports);
|
||||||
37
xm.js
37
xm.js
|
|
@ -9,7 +9,7 @@ if (!window.XMView) {
|
||||||
}
|
}
|
||||||
var XMView = window.XMView;
|
var XMView = window.XMView;
|
||||||
|
|
||||||
player.PeriodForNote = PeriodForNote;
|
player.periodForNote = periodForNote;
|
||||||
player.prettify_effect = prettify_effect;
|
player.prettify_effect = prettify_effect;
|
||||||
player.initAudio = initAudio;
|
player.initAudio = initAudio;
|
||||||
player.loadXM = loadXM;
|
player.loadXM = loadXM;
|
||||||
|
|
@ -23,6 +23,10 @@ player.cur_ticksamp = 0;
|
||||||
player.cur_tick = 6;
|
player.cur_tick = 6;
|
||||||
player.xm = {}; // contains all song data
|
player.xm = {}; // contains all song data
|
||||||
|
|
||||||
|
// exposed for testing
|
||||||
|
player.nextTick = nextTick;
|
||||||
|
player.Envelope = Envelope;
|
||||||
|
|
||||||
// for pretty-printing notes
|
// for pretty-printing notes
|
||||||
var _note_names = [
|
var _note_names = [
|
||||||
"C-", "C#", "D-", "D#", "E-", "F-",
|
"C-", "C#", "D-", "D#", "E-", "F-",
|
||||||
|
|
@ -30,6 +34,10 @@ var _note_names = [
|
||||||
|
|
||||||
var f_smp = 44100; // updated by play callback, default value here
|
var f_smp = 44100; // updated by play callback, default value here
|
||||||
|
|
||||||
|
// per-sample exponential moving average for volume changes (to prevent pops
|
||||||
|
// and clicks); evaluated every 8 samples
|
||||||
|
var popfilter_alpha = 0.9837;
|
||||||
|
|
||||||
function prettify_note(note) {
|
function prettify_note(note) {
|
||||||
if (note < 0) return "---";
|
if (note < 0) return "---";
|
||||||
if (note == 96) return "^^^";
|
if (note == 96) return "^^^";
|
||||||
|
|
@ -72,7 +80,7 @@ function getstring(dv, offset, len) {
|
||||||
|
|
||||||
// Return 2-pole Butterworth lowpass filter coefficients for
|
// Return 2-pole Butterworth lowpass filter coefficients for
|
||||||
// center frequncy f_c (relative to sampling frequency)
|
// center frequncy f_c (relative to sampling frequency)
|
||||||
function FilterCoeffs(f_c) {
|
function filterCoeffs(f_c) {
|
||||||
if (f_c > 0.5) { // we can't lowpass above the nyquist frequency...
|
if (f_c > 0.5) { // we can't lowpass above the nyquist frequency...
|
||||||
f_c = 0.5;
|
f_c = 0.5;
|
||||||
}
|
}
|
||||||
|
|
@ -84,24 +92,21 @@ function FilterCoeffs(f_c) {
|
||||||
return [gain, 2*c, -c*c - s*s];
|
return [gain, 2*c, -c*c - s*s];
|
||||||
}
|
}
|
||||||
|
|
||||||
popfilter = FilterCoeffs(200.0 / 44100.0);
|
function updateChannelPeriod(ch, period) {
|
||||||
popfilter_alpha = 0.9837;
|
|
||||||
|
|
||||||
function UpdateChannelPeriod(ch, period) {
|
|
||||||
var freq = 8363 * Math.pow(2, (1152.0 - period) / 192.0);
|
var freq = 8363 * Math.pow(2, (1152.0 - period) / 192.0);
|
||||||
if (isNaN(freq)) {
|
if (isNaN(freq)) {
|
||||||
console.log("invalid period!", period);
|
console.log("invalid period!", period);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ch.doff = freq / f_smp;
|
ch.doff = freq / f_smp;
|
||||||
ch.filter = FilterCoeffs(ch.doff / 2);
|
ch.filter = filterCoeffs(ch.doff / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PeriodForNote(ch, note) {
|
function periodForNote(ch, note) {
|
||||||
return 1920 - (note + ch.samp.note)*16 - ch.samp.fine / 8.0;
|
return 1920 - (note + ch.samp.note)*16 - ch.samp.fine / 8.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function next_row() {
|
function nextRow() {
|
||||||
if (player.cur_pat == -1 || player.cur_row >= player.xm.patterns[player.cur_pat].length) {
|
if (player.cur_pat == -1 || player.cur_row >= player.xm.patterns[player.cur_pat].length) {
|
||||||
player.cur_row = 0;
|
player.cur_row = 0;
|
||||||
player.cur_songpos++;
|
player.cur_songpos++;
|
||||||
|
|
@ -196,7 +201,7 @@ function next_row() {
|
||||||
// special handling for portamentos: don't trigger the note
|
// special handling for portamentos: don't trigger the note
|
||||||
if (ch.effect == 3 || ch.effect == 5) {
|
if (ch.effect == 3 || ch.effect == 5) {
|
||||||
if (r[i][0] != -1) {
|
if (r[i][0] != -1) {
|
||||||
ch.periodtarget = PeriodForNote(ch, ch.note);
|
ch.periodtarget = periodForNote(ch, ch.note);
|
||||||
}
|
}
|
||||||
triggernote = false;
|
triggernote = false;
|
||||||
if (inst && inst.samplemap) {
|
if (inst && inst.samplemap) {
|
||||||
|
|
@ -224,7 +229,7 @@ function next_row() {
|
||||||
ch.env_vol = new EnvelopeFollower(inst.env_vol);
|
ch.env_vol = new EnvelopeFollower(inst.env_vol);
|
||||||
ch.env_pan = new EnvelopeFollower(inst.env_pan);
|
ch.env_pan = new EnvelopeFollower(inst.env_pan);
|
||||||
if (ch.note != undefined) {
|
if (ch.note != undefined) {
|
||||||
ch.period = PeriodForNote(ch, ch.note);
|
ch.period = periodForNote(ch, ch.note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -279,11 +284,11 @@ EnvelopeFollower.prototype.Tick = function(release) {
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
function next_tick() {
|
function nextTick() {
|
||||||
player.cur_tick++;
|
player.cur_tick++;
|
||||||
if (player.cur_tick >= player.xm.tempo) {
|
if (player.cur_tick >= player.xm.tempo) {
|
||||||
player.cur_tick = 0;
|
player.cur_tick = 0;
|
||||||
next_row();
|
nextRow();
|
||||||
}
|
}
|
||||||
for (var j = 0; j < player.xm.nchan; j++) {
|
for (var j = 0; j < player.xm.nchan; j++) {
|
||||||
var ch = player.xm.channelinfo[j];
|
var ch = player.xm.channelinfo[j];
|
||||||
|
|
@ -305,7 +310,7 @@ function next_tick() {
|
||||||
}
|
}
|
||||||
ch.volE = ch.env_vol.Tick(ch.release);
|
ch.volE = ch.env_vol.Tick(ch.release);
|
||||||
ch.panE = ch.env_pan.Tick(ch.release);
|
ch.panE = ch.env_pan.Tick(ch.release);
|
||||||
UpdateChannelPeriod(ch, ch.period + ch.periodoffset);
|
updateChannelPeriod(ch, ch.period + ch.periodoffset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -386,7 +391,7 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) {
|
||||||
var i = start;
|
var i = start;
|
||||||
var failsafe = 100;
|
var failsafe = 100;
|
||||||
while (i < end) {
|
while (i < end) {
|
||||||
if (failsafe-- == 0) {
|
if (failsafe-- === 0) {
|
||||||
console.log("failsafe in mixing loop! channel", ch.number, k, sample_end,
|
console.log("failsafe in mixing loop! channel", ch.number, k, sample_end,
|
||||||
loopstart, looplen, dk);
|
loopstart, looplen, dk);
|
||||||
break;
|
break;
|
||||||
|
|
@ -518,7 +523,7 @@ function audio_cb(e) {
|
||||||
|
|
||||||
while(buflen > 0) {
|
while(buflen > 0) {
|
||||||
if (player.cur_pat == -1 || player.cur_ticksamp >= ticklen) {
|
if (player.cur_pat == -1 || player.cur_ticksamp >= ticklen) {
|
||||||
next_tick(f_smp);
|
nextTick(f_smp);
|
||||||
player.cur_ticksamp -= ticklen;
|
player.cur_ticksamp -= ticklen;
|
||||||
}
|
}
|
||||||
var tickduration = Math.min(buflen, ticklen - player.cur_ticksamp);
|
var tickduration = Math.min(buflen, ticklen - player.cur_ticksamp);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ 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 arpeggio = [0, ch.effectdata>>4, ch.effectdata&15];
|
||||||
var note = ch.note + arpeggio[player.cur_tick % 3];
|
var note = ch.note + arpeggio[player.cur_tick % 3];
|
||||||
ch.period = player.PeriodForNote(ch, note);
|
ch.period = player.periodForNote(ch, note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -294,4 +294,4 @@ player.effects_t1 = [ // effect functions on tick 1+
|
||||||
eff_unimplemented // z
|
eff_unimplemented // z
|
||||||
];
|
];
|
||||||
|
|
||||||
})(window || {});
|
})(window);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue