Skip to content

Instantly share code, notes, and snippets.

@SamusAranX
Forked from bbbradsmith/nsfe_to_nsf2.py
Last active February 14, 2021 02:49
Show Gist options
  • Save SamusAranX/cf1c5f7701daf4b42555657d713f47d3 to your computer and use it in GitHub Desktop.
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.
#!/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