#!/usr/bin/env python2 #parts of code inspired by cseq2midi import os, sys, struct, io blockIds = {0x5000:'DATA', 0x5001:'LABL'} class argTypes: UINT8 = 0 INT8 = 1 UINT16 = 2 INT16 = 3 RANDOM = 4 VARIABLE = 5 VARLEN = 6 class BCSEQ: def __init__(self, filename): self.blocks = {} #Dicts containing a dict with keys 'offset' and 'size' self.labels = [] self.commands = [] self.commandOffsets = {} self.filename = filename with open(self.filename, 'rb') as fh: self.bcseq = io.BytesIO(fh.read()) #store file in memory as file type object self.bcseq.seek(0) if self.bcseq.read(4) != b'CSEQ': sys.exit('Not a BCSEQ file') self.endianness = readBytes(self.bcseq, 2) self.headerSize = readBytes(self.bcseq, 2) self.version = readBytes(self.bcseq, 4) if self.version != 0x01010000: #1.1.0.0 sys.exit('Encountered unknown CSEQ version: %X' % (self.version)) self.bcseqSize = readBytes(self.bcseq, 4) self.blockCount = readBytes(self.bcseq, 4) for x in xrange(self.blockCount): bId = readBytes(self.bcseq, 4) if bId not in blockIds: sys.exit('Encountered unknown block id: %X' % (bId)) bLabel = blockIds[bId] bOffs = readBytes(self.bcseq, 4) bSize = readBytes(self.bcseq, 4) self.blocks[bLabel] = {'offset':bOffs, 'size':bSize} #print "Found '%s' block. Offset: 0x%X, Size: 0x%X" % (bLabel, bOffs, bSize) if not self.blocks['DATA'] or not self.blocks['LABL']: sys.exit('Missing DATA or LABL block.') self.parseLablBlock() self.parseLabels() def parseLablBlock(self): """ Returns a sorted list of dicts containing labels found in the label block Dict keys are 'offset', 'label', and 'checked' """ labels = [] startPos = self.bcseq.tell() self.bcseq.seek(self.blocks['LABL']['offset']) bLabel = self.bcseq.read(4) bSize = readBytes(self.bcseq, 4) labelInfoCount = readBytes(self.bcseq, 4) for x in xrange(labelInfoCount): labelInfoId = readBytes(self.bcseq, 4) if labelInfoId != 0x5100: sys.exit("Unknown ID in 'LABL' block: 0x%X" % (labelInfoId)) labelInfoOffs = readBytes(self.bcseq, 4) tmp = self.bcseq.tell() self.bcseq.seek(self.blocks['LABL']['offset'] + 8 + labelInfoOffs) #Offset is relative to after the 8 byte block header tmpId = readBytes(self.bcseq, 4) #Should be 0x1F00, but not checking for that labelDataOffs = readBytes(self.bcseq, 4) #Relative to after 8 byte header of DATA block labelStrSize = readBytes(self.bcseq, 4) labelStr = self.bcseq.read(labelStrSize) #Assume label string size >0 self.addLabel(labelDataOffs, labelStr) self.bcseq.seek(tmp) self.bcseq.seek(startPos) def addLabel(self, offset, label): """Adds a label to a list of labels if one doesn't already exist for that offset, and sorts them by offset""" if not any(x['offset'] == offset for x in self.labels): self.labels.append({'offset':offset, 'label':label, 'checked':False}) self.labels.sort(key=lambda x: int(x['offset'])) def labelFromOffset(self, offset, str): #Is this the best way to do this? label = next( (x['label'] for x in self.labels if x['offset'] == offset), None) if label == None: label = '%s_%06X' % (str, offset) self.addLabel(offset, label) return label def parseLabels(self): """Iterates over every label(including ones generated here) and decompiles their commands""" while 1: label = next( (x for x in self.labels if x['checked'] == False), None) if label == None: self.sortCommands() break self.parseLabelCommands(label) label['checked'] = True def parseLabelCommands(self, label): startPos = self.bcseq.tell() self.bcseq.seek(self.blocks['DATA']['offset'] + 8 + label['offset']) parsing = True while parsing: tmpcmd = {'offset':0, 'name':'UNDEFINED', 'suffix1':'', 'suffix2':'', 'suffix3':'', 'arg1Type':None, 'arg2Type':None, 'args':[]} tmpcmd['offset'] = self.bcseq.tell() - self.blocks['DATA']['offset'] - 8 cmd = readBytes(self.bcseq, 1) #Parsing code checks for '_if' first, then one of ['_t', '_tr', '_tv'], then one of ['_r', '_v'] if cmd == 0xA2: #_if tmpcmd['suffix3'] = '_if' cmd = readBytes(self.bcseq, 1) if cmd == 0xA3: #_t tmpcmd['suffix2'] = '_t' tmpcmd['arg2Type'] = argTypes.INT16 cmd = readBytes(self.bcseq, 1) elif cmd == 0xA4: #_tr tmpcmd['suffix2'] = '_tr' tmpcmd['arg2Type'] = argTypes.RANDOM cmd = readBytes(self.bcseq, 1) elif cmd == 0xA5: #_tv tmpcmd['suffix2'] = '_tv' tmpcmd['arg2Type'] = argTypes.VARIABLE cmd = readBytes(self.bcseq, 1) if cmd == 0xA0: #_r tmpcmd['suffix1'] = '_r' tmpcmd['arg1Type'] = argTypes.RANDOM cmd = readBytes(self.bcseq, 1) elif cmd == 0xA1: #_v tmpcmd['suffix1'] = '_v' tmpcmd['arg1Type'] = argTypes.VARIABLE cmd = readBytes(self.bcseq, 1) if cmd < 0x80: #note tmpcmd['name'] = keyToStr(cmd) tmpcmd['args'].append(str(readBytes(self.bcseq, 1))) if tmpcmd['arg1Type'] == None: tmpcmd['arg1Type'] = argTypes.VARLEN tmpcmd['args'] += readArg(self.bcseq, tmpcmd['arg1Type']) elif cmd == 0x80: #wait tmpcmd['name'] = 'wait' if tmpcmd['arg1Type'] == None: tmpcmd['arg1Type'] = argTypes.VARLEN tmpcmd['args'] += readArg(self.bcseq, tmpcmd['arg1Type']) elif cmd == 0x81: #prg tmpcmd['name'] = 'prg' if tmpcmd['arg1Type'] == None: tmpcmd['arg1Type'] = argTypes.VARLEN tmpcmd['args'] += readArg(self.bcseq, tmpcmd['arg1Type']) elif cmd == 0x88: #opentrack tmpcmd['name'] = 'opentrack' tmpcmd['args'].append(str(readBytes(self.bcseq, 1))) label = self.labelFromOffset(readBytes(self.bcseq, 3, endian='big'), 'track') tmpcmd['args'].append(label) elif cmd == 0x89: #jump tmpcmd['name'] = 'jump' label = self.labelFromOffset(readBytes(self.bcseq, 3, endian='big'), 'loc') tmpcmd['args'].append(label) if tmpcmd['suffix3'] == '': parsing = False elif cmd == 0x8A: #call tmpcmd['name'] = 'call' label = self.labelFromOffset(readBytes(self.bcseq, 3, endian='big'), 'sub') tmpcmd['args'].append(label) elif 0xB0 <= cmd <= 0xDF and cmd not in [0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xDE]: if cmd == 0xB1: #env_hold tmpcmd['name'] = 'env_hold' if tmpcmd['arg1Type'] == None: tmpcmd['arg1Type'] = argTypes.INT8 tmpcmd['args'] += readArg(self.bcseq, tmpcmd['arg1Type']) elif cmd == 0xB2: #monophonic suf = 'off' if readBytes(self.bcseq, 1) == 0 else 'on' tmpcmd['name'] = 'monophonic_%s' % (suf) elif cmd == 0xBF: #frontbypass suf = 'off' if readBytes(self.bcseq, 1) == 0 else 'on' tmpcmd['name'] = 'frontbypass_%s' % (suf) elif cmd == 0xC3: #transpose tmpcmd['name'] = 'transpose' if tmpcmd['arg1Type'] == None: tmpcmd['arg1Type'] = argTypes.INT8 tmpcmd['args'] += readArg(self.bcseq, tmpcmd['arg1Type']) elif cmd == 0xC4: #pitchbend tmpcmd['name'] = 'pitchbend' if tmpcmd['arg1Type'] == None: tmpcmd['arg1Type'] = argTypes.INT8 tmpcmd['args'] += readArg(self.bcseq, tmpcmd['arg1Type']) elif cmd == 0xC7: #notewait suf = 'off' if readBytes(self.bcseq, 1) == 0 else 'on' tmpcmd['name'] = 'notewait_%s' % (suf) elif cmd == 0xC8: #tie suf = 'off' if readBytes(self.bcseq, 1) == 0 else 'on' tmpcmd['name'] = 'tie%s' % (suf) elif cmd == 0xC9: #porta tmpcmd['name'] = 'porta' tmpcmd['args'].append(keyToStr(readBytes(self.bcseq, 1))) elif cmd == 0xCC: #mod_type tmpcmd['name'] = 'mod_type' modType = readBytes(self.bcseq, 1) if modType > 2: sys.exit("Unrecognized 'mod_type' value: 0x%02X" % (modType)) tmpcmd['args'].append(['MOD_TYPE_PITCH', 'MOD_TYPE_VOLUME', 'MOD_TYPE_PAN'][modType]) elif cmd == 0xCE: #porta suf = 'off' if readBytes(self.bcseq, 1) == 0 else 'on' tmpcmd['name'] = 'porta_%s' % (suf) elif 0xD0 <= cmd <= 0xD3: #Attack, decay, sustain, release tmpcmd['name'] = ['attack', 'decay', 'sustain', 'release'][cmd - 0xD0] if tmpcmd['arg1Type'] == None: tmpcmd['arg1Type'] = argTypes.INT8 tmpcmd['args'] += readArg(self.bcseq, tmpcmd['arg1Type']) elif cmd == 0xD6: #printvar tmpcmd['name'] = 'printvar' tmpcmd['args'] += readArg(self.bcseq, argTypes.VARIABLE) elif cmd == 0xDF: #damper suf = 'off' if readBytes(self.bcseq, 1) == 0 else 'on' tmpcmd['name'] = 'damper_%s' % (suf) else: b0Range = { 0xB0:'timebase', 0xB3:'velocity_range', 0xB4:'biquad_type', 0xB5:'biquad_value', 0xB6:'bank_select', 0xBD:'mod_phase', 0xBE:'mod_curve', 0xC0:'pan', 0xC1:'volume', 0xC2:'main_volume', 0xC5:'bendrange', 0xC6:'prio', 0xCA:'mod_depth', 0xCB:'mod_speed', 0xCD:'mod_range', 0xCF:'porta_time', 0xD4:'loop_start', 0xD5:'volume2', 0xD7:'span', 0xD8:'lpf_cutoff', 0xD9:'fxsend_a', 0xDA:'fxsend_b', 0xDB:'mainsend', 0xDC:'init_pan', 0xDD:'mute' } tmpcmd['name'] = b0Range[cmd] if tmpcmd['arg1Type'] == None: tmpcmd['arg1Type'] = argTypes.UINT8 tmpcmd['args'] += readArg(self.bcseq, tmpcmd['arg1Type']) if tmpcmd['arg2Type'] != None: tmpcmd['args'] += readArg(self.bcseq, tmpcmd['arg2Type']) elif cmd == 0xE0: #mod_delay tmpcmd['name'] = 'mod_delay' if tmpcmd['arg1Type'] == None: tmpcmd['arg1Type'] = argTypes.INT16 tmpcmd['args'] += readArg(self.bcseq, tmpcmd['arg1Type']) elif cmd == 0xE1: #tempo tmpcmd['name'] = 'tempo' if tmpcmd['arg1Type'] == None: tmpcmd['arg1Type'] = argTypes.INT16 tmpcmd['args'] += readArg(self.bcseq, tmpcmd['arg1Type']) elif cmd == 0xE3: #sweep_pitch tmpcmd['name'] = 'sweep_pitch' if tmpcmd['arg1Type'] == None: tmpcmd['arg1Type'] = argTypes.INT16 tmpcmd['args'] += readArg(self.bcseq, tmpcmd['arg1Type']) elif cmd == 0xE4: #mod_period tmpcmd['name'] = 'mod_period' if tmpcmd['arg1Type'] == None: tmpcmd['arg1Type'] = argTypes.INT16 tmpcmd['args'] += readArg(self.bcseq, tmpcmd['arg1Type']) elif cmd == 0xF0: #extended cmd = readBytes(self.bcseq, 1) if 0x80 <= cmd <= 0x8B or 0x90 <= cmd <= 0x95: f080Range = { 0x80:'setvar', 0x81:'addvar', 0x82:'subvar', 0x83:'mulvar', 0x84:'divvar', 0x85:'shiftvar', 0x86:'randvar', 0x87:'andvar', 0x88:'orvar', 0x89:'xorvar', 0x8A:'notvar', 0x8B:'modvar', 0x90:'cmp_eq', 0x91:'cmp_ge', 0x92:'cmp_gt', 0x93:'cmp_le', 0x94:'cmp_lt', 0x95:'cmp_ne' } tmpcmd['name'] = f080Range[cmd] tmpcmd['args'] += readArg(self.bcseq, argTypes.VARIABLE) if tmpcmd['arg1Type'] == None: tmpcmd['arg1Type'] = argTypes.INT16 tmpcmd['args'] += readArg(self.bcseq, tmpcmd['arg1Type']) elif cmd == 0xA4: #mod2_type tmpcmd['name'] = 'mod2_type' modType = readBytes(self.bcseq, 1) if modType > 2: sys.exit("Unrecognized 'mod2_type' value: 0x%02X" % (modType)) tmpcmd['args'].append(['MOD_TYPE_PITCH', 'MOD_TYPE_VOLUME', 'MOD_TYPE_PAN'][modType]) elif cmd == 0xAA: #mod3_type tmpcmd['name'] = 'mod3_type' modType = readBytes(self.bcseq, 1) if modType > 2: sys.exit("Unrecognized 'mod3_type' value: 0x%02X" % (modType)) tmpcmd['args'].append(['MOD_TYPE_PITCH', 'MOD_TYPE_VOLUME', 'MOD_TYPE_PAN'][modType]) elif cmd == 0xB0: #mod4_type tmpcmd['name'] = 'mod4_type' modType = readBytes(self.bcseq, 1) if modType > 2: sys.exit("Unrecognized 'mod4_type' value: 0x%02X" % (modType)) tmpcmd['args'].append(['MOD_TYPE_PITCH', 'MOD_TYPE_VOLUME', 'MOD_TYPE_PAN'][modType]) elif 0xA0 <= cmd <= 0xB1: f0a0Range = { 0xA0:'mod2_curve', 0xA1:'mod2_phase', 0xA2:'mod2_depth', 0xA3:'mod2_speed', 0xA5:'mod2_range', 0xA6:'mod3_curve', 0xA7:'mod3_phase', 0xA8:'mod3_depth', 0xA9:'mod3_speed', 0xAB:'mod3_range', 0xAC:'mod4_curve', 0xAD:'mod4_phase', 0xAE:'mod4_depth', 0xAF:'mod4_speed', 0xB1:'mod4_range' } tmpcmd['name'] = f0a0Range[cmd] if tmpcmd['arg1Type'] == None: tmpcmd['arg1Type'] = argTypes.UINT8 tmpcmd['args'] += readArg(self.bcseq, tmpcmd['arg1Type']) elif cmd == 0xE0: #userproc tmpcmd['name'] = 'userproc' if tmpcmd['arg1Type'] == None: tmpcmd['arg1Type'] = argTypes.UINT16 tmpcmd['args'] += readArg(self.bcseq, tmpcmd['arg1Type']) elif 0xE1 <= cmd <= 0xE6: f0e0Range = { 0xE1:'mod2_delay', 0xE2:'mod2_period', 0xE3:'mod3_delay', 0xE4:'mod3_period', 0xE5:'mod4_delay', 0xE6:'mod4_period' } tmpcmd['name'] = f0e0Range[cmd] if tmpcmd['arg1Type'] == None: tmpcmd['arg1Type'] = argTypes.INT16 tmpcmd['args'] += readArg(self.bcseq, tmpcmd['arg1Type']) else: sys.exit('Unrecognized extended command: 0xFF 0x%X, at 0x%X' % (cmd, self.blocks['DATA']['offset'] + 8 + tmpcmd['offset'])) elif cmd == 0xFB: #env_reset tmpcmd['name'] = 'env_reset' elif cmd == 0xFC: #loop_end tmpcmd['name'] = 'loop_end' elif cmd == 0xFD: #ret tmpcmd['name'] = 'ret' if tmpcmd['suffix3'] == '': parsing = False elif cmd == 0xFE: #alloctrack tmpcmd['name'] = 'alloctrack' tmpcmd['args'].append('0x%X' % (readBytes(self.bcseq, 2, endian='big'))) #TODO REPLACE elif cmd == 0xFF: #fin tmpcmd['name'] = 'fin' if tmpcmd['suffix3'] == '': parsing = False else: sys.exit('Unrecognized command: 0x%X, at 0x%X' % (cmd, self.blocks['DATA']['offset'] + 8 + tmpcmd['offset'])) if self.commandOffsets.get(tmpcmd['offset'], False) == True: parsing = False else: self.commands.append(tmpcmd) self.commandOffsets[tmpcmd['offset']] = True self.bcseq.seek(startPos) def sortCommands(self): self.commands.sort(key=lambda x: int(x['offset'])) def genCseq(self): out = '' out += ';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n' out += '; Input file: %s\n' % (os.path.basename(self.filename)) out += ';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n\n' for command in self.commands: line = '' label = next( (x for x in self.labels if x['offset'] == command['offset']), None) if label != None: line += label['label'] + ':\n' line += ' ' line += command['name'] + command['suffix1'] + command['suffix2'] + command['suffix3'] if command['args']: line += ' ' + ', '.join(x for x in command['args']) line += '\n' if command['name'] == 'jump' or command['name'] == 'ret' or command['name'] == 'fin': line += '\n' out += line return out #Yuck, just wanted a generic function to handle this... def readBytes(file, bytes, endian='little', signed=False): val = 0 for x in xrange(bytes): if endian == 'little': SHIFT = (x * 8) else: #Assume 'big' if not 'little' SHIFT = ((bytes - 1 - x) * 8) val |= struct.unpack('B', file.read(1))[0] << SHIFT if signed == True: if val >= (1 << ((bytes*8)-1)): val = val - (1 << (bytes*8)) return val def readVarLen(file): val = 0 temp = 0x80 while temp & 0x80 != 0: temp = struct.unpack('B', file.read(1))[0] val = (val << 7) | (temp & 0x7F) return val def readArg(file, argType): """Returns a list because the RANDOM argType has two values""" val = [] if argType == argTypes.UINT8: val.append(str(readBytes(file, 1))) elif argType == argTypes.INT8: val.append(str(readBytes(file, 1, signed=True))) elif argType == argTypes.UINT16: val.append(str(readBytes(file, 2, endian='big'))) elif argType == argTypes.INT16: val.append(str(readBytes(file, 2, endian='big', signed=True))) elif argType == argTypes.RANDOM: val.append(str(readBytes(file, 2, endian='big', signed=True))) val.append(str(readBytes(file, 2, endian='big', signed=True))) elif argType == argTypes.VARIABLE: val.append(varToStr(readBytes(file, 1))) elif argType == argTypes.VARLEN: val.append(str(readVarLen(file))) return val def keyToStr(inp): keys = ['cn', 'cs', 'dn', 'ds', 'en', 'fn', 'fs', 'gn', 'gs', 'an', 'as', 'bn'] key = keys[inp % 12] octave = (inp // 12) - 1 #key 0x00 = C(-1), cnm1 in text CSEQ format suffix = 'm' if octave < 0 else '' return key + suffix + str(abs(octave)) def varToStr(num): prefixes = ['', 'G', 'T'] return '%sVAR_%d' % (prefixes[num // 16], (num % 16)) def main(argc, argv): if argc < 2 or not os.path.isfile(argv[1]): sys.exit('usage: %s file.bcseq' % (os.path.basename(__file__))) cseq = BCSEQ(argv[1]) #for label in cseq.labels: # print label print cseq.genCseq() if __name__ == '__main__': main(len(sys.argv), sys.argv)