NSF 2 is a proposed extension to the NSF file format. It is intended to be backward compatible with the original NSF, provide IRQ timer functionality, and allow track times and other metadata to be contained.
It is currently an unfinished proposal. Currently Nintendulator partially supports it. See link below for reference.
The goals are:
- IRQ support
- "no return" init addresses
- information block
New header fields
|$005||1||BYTE||Version number: $02 (was $01)|
|$07C||1||BYTE|| NSF 2.0 feature enables (was $00)
|$07D||3||WORD||Length of NSF data block in bytes, LSB first, up to $100000 (was $000000)|
NSF 2 files are allowed to use three IRQ sources:
- APU DMC IRQ
- APU Frame Counter IRQ
- A new cycle-counting interval timer at $401B-$401D, handled much as a mapper IRQ
A program using IRQ MUST write to $4017 to enable the interval timer IRQ and to reset the frame IRQ.
The interval timer has three readable and writable ports:
- $401B: Lower 8 bits of the 16-bit timer reload value
- $401C: Upper 8 bits of the 16-bit timer reload value
- $401D: Control register. Clearing bit 0 stops the timer and holds it in reset; setting it to 1 starts it.
In NSF 2 files using IRQ, $FFFE and $FFFF (the IRQ vector) are treated as readable and writable. A program using IRQ MUST write to these locations before enabling IRQ. Writes to $FFFE and $FFFF do not affect the NSF data; they are separate bytes of RAM.
For IRQ support, I figured allowing the use of frame IRQs and DPCM IRQs just like on the NES, and then a 16 bit IRQ timer which would be connected to the CPU in the usual way (via its IRQ input).
This would allow a real NES to play 2.0 NSFs using say a PowerPak or similar.
The timer would decrement at the CPU clock rate, whether it be NTSC or PAL rate (so 1.79MHz or 1.66MHz or so).
The counter is a modulus N counter and has the following behaviour:
When the counter is off (whenever 401d bit 0 is clear) it is constantly being reloaded with the values in 401b/401c, and the counter IRQ flag is cleared.
When the counter is on (401d bit 0 is set), it will decrement once per CPU cycle. When it hits 0, it is reloaded from 401b/401c, the IRQ flag is set and an IRQ is asserted.
This means an IRQ will be generated every N+1 clock cycles, where N is the value loaded into the counter. (it is N+1 because 0 is counted too).
To clear the IRQ flag, you read 401d. I should probably put the IRQ flag at bit 7 (read only) to allow easy testing of IRQ source.
Code might look like this:
timervalue: .equ 01fffh ;desired # of cpu cycles minus 1 starttimer: LDA #000h STA 0401dh ;reset and shut off timer in case it was on LDA #<irqvector STA 0fffeh ;store interrupt vector low of our handler LDA #>irqvector STA 0ffffh ;store interrupt vector high LDA #<timervalue STA 0401bh ;low byte of timer value LDA #>timervalue STA 0401ch ;high byte of timer value LDA #0c0h STA 04017h ;turn off frame IRQs LDA 04015h ;ack any pending DPCM IRQ if, it exists CLI ;enable IRQs LDA #01h STA 0401dh ;turn the timer on RTS stoptimer: SEI ;turn off IRQs LDA #000h STA 0401dh ;turn timer off RTS irqvector: <perform our interrupt code here> .... .... LDA 0401dh ;reading 401d resets IRQ flag RTI ;return from interrupt alternatively, if you wish to determine WHAT caused the IRQ (if you're using more than one source) you'd do something like this... irqvector: BIT 0401dh ;bit 7 indicates we have a timer IRQ waiting BPL + JSR timer ;if bit 7 was set, call timer subroutine + BIT 04015h BPL + PHP ;save flags if we have DPCM int. JSR dpcm ;if bit 7 was set, call DPCM sub PLP + BVC + JSR frame ;bit 6 of 4015 = frame IRQ + RTI ;exit interrupt timer: <do stuff here> RTS dpcm: <do stuff here> RTS frame: <do stuff here> RTS
There's a lot of ways to skin this cat, but this is one particular method. The idea is to read the status regs to figure out which source caused the interrupt, then run code to service it, then go back and check the other sources just in case one of them also needs servicing.
Like the APU period timers and the MMC3 scanline timer, the NSF2 cycle timer includes both the 0 and the reload value in its sequence of states. When it hits 0, the NEXT clock performs a reload.
For example, when the reload value is $0010, the counter would have a period of 17 CPU cycles:
10, 10, 10, [Enable now] 0F, 0E, 0D, 0C, ..., 02, 01, 00, 10*, 0F, 0E, 0D, 0C, ...
where * denotes the IRQ flag becoming true.
An NSF with a non-returning INIT takes more or less complete control of the CPU. In effect, the INIT address becomes the reset vector, and the PLAY address becomes the NMI vector. If the IRQ feature is also in use, IRQ has its vector at $FFFE-$FFFF as above.
The optional metadata block allows an NSF to carry the following additional metadata:
- Longer title, author, and copyright fields
- UTF-8 encoding for wider language support
- Separate field for ripper
- Lengths of tracks
- Possibility of separate track, author, and copyright fields
The metadata is placed at the end so that pre-NSF 2 players will correctly ignore it by placing it at the end of the NSF data. Files MUST also contain data in the original title, author, and copyright fields for compatibility with pre-NSF 2 tools.
Kev's proposal uses a somewhat TIFF-like chunked format for metadata
|3||1||BYTE||track number to which this record applies, or $FF for all tracks|
|N + 4||1||BYTE||next record type...|
Records would be one after another, and a record of 4 $00 bytes would signify the end of the data.
- $00: last record (length 0)
- $01: title (1 line)
- $02: composer (1 line)
- $03: copyright (1 line)
- $04: ripper (1 line)
- $05: Length (in units to be determined)
- $06: Type (background music, jingle, or sound effect; values to be determined)
- $07: Additional description (UTF-8, for example: "Entry 7, cover of 'Breaking the Law' by Judas Priest")
All strings are UTF-8; players should indicate an error for characters that they cannot display. For example, a hardware player supporting only ASCII might replace any run of bytes with the high bit set with �.
To reduce duplication of data, track $FF is a wildcard indicator, which populates fields that don't have a more specific value for a particular track:
- composer, track $FF, jimbob
- title, track 0, my first song
- title, track 1, my second song
- title, track 2, a song by someone else
- composer, track 2, billy
- title, track 3, another song
This would set the composer of tracks 0, 1, and 3 to jimbob.
This format should be easy for a hardware player to parse. If you prefer to enter metadata as JSON, XML, or the like, create a tool that parses your metadata and stuffs it at the end of the NSF.