Created
May 25, 2015 17:47
-
-
Save funrep/f61f8d5a8b2b494fe8ca to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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