Created
April 4, 2016 19:54
-
-
Save aalin/29b4e1a4974ed38d6c2ba7808b97c6d6 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
'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