-
-
Save martinjlowm/d06c59a2d361ba7e0683 to your computer and use it in GitHub Desktop.
This file contains 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 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 = []; | |
var 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); | |
var data_size, row, i, pattern; | |
for (var song_index = 0; song_index < song_length; song_index++) { | |
pattern = {}; | |
pattern.nr = mod.pattern_ord[song_index]; | |
tmp = new Array(4); | |
j = 0; | |
for (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 | |
data_size = toDec(pattern.size); | |
pattern.data = new Uint8Array(data_size); | |
j = 0; | |
for (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)); | |
i = 0; | |
var cell_nr; | |
while (i < toDec(pattern.size)) { | |
row = new Array(toDec(mod.num_channels)); | |
cell_nr = 0; | |
var cell; | |
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); | |
row = null; | |
} | |
} | |
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