Skip to content

Instantly share code, notes, and snippets.

@funrep
Created May 25, 2015 17:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save funrep/f61f8d5a8b2b494fe8ca to your computer and use it in GitHub Desktop.
Save funrep/f61f8d5a8b2b494fe8ca to your computer and use it in GitHub Desktop.
<meta charset="UTF-8">
<html>
<head>
<title>vitracker</title>
<script type="text/javascript" charset="utf-8" src="xm.js"></script>
</head>
<body>
<p>Hello, world</p>
</body>
</html>
// vitracker
// A FT2-clone tracker with vi-like interface in the browser.
// (c) funrep 2015
// Available under WTFPL (http://www.wtfpl.net/txt/copying/)
// Random object to put useful info in,
// then I can play around with stuff in the repl
var debug = {}
// Sometimes there is reserved space
// with just zeros
// in the file, so I use this to
// find out where the file "continues"
function checkZeros(start, stop, data) {
for (var i = start; i < stop; i++) {
if (data[i] !== 0) {
return i;
}
}
}
function toHex(dec) {
return "0x" + dec.toString(16);
}
function toDec(hex) {
return parseInt(hex, 16);
}
// TODO: read up on Uint8Array's methods and
// see if I can simplify this code with for
// example .toString() and .subarray()
function parse_mod(data) {
// state holds the current position
// in data (ie the .XM binary)
var state = 0;
var mod = {}
// HEADER
var id_text = "";
for (var i = state; i < 17; i++) {
id_text += String.fromCharCode(data[i]);
}
state = 17;
debug.id_text = id_text;
if (id_text !== "Extended Module: ") {
window.alert("Invalid module file.");
return null;
}
mod.name = "";
for (var i = state; i < state + 20; i++) {
if (data[i] === 0) {
break;
}
mod.name += String.fromCharCode(data[i]);
}
state += 20;
if (data[state] !== 26) {
// 26 is a special espace char,
// this simply validates if the module is correct
window.alert("Invalid module file.");
return null;
}
state += 1;
mod.tracker = "";
for (var i = state; i < state + 20; i++) {
if (data[i] === 0) {
break;
}
mod.tracker += String.fromCharCode(data[i]);
}
state += 20;
minor_ver = data[state++];
major_ver = data[state++];
mod.revision = major_ver + "." + minor_ver;
var tmp = new Array(4);
j = 0;
for (var i = state; i < state + 4; i++) {
tmp[j++] = data[i];
}
state += 4;
// Converts a pair of 4 ints to hex.
// Kinda fucked up but they come up backwards,
// then translating each int to hex and "combining" them
// makes the appropriate hex value, then one can convert
// hex to decimal to deal with them sanely
mod.header_size = "0x" + tmp[3].toString(16)
+ tmp[2].toString(16) + tmp[1].toString(16) + tmp[0].toString(16);
// Same thing here, but the rest of the info in the module
// are consructed of 2 ints each.
function parseHex(i) {
return "0x" + data[i + 1].toString(16) + data[i].toString(16);
}
mod.song_length = parseHex(state);
state += 2;
mod.restart_pos = parseHex(state);
state += 2;
mod.num_channels = parseHex(state);
state += 2;
mod.num_patterns = parseHex(state);
state += 2;
mod.num_instruments = parseHex(state);
state += 2;
flag = parseHex(state);
state += 2;
if (toDec(flag) === 0) {
mod.flag = "amiga";
} else {
mod.flag = "linear";
}
tempo = parseHex(state);
mod.tempo = toDec(tempo);
state += 2;
bpm = parseHex(state)
mod.bpm = toDec(bpm);
state += 2;
song_length = toDec(mod.song_length);
mod.pattern_ord = []
for (var i = state; i < state + song_length; i++) {
mod.pattern_ord.push(toHex(data[i]));
}
state += song_length;
state = checkZeros(state, 10000, data);
// read checkZero's comment
// PATTERNS
song_length = toDec(mod.song_length);
for (var i = 0; i < song_length; i++) {
pattern = {};
pattern.nr = mod.pattern_ord[i];
var tmp = new Array(4);
j = 0;
for (var i = state; i < state + 4; i++) {
tmp[j++] = data[i];
}
state += 4;
pat_header_length = "0x" + tmp[3].toString(16)
+ tmp[2].toString(16) + tmp[1].toString(16) + tmp[0].toString(16);
pattern.length = toDec(pat_header_length);
// currently assumes a length of 9 in the following code
// some weird thing, usually 0 and doesnt mean anything
// according to the "spec"
pattern.pack_type = data[state++];
pattern.num_rows = parseHex(state);
state += 2;
pattern.size = parseHex(state);
state += 2;
// Actual pattern data
var data_size = toDec(pattern.size);
pattern.data = new Uint8Array(data_size);
j = 0;
for (var i = state; i < state + data_size; i++) {
pattern.data[j++] = data[i];
}
state += data_size;
// INTEPRET PATTERN DATA
pattern.rows = new Array(toDec(pattern.num_rows));
var i = 0;
while (i < toDec(pattern.size)) {
row = new Array(toDec(mod.num_channels));
cell_nr = 0;
while (cell_nr < toDec(mod.num_channels)) {
cell = {};
// these bitwise-operations are
// for the compression scheme used in XM-modules.
// 0b* are binary numbers
if (pattern.data[i] & 0b10000000) {
if (pattern.data[i] & 0b1) {
cell.note = data[i++]; // NOTE
} if (pattern.data[i] & 0b10) {
cell.instrument = data[i++]; // INSTRUMENT
} if (pattern.data[i] & 0b100) {
cell.volume = data[i++]; // VOLUME
} if (pattern.data[i] & 0b1000) {
cell.effect = data[i++]; // EFFECT
} if (pattern.data[i] & 0b10000) {
cell.param = data[i++]; // PARAM
}
} else {
cell.note = pattern.data[i++];
cell.instrument = pattern.data[i++];
cell.volume = pattern.data[i++];
cell.effect = pattern.data[i++];
cell.param = pattern.data[i++];
}
row[cell_nr++] = cell;
}
pattern.rows.push(row);
}
}
debug.state = state;
debug.mod = mod;
}
function load_mod(file, handler) {
req = new XMLHttpRequest();
req.open("GET", file, true);
req.responseType = "arraybuffer";
req.onload = function (e) {
var arraybuffer = req.response;
if (arraybuffer) {
var bytearray = new Uint8Array(arraybuffer);
debug.data = bytearray;
handler(bytearray);
}
}
req.send();
}
// test files
load_mod("wiklund_-_hoffmans_potion.xm", parse_mod);
// load_mod("sample song.xm", parse_mod);
// load_mod("DEADLOCK.XM", parse_mod);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment