#!/usr/bin/env python3 # SPDX-FileCopyrightText: © 2024 ZeldaRET # SPDX-License-Identifier: CC0-1.0 # # Audio Sequence Disassembler # """ The approach for sequence disassembly is roughly as follows: ``` Set COVERAGE=[0 for _ in range(len(data))] Set REF_QUEUE=[] Set OFFSET=0 Set SECTION=SEQ 1: Begin sequential disassembly at OFFSET using section type SECTION Collect reference labels and section types into REF_QUEUE Update entries in COVERAGE to 1 as bytes are read End disassembly at `end` instruction If REF_QUEUE is not empty: Pop a reference from REF_QUEUE Set OFFSET=loc(reference) Set SECTION=section(reference) goto 1 If Any 0s in COVERAGE: Set OFFSET=(index of first 0 in COVERAGE) Set SECTION=guess_section(OFFSET) (make a heuristic guess for section based on neighbors) goto 1 ``` There are some additional subtleties for handling padding and uncommon sections like `array`. For tables used in `dyncall`s, we have to rely on external information to provide the location and size of tables as there is no reliable heuristic for identifying table sizes. TODO sequence beginning with testchan 0 is a buffer (?) OR any ldseq is an array and an array of 0 is a buffer (?) detect section overlaps and mark them as bugged in the output """ from dataclasses import dataclass from enum import Enum, auto from typing import Callable, Dict, List, Optional, Tuple from .audiobank_file import AudiobankFile pitch_names = ( "A0", "BF0", "B0", "C1", "DF1", "D1", "EF1", "E1", "F1", "GF1", "G1", "AF1", "A1", "BF1", "B1", "C2", "DF2", "D2", "EF2", "E2", "F2", "GF2", "G2", "AF2", "A2", "BF2", "B2", "C3", "DF3", "D3", "EF3", "E3", "F3", "GF3", "G3", "AF3", "A3", "BF3", "B3", "C4", "DF4", "D4", "EF4", "E4", "F4", "GF4", "G4", "AF4", "A4", "BF4", "B4", "C5", "DF5", "D5", "EF5", "E5", "F5", "GF5", "G5", "AF5", "A5", "BF5", "B5", "C6", "DF6", "D6", "EF6", "E6", "F6", "GF6", "G6", "AF6", "A6", "BF6", "B6", "C7", "DF7", "D7", "EF7", "E7", "F7", "GF7", "G7", "AF7", "A7", "BF7", "B7", "C8", "DF8", "D8", "EF8", "E8", "F8", "GF8", "G8", "AF8", "A8", "BF8", "B8", "C9", "DF9", "D9", "EF9", "E9", "F9", "GF9", "G9", "AF9", "A9", "BF9", "B9", "C10", "DF10", "D10", "EF10", "E10", "F10", "BFNEG1", "BNEG1", "C0", "DF0", "D0", "EF0", "E0", "F0", "GF0", "G0", "AF0", ) # # VERSIONS # class MMLVersion(Enum): OOT = auto() MM = auto() VERSION_ALL = (MMLVersion.OOT, MMLVersion.MM) # # SECTIONS # class SqSection(Enum): SEQ = ("SEQ", ".sequence") CHAN = ("CHAN", ".channel") LAYER = ("LAYER", ".layer") ARRAY = ("ARRAY", ".array") TABLE = ("TABLE", ".table") ENVELOPE = ("ENVELOPE", ".envelope") FILTER = ("FILTER", ".filter") UNKNOWN = ("UNK", "") def __init__(self, prefix, lbl_prefix): self.prefix = prefix self.lbl_prefix = lbl_prefix SECTION_ALL = (SqSection.SEQ, SqSection.CHAN, SqSection.LAYER) # # ARGS # def maybe_hex(n): if n < 10: return f"{n}" else: return f"0x{n:X}" def sign_extend(x, n): sgn = 1 << (n - 1) return (x & (sgn - 1)) - (x & sgn) class MMLArg: def __init__(self, disas): self.value = self.read(disas) def read(self, disas): raise NotImplementedError() def analyze(self, disas): pass def emit(self, disas): return str(self.value) class MMLArgBits(MMLArg): def read(self, disas): return disas.read_bits(type(self).NBITS) class ArgU8(MMLArg): def read(self, disas): return disas.read_u8() class ArgU4x2(MMLArg): def read(self, disas): return disas.read_u8() def emit(self, disas): return f"{(self.value >> 4) & 0xF}, {self.value & 0xF}" class ArgSeqId(ArgU8): def emit(self, disas): return disas.all_seq_names[self.value] class ArgFontId(ArgU8): # TODO def read(self, disas): return disas.read_u8() class ArgPitchU8(ArgU8): def emit(self, disas): return f"PITCH_{pitch_names[self.value]}" class ArgS8(MMLArg): def read(self, disas): return sign_extend(disas.read_u8(), 8) class ArgU16(MMLArg): def read(self, disas): return disas.read_u16() class ArgS16(MMLArg): def read(self, disas): return sign_extend(disas.read_u16(), 16) class ArgHex8(ArgU8): def emit(self, disas): return f"0x{self.value:02X}" class ArgHex16(ArgU16): def emit(self, disas): return f"0x{self.value:04X}" class ArgBitField16(ArgU16): def emit(self, disas): return bin(self.value) class ArgInstr(ArgU8): def emit(self, disas): builtins = { 126 : "FONTANY_INSTR_SFX", 127 : "FONTANY_INSTR_DRUM", 128 : "FONTANY_INSTR_SAWTOOTH", 129 : "FONTANY_INSTR_TRIANGLE", 130 : "FONTANY_INSTR_SINE", 131 : "FONTANY_INSTR_SQUARE", 132 : "FONTANY_INSTR_NOISE", 133 : "FONTANY_INSTR_BELL", 134 : "FONTANY_INSTR_8PULSE", 135 : "FONTANY_INSTR_4PULSE", 136 : "FONTANY_INSTR_ASM_NOISE", } if self.value in builtins: return builtins[self.value] # Check against first font only, this is fine for 99% of cases since most sequences use just one font font0 : AudiobankFile = disas.used_fonts[0] if self.value in font0.instrument_index_map: name = f"SF{font0.bank_num}_{font0.instrument_name(self.value)}" else: print(f"Invalid instrument sourced from {font0.name}: {self.value}") name = f"{self.value} /* invalid instrument */" return name class ArgVar(MMLArg): def read(self, disas): ret = disas.read_u8() if ret & 0x80: ret = ((ret << 8) & 0x7F00) | disas.read_u8() if ret < 128 and disas.insn_begin not in disas.force_long: print(f"Unnecessary use of long immediate encoding @ 0x{disas.insn_begin:X}: {ret}") disas.force_long.add(disas.insn_begin) return ret class ArgPortamentoMode(ArgHex8): def read(self, disas): ret = disas.read_u8() disas.portamento_is_special = (ret & 0x80) != 0 return ret class ArgStereoConfig(ArgU8): def emit(self, disas): assert (self.value & 0b11000000) == 0 type = (self.value >> 4) & 0b11 strong_right = (self.value >> 3) & 1 strong_left = (self.value >> 2) & 1 strong_rvrb_right = (self.value >> 1) & 1 strong_rvrb_left = (self.value >> 0) & 1 return f"{type}, {strong_right}, {strong_left}, {strong_rvrb_right}, {strong_rvrb_left}" class ArgPortamentoTime(ArgVar): def read(self, disas): if disas.portamento_is_special: return disas.read_u8() else: return super().read(disas) class ArgBits3(MMLArgBits): NBITS = 3 class ArgBits4(MMLArgBits): NBITS = 4 class IOPort3(ArgBits3): def emit(self, disas): assert self.value in range(0,8) return f"IO_PORT_{self.value}" class IOPort8(ArgU8): def emit(self, disas): if self.value in range(0,8): return f"IO_PORT_{self.value}" else: return f"{self.value} # BAD IO PORT NUMBER" class ArgPitch(MMLArgBits): NBITS = 6 def emit(self, disas): return f"PITCH_{pitch_names[self.value]}" class ArgAddr(ArgHex16): def analyze(self, disas): disas.add_ref(self.value) def emit(self, disas): value = self.value target_section = SqSection.UNKNOWN for frag in disas.fragments: if value in range(frag.start,frag.end): target_section = frag.section break addend = disas.addends.get(disas.pos, 0) if disas.cur_section in (SqSection.SEQ, SqSection.CHAN, SqSection.LAYER, SqSection.ENVELOPE) and addend == 0: # turn a label that's partway inside an instruction into a label beginning at the instruction + an addend for start,end in disas.insn_ranges: if value in range(start,end): addend = value - start value = start break prefix = target_section.prefix if addend != 0: return f"{prefix}_{value:04X} + {maybe_hex(addend)}" else: return f"{prefix}_{value:04X}" class ArgRelAddr8(ArgAddr): def read(self, disas): rel_offset = sign_extend(disas.read_u8(), 8) return disas.pos + rel_offset class ArgRelAddr16(ArgAddr): def read(self, disas): rel_offset = sign_extend(disas.read_u16(), 16) return disas.pos + rel_offset class ArgSectionPtr(ArgAddr): def analyze(self, disas): disas.add_ref(self.value, disas.cur_section) class ArgBigSectionPtr(ArgAddr): def analyze(self, disas): disas.add_ref(self.value, disas.cur_section, big=True) class ArgRelSectionPtr(ArgRelAddr8): def analyze(self, disas): disas.add_ref(self.value, disas.cur_section) class ArgSeqPtr(ArgAddr): def analyze(self, disas): disas.add_ref(self.value, SqSection.SEQ, big=True) class ArgChanPtr(ArgAddr): def analyze(self, disas): disas.add_ref(self.value, SqSection.CHAN, big=True) def emit(self, disas): return f"CHAN_{self.value:04X}" class ArgRelChanPtr(ArgRelAddr16): def analyze(self, disas): disas.add_ref(self.value, SqSection.CHAN, big=True) def emit(self, disas): return f"CHAN_{self.value:04X}" class ArgLayerPtr(ArgAddr): def analyze(self, disas): disas.add_ref(self.value, SqSection.LAYER, big=True) def emit(self, disas): return f"LAYER_{self.value:04X}" class ArgRelLayerPtr(ArgRelAddr16): def analyze(self, disas): disas.add_ref(self.value, SqSection.LAYER, big=True) def emit(self, disas): return f"LAYER_{self.value:04X}" class ArgArrayPtr(ArgAddr): def analyze(self, disas): disas.add_ref(self.value, SqSection.ARRAY, big=True) class ArgEnvPtr(ArgAddr): def analyze(self, disas): disas.add_ref(self.value, SqSection.ENVELOPE, big=True) def emit(self, disas): return f"ENVELOPE_{self.value:04X}" class ArgFilterPtr(ArgAddr): def analyze(self, disas): disas.add_ref(self.value, SqSection.FILTER, big=True) def emit(self, disas): return f"FILTER_{self.value:04X}" class ArgTblPtr(ArgAddr): def analyze(self, disas): disas.add_ref(self.value, SqSection.TABLE, big=True) def emit(self, disas): return f"TABLE_{self.value:04X}" class ArgUnkPtr(ArgAddr): def analyze(self, disas): disas.add_ref(self.value, SqSection.UNKNOWN) # # COMMANDS # @dataclass class MMLCmd: cmd_id : int mnemonic : str args : Tuple[MMLArg] = () is_branch : bool = False is_branch_unconditional : bool = False is_terminal : bool = False handler : Callable = None sections : Tuple[SqSection] = SECTION_ALL version : Tuple[MMLVersion] = VERSION_ALL def nesting_decr(cmd, disas): disas.nesting -= 1 if disas.nesting < 0: disas.nesting = 0 def nesting_incr(cmd, disas): disas.nesting += 1 def set_short(cmd, disas): disas.large_notes = False def set_large(cmd, disas): disas.large_notes = True # # NOTE: Changes here must be reflected in aseq.h for re-assembly # CMD_SPEC = ( # # Control Flow Commands # MMLCmd(0xFF, 'end', is_terminal=True, handler=nesting_decr), MMLCmd(0xFE, 'delay1'), MMLCmd(0xFD, 'delay', args=(ArgVar,), sections=(SqSection.SEQ, SqSection.CHAN,)), MMLCmd(0xFC, 'call', args=(ArgBigSectionPtr,)), MMLCmd(0xFB, 'jump', args=(ArgSectionPtr,), is_branch=True, is_branch_unconditional=True), MMLCmd(0xFA, 'beqz', args=(ArgSectionPtr,), is_branch=True), MMLCmd(0xF9, 'bltz', args=(ArgSectionPtr,), is_branch=True), MMLCmd(0xF8, 'loop', args=(ArgU8,), handler=nesting_incr), MMLCmd(0xF7, 'loopend', handler=nesting_decr), MMLCmd(0xF6, 'break', handler=nesting_decr), MMLCmd(0xF5, 'bgez', args=(ArgSectionPtr,), is_branch=True), MMLCmd(0xF4, 'rjump', args=(ArgRelSectionPtr,), is_branch=True, is_branch_unconditional=True), MMLCmd(0xF3, 'rbeqz', args=(ArgRelSectionPtr,), is_branch=True), MMLCmd(0xF2, 'rbltz', args=(ArgRelSectionPtr,), is_branch=True), # # SEQ commands # # non-argbits commands MMLCmd(0xF1, 'allocnotelist', sections=(SqSection.SEQ,), args=(ArgU8,)), MMLCmd(0xF0, 'freenotelist', sections=(SqSection.SEQ,)), MMLCmd(0xEF, 'unk_EF', sections=(SqSection.SEQ,), args=(ArgS16, ArgU8,)), MMLCmd(0xDF, 'transpose', sections=(SqSection.SEQ,), args=(ArgS8,)), MMLCmd(0xDE, 'rtranspose', sections=(SqSection.SEQ,), args=(ArgS8,)), MMLCmd(0xDD, 'tempo', sections=(SqSection.SEQ,), args=(ArgU8,)), MMLCmd(0xDC, 'tempochg', sections=(SqSection.SEQ,), args=(ArgS8,)), MMLCmd(0xDB, 'vol', sections=(SqSection.SEQ,), args=(ArgU8,)), MMLCmd(0xDA, 'volmode', sections=(SqSection.SEQ,), args=(ArgU8, ArgS16)), MMLCmd(0xD9, "volscale", sections=(SqSection.SEQ,), args=(ArgU8,)), MMLCmd(0xD7, 'initchan', sections=(SqSection.SEQ,), args=(ArgBitField16,)), MMLCmd(0xD6, 'freechan', sections=(SqSection.SEQ,), args=(ArgBitField16,)), MMLCmd(0xD5, 'mutescale', sections=(SqSection.SEQ,), args=(ArgS8,)), MMLCmd(0xD4, 'mute', sections=(SqSection.SEQ,)), MMLCmd(0xD3, 'mutebhv', sections=(SqSection.SEQ,), args=(ArgHex8,)), MMLCmd(0xD2, 'ldshortvelarr', sections=(SqSection.SEQ,), args=(ArgArrayPtr,)), # length 16 MMLCmd(0xD1, 'ldshortgatearr', sections=(SqSection.SEQ,), args=(ArgArrayPtr,)), # length 16 MMLCmd(0xD0, 'notealloc', sections=(SqSection.SEQ,), args=(ArgU8,)), MMLCmd(0xCE, 'rand', sections=(SqSection.SEQ,), args=(ArgU8,)), MMLCmd(0xCD, 'dyncall', sections=(SqSection.SEQ,), args=(ArgTblPtr,)), MMLCmd(0xCC, 'ldi', sections=(SqSection.SEQ,), args=(ArgU8,)), MMLCmd(0xC9, 'and', sections=(SqSection.SEQ,), args=(ArgU8,)), MMLCmd(0xC8, 'sub', sections=(SqSection.SEQ,), args=(ArgU8,)), MMLCmd(0xC7, 'stseq', sections=(SqSection.SEQ,), args=(ArgU8, ArgAddr,)), MMLCmd(0xC6, 'stop', sections=(SqSection.SEQ,)), MMLCmd(0xC5, 'scriptctr', sections=(SqSection.SEQ,), args=(ArgU16,)), MMLCmd(0xC4, 'runseq', sections=(SqSection.SEQ,), args=(ArgU8, ArgSeqId,)), MMLCmd(0xC3, 'mutechan', sections=(SqSection.SEQ,), args=(ArgS16,), version=(MMLVersion.MM,)), # argbits commands MMLCmd(0x00, 'testchan', sections=(SqSection.SEQ,), args=(ArgBits4,)), MMLCmd(0x40, 'stopchan', sections=(SqSection.SEQ,), args=(ArgBits4,)), MMLCmd(0x50, 'subio', sections=(SqSection.SEQ,), args=(IOPort3,)), MMLCmd(0x60, 'ldres', sections=(SqSection.SEQ,), args=(ArgBits4, ArgU8, ArgU8,)), MMLCmd(0x70, 'stio', sections=(SqSection.SEQ,), args=(IOPort3,)), MMLCmd(0x80, 'ldio', sections=(SqSection.SEQ,), args=(IOPort3,)), MMLCmd(0x90, 'ldchan', sections=(SqSection.SEQ,), args=(ArgBits4, ArgChanPtr,)), MMLCmd(0xA0, 'rldchan', sections=(SqSection.SEQ,), args=(ArgBits4, ArgRelChanPtr,)), MMLCmd(0xB0, 'ldseq', sections=(SqSection.SEQ,), args=(ArgBits4, ArgSeqId, ArgUnkPtr,)), # # CHAN commands # # non-argbits commands MMLCmd(0xF1, 'allocnotelist', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xF0, 'freenotelist', sections=(SqSection.CHAN,)), MMLCmd(0xEE, 'bendfine', sections=(SqSection.CHAN,), args=(ArgS8,)), MMLCmd(0xED, 'gain', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xEC, 'vibreset', sections=(SqSection.CHAN,)), MMLCmd(0xEB, 'fontinstr', sections=(SqSection.CHAN,), args=(ArgFontId, ArgInstr)), MMLCmd(0xEA, 'stop', sections=(SqSection.CHAN,)), MMLCmd(0xE9, 'notepri', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xE8, 'params', sections=(SqSection.CHAN,), args=(ArgU8, ArgU8, ArgU8, ArgS8, ArgS8, ArgU8, ArgU8, ArgU8,)), MMLCmd(0xE7, 'ldparams', sections=(SqSection.CHAN,), args=(ArgAddr,)), MMLCmd(0xE6, 'samplebook', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xE5, 'reverbidx', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xE4, 'dyncall', sections=(SqSection.CHAN,)), MMLCmd(0xE3, 'vibdelay', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xE2, 'vibdepthgrad', sections=(SqSection.CHAN,), args=(ArgU8, ArgU8, ArgU8,)), MMLCmd(0xE1, 'vibfreqgrad', sections=(SqSection.CHAN,), args=(ArgU8, ArgU8, ArgU8,)), MMLCmd(0xE0, 'volexp', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xDF, 'vol', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xDE, 'freqscale', sections=(SqSection.CHAN,), args=(ArgU16,)), MMLCmd(0xDD, 'pan', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xDC, 'panweight', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xDB, 'transpose', sections=(SqSection.CHAN,), args=(ArgS8,)), MMLCmd(0xDA, 'env', sections=(SqSection.CHAN,), args=(ArgEnvPtr,)), MMLCmd(0xD9, 'releaserate', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xD8, 'vibdepth', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xD7, 'vibfreq', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xD4, 'reverb', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xD3, 'bend', sections=(SqSection.CHAN,), args=(ArgS8,)), MMLCmd(0xD2, 'sustain', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xD1, 'notealloc', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xD0, 'effects', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xCF, 'stptrtoseq', sections=(SqSection.CHAN,), args=(ArgAddr,)), MMLCmd(0xCE, 'ldptr', sections=(SqSection.CHAN,), args=(ArgAddr,)), MMLCmd(0xCD, 'stopchan', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xCC, 'ldi', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xCB, 'ldseq', sections=(SqSection.CHAN,), args=(ArgUnkPtr,)), MMLCmd(0xCA, 'mutebhv', sections=(SqSection.CHAN,), args=(ArgHex8,)), MMLCmd(0xC9, 'and', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xC8, 'sub', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xC7, 'stseq', sections=(SqSection.CHAN,), args=(ArgU8, ArgAddr,)), MMLCmd(0xC6, 'font', sections=(SqSection.CHAN,), args=(ArgFontId,)), MMLCmd(0xC5, 'dyntbllookup', sections=(SqSection.CHAN,)), MMLCmd(0xC4, 'noshort', sections=(SqSection.CHAN,), handler=set_large), MMLCmd(0xC3, 'short', sections=(SqSection.CHAN,), handler=set_short), MMLCmd(0xC2, 'dyntbl', sections=(SqSection.CHAN,), args=(ArgTblPtr,)), MMLCmd(0xC1, 'instr', sections=(SqSection.CHAN,), args=(ArgInstr,)), MMLCmd(0xBE, 'unk_BE', sections=(SqSection.CHAN,), args=(ArgU8,), version=(MMLVersion.MM,)), MMLCmd(0xBD, 'randptr', sections=(SqSection.CHAN,), args=(ArgU16, ArgU16,), version=(MMLVersion.OOT,)), MMLCmd(0xBD, 'samplestart', sections=(SqSection.CHAN,), args=(ArgU8,), version=(MMLVersion.MM,)), MMLCmd(0xBC, 'ptradd', sections=(SqSection.CHAN,), args=(ArgHex16,)), MMLCmd(0xBB, 'combfilter', sections=(SqSection.CHAN,), args=(ArgU8, ArgU16)), MMLCmd(0xBA, 'randgate', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xB9, 'randvel', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xB8, 'rand', sections=(SqSection.CHAN,), args=(ArgU8,)), MMLCmd(0xB7, 'randtoptr', sections=(SqSection.CHAN,), args=(ArgU16,)), MMLCmd(0xB6, 'dyntblv', sections=(SqSection.CHAN,)), MMLCmd(0xB5, 'dyntbltoptr', sections=(SqSection.CHAN,)), MMLCmd(0xB4, 'ptrtodyntbl', sections=(SqSection.CHAN,)), MMLCmd(0xB3, 'filter', sections=(SqSection.CHAN,), args=(ArgU4x2,)), MMLCmd(0xB2, 'ldseqtoptr', sections=(SqSection.CHAN,), args=(ArgTblPtr,)), MMLCmd(0xB1, 'freefilter', sections=(SqSection.CHAN,)), MMLCmd(0xB0, 'ldfilter', sections=(SqSection.CHAN,), args=(ArgFilterPtr,)), MMLCmd(0xAA, 'unk_AA', sections=(SqSection.CHAN,), args=(), version=(MMLVersion.MM,)), MMLCmd(0xA8, 'randptr', sections=(SqSection.CHAN,), args=(ArgU16, ArgU16,), version=(MMLVersion.MM,)), MMLCmd(0xA7, 'unk_A7', sections=(SqSection.CHAN,), args=(ArgVar,), version=(MMLVersion.MM,)), MMLCmd(0xA6, 'unk_A6', sections=(SqSection.CHAN,), args=(ArgVar, ArgVar,), version=(MMLVersion.MM,)), MMLCmd(0xA5, 'unk_A5', sections=(SqSection.CHAN,), args=(), version=(MMLVersion.MM,)), MMLCmd(0xA4, 'unk_A4', sections=(SqSection.CHAN,), args=(ArgVar,), version=(MMLVersion.MM,)), MMLCmd(0xA3, 'unk_A3', sections=(SqSection.CHAN,), args=(), version=(MMLVersion.MM,)), MMLCmd(0xA2, 'unk_A2', sections=(SqSection.CHAN,), args=(ArgVar,), version=(MMLVersion.MM,)), MMLCmd(0xA1, 'unk_A1', sections=(SqSection.CHAN,), args=(), version=(MMLVersion.MM,)), MMLCmd(0xA0, 'unk_A0', sections=(SqSection.CHAN,), args=(ArgVar,), version=(MMLVersion.MM,)), # argbits commands MMLCmd(0x00, 'cdelay', sections=(SqSection.CHAN,), args=(ArgBits4,)), MMLCmd(0x10, 'sample', sections=(SqSection.CHAN,), args=(ArgBits3, ArgAddr,)), MMLCmd(0x18, 'sampleptr', sections=(SqSection.CHAN,), args=(ArgBits3, ArgAddr,)), MMLCmd(0x20, 'ldchan', sections=(SqSection.CHAN,), args=(ArgBits4, ArgChanPtr,)), MMLCmd(0x30, 'stcio', sections=(SqSection.CHAN,), args=(ArgBits4, IOPort8,)), MMLCmd(0x40, 'ldcio', sections=(SqSection.CHAN,), args=(ArgBits4, IOPort8,)), MMLCmd(0x50, 'subio', sections=(SqSection.CHAN,), args=(IOPort3,)), MMLCmd(0x60, 'ldio', sections=(SqSection.CHAN,), args=(IOPort3,)), MMLCmd(0x70, 'stio', sections=(SqSection.CHAN,), args=(IOPort3,)), MMLCmd(0x78, 'rldlayer', sections=(SqSection.CHAN,), args=(ArgBits3, ArgRelLayerPtr,)), MMLCmd(0x80, 'testlayer', sections=(SqSection.CHAN,), args=(ArgBits3,)), MMLCmd(0x88, 'ldlayer', sections=(SqSection.CHAN,), args=(ArgBits3, ArgLayerPtr,)), MMLCmd(0x90, 'dellayer', sections=(SqSection.CHAN,), args=(ArgBits3,)), MMLCmd(0x98, 'dynldlayer', sections=(SqSection.CHAN,), args=(ArgBits3,)), # # LAYER commands # # non-argbits commands MMLCmd(0xC0, 'ldelay', sections=(SqSection.LAYER,), args=(ArgVar,)), MMLCmd(0xC1, 'shortvel', sections=(SqSection.LAYER,), args=(ArgU8,)), MMLCmd(0xC2, 'transpose', sections=(SqSection.LAYER,), args=(ArgS8,)), MMLCmd(0xC3, 'shortdelay', sections=(SqSection.LAYER,), args=(ArgVar,)), MMLCmd(0xC4, 'legato', sections=(SqSection.LAYER,)), MMLCmd(0xC5, 'nolegato', sections=(SqSection.LAYER,)), MMLCmd(0xC6, 'instr', sections=(SqSection.LAYER,), args=(ArgInstr,)), MMLCmd(0xC7, 'portamento', sections=(SqSection.LAYER,), args=(ArgPortamentoMode, ArgPitchU8, ArgPortamentoTime,)), MMLCmd(0xC8, 'noportamento', sections=(SqSection.LAYER,)), MMLCmd(0xC9, 'shortgate', sections=(SqSection.LAYER,), args=(ArgU8,)), MMLCmd(0xCA, 'notepan', sections=(SqSection.LAYER,), args=(ArgU8,)), MMLCmd(0xCB, 'env', sections=(SqSection.LAYER,), args=(ArgEnvPtr, ArgU8,)), MMLCmd(0xCC, 'nodrumpan', sections=(SqSection.LAYER,)), MMLCmd(0xCD, 'stereo', sections=(SqSection.LAYER,), args=(ArgStereoConfig,)), MMLCmd(0xCE, 'bendfine', sections=(SqSection.LAYER,), args=(ArgS8,)), MMLCmd(0xCF, 'releaserate', sections=(SqSection.LAYER,), args=(ArgU8,)), MMLCmd(0xD0, 'ldshortvel', sections=(SqSection.LAYER,), args=(ArgBits4,)), MMLCmd(0xE0, 'ldshortgate', sections=(SqSection.LAYER,), args=(ArgBits4,)), MMLCmd(0xF0, 'unk_F0', sections=(SqSection.LAYER,), args=(ArgS16,), version=(MMLVersion.MM,)), MMLCmd(0xF1, 'surroundeffect', sections=(SqSection.LAYER,), args=(ArgU8,), version=(MMLVersion.MM,)), # argbits commands # large layer MMLCmd(0x00, 'notedvg', sections=(SqSection.LAYER,), args=(ArgPitch, ArgVar, ArgU8, ArgU8,)), MMLCmd(0x40, 'notedv', sections=(SqSection.LAYER,), args=(ArgPitch, ArgVar, ArgU8,)), MMLCmd(0x80, 'notevg', sections=(SqSection.LAYER,), args=(ArgPitch, ArgU8, ArgU8,)), # small layer MMLCmd(0x00, 'shortdvg', sections=(SqSection.LAYER,), args=(ArgPitch, ArgVar,)), MMLCmd(0x40, 'shortdv', sections=(SqSection.LAYER,), args=(ArgPitch,)), MMLCmd(0x80, 'shortvg', sections=(SqSection.LAYER,), args=(ArgPitch,)), ) # # DISASSEMBLER # class SequenceFragment: def __init__(self, disas, section, data, start, end): assert len(data) == end - start , f"Bad: got {len(data)} bytes for range [{start}:{end}] {data}" self.section = section self.data = data self.start = start self.end = end self.disas = disas def __str__(self): return f"Fragment ({self.section}) [{self.start}, {self.end}]" def __lt__(self, other): return self.start < other.start @staticmethod def merge(frag1, frag2): if frag1 == frag2: return frag1 if frag1.section != frag2.section: return None # don't merge envelopes or tables ever if frag1.section in (SqSection.ENVELOPE, SqSection.TABLE): return None min_start = min(frag1.start, frag2.start) max_start = max(frag1.start, frag2.start) min_end = min(frag1.end, frag2.end) max_end = max(frag1.end, frag2.end) if max_start > min_end: return None data1, data2 = frag1.data, frag2.data if frag2.start < frag1.start: data1, data2 = data2, data1 if min_end == max_end: # data1 contains data2 return SequenceFragment(frag1.disas, frag1.section, data1, min_start, max_end) assert data1[max_start:] == data2[:len(data1)-max_start] , \ f"Data does not agree on overlap between\n{frag1}\n{frag2}\n{data2[:len(data1)-max_start]}" return SequenceFragment(frag1.disas, frag1.section, data1[:max_start] + data2, min_start, max_end) @dataclass class SequenceTableSpec: start_offset : int num_entries : int addend : int sectype : SqSection def contains_loc(self, pos): return pos in range(self.start_offset, self.start_offset + 2 * self.num_entries) class SequenceDisassembler: def __init__(self, seq_num : int, data : bytes, tables : Optional[Tuple[SequenceTableSpec]], cmds : Tuple[MMLCmd], version : MMLVersion, outpath : str, seq_name : str, used_fonts : List[AudiobankFile], all_seq_names): self.seq_num = seq_num self.seq_name = seq_name self.used_fonts = used_fonts self.all_seq_names = all_seq_names self.pos = 0 self.insn_begin = 0 self.data = data self.hit_eof = False self.cur_section = SqSection.SEQ self.nesting = 0 self.portamento_is_special = False self.large_notes = True self.outpath = outpath self.cmds : Dict[SqSection, Dict[int, MMLCmd]] = { SqSection.SEQ : {}, SqSection.CHAN : {}, SqSection.LAYER : {}, } # preprocess command list into dictionary, possibly duplicating into # several id keys if any lsbits are used as an arg for cmd in cmds: # ignore commands not in this version if version not in cmd.version: continue # find number of lsbits that don't contribute to the command id if len(cmd.args) > 0 and issubclass(cmd.args[0], MMLArgBits): nbits = cmd.args[0].NBITS else: nbits = 0 id = cmd.cmd_id for section in cmd.sections: cmds_s = self.cmds[section] for i in range(1 << nbits): new = cmd old = cmds_s.get(id + i, None) if old is not None: assert old.mnemonic in ("notedvg", "notedv", "notevg"), (old.mnemonic, cmd.mnemonic) new = (old, cmd) cmds_s[id + i] = new self.force_long = set() self.insn_ranges = [] self.coverage = [0] * len(self.data) self.fragments = [] self.branch_targets = {} self.big_labels = set() self.all_ranges = [] self.decode_list = [] self.all_seen = [] self.tables : Optional[Tuple[SequenceTableSpec]] = tables self.table_cache = set() self.addends = {} self.unused = [] # general helpers def read_bits(self, nbits): return self.bits_val def read_u8(self): if self.hit_eof: raise Exception() if self.pos == len(self.data): self.hit_eof = True ret = None else: ret = self.data[self.pos] self.pos += 1 return ret def read_u16(self): return (self.read_u8() << 8) | self.read_u8() def read_s16(self): return sign_extend(self.read_u16(), 16) def lookup_cmd(self, id : int) -> MMLCmd: # lookup command info cmd : MMLCmd = self.cmds[self.cur_section].get(id, None) assert cmd is not None , (self.cur_section, id, self.cmds) if isinstance(cmd, tuple): # select based on whether we're dealing with large or short notes cmd = cmd[int(not self.large_notes)] # part of the command byte may be an arg, save the value mask = 0 if len(cmd.args) > 0 and issubclass(cmd.args[0], MMLArgBits): mask = (1 << cmd.args[0].NBITS) - 1 self.bits_val = id & mask return cmd # # analysis helpers # def register_addend(self, pos, value): self.addends[pos] = value def add_branch_target(self, value, section, big=False): self.branch_targets[value] = section if big: self.big_labels.add(value) def add_ref(self, value, section=None, big=False): if section is None: self.add_branch_target(value, SqSection.UNKNOWN) return self.add_branch_target(value, section, big=big) self.add_job(value, section, self.cur_section) def add_job(self, value, section, from_section=None): if value not in self.all_seen: self.all_seen.append(value) self.decode_list.append((value, section, from_section or section)) def merge_frags(self): self.fragments = list(sorted(self.fragments)) if len(self.fragments) < 2: return i = 0 while i != len(self.fragments) - 1: frag1 = self.fragments[i] frag2 = self.fragments[i + 1] merged = SequenceFragment.merge(frag1, frag2) if merged is not None: self.fragments[i] = merged del self.fragments[i + 1] else: i += 1 # # analysis handlers # def analyze_code(self): # sequence, channel, layer start_pos = self.pos # print(f" << [0x{start_pos:X}/0x{len(self.data):X}] :: {self.cur_section} >>") self.insn_begin = start_pos cmd_byte = self.read_u8() cmd = self.lookup_cmd(cmd_byte) assert cmd is not None , f"Bad command ID 0x{cmd_byte:02X} for section {self.cur_section.name} at 0x{start_pos:X}" # print(hex(cmd_byte)) args = [argtype(self) for argtype in cmd.args] raw_data = self.data[start_pos:self.pos] self.insn_ranges.append((start_pos, self.pos)) for i in range(start_pos,self.pos): self.coverage[i] = self.cur_section # print(f"/* 0x{start_pos:04X} [{' '.join([f'0x{b:02X}' for b in raw_data]):24}] */ {cmd.mnemonic:11} {', '.join([arg.emit(self) for arg in args])}".strip()) for arg in args: arg.analyze(self) if cmd.handler is not None: cmd.handler(cmd, self) self.fragments.append(SequenceFragment(self, self.cur_section, raw_data, start_pos, self.pos)) if not (cmd.is_branch_unconditional or cmd.is_terminal): self.decode_list.append((self.pos, self.cur_section, self.cur_section)) def analyze_table(self): assert self.tables is not None, "Found a table but no table spec provided." for table_spec in self.tables: if table_spec.contains_loc(self.pos): break else: assert False , f"Found table at {self.pos:04X} but no entry number provided" start_pos = self.pos = table_spec.offset if start_pos in self.table_cache: return for _ in range(table_spec.num_entries): curpos = self.pos cur = self.read_u16() - table_spec.addend if cur >= len(self.data) - 1: assert False , "Bad table pointer" self.add_branch_target(cur, table_spec.sectype, big=True) self.add_job(cur, table_spec.sectype, table_spec.sectype) self.register_addend(curpos, table_spec.addend) self.fragments.append(SequenceFragment(self, self.cur_section, self.data[start_pos:self.pos], start_pos, self.pos)) self.table_cache.add(start_pos) def analyze_array(self): start_pos = self.pos # TODO better heuristic than just hardcoding 16... # it would be better to wait until later to resize arrays though, up to the next identified fragment # ARRAY + UNK + OTHER -> ARRAY + OTHER for _ in range(16): assert self.read_u8() == 0 self.fragments.append(SequenceFragment(self, self.cur_section, self.data[start_pos:self.pos], start_pos, self.pos)) def analyze_filter(self): start_pos = self.pos for _ in range(8): assert self.read_u16() == 0 self.fragments.append(SequenceFragment(self, self.cur_section, self.data[start_pos:self.pos], start_pos, self.pos)) def analyze_envelope(self): start_pos = self.pos while True: # dangerous delay = self.read_s16() arg = self.read_s16() if delay < 0: break self.fragments.append(SequenceFragment(self, self.cur_section, self.data[start_pos:self.pos], start_pos, self.pos)) def analyze_unknown(self): self.fragments.append(SequenceFragment(self, self.cur_section, self.data[self.pos:self.pos+2], self.pos, self.pos+2)) def analyze(self): # mark offset 0 as a SEQ section self.add_branch_target(0, SqSection.SEQ, big=True) self.decode_list.append((0, SqSection.SEQ, SqSection.SEQ)) # analyze all sections, following branches to locate new sections while len(self.decode_list) != 0: self.pos, self.cur_section, self.refd_from = self.decode_list.pop() if self.pos >= len(self.data): # ignore sections that begin past the end of the file # TODO should be an error or warning? continue # execute handler based on section { SqSection.SEQ : self.analyze_code, SqSection.CHAN : self.analyze_code, SqSection.LAYER : self.analyze_code, SqSection.TABLE : self.analyze_table, SqSection.ARRAY : self.analyze_array, SqSection.FILTER : self.analyze_filter, SqSection.ENVELOPE : self.analyze_envelope, SqSection.UNKNOWN : self.analyze_unknown, }[self.cur_section]() # merge fragments self.merge_frags() # update coverage self.final_cvg = [0] * len(self.data) for frag in self.fragments: for i in range(frag.start,frag.end): self.final_cvg[i] = frag.section # resolve gaps in coverage while True: # keeps going until there's no zeros except for padding try: first_zero_idx = self.final_cvg.index(0) except ValueError: break # no more gaps # there was a gap, handle it if ((first_zero_idx + 0xF) & ~0xF) == len(self.data) and \ all(b == 0 for b in self.final_cvg[first_zero_idx:]) and \ all(b == 0 for b in self.data[first_zero_idx:]): # there's only padding left, we're done break else: # resolve non-padding gaps with heuristics # TODO any unknown data after a `jump` in a sequence frag should extend the sequence frag # TODO any unknown data before a filter should be a balign 16 last_zero_idx = first_zero_idx while self.final_cvg[last_zero_idx] == 0 and last_zero_idx < len(self.final_cvg)-1: self.final_cvg[last_zero_idx] = SqSection.UNKNOWN last_zero_idx += 1 num_unk = last_zero_idx - first_zero_idx emit_unk = True prev_frag = None prev_frag_idx = None next_frag = None next_frag_idx = None for i,frag in enumerate(self.fragments): if frag.end == first_zero_idx: prev_frag = frag prev_frag_idx = i elif frag.start == last_zero_idx: next_frag = frag next_frag_idx = i # SEQ + UNK -> SEQ if prev_frag is not None: if prev_frag.section == SqSection.SEQ: self.fragments[prev_frag_idx] = SequenceFragment(self, SqSection.SEQ, self.data[prev_frag.start:last_zero_idx], prev_frag.start, last_zero_idx) emit_unk = False if next_frag is not None: # UNK + FILTER -> FILTER if next_frag.section == SqSection.FILTER and num_unk < 16: emit_unk = False # UNK + TABLE -> TABLE if next_frag.section == SqSection.TABLE and num_unk < 2: emit_unk = False if prev_frag is not None and next_frag is not None: # LAYER + UNK + LAYER -> LAYER LAYER LAYER if prev_frag.section == SqSection.LAYER and next_frag.section == SqSection.LAYER: self.fragments.append(SequenceFragment(self, SqSection.LAYER, self.data[first_zero_idx:last_zero_idx], first_zero_idx, last_zero_idx)) emit_unk = False # LAYER + UNK + CHANNEL -> LAYER LAYER CHANNEL if prev_frag.section == SqSection.LAYER and next_frag.section == SqSection.CHAN: self.fragments.append(SequenceFragment(self, SqSection.LAYER, self.data[first_zero_idx:last_zero_idx], first_zero_idx, last_zero_idx)) emit_unk = False # TABLE + UNK + ENVELOPE -> TABLE + ENVELOPE.. + ENVELOPE if prev_frag.section == SqSection.TABLE and next_frag.section == SqSection.ENVELOPE: self.fragments.append(SequenceFragment(self, SqSection.ENVELOPE, self.data[first_zero_idx:last_zero_idx], first_zero_idx, last_zero_idx)) emit_unk = False # ENVELOPE + UNK + ENVELOPE -> ENVELOPE + ENVELOPE.. + ENVELOPE if prev_frag.section == SqSection.ENVELOPE and next_frag.section == SqSection.ENVELOPE: self.fragments.append(SequenceFragment(self, SqSection.ENVELOPE, self.data[first_zero_idx:last_zero_idx], first_zero_idx, last_zero_idx)) emit_unk = False if prev_frag is not None and next_frag is None: # ENVELOPE + UNK + END -> ENVELOPE + ENVELOPE.. + FILTER.. + END if prev_frag.section == SqSection.ENVELOPE: if all(b == 0 for b in self.data[first_zero_idx:]): for k in range(first_zero_idx, len(self.data), 16): if k + 16 > len(self.data): # padding break self.fragments.append(SequenceFragment(self, SqSection.FILTER, self.data[k:k+16], k, k + 16)) else: self.fragments.append(SequenceFragment(self, SqSection.ENVELOPE, self.data[first_zero_idx:last_zero_idx], first_zero_idx, last_zero_idx)) emit_unk = False if emit_unk: # leave it unknown for now, TODO make reasonable guesses self.add_branch_target(first_zero_idx, SqSection.UNKNOWN) self.fragments.append(SequenceFragment(self, SqSection.UNKNOWN, self.data[first_zero_idx:last_zero_idx], first_zero_idx, last_zero_idx)) # # disas helpers # def label_name(self, value, section, force_big=False): if value in self.big_labels or force_big: lbl_prefix = section.lbl_prefix + " " suffix = "" else: lbl_prefix = "" suffix = ":" return f"{lbl_prefix}{section.prefix}_{value:04X}{suffix}" def emit_branch_target_real(self, outfile, value, section, force_big=False): if section is SqSection.UNKNOWN: for frag in self.fragments: if value in range(frag.start, frag.end): section = frag.section break outfile.write(f"{self.label_name(value, section, force_big)}\n") def emit_branch_target(self, outfile, start, end, force_big=False): did_emit = False for b_tgt in self.branch_targets: if b_tgt in range(start,end): self.emit_branch_target_real(outfile, start, self.branch_targets[b_tgt], force_big) did_emit = True return did_emit # # disas handlers # def disas_section(self, frag : SequenceFragment, outfile): force_big_lbl = False if self.pos == frag.start: # If the previous frag is not the same type as this frag, force the first label to be a big label for frag2 in self.fragments: frag2 : SequenceFragment if frag2.end == frag.start: if frag2.section != frag.section: force_big_lbl = True break while self.pos < frag.end: start_pos = self.pos self.insn_begin = start_pos cmd_byte = self.read_u8() cmd = self.lookup_cmd(cmd_byte) mnemonic = cmd.mnemonic # Hacky fixups for commands using long var encodings when it was not necessary for them to do so, the usual # macros for re-assembly only select the long encoding when necessary so switch to special macros that # always use the long encoding unconditionally. if self.insn_begin in self.force_long: if mnemonic == "notedv": mnemonic = "noteldv" elif mnemonic in ("delay", "ldelay"): mnemonic = "lldelay" else: assert False , mnemonic args = [argtype(self) for argtype in cmd.args] raw_data = self.data[start_pos:self.pos] self.emit_branch_target(outfile, start_pos, self.pos, force_big_lbl) force_big_lbl = False args_str = ', '.join([arg.emit(self) for arg in args]) data_str = ' '.join([f'0x{b:02X}' for b in raw_data]) outfile.write(f"/* 0x{start_pos:04X} [{data_str:24}] */ {mnemonic:11} {args_str}".strip() + "\n") if cmd.is_terminal or cmd.is_branch_unconditional: outfile.write("\n") def disas_table(self, frag : SequenceFragment, outfile): base_pos = self.pos while self.pos < frag.end: start_pos = self.pos addend = self.addends.get(start_pos, 0) ent = self.read_u16() - addend self.emit_branch_target(outfile, start_pos, self.pos) section = self.branch_targets.get(ent, None) # TODO if section is None: section = SqSection.UNKNOWN if addend != 0: addend_str = f" + {addend}" else: addend_str = "" # TODO proper label name outfile.write(f" entry {section.prefix}_{ent:04X}{addend_str}\n") outfile.write("\n") def disas_filter(self, frag : SequenceFragment, outfile): start_pos = self.pos num_filters, align = divmod(len(frag.data), 2 * 8) assert all(b == 0 for b in frag.data) assert align == 0 for n in range(num_filters): self.emit_branch_target_real(outfile, start_pos + n * 2 * 8, SqSection.FILTER, force_big=True) outfile.write(" filter 0, 0, 0, 0, 0, 0, 0, 0\n\n") def disas_envelope(self, frag : SequenceFragment, outfile): start_pos = self.pos self.emit_branch_target(outfile, start_pos, frag.end) while self.pos < frag.end: delay = self.read_s16() arg = self.read_s16() if delay == 0 and arg == 0: outfile.write(" disable\n") elif delay == -1 and arg == 0: outfile.write(" hang\n") elif delay == -2: outfile.write(f" goto {arg}\n") elif delay == -3 and arg == 0: outfile.write(" restart\n") else: assert delay > 0 outfile.write(f" point {delay}, {arg}\n") if delay < 0 and self.pos not in self.branch_targets: outfile.write("\n") self.emit_branch_target_real(outfile, self.pos, frag.section) outfile.write("\n") def disas_array(self, frag : SequenceFragment, outfile): self.emit_branch_target(outfile, frag.start, frag.end) array_data = self.data[frag.start:frag.end] if all(b == 0 for b in array_data): outfile.write(f".fill 0x{len(array_data):X}\n\n") else: for b in array_data: outfile.write(f".byte 0x{b:2X}\n") outfile.write("\n") def disas_unknown(self, frag : SequenceFragment, outfile): start_pos = self.pos prev = start_pos for b_tgt in sorted(self.branch_targets): if b_tgt in range(start_pos+1,frag.end): # emit data between this branch target and the previous outfile.write(" .byte " + ", ".join(f"0x{b:02X}" for b in self.data[prev:b_tgt]) + "\n\n") if b_tgt in range(start_pos,frag.end): # emit the branch target self.emit_branch_target_real(outfile, b_tgt, SqSection.UNKNOWN) prev = b_tgt # write any remaining data if the final branch target was not the end of the frag if prev != frag.end: outfile.write(" .byte " + ", ".join(f"0x{b:02X}" for b in self.data[prev:frag.end]) + "\n\n") # # emit disassembled text # def emit(self): with open(self.outpath, "w") as outfile: # emit header outfile.write("#include \"aseq.h\"\n") # emit fonts for font in self.used_fonts: outfile.write(f"#include \"{font.file_name}.h\"\n") outfile.write("\n") outfile.write(f".startseq {self.seq_name}\n\n") # emit fragments for frag in sorted(self.fragments): frag : SequenceFragment self.cur_section = frag.section self.pos = frag.start { SqSection.SEQ : self.disas_section, SqSection.CHAN : self.disas_section, SqSection.LAYER : self.disas_section, SqSection.TABLE : self.disas_table, SqSection.ARRAY : self.disas_array, SqSection.FILTER : self.disas_filter, SqSection.ENVELOPE : self.disas_envelope, SqSection.UNKNOWN : self.disas_unknown, }[frag.section](frag, outfile) outfile.write(f".endseq {self.seq_name}\n") if __name__ == '__main__': import sys in_path = sys.argv[1] out_path = sys.argv[2] with open(in_path, "rb") as infile: data = bytearray(infile.read()) class FontDummy: def __init__(self, file_name) -> None: self.name = file_name self.file_name = file_name self.instrument_index_map = {} disas = SequenceDisassembler(0, data, None, CMD_SPEC, MMLVersion.MM, out_path, "", [FontDummy("wow")], []) disas.analyze() disas.emit()