nes-cdl-analyze.py with syntax coloring

This HTML file was generated with Kalle's syntaxcolor.py


   1"""
   2Prints a summary of an FCEUX .cdl file in CSV format.
   3By Kalle (http://qalle.net)
   4
   5Format of each CDL byte (from FCEUX help file):
   6    for PRG-ROM: -PdcAADC
   7        P  = logged as PCM audio data
   8        d  = indirectly accessed as data; e.g. JMP ($nnnn)
   9        c  = indirectly accessed as code; e.g. LDA ($nn),Y
  10        AA = ROM bank when last accessed; $8000 + AA * $2000
  11        D  = accessed as data
  12        C  = accessed as code
  13    for CHR-ROM: ------RD
  14        R  = read programmatically via $2007
  15        D  = drawn on screen (rendered by PPU)
  16"""
  17
  18import sys
  19
  20BUFFER_SIZE = 2 ** 20
  21
  22MIN_CDL_SIZE = 16 * 1024
  23MAX_CDL_SIZE = 0xff * (16 + 8) * 1024
  24CDL_SIZE_MULTIPLE = 8 * 1024
  25
  26MIN_PRG_SIZE = 16 * 1024
  27MAX_PRG_SIZE = 0xff * 16 * 1024
  28PRG_SIZE_MULTIPLE = 16 * 1024
  29
  30HELP_TEXT = """\
  31Reads a .cdl file created with FCEUX's Code/Data Logger and prints it in CSV
  32format, with runs of same consecutive bytes combined.
  33
  34Arguments: CDLFile PrgSize Part
  35    CDLFile
  36        .cdl file to read
  37    PrgSize
  38        size of the PRG-ROM part of the CDL file, in kilobytes
  39    Part
  40        which part to read from the .cdl file:
  41        "p" or "P" = PRG-ROM
  42        "c" or "C" = CHR-ROM\
  43"""
  44
  45def validate_file_size(size, type_, min_, max_, multiple):
  46    """Exit if size doesn't match conditions."""
  47
  48    if not min_ <= size <= max_ or size % multiple > 0:
  49        exit(
  50            "{:s} file size must be between {:d} and {:d} bytes and a "
  51            "multiple of {:d} bytes."
  52            .format(type_, min_, max_, multiple)
  53        )
  54
  55def read_CDL_chunks(hnd, prgSize, part):
  56    """
  57    Read specified part of .cdl file and encode it as a list of chunks.
  58
  59    Args:
  60        hnd: handle to read
  61        prgSize: PRG-ROM size in bytes
  62        part: "P" = PRG-ROM chunks, "C" = CHR-ROM chunks
  63
  64    Returns:
  65        [(chunk1Addr, chunk1Len, chunk1Byte), ...],
  66    """
  67
  68    fileSize = hnd.seek(0, 2)
  69
  70    if part == "P":
  71        firstAddr = 0
  72        lastAddr = prgSize
  73        ANDMask = 0x7f
  74    else:
  75        firstAddr = prgSize
  76        lastAddr = fileSize
  77        ANDMask = 0x03
  78
  79    if firstAddr == lastAddr:
  80        exit("Nothing to read.")
  81
  82    hnd.seek(firstAddr)
  83
  84    chunkAddr = 0   # start address of current chunk
  85    chunkByte = -1  # type of current chunk
  86    chunks = []
  87
  88    while hnd.tell() < lastAddr:
  89        # fill buffer
  90        bufferAddr = hnd.tell()
  91        buffer = hnd.read(min(BUFFER_SIZE, lastAddr - hnd.tell()))
  92
  93        # process buffer
  94        for (offset, byte) in enumerate(buffer):
  95            byte &= ANDMask
  96
  97            # if chunk type changes, save chunk info
  98            if byte != chunkByte:
  99                addr = bufferAddr + offset
 100
 101                if addr > firstAddr:
 102                    chunks.append(
 103                        (chunkAddr - firstAddr, addr - chunkAddr, chunkByte)
 104                    )
 105
 106                chunkAddr = addr
 107                chunkByte = byte
 108
 109    # save last chunk info
 110    chunks.append((chunkAddr - firstAddr, lastAddr - chunkAddr, chunkByte))
 111
 112    return chunks
 113
 114def print_prg_chunks(chunks):
 115    headings = (
 116        "Chunk address",
 117        "Chunk length",
 118        "Byte AND $7F",
 119        "PCM",
 120        "Indirect data",
 121        "Indirect code",
 122        "Bank",
 123        "Data",
 124        "Code",
 125    )
 126
 127    print('"' + '","'.join(headings) + '"')
 128
 129    formatCode = "{:d},{:d},{:d},{:d},{:d},{:d},{:d},{:d},{:d}"
 130
 131    for (addr, len_, byte) in chunks:
 132        PCM = (byte >> 6) & 0x01
 133        indirData = (byte >> 5) & 0x01
 134        indirCode = (byte >> 4) & 0x01
 135        bank = 0x8000 + ((byte >> 2) & 0x03) * 0x2000
 136        data = (byte >> 1) & 0x01
 137        code = byte & 0x01
 138
 139        print(formatCode.format(
 140            addr, len_, byte, PCM, indirData, indirCode, bank, data, code
 141        ))
 142
 143def print_chr_chunks(chunks):
 144    headings = (
 145        "Chunk address",
 146        "Chunk length",
 147        "Byte AND $03",
 148        "Read via $2007",
 149        "Rendered",
 150    )
 151
 152    print('"' + '","'.join(headings) + '"')
 153
 154    formatCode = "{:d},{:d},{:d},{:d},{:d}"
 155
 156    for (addr, len_, byte) in chunks:
 157        read = (byte >> 1) & 0x01
 158        rendered = byte & 0x01
 159
 160        print(formatCode.format(addr, len_, byte, read, rendered))
 161
 162def main():
 163    if len(sys.argv) != 4:
 164        exit(HELP_TEXT)
 165
 166    (source, prgSizeArg, part) = sys.argv[1:]
 167
 168    # validate PRG-ROM size argument
 169
 170    try:
 171        prgSizeKB = int(prgSizeArg)
 172    except ValueError:
 173        exit("PRG-ROM size argument must be a number.")
 174
 175    prgSize = prgSizeKB * 1024
 176
 177    validate_file_size(
 178        prgSize, "PRG", MIN_PRG_SIZE, MAX_PRG_SIZE, PRG_SIZE_MULTIPLE
 179    )
 180
 181    # validate part argument
 182    part = part.upper()
 183    if part not in ("P", "C"):
 184        exit("Invalid part argument.")
 185
 186    # read source file
 187    try:
 188        with open(source, "rb") as hnd:
 189            fileSize = hnd.seek(0, 2)
 190
 191            validate_file_size(
 192                fileSize, "CDL", MIN_CDL_SIZE, MAX_CDL_SIZE, CDL_SIZE_MULTIPLE
 193            )
 194
 195            if prgSize > fileSize:
 196                exit("PRG-ROM size is greater than file size.")
 197
 198            chunks = read_CDL_chunks(hnd, prgSize, part)
 199    except FileNotFoundError:
 200        exit("Source file not found.")
 201    except PermissionError:
 202        exit("Source file permission denied.")
 203    except OSError:
 204        exit("Error reading source file.")
 205
 206    # print summary
 207    if part == "P":
 208        print_prg_chunks(chunks)
 209    else:
 210        print_chr_chunks(chunks)
 211
 212if __name__ == "__main__":
 213    main()