-
-
Save SamusAranX/cf1c5f7701daf4b42555657d713f47d3 to your computer and use it in GitHub Desktop.
Merge of Brad Smith's nsfe_to_nsf2.py and nsf2_strip.py scripts with a fancy argparse interface slapped on it. I needed this to downgrade an NSFe file far enough so nsf2midi would recognize it. Putting this here in case anyone else has the same problem.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import sys | |
assert sys.version_info[0] >= 3, "Python 3 required." | |
# | |
# nsfe_to_nsf2.py | |
# Brad Smith, 2018-08-24 | |
# | |
# This converts an NSFe file to a preliminary "NSF2 with metadata" format. | |
# | |
import argparse | |
import struct | |
from os.path import exists, split, splitext, basename, join | |
def nsfe_to_nsf2(nsfe, show_chunks): | |
# NSF structure to fill in | |
nsf_header = bytearray([0]*0x80) | |
nsf_data = bytearray() | |
nsf_suffix = bytearray() | |
info = False | |
data = False | |
# build default NSF header info | |
nsf_header[0:5] = b"NESM\x1A" | |
nsf_header[5] = 1 # version | |
nsf_header[0x6E:0x70] = struct.pack("<H",16639) # NTSC speed | |
nsf_header[0x78:0x7A] = struct.pack("<H",19997) # PAL speed | |
# parse NSFe header | |
if len(nsfe) < 4: | |
raise Exception("Does not contain header.") | |
if nsfe[0:4] != b"NSFE": | |
raise Exception("Does not begin with 'NSFE' fourCC.") | |
nsfe = nsfe[4:] | |
# parse NSFe chunks | |
while len(nsfe) > 0: | |
if len(nsfe) < 8: | |
raise Exception("Malformed chunk, does not have 8 bytes for size/fourCC.") | |
size = struct.unpack("<L",nsfe[0:4])[0] | |
fourcc = nsfe[4:8] | |
if show_chunks: | |
print ("'%c%c%c%c' (%d bytes)" % (fourcc[0],fourcc[1],fourcc[2],fourcc[3],size)) | |
if (size+8) > len(nsfe): | |
raise Exception("EOF reached in the middle of a chunk. Incomplete file?") | |
# parse chunk | |
raw_chunk = nsfe[0:8+size] # chunk with header | |
chunk = raw_chunk[8:] # chunk without header | |
if fourcc == b"NEND": | |
nsf_suffix += raw_chunk # append this chunk | |
break # stop parsing here (NEND signals end) | |
elif fourcc == b"INFO": | |
# parse NSFe INFO | |
if size < 9: | |
raise Exception("INFO chunk must be at least 9 bytes.") | |
nsfe_load = chunk[0:2] | |
nsfe_init = chunk[2:4] | |
nsfe_play = chunk[4:6] | |
nsfe_reg = chunk[6] | |
nsfe_exp = chunk[7] | |
nsfe_songs = chunk[8] | |
nsfe_start = 0 | |
if size >= 10: | |
nsfe_start = chunk[9] | |
info = True | |
# fill NSF header | |
nsf_header[0x08:0x0A] = nsfe_load | |
nsf_header[0x0A:0x0C] = nsfe_init | |
nsf_header[0x0C:0x0E] = nsfe_play | |
nsf_header[0x7A] = nsfe_reg | |
nsf_header[0x7B] = nsfe_exp | |
nsf_header[0x06] = nsfe_songs | |
nsf_header[0x07] = (nsfe_start+1) & 255 | |
elif fourcc == b"DATA": | |
nsf_data = bytearray(chunk) | |
data = True | |
elif fourcc == b"BANK": | |
for i in range(0,min(8,size)): | |
nsf_header[0x70+i] = chunk[i] | |
elif fourcc == b"RATE": | |
if size >= 2: | |
nsf_header[0x6E:0x70] = chunk[0:2] # NTSC rate | |
if size >= 4: | |
nsf_header[0x78:0x7A] = chunk[2:4] # PAL rate | |
if size >= 6: | |
nsf_suffix += raw_chunk # append to pass Dendy rate | |
elif fourcc == b"NSF2": | |
nsf_header[5] = 2 # version | |
if size > 0: | |
nsf_header[0x7E] = chunk[0] # pass NSF2 bitfield | |
elif fourcc == b"auth": | |
append = False | |
ci = 0 | |
oi = 0 | |
for ci in range(ci,size): | |
c = chunk[ci] | |
if c != 0: | |
if (oi < 31): | |
nsf_header[0x0E+oi] = c | |
oi += 1 | |
else: | |
append = True | |
else: | |
break | |
ci += 1 | |
oi = 0 | |
for ci in range(ci,size): | |
c = chunk[ci] | |
if c != 0: | |
if (oi < 31): | |
nsf_header[0x2E+oi] = c | |
oi += 1 | |
else: | |
append = True | |
else: | |
break | |
ci += 1 | |
oi = 0 | |
for ci in range(ci,size): | |
c = chunk[ci] | |
if c != 0: | |
if (oi < 31): | |
nsf_header[0x4E+oi] = c | |
oi += 1 | |
else: | |
append = True | |
else: | |
break | |
ci += 1 | |
if size > ci: | |
append = True # ripper name as well | |
if append: # these strings won't fit | |
nsf_suffix += raw_chunk | |
else: # other/unknown chunk | |
nsf_suffix += raw_chunk # append the chunk as-is | |
nsfe = nsfe[8+size:] # next chunk | |
if info == False: | |
raise Exception("No INFO chunk found?") | |
if data == False: | |
raise Exception("No DATA chunk found?") | |
# data length, offset to NSFe suffix | |
nsf_header[0x7D:0x80] = struct.pack("<L",len(nsf_data))[0:3] | |
return nsf_header + nsf_data + nsf_suffix | |
def nsf2_strip(nsf2, show_cut): | |
if len(nsf2) < 0x80: | |
raise Exception("NSF2 too short for header?") | |
nsf = bytearray(nsf2) | |
version = nsf[5] | |
nsf2_bits = nsf[0x7E] | |
if version >= 2 and (nsf2_bits & 0x80) != 0: | |
raise Exception("NSF2 metadata is flagged as essential.") | |
suffix = nsf[0x7D] | (nsf[0x7E]<<8) | (nsf[0x7F]<<16) | |
nsf[0x7D] = 0 | |
nsf[0x7E] = 0 | |
nsf[0x7F] = 0 | |
stripped = 0 | |
if suffix != 0: | |
cut = suffix + 0x80 | |
if cut > len(nsf): | |
raise Exception("NSF2 metadata pointer is past end of file?") | |
stripped = len(nsf) - cut | |
nsf = nsf[0:cut] | |
if show_cut: | |
print("%d bytes stripped." % stripped) | |
return nsf | |
def main(args): | |
for f in args.file: | |
try: | |
nsf_dir, nsf_base = split(f.name) | |
nsf_name, _ = splitext(nsf_base) | |
if nsf_name + ".nsf" == nsf_base: | |
print("Error: Input file is output file.") | |
continue | |
new_nsf_name = join(nsf_dir, nsf_name + ".nsf") | |
print(f"Reading {nsf_base} ...") | |
nsfe = f.read() | |
nsf2 = nsfe_to_nsf2(nsfe, args.debug) | |
nsf = nsf2_strip(nsf2, args.debug) | |
print(f"Saving {basename(new_nsf_name)}...") | |
open(new_nsf_name, "wb").write(nsf) | |
print("Done.") | |
print("-" * 25) | |
except Exception as e: | |
raise e | |
print("Done with all files.") | |
if __name__ == "__main__": | |
parser = argparse.ArgumentParser(description="Specify one or more NSFE files to downgrade. The resulting .nsf files will be written next to the .nsfe files and will overwrite any .nsf files that happen to already exist (unless the new file's name is the same as the input file's).") | |
parser.add_argument("file", type=argparse.FileType("rb"), nargs="+") | |
parser.add_argument("-d", "--debug", action="store_true", help="Show more information") | |
args = parser.parse_args() | |
main(args) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment