1
0
Fork 0
mirror of https://github.com/zeldaret/oot.git synced 2024-12-04 16:55:56 +00:00
oot/tools/audio/extraction/disassemble_sequence.py
Tharo dbbeb656f8
[Audio 8/?] Check-in handwritten sequences, build sequences, automate various sfx arrays (#2137)
* [Audio 8/?] Check-in handwritten sequences, build sequences, automate various sfx arrays

* Fix whitespace in aseq.h

* Fix sequence 0 sfx id generator

* Suggested changes, adjust some MML syntax and add more instruction descriptions

* Correct some formatting in aseq.h

* Add the dir of the input .seq file to the list of includes to sequence assembling so that assembler-level includes like .include or .incbin work intuitively

* aseq.h tweaks

* MM review suggestions, aseq.h adjustments
2024-09-15 18:26:27 -04:00

1296 lines
50 KiB
Python

#!/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.bits_val
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 ArgEffectsConfig(ArgU8):
def emit(self, disas):
assert (self.value & 0b01000000) == 0
headset = str(bool((self.value >> 7) & 1)).upper()
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"{headset}, {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)
class ArgLdSampleInst(MMLArg):
def read(self, disas):
return None
def emit(self, disas):
return "LDSAMPLE_INST"
class ArgLdSampleSfx(MMLArg):
def read(self, disas):
return None
def emit(self, disas):
return "LDSAMPLE_SFX"
#
# 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=(ArgU4x2,)),
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=(ArgEffectsConfig,)),
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(0xA8, 'randptr', sections=(SqSection.CHAN,), args=(ArgU16, ArgU16,), version=(MMLVersion.MM,)),
MMLCmd(0xA7, 'unk_A7', sections=(SqSection.CHAN,), args=(ArgHex8,), version=(MMLVersion.MM,)),
MMLCmd(0xA6, 'unk_A6', sections=(SqSection.CHAN,), args=(ArgU8, ArgS16,), version=(MMLVersion.MM,)),
MMLCmd(0xA5, 'unk_A5', sections=(SqSection.CHAN,), args=(), version=(MMLVersion.MM,)),
MMLCmd(0xA4, 'unk_A4', sections=(SqSection.CHAN,), args=(ArgU8,), version=(MMLVersion.MM,)),
MMLCmd(0xA3, 'unk_A3', sections=(SqSection.CHAN,), args=(), version=(MMLVersion.MM,)),
MMLCmd(0xA2, 'unk_A2', sections=(SqSection.CHAN,), args=(ArgS16,), version=(MMLVersion.MM,)),
MMLCmd(0xA1, 'unk_A1', sections=(SqSection.CHAN,), args=(), version=(MMLVersion.MM,)),
MMLCmd(0xA0, 'unk_A0', sections=(SqSection.CHAN,), args=(ArgS16,), version=(MMLVersion.MM,)),
# argbits commands
MMLCmd(0x00, 'cdelay', sections=(SqSection.CHAN,), args=(ArgBits4,)),
MMLCmd(0x10, 'ldsample', sections=(SqSection.CHAN,), args=(ArgLdSampleInst, IOPort3,)),
MMLCmd(0x18, 'ldsample', sections=(SqSection.CHAN,), args=(ArgLdSampleSfx, IOPort3,)),
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
nbits = 0
for arg in cmd.args:
if issubclass(arg, MMLArgBits):
assert nbits == 0, f"Multiple argbits-type arguments: {cmd}"
nbits = arg.NBITS
id = cmd.cmd_id
for section in cmd.sections:
cmds_s = self.cmds[section]
for i in range(1 << nbits):
new = cmd
new.mask = (1 << nbits) - 1
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_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
self.bits_val = id & cmd.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()