Skip to content

Instantly share code, notes, and snippets.

@bbbradsmith
Last active November 5, 2022 09:12
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save bbbradsmith/4bc17ae16b10a9be03e80addfbea5009 to your computer and use it in GitHub Desktop.
Save bbbradsmith/4bc17ae16b10a9be03e80addfbea5009 to your computer and use it in GitHub Desktop.
Utility to convert NSFe music files into the backward compatible NSF2 with metadata format.
#!/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 struct
in_nsfe = "in.nsfe"
out_nsf = "out.nsf"
show_chunks = True
def nsfe_to_nsf2(nsfe):
# 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
print("Reading " + in_nsfe + "...")
nsfe = open(in_nsfe,"rb").read()
nsf = nsfe_to_nsf2(nsfe)
print("Saving " + out_nsf + "...")
open(out_nsf,"wb").write(nsf)
print ("Done.")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment