Writing a NSF player

Discuss NSF files, FamiTracker, MML tools, or anything else related to NES music.

Moderator: Moderators

User avatar
SusiKette
Posts: 147
Joined: Fri Mar 16, 2018 1:52 pm
Location: Finland

Writing a NSF player

Post by SusiKette »

Long story short, I'm trying to write a NSF player to handle music and sound effects in a project I'm making and I need some help understanding the file structure and how NSF players commonly work. Do NSF players generate the sound (aside DMC samples) or do they use prerecorded samples?
I probably don't need to implement all expansion chips and features either, since VRC6 is the chip I'm using for the music. I don't plan on using PAL mode either.

I did try to look into NSFPlay's source code, but its written in C++ which isn't my strongest language so I don't think I can use that as a reference either.
Avatar is pixel art of Noah Prime from Astral Chain
Rahsennor
Posts: 479
Joined: Thu Aug 20, 2015 3:09 am

Re: Writing a NSF player

Post by Rahsennor »

An NSF file is essentially a ROM fragment. You need to emulate the CPU, APU and any expansion audio chips as you would for a full NES emulator. On the other hand, you don't have to worry about the PPU or mappers (the hard parts) and you don't really need to be cycle-accurate unless you want to play PCM. Full details of the format can be found on the wiki.

You can use pre-recorded samples, but unless you're building patches for an existing synthesizer you're probably in for a similar amount of work to writing a basic APU emulator, so you may be better off just pre-recording the entire soundtrack at that point.

What exactly does this project of yours need? What languages do you use/prefer? Is there a specific reason why you can't use an existing player?
User avatar
SusiKette
Posts: 147
Joined: Fri Mar 16, 2018 1:52 pm
Location: Finland

Re: Writing a NSF player

Post by SusiKette »

If by emulating the CPU you mean defining what each opcode does and handling the program counter, then that shouldn't be too hard. I'm not sure how easy the APU would be as I have not done much audio processing. The wiki does seem to have all the algorithms, but I'm not entirely sure of how exactly you output the sound.

If by playing PCM you mean DCM samples, then yes that is something I was planning on using. Is there a particular reason why it has to be cycle accurate and does if go for only CPU or APU or both?

The project I'm making is a game to be specific. That is why I don't really want to use existing player unless it is free to use. NSFPlay would have been, but the programming language is not correct. As I mentioned it's C++ and I'm using C#. The engine I'm using only supports C# and JavaScript as far as I'm concerned.

EDIT: By the way, since I'm using FamiTracker, is there already a code that can be used to play NSF files exported from it? Famitone is a good example although it is for NES games specifically.
Avatar is pixel art of Noah Prime from Astral Chain
Rahsennor
Posts: 479
Joined: Thu Aug 20, 2015 3:09 am

Re: Writing a NSF player

Post by Rahsennor »

SusiKette wrote:The wiki does seem to have all the algorithms, but I'm not entirely sure of how exactly you output the sound.
The simplest - though not necessarily fastest - method is to emulate the APU cycle by cycle, producing one sample of output per CPU cycle, and then feed that into a resampler. You can fit a usable BLIP/BLEP resampler in a few hundred lines of code, or you can find a good FFT-based resampling library and use that.

I don't recommend using a 'traditional' resampler like SRC, as they require a huge number of taps and run quite slowly when downsampling from 1.7 MHz to 48 kHz.
SusiKette wrote:If by playing PCM you mean DCM samples, then yes that is something I was planning on using. Is there a particular reason why it has to be cycle accurate and does if go for only CPU or APU or both?
By PCM I mean timed $4011 writes, for which you obviously need accurate timing throughout. The DMC channel is no harder than the other APU channels if you don't need that kind of accuracy.
SusiKette wrote:The engine I'm using only supports C# and JavaScript as far as I'm concerned.
I think Mesen is written in C#, so you might try looking there. It's a rather heavy-duty, high-accuracy emulator so I doubt it will work for you directly even if the license is good, but the code should (hopefully) be readable.
SusiKette wrote:EDIT: By the way, since I'm using FamiTracker, is there already a code that can be used to play NSF files exported from it? Famitone is a good example although it is for NES games specifically.
Not that I'm aware of. And my experience suggests it's probably easier to emulate the NSFs than mess around with Famitracker's internals and documentation. It's not really designed to target anything else, after all.

You could convert the NSFs to a log-based format like VGM and play that, if you want to skip the CPU emulation, but I'm not familiar with any tools for the conversion. I only know they exist somewhere, because there's plenty of NES VGMs out there.
User avatar
SusiKette
Posts: 147
Joined: Fri Mar 16, 2018 1:52 pm
Location: Finland

Re: Writing a NSF player

Post by SusiKette »

Do you have any recommendations on the FFT library? Although if writing a resampler that has the necessary functionality for this project isn't hard to make and has usable documentation on how to make one then that can be an option as well.

As for Mesen, it seemed to be written in C++ as well, so that won't be useful either.
Avatar is pixel art of Noah Prime from Astral Chain
User avatar
cpow
NESICIDE developer
Posts: 1097
Joined: Mon Oct 13, 2008 7:55 pm
Location: Minneapolis, MN
Contact:

Re: Writing a NSF player

Post by cpow »

SusiKette wrote:EDIT: By the way, since I'm using FamiTracker, is there already a code that can be used to play NSF files exported from it? Famitone is a good example although it is for NES games specifically.
Have you considered using the Qt FamiTracker library I provide as part of nesicide? I built a "front-end player" and a WinAmp plugin that demonstrate its usage. Basically with that you can play regular FamiTracker modules without having to export them or find a different player. The library is essentially full-blown FamiTracker with exposed APIs for playing, pausing, changing tracks, etc.
https://github.com/christopherpow/nesic ... amitracker -- the Qt FamiTracker library
https://github.com/christopherpow/nesic ... famiplayer -- the Qt FamiTracker player GUI
https://github.com/christopherpow/nesic ... ibs/in_ftm -- the WinAmp input plugin
User avatar
SusiKette
Posts: 147
Joined: Fri Mar 16, 2018 1:52 pm
Location: Finland

Re: Writing a NSF player

Post by SusiKette »

That would most likely work if it wasn't in C++
I already have a general idea on how I should structure the code for the CPU part, but what would be the best way to implement the RAM that the code in the NSF file uses? Should I just make a byte array that goes from $0000 to $07FF or is there a better way to do this? Mostly the size of the RAM area is what I'm concerned of since I can't know where FamiTracker's NSF files try to store variables and how many bytes of RAM it needs.

EDIT: Also, does FTM files have any implementation on sound effects and sort of a priority system to decide what to play?
Avatar is pixel art of Noah Prime from Astral Chain
User avatar
rainwarrior
Posts: 8734
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Re: Writing a NSF player

Post by rainwarrior »

FTM doesn't have sound effects.

Though, the FTM format is kind of a time grid of note events, and each note plays an "instrument" at a specific pitch. The instrument is sort of like a sound effect... but they don't overlay on the music, they are how you make the music. No priority system, each note plays on the channel where it's placed.
Rahsennor
Posts: 479
Joined: Thu Aug 20, 2015 3:09 am

Re: Writing a NSF player

Post by Rahsennor »

SusiKette wrote:Do you have any recommendations on the FFT library? Although if writing a resampler that has the necessary functionality for this project isn't hard to make and has usable documentation on how to make one then that can be an option as well.

As for Mesen, it seemed to be written in C++ as well, so that won't be useful either.
Oh. I've never used C# so I'm afraid I don't really have any other suggestions that would be useful to you. Sorry.

Writing a BLEP resampler is fairly straightforward; the only complicated mathematics required are for generating the filter table. Once you have that, it's a simple convolution: read a sample, multiply by a row of the table, add to the output. Since most PSG output is stepwise, if you differentiate the signal first then most of the samples going into the resampler will be zero, meaning you can skip them to save CPU. Since the rate of transitions is (roughly) limited to the audible range regardless of sample rate, the performance is as well. Reverse the differentiation with a leaky integrator afterwards and you're done.

I can't remember any good tutorials off the top of my head, though. I've been meaning to write one but never found the time.
SusiKette wrote:I already have a general idea on how I should structure the code for the CPU part, but what would be the best way to implement the RAM that the code in the NSF file uses? Should I just make a byte array that goes from $0000 to $07FF or is there a better way to do this? Mostly the size of the RAM area is what I'm concerned of since I can't know where FamiTracker's NSF files try to store variables and how many bytes of RAM it needs.
The NSF wiki page lists all the required address ranges, under "Summary of Addresses". RAM when not using FDS or MMC5 is at $0000-$07FF and $6000-$7FFF, just like a normal NES ROM with WRAM. For a stripped-down NSF player you can map RAM from $0000 to $7FFF if you want to keep it simple. The only readable port in that that range (assuming no FDS, MMC5 or 163 expansions) is $4015, so as long as you special-case that any conforming NSF should work fine.
User avatar
SusiKette
Posts: 147
Joined: Fri Mar 16, 2018 1:52 pm
Location: Finland

Re: Writing a NSF player

Post by SusiKette »

So if I only want to support VRC6 I only need RAM address from $0000 to $07FF? One way to implement the $6000 to $7FFF could be to create a separate array and see if bits %0110 0000 0000 0000 are set to see that the address is above $6000. If this is the case, the $6000-$7FFF range array is used and they you subtract $6000 from the address to get relative address from the array (i.e. $6003 would be index 3 from the array).

And if I understood correctly the loading, initializing and playing the tune that is described on the wiki page are something that is already in the NSF and not something that I have to worry about?

EDIT: Do NSF players and emulators commonly keep the CPU status flags as individual bool values and put them on a byte if you need to push them to the stack etc. or kept as a byte like they are on the NES?
Avatar is pixel art of Noah Prime from Astral Chain
User avatar
rainwarrior
Posts: 8734
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Re: Writing a NSF player

Post by rainwarrior »

SusiKette wrote:So if I only want to support VRC6 I only need RAM address from $0000 to $07FF? One way to implement the $6000 to $7FFF could be to create a separate array and see if bits %0110 0000 0000 0000 are set to see that the address is above $6000. If this is the case, the $6000-$7FFF range array is used and they you subtract $6000 from the address to get relative address from the array (i.e. $6003 would be index 3 from the array).
RAM is present at $6000-7FFF for all expansions (or no expansions). Many NSFs will not use it, but it is available there per the spec for all NSFs.

FDS has some additional RAM requirements (and $6000-7FFF becomes bankable), but it's the only expansion that alters the specified RAM.
SusiKette wrote:And if I understood correctly the loading, initializing and playing the tune that is described on the wiki page are something that is already in the NSF and not something that I have to worry about?
No, that describes the situation you need to provide before calling the INIT subroutine in the NSF.
SusiKette wrote:EDIT: Do NSF players and emulators commonly keep the CPU status flags as individual bool values and put them on a byte if you need to push them to the stack etc. or kept as a byte like they are on the NES?
Yes I think that is common.
User avatar
SusiKette
Posts: 147
Joined: Fri Mar 16, 2018 1:52 pm
Location: Finland

Re: Writing a NSF player

Post by SusiKette »

There might still be hope for using some of the C++ projects that has been suggested. I figured that if the necessary bits are converted to a .dll they can be imported to the program I'm using. Although I'm not exactly sure how the conversion works I guess it's worth a try. I don't think there is any point in writing a completely new player if an existing one can be used.
Avatar is pixel art of Noah Prime from Astral Chain
Rahsennor
Posts: 479
Joined: Thu Aug 20, 2015 3:09 am

Re: Writing a NSF player

Post by Rahsennor »

SusiKette wrote:EDIT: Do NSF players and emulators commonly keep the CPU status flags as individual bool values and put them on a byte if you need to push them to the stack etc. or kept as a byte like they are on the NES?
I keep them as a byte. I found it simpler that way. YMMV.
rainwarrior wrote:FDS has some additional RAM requirements (and $6000-7FFF becomes bankable), but it's the only expansion that alters the specified RAM.
Nitpick: MMC5 adds EXRAM at $5C00. Because why not.
User avatar
SusiKette
Posts: 147
Joined: Fri Mar 16, 2018 1:52 pm
Location: Finland

Re: Writing a NSF player

Post by SusiKette »

Using .dll seemed to be useless effort.
As for the player, do I have to map the contents of the NSF file to the $8000 - $FFFF region similarly how I map RAM to $0000 - $07FF and $6000 - $7FFF?
Avatar is pixel art of Noah Prime from Astral Chain
User avatar
rainwarrior
Posts: 8734
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Re: Writing a NSF player

Post by rainwarrior »

Yes. See the section on loading and bankswitching.

The NSF data can be up to 1MB in size, mapped into 8 x 4k banks.

If it's not a bankswitching NSF, you simply load the data from the file at the LOAD address specified in the header (somewhere within $8000-FFFF).
Post Reply