Task 461: .NSF File Format
Task 461: .NSF File Format
1. File Format Specifications for the .NSF File Format
The .NSF (NES Sound Format) file format is used to store music and sound data from Nintendo Entertainment System (NES) / Famicom games. It consists of a 128-byte (0x80) header followed by binary data containing 6502 machine code and music assets. The format emulates NES audio hardware, including optional expansion chips. The specification is primarily for NSF version 1 (most common), with NSF2 as a rarely used extension that adds features like non-returning play routines and larger data sizes but maintains backward compatibility. All multi-byte values are little-endian. The data payload starts at offset 0x80 and is loaded into NES memory at the specified load address (typically $8000-$FFFF), with optional bankswitching.
Sources for the specification include detailed documentation from OverClocked ReMix and VGMrips, which align closely.
2. List of All Properties Intrinsic to the .NSF File Format
The .NSF format is a flat binary file format, not a file system (e.g., no directories, inodes, or allocation tables). Its "intrinsic properties" refer to the structured metadata in the 128-byte header, which defines how the payload (code and data) is loaded and executed. There are no additional file system-like properties; the format is linear with no internal hierarchy. Unknown text fields default to "<?>". Reserved fields must be 0x00. The payload has no fixed properties beyond size (file length - 0x80) and is interpreted by the code at init/play addresses.
- Signature (offset 0x00, 5 bytes): Fixed ASCII string "NESM\x1A" (NESM followed by 0x1A). Identifies the file as NSF.
- Version (offset 0x05, 1 byte): Format version (0x01 for NSF1; 0x02 for NSF2, which supports non-returning play routines and extended data).
- Total Songs (offset 0x06, 1 byte): Number of songs/tunes (1-255).
- Starting Song (offset 0x07, 1 byte): Initial song index (1-255; 1-based).
- Load Address (offset 0x08, 2 bytes): 16-bit little-endian address ($8000-$FFFF) where payload is loaded into NES memory. For bankswitched files, low 12 bits indicate padding in bank 0.
- Init Address (offset 0x0A, 2 bytes): 16-bit little-endian address ($8000-$FFFF) of initialization routine (called once with A = song-1, X = 0/1 for NTSC/PAL).
- Play Address (offset 0x0C, 2 bytes): 16-bit little-endian address ($8000-$FFFF) of playback routine (called periodically for audio generation).
- Song Name (offset 0x0E, 32 bytes): Null-terminated ASCII string (max 31 chars + 0x00; padded with 0x00). Title of the song/game.
- Artist (offset 0x2E, 32 bytes): Null-terminated ASCII string (max 31 chars + 0x00; padded with 0x00). Composer or artist name.
- Copyright (offset 0x4E, 32 bytes): Null-terminated ASCII string (max 31 chars + 0x00; padded with 0x00). Copyright information.
- NTSC Speed (offset 0x6E, 2 bytes): 16-bit little-endian tick interval (in 1/1,000,000th seconds) for NTSC playback rate (default ~16666 for 60 Hz).
- Bankswitch Init Values (offset 0x70, 8 bytes): Array of 8 bytes for initial 4KB PRG bank values ($8000-$FFFF). All 0x00 disables bankswitching; non-zero enables it (written to $5FF8-$5FFF).
- PAL Speed (offset 0x78, 2 bytes): 16-bit little-endian tick interval (in 1/1,000,000th seconds) for PAL playback rate (default ~20000 for 50 Hz).
- Video System (offset 0x7A, 1 byte): Bitfield for region: bit 0 set = PAL tune; bit 1 set = dual NTSC/PAL; bits 2-7 must be 0.
- Extra Sound Chips (offset 0x7B, 1 byte): Bitfield for expansion audio: bit 0 = VRC6; bit 1 = VRC7; bit 2 = FDS; bit 3 = MMC5; bit 4 = Namco 163; bit 5 = Sunsoft 5B/FME-07; bits 6-7 must be 0.
- Reserved (offset 0x7C, 4 bytes): Must be 0x00; for future expansion (NSF2 may use for additional flags).
- Data Payload (offset 0x80 to EOF): Variable-length binary data (6502 code, music, samples). Loaded per load/bankswitch rules; no intrinsic sub-properties.
3. Two Direct Download Links for .NSF Files
- Super Mario Bros. NSF: http://nesninja.com/public/NSF/Super Mario Bros.nsf
- The Legend of Zelda NSF: http://nesninja.com/public/NSF/The Legend of Zelda.nsf
4. Ghost Blog Embedded HTML/JavaScript for Drag-and-Drop .NSF Property Dump
This is a simple self-contained HTML page with embedded JavaScript that acts as a "ghost blog" (minimal static page) for dragging and dropping an .NSF file. It reads the file as binary, decodes the header properties, and dumps them to the screen. It uses FileReader for browser-based parsing.
Drag and Drop .NSF File to Dump Properties
5. Python Class for .NSF Handling
This Python class opens an .NSF file, decodes the header properties, prints them to console, and can write modified properties back to a new file.
import struct
import sys
class NSFHandler:
def __init__(self, filename):
self.filename = filename
self.properties = {}
self.payload = b''
self._read()
def _read(self):
with open(self.filename, 'rb') as f:
data = f.read()
if len(data) < 128:
raise ValueError("Invalid NSF file: too short")
# Signature
self.properties['signature'] = data[0:5].decode('ascii', errors='ignore')
# Version
self.properties['version'] = data[5]
# Total Songs
self.properties['total_songs'] = data[6]
# Starting Song
self.properties['starting_song'] = data[7]
# Load Address
self.properties['load_address'] = struct.unpack('<H', data[8:10])[0]
# Init Address
self.properties['init_address'] = struct.unpack('<H', data[10:12])[0]
# Play Address
self.properties['play_address'] = struct.unpack('<H', data[12:14])[0]
# Song Name
self.properties['song_name'] = self._get_null_term_string(data[14:46])
# Artist
self.properties['artist'] = self._get_null_term_string(data[46:78])
# Copyright
self.properties['copyright'] = self._get_null_term_string(data[78:110])
# NTSC Speed
self.properties['ntsc_speed'] = struct.unpack('<H', data[110:112])[0]
# Bankswitch Init
self.properties['bankswitch_init'] = list(data[112:120])
# PAL Speed
self.properties['pal_speed'] = struct.unpack('<H', data[120:122])[0]
# Video System
self.properties['video_system'] = data[122]
# Extra Sound Chips
self.properties['extra_chips'] = data[123]
# Reserved
self.properties['reserved'] = list(data[124:128])
# Payload
self.payload = data[128:]
def _get_null_term_string(self, bytes_data):
null_index = bytes_data.find(b'\x00')
return bytes_data[:null_index].decode('ascii', errors='ignore') if null_index != -1 else bytes_data.decode('ascii', errors='ignore') or '<?>'
def print_properties(self):
print("NSF Properties:")
for key, value in self.properties.items():
print(f"{key.capitalize().replace('_', ' ')}: {value}")
print(f"Data Payload Size: {len(self.payload)} bytes")
def write(self, output_filename):
header = bytearray(128)
# Signature
header[0:5] = self.properties['signature'].encode('ascii')
# Version
header[5] = self.properties['version']
# Total Songs
header[6] = self.properties['total_songs']
# Starting Song
header[7] = self.properties['starting_song']
# Load Address
struct.pack_into('<H', header, 8, self.properties['load_address'])
# Init Address
struct.pack_into('<H', header, 10, self.properties['init_address'])
# Play Address
struct.pack_into('<H', header, 12, self.properties['play_address'])
# Song Name
song_name_bytes = self.properties['song_name'].encode('ascii')[:31] + b'\x00'
header[14:14+len(song_name_bytes)] = song_name_bytes
header[14+len(song_name_bytes):46] = b'\x00' * (32 - len(song_name_bytes))
# Artist
artist_bytes = self.properties['artist'].encode('ascii')[:31] + b'\x00'
header[46:46+len(artist_bytes)] = artist_bytes
header[46+len(artist_bytes):78] = b'\x00' * (32 - len(artist_bytes))
# Copyright
copyright_bytes = self.properties['copyright'].encode('ascii')[:31] + b'\x00'
header[78:78+len(copyright_bytes)] = copyright_bytes
header[78+len(copyright_bytes):110] = b'\x00' * (32 - len(copyright_bytes))
# NTSC Speed
struct.pack_into('<H', header, 110, self.properties['ntsc_speed'])
# Bankswitch Init
header[112:120] = bytes(self.properties['bankswitch_init'])
# PAL Speed
struct.pack_into('<H', header, 120, self.properties['pal_speed'])
# Video System
header[122] = self.properties['video_system']
# Extra Sound Chips
header[123] = self.properties['extra_chips']
# Reserved
header[124:128] = bytes(self.properties['reserved'])
with open(output_filename, 'wb') as f:
f.write(header + self.payload)
# Example usage
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python nsf_handler.py <input.nsf> [output.nsf]")
sys.exit(1)
handler = NSFHandler(sys.argv[1])
handler.print_properties()
if len(sys.argv) > 2:
handler.write(sys.argv[2])
print(f"Written to {sys.argv[2]}")
6. Java Class for .NSF Handling
This Java class opens an .NSF file, decodes the properties, prints them to console, and can write to a new file.
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Paths;
public class NSFHandler {
private String filename;
private byte[] header = new byte[128];
private byte[] payload;
private String signature;
private int version;
private int totalSongs;
private int startingSong;
private int loadAddress;
private int initAddress;
private int playAddress;
private String songName;
private String artist;
private String copyright;
private int ntscSpeed;
private byte[] bankswitchInit = new byte[8];
private int palSpeed;
private int videoSystem;
private int extraChips;
private byte[] reserved = new byte[4];
public NSFHandler(String filename) throws IOException {
this.filename = filename;
read();
}
private void read() throws IOException {
byte[] data = Files.readAllBytes(Paths.get(filename));
if (data.length < 128) {
throw new IOException("Invalid NSF file: too short");
}
System.arraycopy(data, 0, header, 0, 128);
payload = new byte[data.length - 128];
System.arraycopy(data, 128, payload, 0, payload.length);
ByteBuffer bb = ByteBuffer.wrap(header).order(ByteOrder.LITTLE_ENDIAN);
// Signature
signature = new String(header, 0, 5, "ASCII");
// Version
version = bb.get(5) & 0xFF;
// Total Songs
totalSongs = bb.get(6) & 0xFF;
// Starting Song
startingSong = bb.get(7) & 0xFF;
// Load Address
loadAddress = bb.getShort(8) & 0xFFFF;
// Init Address
initAddress = bb.getShort(10) & 0xFFFF;
// Play Address
playAddress = bb.getShort(12) & 0xFFFF;
// Song Name
songName = getNullTermString(header, 14, 32);
// Artist
artist = getNullTermString(header, 46, 32);
// Copyright
copyright = getNullTermString(header, 78, 32);
// NTSC Speed
ntscSpeed = bb.getShort(110) & 0xFFFF;
// Bankswitch Init
System.arraycopy(header, 112, bankswitchInit, 0, 8);
// PAL Speed
palSpeed = bb.getShort(120) & 0xFFFF;
// Video System
videoSystem = bb.get(122) & 0xFF;
// Extra Chips
extraChips = bb.get(123) & 0xFF;
// Reserved
System.arraycopy(header, 124, reserved, 0, 4);
}
private String getNullTermString(byte[] data, int offset, int maxLen) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < maxLen; i++) {
byte b = data[offset + i];
if (b == 0) break;
sb.append((char) b);
}
String str = sb.toString();
return str.isEmpty() ? "<?>" : str;
}
public void printProperties() {
System.out.println("NSF Properties:");
System.out.println("Signature: " + signature);
System.out.println("Version: " + version);
System.out.println("Total Songs: " + totalSongs);
System.out.println("Starting Song: " + startingSong);
System.out.println("Load Address: 0x" + Integer.toHexString(loadAddress).toUpperCase());
System.out.println("Init Address: 0x" + Integer.toHexString(initAddress).toUpperCase());
System.out.println("Play Address: 0x" + Integer.toHexString(playAddress).toUpperCase());
System.out.println("Song Name: " + songName);
System.out.println("Artist: " + artist);
System.out.println("Copyright: " + copyright);
System.out.println("NTSC Speed: " + ntscSpeed);
System.out.print("Bankswitch Init: [");
for (int i = 0; i < 8; i++) {
System.out.print("0x" + Integer.toHexString(bankswitchInit[i] & 0xFF).toUpperCase());
if (i < 7) System.out.print(", ");
}
System.out.println("]");
System.out.println("PAL Speed: " + palSpeed);
System.out.println("Video System: 0x" + Integer.toHexString(videoSystem).toUpperCase());
System.out.println("Extra Sound Chips: 0x" + Integer.toHexString(extraChips).toUpperCase());
System.out.print("Reserved: [");
for (int i = 0; i < 4; i++) {
System.out.print("0x" + Integer.toHexString(reserved[i] & 0xFF).toUpperCase());
if (i < 3) System.out.print(", ");
}
System.out.println("]");
System.out.println("Data Payload Size: " + payload.length + " bytes");
}
public void write(String outputFilename) throws IOException {
ByteBuffer bb = ByteBuffer.wrap(header).order(ByteOrder.LITTLE_ENDIAN);
// Signature
byte[] sigBytes = signature.getBytes("ASCII");
System.arraycopy(sigBytes, 0, header, 0, 5);
// Version
bb.put(5, (byte) version);
// Total Songs
bb.put(6, (byte) totalSongs);
// Starting Song
bb.put(7, (byte) startingSong);
// Load Address
bb.putShort(8, (short) loadAddress);
// Init Address
bb.putShort(10, (short) initAddress);
// Play Address
bb.putShort(12, (short) playAddress);
// Song Name
setNullTermString(header, 14, songName, 32);
// Artist
setNullTermString(header, 46, artist, 32);
// Copyright
setNullTermString(header, 78, copyright, 32);
// NTSC Speed
bb.putShort(110, (short) ntscSpeed);
// Bankswitch Init
System.arraycopy(bankswitchInit, 0, header, 112, 8);
// PAL Speed
bb.putShort(120, (short) palSpeed);
// Video System
bb.put(122, (byte) videoSystem);
// Extra Chips
bb.put(123, (byte) extraChips);
// Reserved
System.arraycopy(reserved, 0, header, 124, 4);
try (FileOutputStream fos = new FileOutputStream(outputFilename)) {
fos.write(header);
fos.write(payload);
}
}
private void setNullTermString(byte[] header, int offset, String str, int maxLen) {
byte[] strBytes = str.getBytes();
int len = Math.min(strBytes.length, maxLen - 1);
System.arraycopy(strBytes, 0, header, offset, len);
header[offset + len] = 0;
for (int i = len + 1; i < maxLen; i++) {
header[offset + i] = 0;
}
}
public static void main(String[] args) throws IOException {
if (args.length < 1) {
System.out.println("Usage: java NSFHandler <input.nsf> [output.nsf]");
System.exit(1);
}
NSFHandler handler = new NSFHandler(args[0]);
handler.printProperties();
if (args.length > 1) {
handler.write(args[1]);
System.out.println("Written to " + args[1]);
}
}
}
7. JavaScript Class for .NSF Handling
This Node.js class opens an .NSF file, decodes properties, prints to console, and can write to a new file. Run with Node.js (e.g., node nsf_handler.js input.nsf [output.nsf]).
const fs = require('fs');
class NSFHandler {
constructor(filename) {
this.filename = filename;
this.properties = {};
this.payload = Buffer.alloc(0);
this.read();
}
read() {
const data = fs.readFileSync(this.filename);
if (data.length < 128) {
throw new Error('Invalid NSF file: too short');
}
const header = data.slice(0, 128);
this.payload = data.slice(128);
// Signature
this.properties.signature = header.slice(0, 5).toString('ascii');
// Version
this.properties.version = header.readUInt8(5);
// Total Songs
this.properties.total_songs = header.readUInt8(6);
// Starting Song
this.properties.starting_song = header.readUInt8(7);
// Load Address
this.properties.load_address = header.readUInt16LE(8);
// Init Address
this.properties.init_address = header.readUInt16LE(10);
// Play Address
this.properties.play_address = header.readUInt16LE(12);
// Song Name
this.properties.song_name = this.getNullTermString(header.slice(14, 46));
// Artist
this.properties.artist = this.getNullTermString(header.slice(46, 78));
// Copyright
this.properties.copyright = this.getNullTermString(header.slice(78, 110));
// NTSC Speed
this.properties.ntsc_speed = header.readUInt16LE(110);
// Bankswitch Init
this.properties.bankswitch_init = Array.from(header.slice(112, 120));
// PAL Speed
this.properties.pal_speed = header.readUInt16LE(120);
// Video System
this.properties.video_system = header.readUInt8(122);
// Extra Sound Chips
this.properties.extra_chips = header.readUInt8(123);
// Reserved
this.properties.reserved = Array.from(header.slice(124, 128));
}
getNullTermString(buffer) {
const nullIndex = buffer.indexOf(0);
const str = nullIndex !== -1 ? buffer.slice(0, nullIndex).toString('ascii') : buffer.toString('ascii');
return str || '<?>';
}
printProperties() {
console.log('NSF Properties:');
for (const [key, value] of Object.entries(this.properties)) {
if (Array.isArray(value)) {
console.log(`${key.replace(/_/g, ' ')}: [${value.map(v => `0x${v.toString(16).padStart(2, '0')}`).join(', ')}]`);
} else if (typeof value === 'number' && key.includes('address')) {
console.log(`${key.replace(/_/g, ' ')}: 0x${value.toString(16).toUpperCase().padStart(4, '0')}`);
} else if (typeof value === 'number') {
console.log(`${key.replace(/_/g, ' ')}: ${value}`);
} else {
console.log(`${key.replace(/_/g, ' ')}: ${value}`);
}
}
console.log(`Data Payload Size: ${this.payload.length} bytes`);
}
write(outputFilename) {
const header = Buffer.alloc(128);
// Signature
header.write(this.properties.signature, 0, 5, 'ascii');
// Version
header.writeUInt8(this.properties.version, 5);
// Total Songs
header.writeUInt8(this.properties.total_songs, 6);
// Starting Song
header.writeUInt8(this.properties.starting_song, 7);
// Load Address
header.writeUInt16LE(this.properties.load_address, 8);
// Init Address
header.writeUInt16LE(this.properties.init_address, 10);
// Play Address
header.writeUInt16LE(this.properties.play_address, 12);
// Song Name
this.setNullTermString(header, 14, this.properties.song_name, 32);
// Artist
this.setNullTermString(header, 46, this.properties.artist, 32);
// Copyright
this.setNullTermString(header, 78, this.properties.copyright, 32);
// NTSC Speed
header.writeUInt16LE(this.properties.ntsc_speed, 110);
// Bankswitch Init
Buffer.from(this.properties.bankswitch_init).copy(header, 112);
// PAL Speed
header.writeUInt16LE(this.properties.pal_speed, 120);
// Video System
header.writeUInt8(this.properties.video_system, 122);
// Extra Sound Chips
header.writeUInt8(this.properties.extra_chips, 123);
// Reserved
Buffer.from(this.properties.reserved).copy(header, 124);
fs.writeFileSync(outputFilename, Buffer.concat([header, this.payload]));
}
setNullTermString(buffer, offset, str, maxLen) {
const strBytes = Buffer.from(str);
const len = Math.min(strBytes.length, maxLen - 1);
strBytes.copy(buffer, offset, 0, len);
buffer.writeUInt8(0, offset + len);
for (let i = len + 1; i < maxLen; i++) {
buffer.writeUInt8(0, offset + i);
}
}
}
// Example usage
if (process.argv.length < 3) {
console.log('Usage: node nsf_handler.js <input.nsf> [output.nsf]');
process.exit(1);
}
const handler = new NSFHandler(process.argv[2]);
handler.printProperties();
if (process.argv.length > 3) {
handler.write(process.argv[3]);
console.log(`Written to ${process.argv[3]}`);
}
8. C++ Class for .NSF Handling
This C++ class opens an .NSF file, decodes properties, prints to console, and can write to a new file. Compile with g++ nsf_handler.cpp -o nsf_handler and run ./nsf_handler input.nsf [output.nsf].
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <iomanip>
#include <cstring>
class NSFHandler {
private:
std::string filename;
std::vector<unsigned char> header;
std::vector<unsigned char> payload;
std::string signature;
unsigned char version;
unsigned char total_songs;
unsigned char starting_song;
unsigned short load_address;
unsigned short init_address;
unsigned short play_address;
std::string song_name;
std::string artist;
std::string copyright_info;
unsigned short ntsc_speed;
unsigned char bankswitch_init[8];
unsigned short pal_speed;
unsigned char video_system;
unsigned char extra_chips;
unsigned char reserved[4];
public:
NSFHandler(const std::string& fn) : filename(fn) {
read();
}
void read() {
std::ifstream file(filename, std::ios::binary | std::ios::ate);
if (!file) {
throw std::runtime_error("Cannot open file");
}
size_t size = file.tellg();
file.seekg(0);
if (size < 128) {
throw std::runtime_error("Invalid NSF file: too short");
}
std::vector<unsigned char> data(size);
file.read(reinterpret_cast<char*>(data.data()), size);
header.assign(data.begin(), data.begin() + 128);
payload.assign(data.begin() + 128, data.end());
// Signature
signature.assign(header.begin(), header.begin() + 5);
// Version
version = header[5];
// Total Songs
total_songs = header[6];
// Starting Song
starting_song = header[7];
// Load Address
load_address = (header[9] << 8) | header[8];
// Init Address
init_address = (header[11] << 8) | header[10];
// Play Address
play_address = (header[13] << 8) | header[12];
// Song Name
song_name = get_null_term_string(&header[14], 32);
// Artist
artist = get_null_term_string(&header[46], 32);
// Copyright
copyright_info = get_null_term_string(&header[78], 32);
// NTSC Speed
ntsc_speed = (header[111] << 8) | header[110];
// Bankswitch Init
std::memcpy(bankswitch_init, &header[112], 8);
// PAL Speed
pal_speed = (header[121] << 8) | header[120];
// Video System
video_system = header[122];
// Extra Chips
extra_chips = header[123];
// Reserved
std::memcpy(reserved, &header[124], 4);
}
std::string get_null_term_string(const unsigned char* ptr, size_t max_len) {
std::string str;
for (size_t i = 0; i < max_len; ++i) {
if (ptr[i] == 0) break;
str += static_cast<char>(ptr[i]);
}
return str.empty() ? "<?>" : str;
}
void print_properties() {
std::cout << "NSF Properties:" << std::endl;
std::cout << "Signature: " << signature << std::endl;
std::cout << "Version: " << static_cast<int>(version) << std::endl;
std::cout << "Total Songs: " << static_cast<int>(total_songs) << std::endl;
std::cout << "Starting Song: " << static_cast<int>(starting_song) << std::endl;
std::cout << "Load Address: 0x" << std::hex << std::uppercase << std::setw(4) << std::setfill('0') << load_address << std::endl;
std::cout << "Init Address: 0x" << std::hex << std::uppercase << std::setw(4) << std::setfill('0') << init_address << std::endl;
std::cout << "Play Address: 0x" << std::hex << std::uppercase << std::setw(4) << std::setfill('0') << play_address << std::endl;
std::cout << "Song Name: " << song_name << std::endl;
std::cout << "Artist: " << artist << std::endl;
std::cout << "Copyright: " << copyright_info << std::endl;
std::cout << "NTSC Speed: " << ntsc_speed << std::endl;
std::cout << "Bankswitch Init: [";
for (int i = 0; i < 8; ++i) {
std::cout << "0x" << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << static_cast<int>(bankswitch_init[i]);
if (i < 7) std::cout << ", ";
}
std::cout << "]" << std::endl;
std::cout << "PAL Speed: " << pal_speed << std::endl;
std::cout << "Video System: 0x" << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << static_cast<int>(video_system) << std::endl;
std::cout << "Extra Sound Chips: 0x" << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << static_cast<int>(extra_chips) << std::endl;
std::cout << "Reserved: [";
for (int i = 0; i < 4; ++i) {
std::cout << "0x" << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << static_cast<int>(reserved[i]);
if (i < 3) std::cout << ", ";
}
std::cout << "]" << std::endl;
std::cout << "Data Payload Size: " << payload.size() << " bytes" << std::endl;
}
void write(const std::string& output_filename) {
// Rebuild header
header[5] = version;
header[6] = total_songs;
header[7] = starting_song;
header[8] = load_address & 0xFF;
header[9] = (load_address >> 8) & 0xFF;
header[10] = init_address & 0xFF;
header[11] = (init_address >> 8) & 0xFF;
header[12] = play_address & 0xFF;
header[13] = (play_address >> 8) & 0xFF;
set_null_term_string(&header[14], song_name, 32);
set_null_term_string(&header[46], artist, 32);
set_null_term_string(&header[78], copyright_info, 32);
header[110] = ntsc_speed & 0xFF;
header[111] = (ntsc_speed >> 8) & 0xFF;
std::memcpy(&header[112], bankswitch_init, 8);
header[120] = pal_speed & 0xFF;
header[121] = (pal_speed >> 8) & 0xFF;
header[122] = video_system;
header[123] = extra_chips;
std::memcpy(&header[124], reserved, 4);
std::ofstream out(output_filename, std::ios::binary);
if (!out) {
throw std::runtime_error("Cannot write file");
}
out.write(reinterpret_cast<const char*>(header.data()), 128);
out.write(reinterpret_cast<const char*>(payload.data()), payload.size());
}
void set_null_term_string(unsigned char* ptr, const std::string& str, size_t max_len) {
size_t len = std::min(str.length(), max_len - 1);
std::memcpy(ptr, str.c_str(), len);
ptr[len] = 0;
std::memset(ptr + len + 1, 0, max_len - len - 1);
}
};
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "Usage: " << argv[0] << " <input.nsf> [output.nsf]" << std::endl;
return 1;
}
try {
NSFHandler handler(argv[1]);
handler.print_properties();
if (argc > 2) {
handler.write(argv[2]);
std::cout << "Written to " << argv[2] << std::endl;
}
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return 0;
}