diff --git a/album.txt b/album.txt new file mode 100644 index 0000000..75cdafd --- /dev/null +++ b/album.txt @@ -0,0 +1,76 @@ +# EZNSF album description file + +NSF debussy.nsf + +# set NROM to 1 to attempt an NROM (non-banking) build, +# otherwise uses mapper 31 +NROM 0 + +TITLE Suite Bergamasque +ARTIST bradsmith / Claude Debussy +COPYRIGHT 2011 + +# each TRACK has: +# time: either as a "minutes:seconds" or just "seconds" number +# song: the original number of the song in the NSF +# title: after the song number the rest of the line is a title for the track + +TRACK 03:03 1 Prelude +TRACK 03:08 2 Menuet +TRACK 03:26 3 Clair de Lune +TRACK 03:33 4 Passepied + +# text for the info screen + +INFO Information: +INFO +INFO This ROM was built with the +INFO EZNSF music ROM tool, +INFO by Brad Smith, 2016. +INFO +INFO B - Play Track +INFO A - Play All +INFO START - Pause +INFO SELECT - Cancel +INFO +INFO http://rainwarrior.ca + +# each SCREEN has: +# 1k nametable + attributes +# 4k background tiles (nametable) +# 4k foreground tiles (sprites) +# 16b background palette +# 16b foreground palette + +SCREEN TITLE title.nam tiles.chr tiles.chr colors.pal colors.pal +SCREEN INFO info.nam tiles.chr tiles.chr colors.pal colors.pal +SCREEN TRACKS tracks.nam tiles.chr tiles.chr colors.pal colors.pal +SCREEN PLAY play.nam tiles.chr tiles.chr colors.pal colors.pal + +# tile coordinates of text, or pixel coordinates of visual elements + +COORD TITLE_TITLE 2 6 +COORD TITLE_ARTIST 2 7 +COORD TITLE_COPYRIGHT 2 8 +COORD TITLE_START 16 128 # pixel position of start indicator +COORD TITLE_INFO 16 144 # pixel position of info indicator + +COORD INFO 2 2 # subsequent lines start below this point + +COORD TRACKS_TITLE 2 2 +COORD TRACKS_ARTIST 2 3 +COORD TRACKS_COPYRIGHT 2 4 +COORD TRACKS_TRACK 4 6 # subsequent tracks on each line + +COORD PLAY_TRACK 2 23 # tile position of track name +COORD PLAY_TIME 16 168 # pixel position of time indicator + +# sprite tiles used + +CONST SPRITE_CHOOSE 4 +CONST SPRITE_PLAY 4 +CONST SPRITE_PLAY_ALL 5 +CONST SPRITE_PAUSE 6 +CONST SPRITE_STOP 7 +CONST SPRITE_ZERO 48 +CONST SPRITE_COLON 58 diff --git a/album_nrom.txt b/album_nrom.txt new file mode 100644 index 0000000..b0fbce1 --- /dev/null +++ b/album_nrom.txt @@ -0,0 +1,74 @@ +# EZNSF album description file + +NSF brahms.nsf + +# set NROM to 1 to attempt an NROM (non-banking) build, +# otherwise uses mapper 31 +NROM 1 + +TITLE Intermezzo +ARTIST bradsmith / Johannes Brahms +COPYRIGHT 2011 + +# each TRACK has: +# time: either as a "minutes:seconds" or just "seconds" number +# song: the original number of the song in the NSF +# title: after the song number the rest of the line is a title for the track + +TRACK 02:50 1 Op. 75, No. 7 +TRACK 05:09 2 Op. 119, No. 2 + +# text for the info screen + +INFO Information: +INFO +INFO This ROM was built with the +INFO EZNSF music ROM tool, +INFO by Brad Smith, 2016. +INFO +INFO B - Play Track +INFO A - Play All +INFO START - Pause +INFO SELECT - Cancel +INFO +INFO http://rainwarrior.ca + +# each SCREEN has: +# 1k nametable + attributes +# 4k background tiles (nametable) +# 4k foreground tiles (sprites) +# 16b background palette +# 16b foreground palette + +SCREEN TITLE title.nam tiles.chr tiles.chr colors.pal colors.pal +SCREEN INFO info.nam tiles.chr tiles.chr colors.pal colors.pal +SCREEN TRACKS tracks.nam tiles.chr tiles.chr colors.pal colors.pal +SCREEN PLAY play.nam tiles.chr tiles.chr colors.pal colors.pal + +# tile coordinates of text, or pixel coordinates of visual elements + +COORD TITLE_TITLE 2 6 +COORD TITLE_ARTIST 2 7 +COORD TITLE_COPYRIGHT 2 8 +COORD TITLE_START 16 128 # pixel position of start indicator +COORD TITLE_INFO 16 144 # pixel position of info indicator + +COORD INFO 2 2 # subsequent lines start below this point + +COORD TRACKS_TITLE 2 2 +COORD TRACKS_ARTIST 2 3 +COORD TRACKS_COPYRIGHT 2 4 +COORD TRACKS_TRACK 4 6 # subsequent tracks on each line + +COORD PLAY_TRACK 2 23 # tile position of track name +COORD PLAY_TIME 16 168 # pixel position of time indicator + +# sprite tiles used + +CONST SPRITE_CHOOSE 4 +CONST SPRITE_PLAY 4 +CONST SPRITE_PLAY_ALL 5 +CONST SPRITE_PAUSE 6 +CONST SPRITE_STOP 7 +CONST SPRITE_ZERO 48 +CONST SPRITE_COLON 58 diff --git a/brahms.ftm b/brahms.ftm new file mode 100644 index 0000000..e87ef2e Binary files /dev/null and b/brahms.ftm differ diff --git a/brahms.nsf b/brahms.nsf new file mode 100644 index 0000000..8278c75 Binary files /dev/null and b/brahms.nsf differ diff --git a/colors.pal b/colors.pal new file mode 100644 index 0000000..5f220c4 Binary files /dev/null and b/colors.pal differ diff --git a/debussy.ftm b/debussy.ftm new file mode 100644 index 0000000..1f9fcce Binary files /dev/null and b/debussy.ftm differ diff --git a/debussy.nsf b/debussy.nsf new file mode 100644 index 0000000..ee37121 Binary files /dev/null and b/debussy.nsf differ diff --git a/eznsf.bat b/eznsf.bat new file mode 100644 index 0000000..c0b7113 --- /dev/null +++ b/eznsf.bat @@ -0,0 +1,2 @@ +python eznsf.py +pause \ No newline at end of file diff --git a/eznsf.cfg b/eznsf.cfg new file mode 100644 index 0000000..8b3d783 --- /dev/null +++ b/eznsf.cfg @@ -0,0 +1,21 @@ +MEMORY { + ZP: start = $00FC, size = $0004, type = rw, file = ""; + RAM: start = $0600, size = $0100, type = rw, file = ""; + OAM: start = $0700, size = $0100, type = rw, file = ""; + HDR: start = $0000, size = $0010, type = ro, file = "", fill = yes, fillval = $00; + PRG: start = $F000, size = $1000, type = ro, file = %O, fill = yes, fillval = $00; + NSF: start = $F000, size = $1000, type = ro, file = "", fill = yes, fillval = $00; +} + +SEGMENTS { + ZEROPAGE: load = ZP, type = zp, define = yes; + BSS: load = RAM, type = bss; + OAM: load = OAM, type = bss, align = 256; + HEADER: load = HDR, type = ro; + CODE: load = PRG, type = ro; + RAMCODE: load = PRG, run = RAM, type = ro, define = yes; + ALIGN: load = PRG, type = ro, align = 32, optional = yes; + VECTORS: load = PRG, type = ro, start = $FFFA; + NSF_F000: load = NSF, type = ro, start = $F000; + NSF_VECTORS: load = NSF, type = ro, START = $FFFA; +} diff --git a/eznsf.py b/eznsf.py new file mode 100644 index 0000000..59b7ebd --- /dev/null +++ b/eznsf.py @@ -0,0 +1,775 @@ +#!/usr/bin/env python3 +import sys + +if sys.version_info[0] < 3: + print("Python 3 required.") + sys.exit(1) + +# +# EZNSF +# +# bradsmith, 2016 +# http://rainwarrior.ca +# + +import os +import datetime +import shlex +import subprocess + +album = "album.txt" +outdir = "temp" +ca65 = "tools/ca65.exe" +ld65 = "tools/ld65.exe" +output_nsfe = True + +def errmsg(msg): + print("Error: " + msg) + sys.exit(1) + +if len(sys.argv) > 1: + album = sys.argv[1] +if len(sys.argv) > 2: + outdir = sys.argv[2] +if len(sys.argv) > 3: + errmsg("Error: Too many arguments on command line.\n" + \ + "Usage: eznsf.py [album] [directory]") + +now_string = datetime.datetime.now().strftime("%a %b %d %H:%M:%S %Y") + +nsf_file = "" +nsf_nrom = 0 +nsf_title = "" +nsf_artist = "" +nsf_copyright = "" +nsf_tracks = [] +nsf_screens = [] +nsf_info = [] +nsf_coord = [] +nsf_const = [] + +# STEP 0: create output directory and remove files to be generated + +try: + os.makedirs(outdir) +except OSError: + if not os.path.isdir(outdir): + raise + +for file in os.listdir(outdir): + if \ + file.endswith(".bin") \ + or file.endswith(".sh") \ + or file.endswith(".o") \ + or file.endswith(".nes") \ + or file.endswith(".map") \ + or file.endswith(".lab") \ + or file.endswith(".nsfe") \ + : + path = os.path.join(outdir,file) + try: + os.remove(path) + except: + errmsg("Unable to remove temporary file: " + path) + +# STEP 1: parse album file + +try: + album_lines = open(album,"rt").readlines() +except: + errmsg("Unable to read album file: " + album) + +for i in range(len(album_lines)): + def line_error(msg): + errmsg(("Line %d: " % (i+1)) + msg) + l = album_lines[i] + # strip comments + comment = l.find("#") + if comment >= 0: + l = l[0:comment] + # strip trailing whitespace and newline + l = l.rstrip() + # process line + try: + tokens = shlex.split(l) + except Exception as e: + line_error("shlex parsing error: " + str(e)) + if (len(tokens) < 1): + continue # skip blank lines + c = tokens[0] + if c == "NSF": + nsf_file = l[l.find(c)+len(c)+1:] # line to end + elif c == "NROM": + if len(tokens) != 2: + line_error("NROM expects one argument.") + if tokens[1] != "0" and tokens[1] != "1": + line_error("NROM expects 0 or 1.") + nsf_nrom = int(tokens[1]) + elif c == "TITLE": + nsf_title = l[l.find(c)+len(c)+1:] # line to end + elif c == "ARTIST": + nsf_artist = l[l.find(c)+len(c)+1:] # line to end + elif c == "COPYRIGHT": + nsf_copyright = l[l.find(c)+len(c)+1:] # line to end + elif c == "TRACK": + if len(tokens) < 3: + line_error("TRACK expects a time and song argument.") + time = tokens[1] + tnum = tokens[2] + track = l[l.find(tnum,l.find(time)+len(time))+len(tnum)+1:] # line to end + time_mins = "0" + time_secs = time + colon = time.find(":") + if colon >= 0: + time_mins = time[0:colon] + time_secs = time[colon+1:] + try: + mins = int(time_mins) + secs = int(time_secs) + num = int(tnum) + if (num < 1): + line_errot("TRACK song number may not be less than 1.") + nsf_tracks.append((track,num-1,mins,secs)) + except: + line_error("Unable to read time or track argument for TRACK.") + elif c == "SCREEN": + if len(tokens) != 7: + line_error("SCREEN expects 6 arguments.") + nsf_screens.append((tokens[1],tokens[2],tokens[3],tokens[4],tokens[5],tokens[6])) + elif c == "INFO": + nsf_info.append(l[l.find(c)+len(c)+1:]) # line to end + elif c == "COORD": + if len(tokens) != 4: + line_error("COORD expects 3 arguments.") + coord = tokens[1] + try: + coord_x = int(tokens[2]) + coord_y = int(tokens[3]) + nsf_coord.append((coord,coord_x,coord_y)) + except: + line_error("Unable to read number argument for COORD.") + elif c == "CONST": + if len(tokens) != 3: + line_error("CONST expects 2 arguments.") + ct = tokens[1] + try: + cv = int(tokens[2]) + nsf_const.append((ct,cv)) + except: + line_error("Unable to read number argument for CONST.") + else: + line_error("Unknown statement type.") + #print("%d: %s" % (i+1, l)) # diagnostic + +print("album info:") +print(" file: " + nsf_file) +print(" title: " + nsf_title) +print(" artist: " + nsf_artist) +print(" copyright: " + nsf_copyright) +print(" tracks: %d" % len(nsf_tracks)) +print(" screens: %d" % len(nsf_screens)) +print(" info lines: %d" % len(nsf_info)) +print(" coordinates: %d" % len(nsf_coord)) +print(" constants: %d" % len(nsf_const)) +print() + +# STEP 2: parse and package NSF + +try: + nsf = open(nsf_file,"rb").read() +except: + errmsg("Unable to read NSF file: " + nsf_file) + +if len(nsf) < 0x80: + errmsg("NSF file too small: " + nsf_file) + +nsf_bank = [ 0,0,0,0,0,0,0,0 ] +nsf_banks = 0 +nsf_load_addr = nsf[0x08] + (nsf[0x09] << 8) +nsf_init_addr = nsf[0x0A] + (nsf[0x0B] << 8) +nsf_play_addr = nsf[0x0C] + (nsf[0x0D] << 8) +nsf_region = nsf[0x7A] +nsf_banked = False + +for i in range(8): + b = nsf[0x70+i] + nsf_bank[i] = b + if b != 0: + nsf_banked = True + +if not nsf_banked and nsf_load_addr < 0x8000: + errmsg("NSF LOAD address below $8000. WRAM or FDS not supported: " + nsf_file) + +nsf_rom_padding = 0 + +if nsf_banked: + nsf_rom_padding = nsf_load_addr & 0x0FFF + if nsf_nrom != 0: + errmsg("NSF requires bankswitching, cannot be used with NROM: " + nsf_file) +else: + nsf_rom_padding = nsf_load_addr - 0x8000 + for i in range(8): + nsf_bank[i] = i + +nsf_highest_bank = 0 +for i in range(8): + if nsf_bank[i] > nsf_highest_bank: + nsf_highest_bank = nsf_bank[i] + +nsf_f000 = nsf_bank[7] +nsf_rom = bytearray([0] * nsf_rom_padding) + nsf[0x80:] + +print("NSF:") +print(" LOAD: %04X" % nsf_load_addr) +print(" INIT: %04X" % nsf_init_addr) +print(" PLAY: %04X" % nsf_play_addr) +print(" ROM size: %d bytes" % len(nsf_rom)) +print() + +def output_banks(prefix,data,trim=-1,minbanks=0): + banks = 0 + extent = max(len(data),minbanks * 0x1000) + while (banks * 0x1000) < extent: + bank = banks + of = os.path.join(outdir,prefix + ("%02X.bin" % bank)) + try: + offset = bank * 0x1000 + s = data[offset : offset + 0x1000] + if len(s) < 0x1000: + s += bytearray([0] * (0x1000 - len(s))) # pad up to 4k + if bank == trim: + s = s[0:len(s)-6] # trim to make space for vectors + open(of,"wb").write(s) + print("Output: " + of) + except: + errmsg("Unable to write file: " + of) + banks += 1 + return banks + +if nsf_nrom != 0: + # NROM mode = single binary blob + of = os.path.join(outdir,"nsf_nrom.bin") + try: + open(of,"wb").write(nsf_rom) + print("Output: " + of) + except: + errmsg("Unable to write file: " + of) +else: + nsf_banks = output_banks("nsf_",nsf_rom,nsf_f000,nsf_highest_bank+1) + print("NSF 4k banks: %d" % nsf_banks) +print() + +# STEP 3: parse and package screen data + +def unpack_ppu(rle): + data = bytearray() + run = 0 + command = 0 + # command: + # 0 = read new RLE packet + # 1 = read RLE length + # 2 = read RLE byte and emit + # 3 = read uncompressed bytes + # 4 = end of stream reached + for b in rle: + if command == 0: + if b == 0: + command = 1 + else: + run = b + command = 3 + elif command == 1: + if b == 0: + command = 4 + break # end of stream + else: + run = b + command = 2 + elif command == 2: + data = data + bytearray([b] * run) + command = 0 + elif command == 3: + data.append(b) + run -= 1 + if (run < 1): + command = 0 + else: + errmsg("Internal problem in RLE verificaiton.") + if command != 4: + errmsg("RLE end of stream reached without marker.") + return data + +def pack_ppu(data): + # RLE compression + # 1. 0 = RLE to follow + # 1-255 = 1-255 uncompressed bytes to follow, return to 1. + # 2. 0 = end of stream + # 1-255 = number of repeated bytes to follow + # 3. byte to be repeated, return to 1. + + # helper functions to compress an RLE or non-RLE chunk of the stream + def emit_compressed(s): + output = bytearray() + # ensure that s is all the same byte before we RLE compress + for sb in s: + if sb != s[0]: + errmsg("Internal problem in RLE compressor!") + # emit compressed packets + while (len(s) > 0): + emit = min(len(s),255) + output.append(0) + output.append(emit) + output.append(s[0]) + s = s[emit:] + return output + def emit_uncompressed(s): + output = bytearray() + # emit uncompressed packets + while (len(s) > 0): + emit = min(len(s),255) + output.append(emit) + output += s[0:emit] + s = s[emit:] + return output + + # do the compression + rle = bytearray() + run = bytearray() + running = False + for d in data: + run.append(d) + rl = len(run) + if running: + if d != run[rl-2]: + rle += emit_compressed(run[0:rl-1]) + run = run[rl-1:] + running = False + else: + if rl >= 4: + if d == run[rl-2] and d == run[rl-3] and d == run[rl-4]: + rle += emit_uncompressed(run[0:rl-4]) + run = run[rl-4:] + running = True + # any unfinished data left in the buffer should be emitted + if running: + rle += emit_compressed(run) + else: + rle += emit_uncompressed(run) + # mark end of stream + rle.append(0) + rle.append(0) + # verify: + unpacked = unpack_ppu(rle) + if len(unpacked) != len(data): + #compare_rle(data,unpacked,rle) # diagnostic + errmsg("RLE packing verification failed; length mismatch.") + for i in range(0,len(data)): + if unpacked[i] != data[i]: + #compare_rle(data,unpacked,rle) # diagnostic + errmsg("RLE packing verification failed.") + # ready + return rle + +def compare_rle(data,unpacked,packed): + def printbin(s,name): + print("data %s, size: %d" % (name,len(s))) + os = "" + pos = 0 + line_len = 32 + o = 0 + for b in s: + os += (" %02X " % b) + o += 1 + if (o >= line_len): + print (("%04X: " % pos) + os) + os = "" + pos += o + o = 0 + if (o > 0): + print(("%04X: " % pos) + os) + printbin(data,"data") + printbin(unpacked,"unpacked") + printbin(packed,"packed") + +ppu_files = {} +ppu_offsets = {} +ppu_data = bytearray() + +nrom_chr0 = "" +nrom_chr1 = "" + +print("PPU data compression:") +for s in nsf_screens: + for i in range(1,6): + f = s[i] + if nsf_nrom != 0: + if i == 2: + if nrom_chr0 == "": + nrom_chr0 = f + elif nrom_chr0 != f: + errmsg("All NROM screens must use the same two CHR pages.") + elif i == 3: + if nrom_chr1 == "": + nrom_chr1 = f + elif nrom_chr1 != f: + errmsg("All NROM screens must use the same two CHR pages.") + if f in ppu_files: + continue + ppu_files[f] = len(ppu_files) + +ppu_offset = 0 +ppu_files_immutable = sorted(ppu_files.items()) +for (f,i) in ppu_files_immutable: + try: + data = open(f,"rb").read() + except: + errmsg("Unable to read file: " + f) + packed = pack_ppu(data) + ppu_offsets[f] = ppu_offset + if (nrom_chr0 == f) or (nrom_chr1 == f): + packed = bytearray([0,0]) + ppu_offset += len(packed) + ppu_data += packed + print("Compressed: %-20s from %4d / %4d bytes" % (f,len(packed),len(data))) + +if nsf_nrom != 0: + # NROM mode = single binary blob + of = os.path.join(outdir,"ppu_nrom.bin") + try: + open(of,"wb").write(ppu_data) + print("Output: " + of) + except: + errmsg("Unable to write file: " + of) +else: + ppu_banks = output_banks("ppu_",ppu_data) + print("PPU 4k banks: %d" % ppu_banks) + if (ppu_banks > 7): + errmsg("Too many PPU banks! Maximum: 7") +print() + +# STEP 4: generate enums and tables + +# compute needed banks for building +if nsf_nrom == 0: + needed_banks = nsf_banks + ppu_banks + 1 + padded_banks = 1 + while padded_banks < needed_banks: + padded_banks *= 2 +else: + padded_banks = int(32 / 4) + +def ppu_file_enum(s): + so = "" + for c in s: + so += c if ((c>='a' and c<='z') or (c>='A' and c<='Z')) else "_" + return so + +s = "" +s += "; automatically generated by eznsf.py\n" +s += "; " + now_string + "\n" +s += "\n" +s += "MAPPER = %d\n" % (0 if (nsf_nrom != 0) else 31) +s += "BANKS = %d ; 4k bank count\n" % padded_banks +if (nsf_nrom != 0): + s += ".define NROM_CHR0 \"%s\"\n" % nrom_chr0 + s += ".define NROM_CHR1 \"%s\"\n" % nrom_chr1 + s += ".define PPU_NROM_BIN \"%s/ppu_nrom.bin\"\n" % outdir + s += ".define NSF_NROM_BIN \"%s/nsf_nrom.bin\"\n" % outdir +else: + s += ".define NSF_F000 \"%s/nsf_%02X.bin\"\n" % (outdir,nsf_f000) +s += "\n" +s += ".enum eNSF\n" +s += "\tINIT = $%04X\n" % nsf_init_addr +s += "\tPLAY = $%04X\n" % nsf_play_addr +s += "\tREGION = %d\n" % nsf_region +s += "\tTRACKS = %d\n" % len(nsf_tracks) +s += "\tBANK_F000 = $%02X\n" % nsf_f000 +if (nsf_nrom == 0): + s += "\tBANKS = $%02X\n" % nsf_banks +s += ".endenum\n" +s += "\n" +s += ".enum eScreen\n" +for i in range(len(nsf_screens)): + s += "\t%-20s = %2d\n" % (nsf_screens[i][0],i) +s += ".endenum\n" +s += "\n" +s += ".enum ePPU\n" +i = 0 +for (k,v) in ppu_files_immutable: + s += "\t%-20s = %2d\n" % (ppu_file_enum(k),i) + i += 1 +s += ".endenum\n" +s += "\n" +s += ".enum eCoord\n" +for coord in nsf_coord: + s += "\t%-30s = %d\n" % (coord[0] + "_X", coord[1]) + s += "\t%-30s = %d\n" % (coord[0] + "_Y", coord[2]) +s += ".endenum\n" +s += "\n" +s += ".enum eConst\n" +for c in nsf_const: + s += "\t%-30s = %d\n" % (c[0], c[1]) +s += ".endenum\n" +s += "\n" +s += "; end of file\n" + +#print(s) # diagnostic +of = os.path.join(outdir,"enums.sh") +try: + open(of,"wt").write(s) + print("Output: " + of) +except: + errmsg("Unable to write file: " + of) + +s = "" +s += "; automatically generated by eznsf.py\n" +s += "; " + now_string + "\n" +s += "\n" +s += ".scope dString\n" +s += "\ttitle: .asciiz \"" + nsf_title + "\"\n" +s += "\tartist: .asciiz \"" + nsf_artist + "\"\n" +s += "\tcopyright: .asciiz \"" + nsf_copyright + "\"\n" +for i in range(0,len(nsf_tracks)): + s += "\ttrack_%02d: .asciiz \"%s\"\n" % (i,nsf_tracks[i][0]) +s += "\tinfo:\n" +for si in nsf_info: + s += "\t\t.byte \"%s\",13\n" % si +s += "\t\t.byte 0\n" +s += ".endscope\n" +s += "\n" +s += ".scope dNSF\n" +s += "\tbank: .byte $%02X" % nsf_bank[0] +for i in range(1,8): + s += ", $%02X" % nsf_bank[i] +s += "\n" +s += ".endscope\n" +s += "\n" +s += ".scope dTrack\n" +s += "\tstring_table:\n" +for i in range(len(nsf_tracks)): + s += "\t\t.addr dString::track_%02d ; %s\n" % (i,nsf_tracks[i][0]) +s += "\tsong_table:\n" +for i in range(len(nsf_tracks)): + s += "\t\t.byte %3d ; %s\n" % (nsf_tracks[i][1],nsf_tracks[i][0]) +s += "\tlength_table:\n" +for i in range(len(nsf_tracks)): + s += "\t\t.word (%2d * 60) + %2d ; %s\n" % (nsf_tracks[i][2],nsf_tracks[i][3],nsf_tracks[i][0]) +s += ".endscope\n" +s += "\n" +s += ".scope dScreen\n" +s += "\tname_table:\n" +for i in range(len(nsf_screens)): + s += "\t\t.byte ePPU::%-25s ; %s\n" % (ppu_file_enum(nsf_screens[i][1]),nsf_screens[i][0]) +s += "\tchr0_table:\n" +for i in range(len(nsf_screens)): + s += "\t\t.byte ePPU::%-25s ; %s\n" % (ppu_file_enum(nsf_screens[i][2]),nsf_screens[i][0]) +s += "\tchr1_table:\n" +for i in range(len(nsf_screens)): + s += "\t\t.byte ePPU::%-25s ; %s\n" % (ppu_file_enum(nsf_screens[i][3]),nsf_screens[i][0]) +s += "\tpal0_table:\n" +for i in range(len(nsf_screens)): + s += "\t\t.byte ePPU::%-25s ; %s\n" % (ppu_file_enum(nsf_screens[i][4]),nsf_screens[i][0]) +s += "\tpal1_table:\n" +for i in range(len(nsf_screens)): + s += "\t\t.byte ePPU::%-25s ; %s\n" % (ppu_file_enum(nsf_screens[i][5]),nsf_screens[i][0]) +s += ".endscope\n" +s += "\n" +s += ".scope dPPU\n" +s += "\tdata_table:\n" +i = 0 +for (k,v) in ppu_files_immutable: + s += "\t\t.addr data_base + $%04X ; %-20s = %d\n" % (ppu_offsets[k],ppu_file_enum(k),i) + i += 1 +s += ".endscope\n" +s += "\n" +s += "; end of file\n" + +#print(s) # diagnostic +of = os.path.join(outdir,"tables.sh") +try: + open(of,"wt").write(s) + print("Output: " + of) +except: + errmsg("Unable to write file: " + of) + +print() + +# STEP 5: build the code + +def execute(args): + print("Run: " + " ".join(args)) + print() + proc = subprocess.Popen(args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE) + proc.wait() + for l in proc.stdout: + print (l.decode().rstrip()) + return proc.returncode + +link_object = os.path.join(outdir,"eznsf.o") + +#assemble +if (0 != execute([ca65, "eznsf.s", "-I", outdir, "-g", "-o", link_object])): + print() + errmsg("Assemble of eznsf.s has failed!") + +#link +ld65_debug = [ "-m", os.path.join(outdir,"eznsf.map"), "-Ln", os.path.join(outdir,"eznsf.lab") ] +if nsf_nrom != 0: + if (0 != execute([ld65, "-o", os.path.join(outdir,"eznsf.nes"), "-C", "eznsf_nrom.cfg"] + ld65_debug + [link_object])): + print() + errmsg("Link of eznsf.nes has failed!") +else: + if (0 != execute([ld65, "-o", os.path.join(outdir,"eznsf.bin"), "-C", "eznsf.cfg"] + ld65_debug + [link_object])): + print() + errmsg("Link of eznsf.bin has failed!") + if (0 != execute([ld65, "-o", os.path.join(outdir,"f000.bin"), "-C", "eznsf_f000.cfg"] + [link_object])): + print() + errmsg("Link of f000.bin has failed!") + if (0 != execute([ld65, "-o", os.path.join(outdir,"header.bin"), "-C", "eznsf_header.cfg"] + [link_object])): + print() + errmsg("Link of header.bin has failed!") + + # concatenate banks into the ROM file + def readbin(f,seg): + try: + b = open(f,"rb").read() + except: + errmsg("Unable to read file: " + f) + print ("Segment %03X: %s" % (seg & 0xFFF, f)) + return b + + print("Concatenating %d banks..." % padded_banks) + output_nes = bytearray() + output_nes += readbin(os.path.join(outdir,"header.bin"),-1) + seg = 0 + for i in range(nsf_banks): + if i == nsf_f000: + output_nes += readbin(os.path.join(outdir,"f000.bin"), seg) + else: + output_nes += readbin(os.path.join(outdir,"nsf_%02X.bin" % i), seg) + seg += 1 + for i in range(ppu_banks): + output_nes += readbin(os.path.join(outdir,"ppu_%02X.bin" % i), seg) + seg += 1 + for i in range(padded_banks - (nsf_banks + ppu_banks)): + output_nes += readbin(os.path.join(outdir,"eznsf.bin"), seg) + seg += 1 + + # output the file + try: + of = os.path.join(outdir,"eznsf.nes") + open(of,"wb").write(output_nes) + print("Output: " + of) + except: + errmsg("Unable to write file: " + of) + print() + +# STEP 6: assemble NSFE + +# reorganize list by track number +nsfe_tracks = {} +for (t,n,m,s) in nsf_tracks: + nsfe_tracks[n] = (t,m,s) + +if (output_nsfe): + def fourcc(s): + fcc = bytearray() + for i in range(4): + fcc.append(ord(s[i])) + return fcc + + def packword(w): + w = w & 0xFFFF + wb = bytearray() + wb.append(w & 255) + wb.append(w >> 8) + return wb + + def packlong(l): + l = l & 0xFFFFFFFF + lb = bytearray() + lb.append(l & 255) + lb.append((l >> 8) & 255) + lb.append((l >> 16) & 255) + lb.append(l >> 24) + return lb + + def packstring(s): + sb = bytearray(s.encode(encoding="utf-8")) + sb.append(0) + return sb + + def nsfe_chunk(fcc,data): + chunk = bytearray() + chunk += packlong(len(data)) + chunk += fourcc(fcc) + chunk += data + return chunk + + song_count = nsf[0x06] + + nsfe_rom = bytearray() + nsfe_rom += fourcc("NSFE") + + nsfe_info = bytearray() + nsfe_info += packword(nsf_load_addr) + nsfe_info += packword(nsf_init_addr) + nsfe_info += packword(nsf_play_addr) + nsfe_info.append(nsf_region) + nsfe_info.append(nsf[0x7B]) # Expansion + nsfe_info.append(song_count) + nsfe_info.append(nsf[0x07]-1) # Starting song + nsfe_rom += nsfe_chunk("INFO",nsfe_info) + + if nsf_banked: + nsfe_rom += nsfe_chunk("BANK",bytearray(nsf_bank)) + + nsfe_rom += nsfe_chunk("DATA",nsf[0x80:]) + + nsfe_auth = bytearray() + nsfe_auth += packstring(nsf_title) + nsfe_auth += packstring(nsf_artist) + nsfe_auth += packstring(nsf_copyright) + nsfe_auth += packstring("eznsf.py") + nsfe_rom += nsfe_chunk("auth",nsfe_auth) + + nsfe_plst = bytearray() + for (t,n,m,s) in nsf_tracks: + nsfe_plst.append(n) + nsfe_rom += nsfe_chunk("plst",nsfe_plst) + + nsfe_time = bytearray() + for ti in range(0,song_count): + if ti in nsfe_tracks: + (t,m,s) = nsfe_tracks[ti] + nsfe_time += packlong(1000 * ((m*60) + s)) + else: + nsfe_time += packlong(-1) + nsfe_rom += nsfe_chunk("time",nsfe_time) + + nsfe_tlbl = bytearray() + for ti in range(0,song_count): + if ti in nsfe_tracks: + (t,m,s) = nsfe_tracks[ti] + nsfe_tlbl += packstring(t) + else: + nsfe_tlbl.append(0) + nsfe_tlbl.append(0) + nsfe_rom += nsfe_chunk("tlbl",nsfe_tlbl) + + nsfe_rom += nsfe_chunk("text",packstring("eznsf.py")) + + nsfe_rom += nsfe_chunk("NEND",bytearray()) + + of = os.path.join(outdir,"eznsf.nsfe") + try: + open(of,"wb").write(nsfe_rom) + print("Output: " + of) + except: + errmsg("Unable to write file: " + of) + print() + +# STEP OFF! + +print("Success!") +sys.exit(0) diff --git a/eznsf.s b/eznsf.s new file mode 100644 index 0000000..03cc02f --- /dev/null +++ b/eznsf.s @@ -0,0 +1,1091 @@ +; +; EZNSF +; +; bradsmith, 2016 +; http://rainwarrior.ca + +; +; ram usage +; + +.segment "OAM" +.align 256 +oam: .res 256 + +.segment "ZEROPAGE" +ptr: .res 2 ; pointer for indirect addressing +nmi_count: .res 1 ; incremented by NMI handler +gamepad: .res 1 ; gamepad poll result + +.segment "BSS" +pal: .res 1 ; 0 = NTSC (60 fps), 1 = PAL (50 fps) +fps: .res 1 ; reflects NTSC/PAL +frame: .res 1 ; counts 0 to fps to measure a second +sec0: .res 1 ; display timer +sec1: .res 1 +min0: .res 1 +min1: .res 1 +title_choose: .res 1 ; title screen selection +track_choose: .res 1 ; track selection +play_through: .res 1 ; 0 = stop at end of track, 1 = stop at end of album +play_paused: .res 1 ; 0 = playing, 1 = paused, 2 = stopped +play_len: .res 2 ; seconds in track +play_secs: .res 2 ; seconds played so far +gamepad_last: .res 1 ; gamepad from last poll +gamepad_new: .res 1 ; new buttons this poll +temp: .res 2 + +; +; useful definitions +; + +PAD_A = $01 +PAD_B = $02 +PAD_SELECT = $04 +PAD_START = $08 +PAD_U = $10 +PAD_D = $20 +PAD_L = $40 +PAD_R = $80 + +.macro PPU_LATCH addr + lda $2002 + lda #>(addr) + sta $2006 + lda #<(addr) + sta $2006 +.endmacro + +.define PPU_TILE(ax,ay) ($2000+(ay*32)+ax) + +.macro PTR_LOAD addr + lda #<(addr) + sta ptr+0 + lda #>(addr) + sta ptr+1 +.endmacro + +; shorthand for lda/sta +.macro STB addr, val + lda val + sta addr +.endmacro + +; +; generated data +; + +.segment "CODE" +.include "enums.sh" +.include "tables.sh" + +; +; title +; + +.proc title_sprite + jsr oam_clear + STB oam+(0*4)+1, #eConst::SPRITE_CHOOSE + STB oam+(0*4)+2, #2 + lda title_choose + bne @choose_info +@choose_start: + STB oam+(0*4)+3, #eCoord::TITLE_START_X + STB oam+(0*4)+0, #eCoord::TITLE_START_Y-1 + rts +@choose_info: + STB oam+(0*4)+3, #eCoord::TITLE_INFO_X + STB oam+(0*4)+0, #eCoord::TITLE_INFO_Y-1 + rts +.endproc + +.proc mode_title + lda #eScreen::TITLE + jsr load_screen + PPU_LATCH PPU_TILE(eCoord::TITLE_TITLE_X, eCoord::TITLE_TITLE_Y) + PTR_LOAD dString::title + jsr ppu_string + PPU_LATCH PPU_TILE(eCoord::TITLE_ARTIST_X, eCoord::TITLE_ARTIST_Y) + PTR_LOAD dString::artist + jsr ppu_string + PPU_LATCH PPU_TILE(eCoord::TITLE_COPYRIGHT_X, eCoord::TITLE_COPYRIGHT_Y) + PTR_LOAD dString::copyright + jsr ppu_string +@loop: + jsr title_sprite + jsr render_on + jsr gamepad_poll + lda gamepad_new + and #(PAD_L | PAD_R | PAD_U | PAD_D | PAD_SELECT) + beq @move_end + lda title_choose + eor #1 + sta title_choose + jmp @loop + @move_end: + lda gamepad_new + and #(PAD_START | PAD_A | PAD_B) + beq @loop + lda title_choose + bne :+ + jmp mode_tracks + : + jmp mode_info + ; + ; +.endproc + +; +; info +; + +.proc mode_info + lda #eScreen::INFO + jsr load_screen + INFO_ADDR = PPU_TILE(eCoord::INFO_X, eCoord::INFO_Y) + lda #>INFO_ADDR + sta temp+1 + sta $2006 + lda #TRACK_ADDR + sta temp+1 + sta $2006 + lda # 0), error, "RAMCODE segment empty." + ldx #0 + : + lda __RAMCODE_LOAD__, X + sta __RAMCODE_RUN__, X + inx + cpx #<__RAMCODE_SIZE__ + bcc :- + rts +.endproc + +; +; main +; + +.proc main + ; clear second nametable (never used) + PPU_LATCH $2400 + ldy #4 + lda #0 + tax + : + sta $2007 + inx + bne :- + dey + bne :- + jmp mode_title +.endproc + +; +; various helpful functions +; + +; auto-incrementing read +; ptr = pointer to be read from +; Y = 0 +.proc read_ptr + lda (ptr), Y + inc ptr+0 + bne :+ + inc ptr+1 + : + cmp #0 + rts +.endproc + +; A = ePPU index to a PPU data chunk +; data will be unpacked and written directly to $2007 +.proc ppu_unpack + asl + tax + lda dPPU::data_table+0, X + sta ptr+0 + lda dPPU::data_table+1, X + sta ptr+1 + ; RLE format + ; 1. 0 = RLE data follow, 1-255 = this many bytes of uncompressed data follows (return to 1) + ; 2. 0 = data stream is finished, 1-255 = this any bytes of the same byte follows + ; 3. byte to repeated number of times specified in 2, return to 1 + ldy #0 + @rle_loop: + jsr read_ptr + beq @compressed + @uncompressed: + tax + : + jsr read_ptr + sta $2007 + dex + bne :- + jmp @rle_loop + @compressed: + jsr read_ptr + beq @finished + tax + jsr read_ptr + : + sta $2007 + dex + bne :- + jmp @rle_loop + @finished: + rts +.endproc + +; ptr = null or newline terminated string to write to screen +.proc ppu_string + ldy #0 + : + jsr read_ptr + beq :+ + cmp #13 ; newline + beq :+ + sta $2007 + jmp :- + : + rts +.endproc + +; A = eScreen to load +.proc load_screen + sta temp + jsr load_ppu_banks + jsr render_off + ; palettes first so they'll be within vblank + PPU_LATCH $3F00 + ldx temp + lda dScreen::pal0_table, X + jsr ppu_unpack + ldx temp + lda dScreen::pal1_table, X + jsr ppu_unpack + PPU_LATCH $0000 + ldx temp + lda dScreen::chr0_table, X + jsr ppu_unpack + ldx temp + lda dScreen::chr1_table, X + jsr ppu_unpack + PPU_LATCH $2000 + ldx temp + lda dScreen::name_table, X + jsr ppu_unpack + rts +.endproc + +.proc oam_clear + lda #$FF + ldx #0 + : + sta oam, X + inx + inx + inx + inx + bne :- + rts +.endproc + +.proc ppu_temp_line + lda temp+0 + clc + adc #<32 + sta temp+0 + lda temp+1 + adc #>32 + sta temp+1 + sta $2006 + lda temp+0 + sta $2006 + rts +.endproc + +.proc wait_nmi + lda #%10000000 + sta $2000 ; ensure NMI is running before waiting on it + lda nmi_count + : + cmp nmi_count + beq :- + rts +.endproc + +.proc render_off + jsr wait_nmi + lda #0 + sta $2001 + rts +.endproc + +.proc render_on + jsr wait_nmi + ; update sprites + lda #0 + sta $2003 + lda #>oam + sta $4014 + ; set scroll + lda $2002 + lda #0 + sta $2005 + sta $2005 + ; turn on rendering + lda #%00011110 + sta $2001 + rts +.endproc + +; +; gamepad +; + +.proc gamepad_poll + ; remember last state + lda gamepad + sta gamepad_last + ; latch the current controller state + lda #1 + sta $4016 + lda #0 + sta $4016 + ; store high bit in gamepad to mark end of read + lda #%10000000 + sta gamepad + ; read 8 bits from controller port + : + lda $4016 + and #%00000011 + cmp #%00000001 + ror gamepad + bcc :- + ; DPCM conflict may have corrupted first read, test again to make sure +@reread: + lda gamepad + pha ; store previous read on the stack + lda #1 + sta $4016 + lda #0 + sta $4016 + lda #%10000000 + sta gamepad + : + lda $4016 + and #%00000011 + cmp #%00000001 + ror gamepad + bcc :- + pla ; pop the first read to compare + cmp gamepad + bne @reread + ; store buttons pressed this frame + lda gamepad_last + eor gamepad + and gamepad + sta gamepad_new + rts +.endproc + +; +; vector handlers +; + +.proc vec_reset + ; standard startup + sei ; set interrupt flag (unnecessary, unless reset is called from code) + cld ; disable decimal mode + ldx #$40 + stx $4017 ; disable APU IRQ + ldx #$ff + txs ; set up stack + ldx #$00 + stx $2000 ; disable NMI + stx $2001 ; disable render + stx $4010 ; disable DPCM IRQ + stx $4015 ; mute APU + ; + bit $2002 ; clear vblank flag + ; wait for vblank + : + bit $2002 + bpl :- + ; clear memory + ldx #$00 + : + lda #$00 + sta $0000, X + sta $0100, X + sta $0200, X + sta $0300, X + sta $0500, X + sta $0600, X + sta $0700, X + lda #$FF ; OAM clear + sta $0400, X + inx + bne :- + ; wait for second vblank + : + bit $2002 + bpl :- + ; PPU is now warmed up, NES is ready to go! + jsr load_ramcode + ; detect NTSC/PAL + lda $2002 + lda #%10000000 + sta $2000 + jsr detect_region + bne :+ + lda #60 + sta fps + lda #0 + jmp :++ + : + lda #50 + sta fps + lda #1 + : + sta pal + ; leave NMI on forever and begin + jmp main +.endproc + +vec_nmi: + inc nmi_count +vec_irq: + rti + +; +; mapper 31 +; + +.if (MAPPER = 31) + +dPPU::data_base = $8000 +INES_CHR = 0 ; CHR RAM + +.segment "CODE" +.proc load_ppu_banks + ; prepare PPU data at the correct location + ldx #eNSF::BANKS + stx $5FF8 + inx + stx $5FF9 + inx + stx $5FFA + inx + stx $5FFB + inx + stx $5FFC + inx + stx $5FFD + inx + stx $5FFE + rts +.endproc + +.segment "NSF_F000" +.incbin NSF_F000 + +.segment "NSF_VECTORS" +.addr ramcode_nmi +.addr ramcode_reset +.addr ramcode_irq + +; +; mapper 0 +; + +.else + +.segment "CODE" +ppu_nrom: + .incbin PPU_NROM_BIN + dPPU::data_base = ppu_nrom + +.segment "TILES" + .incbin NROM_CHR0 + .incbin NROM_CHR1 + INES_CHR = 1 + +.segment "NSF" +nsf_nrom: + .incbin NSF_NROM_BIN + .assert (nsf_nrom = $8000), error, "nsf_nrom.bin loaded at the wrong address?" + +.segment "CODE" +.proc load_ppu_banks + rts +.endproc + +.endif + +; +; region detection +; + +.if (eNSF::REGION & 2) ; dual region NSF should auto-detect for region + .segment "ALIGN" + .proc detect_region + ; region detect based on code by Damian Yerrick + ; http://wiki.nesdev.com/w/index.php/Detect_TV_system + .align 32 + ldx #0 + ldy #0 + lda nmi_count + @wait1: + cmp nmi_count + beq @wait1 + lda nmi_count + @wait2: + inx + bne :+ + iny + : + cmp nmi_count + beq @wait2 + tya + sec + sbc #10 + ; result is 0 for NTSC, otherwise PAL, Dendy, or Unknown + rts + .endproc +.else + .segment "CODE" + .proc detect_region + lda #(eNSF::REGION & 1) + rts + .endproc +.endif + +; +; vectors +; + +.segment "VECTORS" +.addr vec_nmi +.addr vec_reset +.addr vec_irq + +; +; header +; + +.segment "HEADER" +INES_MAPPER = MAPPER +INES_MIRROR = 1 +INES_SRAM = 0 +.byte 'N', 'E', 'S', $1A ; ID +.byte BANKS / 4 ; 16k iNES units divided into 4k banks +.byte INES_CHR ; CHR RAM if using mapper 31, CHR ROM if NROM +.byte INES_MIRROR | (INES_SRAM << 1) | ((INES_MAPPER & $f) << 4) +.byte (INES_MAPPER & %11110000) +.byte $0, $0, $0, $0, $0, $0, $0, $0 ; padding + +; end of file diff --git a/eznsf_f000.cfg b/eznsf_f000.cfg new file mode 100644 index 0000000..bcbd0ff --- /dev/null +++ b/eznsf_f000.cfg @@ -0,0 +1,21 @@ +MEMORY { + ZP: start = $00FC, size = $0004, type = rw, file = ""; + RAM: start = $0600, size = $0100, type = rw, file = ""; + OAM: start = $0700, size = $0100, type = rw, file = ""; + HDR: start = $0000, size = $0010, type = ro, file = "", fill = yes, fillval = $00; + PRG: start = $F000, size = $1000, type = ro, file = "", fill = yes, fillval = $00; + NSF: start = $F000, size = $1000, type = ro, file = %O, fill = yes, fillval = $00; +} + +SEGMENTS { + ZEROPAGE: load = ZP, type = zp, define = yes; + BSS: load = RAM, type = bss; + OAM: load = OAM, type = bss, align = 256; + HEADER: load = HDR, type = ro; + CODE: load = PRG, type = ro; + RAMCODE: load = PRG, run = RAM, type = ro, define = yes; + ALIGN: load = PRG, type = ro, align = 32, optional = yes; + VECTORS: load = PRG, type = ro, start = $FFFA; + NSF_F000: load = NSF, type = ro, start = $F000; + NSF_VECTORS: load = NSF, type = ro, START = $FFFA; +} diff --git a/eznsf_header.cfg b/eznsf_header.cfg new file mode 100644 index 0000000..3210ee7 --- /dev/null +++ b/eznsf_header.cfg @@ -0,0 +1,21 @@ +MEMORY { + ZP: start = $00FC, size = $0004, type = rw, file = ""; + RAM: start = $0600, size = $0100, type = rw, file = ""; + OAM: start = $0700, size = $0100, type = rw, file = ""; + HDR: start = $0000, size = $0010, type = ro, file = %O, fill = yes, fillval = $00; + PRG: start = $F000, size = $1000, type = ro, file = "", fill = yes, fillval = $00; + NSF: start = $F000, size = $1000, type = ro, file = "", fill = yes, fillval = $00; +} + +SEGMENTS { + ZEROPAGE: load = ZP, type = zp, define = yes; + BSS: load = RAM, type = bss; + OAM: load = OAM, type = bss, align = 256; + HEADER: load = HDR, type = ro; + CODE: load = PRG, type = ro; + RAMCODE: load = PRG, run = RAM, type = ro, define = yes; + ALIGN: load = PRG, type = ro, align = 32, optional = yes; + VECTORS: load = PRG, type = ro, start = $FFFA; + NSF_F000: load = NSF, type = ro, start = $F000; + NSF_VECTORS: load = NSF, type = ro, START = $FFFA; +} diff --git a/eznsf_nrom.cfg b/eznsf_nrom.cfg new file mode 100644 index 0000000..3248bea --- /dev/null +++ b/eznsf_nrom.cfg @@ -0,0 +1,21 @@ +MEMORY { + ZP: start = $00FC, size = $0004, type = rw, file = ""; + RAM: start = $0600, size = $0100, type = rw, file = ""; + OAM: start = $0700, size = $0100, type = rw, file = ""; + HDR: start = $0000, size = $0010, type = ro, file = %O, fill = yes, fillval = $00; + PRG: start = $8000, size = $8000, type = ro, file = %O, fill = yes, fillval = $00; + CHR: start = $0000, size = $2000, type = ro, file = %O, fill = yes, fillval = $00; +} + +SEGMENTS { + ZEROPAGE: load = ZP, type = zp, define = yes; + BSS: load = RAM, type = bss; + OAM: load = OAM, type = bss, align = 256; + HEADER: load = HDR, type = ro; + NSF: load = PRG, type = ro, start = $8000; + CODE: load = PRG, type = ro; + RAMCODE: load = PRG, run = RAM, type = ro, define = yes; + ALIGN: load = PRG, type = ro, align = 32, optional = yes; + VECTORS: load = PRG, type = ro, start = $FFFA; + TILES: load = CHR, type = ro; +} diff --git a/info.nam b/info.nam new file mode 100644 index 0000000..2a73164 Binary files /dev/null and b/info.nam differ diff --git a/play.nam b/play.nam new file mode 100644 index 0000000..4d27a46 Binary files /dev/null and b/play.nam differ diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..4aad6cf --- /dev/null +++ b/readme.txt @@ -0,0 +1,95 @@ + --------------------------------------------- + / EEEEE ZZZZZ N N SSSS FFFFF / + / E Z NN N S F / + / EEE Z N N N SSS FFF / + / E Z N NN S F / + / EEEEE ZZZZZ N N SSSS F / +--------------------------------------------- + + EZNSF is a tool for transforming NSF music files into NES ROMs + + Version 1.0 + Written by Brad Smith, 2016-12-05 + + +Prerequisites: + + 1. Python 3 + Available here: https://www.python.org/downloads/ + 2. CC65 assembler and linker + Available here: http://cc65.github.io/cc65/ + A windows version of CC65's assembler and linker are included in the tools folder. + Users with other platforms can adapt the python script to use another version of CC65. + 3. NES Screen Tool + Available here: https://shiru.untergrund.net/software.shtml + The screen tool is helpful if you wish to modify the graphics produced by the ROM, + but it is not needed for simply building ROMs. + +Directions: + + 1. Create a suitable NSF (see below for requirements). + 2. Edit album.txt with track times and names and other info. + 3. Run eznsf.py to build a ROM as "temp/output.nes". + If there are any errors, look at the output from eznsf.py and try to fix them. + Make sure to run eznsf.py from a command line or shell where you can read its result. + (The included "eznsf.bat" will open a command window and pause to show the output.) + + +NSF Requirements: + + * No expansion sound. + * Standard engine rate only (60/50 Hz) + * NSF may not bankswitch F000 + * NSF may not use RAM (or clear it during init) in the range $600-7FF + * NSF may not use zero page (or clear it) in the range $FC-FF + * NSF may not depend on WRAM present at $6000-7FFF + + This program was primarily intended for use with Famitracker, which + should normally obey all of these requirements since at least 0.4.5 if not earlier. + It can potentially work with many other NSFs. + +Notes: + + The produced ROM will use mapper 31 by default, which is a relatively new creation. + Many old emulators will not support this. Recent versions of the following will: + * FCEUX: http://www.fceux.com/ + * MESS: http://www.mess.org/ + * BizHawk: http://tasvideos.org/BizHawk.html + * Nintendulator: http://www.qmtpro.com/~nes/nintendulator/ + * puNES: http://forums.nesdev.com/viewtopic.php?t=6928 + * Everdrive N8: http://krikzz.com/store/home/31-everdrive-n8-nes.html + + If the NSF does not require bankswitching (many NSFs under 32k), it may be possible to + build as the simple NROM mapper 0, which is supported by all emulators. + Simply create a line that says "NROM = 1" in your album.txt file. + The NROM version will require about 3kb of empty space, so the build may fail if not + enough room can be found. An example is included as "album_nrom.txt". + + This tool also uses the track times and names to produce an NSFe file in the output + directory. An NSFe is like an NSF but with playlist features like individual track + names and times. If undesired you can turn this extra step off in eznsf.py by + setting "output_nsfe = False" near the top of the file. + + NSFs with the "dual region" bit set will attempt to auto-detect the system region on + startup, and will pass the appropriate value to the NSF INIT function when tracks are played. + Otherwise, the detection will be omitted and the region speciied in the NSF header will be used. + + This program is open source, and I give permission to modify and reuse it in any way you like. + I encourage experimentation. This is hopefully intended as a learning example, and not just as + a tool. + + The eznsf.py script can be run with two command line arguments. The first argument is the name + of the album.txt file (if omitted it will use "album.txt"). The second argument is the output + folder (if omitted it will use "temp"). + + The sample music is from the Classic Chips album of classical music arranged for NES: + http://rainwarrior.ca/music/classic_chips.html + + The graphics in this are configurable with album.txt, and by editing their definitions in that file. + Take a look at the comments and experiment to see what they do. Use the NES Screen Tool + to modify the example CHR/NAM/PAL files. + + Contact me if you have questions or comments. + http://rainwarrior.ca + +--- end of file --- diff --git a/temp/eznsf.nes b/temp/eznsf.nes new file mode 100644 index 0000000..e2dc97c Binary files /dev/null and b/temp/eznsf.nes differ diff --git a/temp/eznsf_nrom.nes b/temp/eznsf_nrom.nes new file mode 100644 index 0000000..f0749ea Binary files /dev/null and b/temp/eznsf_nrom.nes differ diff --git a/tiles.chr b/tiles.chr new file mode 100644 index 0000000..2f91b1e Binary files /dev/null and b/tiles.chr differ diff --git a/title.nam b/title.nam new file mode 100644 index 0000000..a8a2015 Binary files /dev/null and b/title.nam differ diff --git a/tools/ca65.exe b/tools/ca65.exe new file mode 100644 index 0000000..dd13948 Binary files /dev/null and b/tools/ca65.exe differ diff --git a/tools/cc65.txt b/tools/cc65.txt new file mode 100644 index 0000000..dfe6ce9 --- /dev/null +++ b/tools/cc65.txt @@ -0,0 +1,6 @@ +ca65.exe +ld65.exe + +From the CC65 Windows Snapshot, 2016-12-04, available at: + +http://cc65.github.io/cc65/ diff --git a/tools/ld65.exe b/tools/ld65.exe new file mode 100644 index 0000000..15341e4 Binary files /dev/null and b/tools/ld65.exe differ diff --git a/tracks.nam b/tracks.nam new file mode 100644 index 0000000..b8a2ec0 Binary files /dev/null and b/tracks.nam differ