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.

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.

NSF Property Dumper

Drag and Drop .NSF File to Dump Properties

Drop .NSF file here

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;
}