Skip to content

Instantly share code, notes, and snippets.

@aalin
Created April 4, 2016 19:54
Show Gist options
  • Save aalin/29b4e1a4974ed38d6c2ba7808b97c6d6 to your computer and use it in GitHub Desktop.
Save aalin/29b4e1a4974ed38d6c2ba7808b97c6d6 to your computer and use it in GitHub Desktop.
'use strict';
const fs = require("fs");
function leftPad(str, length, chr) {
str = str.toString();
if (str.length >= length) {
return str;
}
if (!chr && chr !== 0) {
chr = ' ';
}
return chr.repeat(length - str.length) + str;
}
function formatValue(value) {
if (value instanceof Array) {
return '[' + value.toString() + ']';
}
if (value instanceof Buffer) {
return value.slice(0, 30).toString('hex') + '…';
}
if (typeof value === 'number') {
const str = value.toString(16);
return leftPad(value, 10) + ' 0x' + leftPad(str.toString(16), str.length + str.length % 2, '0');
}
if (typeof value === 'string') {
return JSON.stringify(value);
}
return JSON.stringify(value);
}
const _buffer = Symbol();
const _position = Symbol();
class BufferReader {
constructor(buffer, index) {
this[_buffer] = buffer;
this[_position] = index || 0;
}
get position() {
return this[_position];
}
readBuffer(length) {
const indexes = this.skip(length);
return this[_buffer].slice(indexes[0], indexes[1]);
}
readString(length) {
return this.readBuffer(length).toString().replace(/\u0000+$/, ' ');;
}
readUInt8() {
return this[_buffer].readUInt8(this.skip(1)[0]);
}
readUInt16() {
return this[_buffer].readUInt16LE(this.skip(2)[0]);
}
readUInt32() {
return this[_buffer].readUInt32LE(this.skip(4)[0]);
}
seek(position) {
this[_position] = position;
}
peek(length) {
return this[_buffer].slice(this[_position], this[_position] + length);
}
// Returns an array of the previous length and the new position
skip(length) {
var prevIndex = this[_position];
this[_position] = prevIndex + length;
return [prevIndex, this[_position]];
}
loadStruct(struct) {
const initialIndex = this[_position];
return Object.keys(struct).reduce((obj, key) => {
const index = this[_position];
const value = this.loadType(struct[key], obj);
console.log(
'read',
leftPad('+' + (index - initialIndex), 5),
leftPad(index, 10) + leftPad(key, 30),
leftPad(struct[key], 40) + ' '.repeat(5),
formatValue(value)
);
return Object.assign(obj, { [key]: value });
}, {});
}
loadType(type, obj) {
var typeName = type;
var typeLength = 0;
if (type instanceof Array) {
typeName = type[0];
typeLength = type[1];
if (typeof typeLength === 'string') {
typeLength = obj[type[1]];
}
}
switch (typeName) {
case 'string':
return this.readString(typeLength);
case 'buffer':
return this.readBuffer(typeLength);
case 'array':
return Array.from({ length: typeLength }, () => this.loadStruct(type[2]));
case 'byte':
return this.readUInt8();
case 'word':
return this.readUInt16();
case 'dword':
return this.readUInt32();
default:
throw `Unhandled ${typeName}`;
}
}
}
function loadXM(buffer) {
const reader = new BufferReader(buffer);
const xmHeader = reader.loadStruct({
idText: ['string', 17],
moduleName: ['string', 20],
'$1a': 'byte',
trackerName: ['string', 20],
versionNumber: 'word',
headerSize: 'dword',
songLength: 'word',
restartPosition: 'word',
numChannels: 'word',
numPatterns: 'word',
numInstruments: 'word',
flags: 'word',
defaultTempo: 'word',
defaultBPM: 'word',
patternOrderTable: ['buffer', 'songLength']
});
reader.seek(60 + xmHeader.headerSize);
console.log('Patterns');
const patterns = Array.from({ length: xmHeader.numPatterns }, () => {
return reader.loadStruct({
headerLength: 'dword',
packingType: 'byte',
numRows: 'word',
packedDataSize: 'word',
packedData: ['buffer', 'packedDataSize']
});
});
console.log('Instruments');
const instruments = Array.from({ length: xmHeader.numInstruments }, (_, i) => {
const instrumentPosition = reader.position;
console.log('Instrument header');
const instrument = reader.loadStruct({
instrumentSize: 'dword',
name: ['string', 22],
type: 'byte',
numSamples: 'word'
});
if (instrument.numSamples > 0) {
console.log('2nd part of instrument header');
Object.assign(instrument, reader.loadStruct({
sampleHeaderSize: 'dword',
keymapAssignments: ['buffer', 96],
volumeEnvelopePoints: ['buffer', 48],
panningEnvelopePoints: ['buffer', 48],
numVolumePoints: 'byte',
numPanningPoints: 'byte',
volumeSustainPoint: 'byte',
volumeLoopStartPoint: 'byte',
volumeLoopEndPoint: 'byte',
panningSustainPoint: 'byte',
panningLoopStartPoint: 'byte',
panningLoopEndPoint: 'byte',
volumeType: 'byte',
panningType: 'byte',
vibratoType: 'byte',
vibratoSweep: 'byte',
vibratoDepth: 'byte',
vibratoRate: 'byte',
volumeFadeout: 'byte',
}));
reader.seek(instrumentPosition + instrument.instrumentSize);
Object.assign(instrument, reader.loadStruct({
samples: ['array', instrument.numSamples, {
sampleLength: 'dword',
sampleLoopStart: 'dword',
sampleLoopLength: 'dword',
volume: 'byte',
finetune: 'byte',
type: 'byte',
panning: 'byte',
relativeNoteNumber: 'byte',
packType: 'byte',
sampleName: ['string', 22],
}]
}));
Object.assign(instrument, {
sampleData: instrument.samples.map((sample) => {
return reader.loadStruct({
data: ['buffer', sample.sampleLength]
});
})
});
}
return instrument;
});
return {
header: xmHeader,
patterns: patterns,
instruments: instruments
};
}
fs.readFile('DEADLOCK.XM', function (err, data) {
if (err) {
throw err;
};
console.log(loadXM(data));
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment